Compare commits

..

7 Commits

Author SHA1 Message Date
Jiang Bohan
111d8aa5df docs(handler): note that GetConfig is public-only and what may be returned
Adds a doc comment on GetConfig spelling out that the endpoint is mounted on
the unauthenticated route group (so the login page can fetch GoogleClientID /
AllowSignup before the user is signed in) and that only instance-level public
fields may be added. Prevents accidentally returning user- or tenant-scoped
data from this handler in the future.
2026-04-23 01:50:06 +08:00
Black
98edc6b9ff fix(auth): make /api/config publicly accessible (#1530) 2026-04-23 01:49:21 +08:00
Jiayuan Zhang
88b892f1ca fix(desktop): preserve last-opened workspace on app start (MUL-1269) (#1515)
The workspace query defaults `data` to `[]` before the first fetch, so the
bootstrap effect ran with an empty valid-slug set, wiped the persisted
`activeWorkspaceSlug`, then fell back to `workspaces[0]` once the real list
arrived — dropping the user on the default workspace on every launch.

Gate the effect on `workspaceListFetched` so validation runs only against
the real list, and re-read the store after `validateWorkspaceSlugs` to
avoid acting on a stale snapshot.

Co-authored-by: Lambda <f252c2c5-7d1d-4f3c-b394-a61abfe673fc@users.noreply.multica.ai>
2026-04-23 00:20:38 +08:00
Bohan Jiang
2cced51d64 docs(changelog): publish v0.2.14 + v0.2.15 release notes (#1517)
* docs(changelog): publish v0.2.14 + v0.2.15 release notes

Summarises the 25 commits shipped today across both releases for the public changelog page, in English and Chinese.

* docs(changelog): merge v0.2.14+v0.2.15 into one entry, trim, reclassify Gemini as fix

Per review: today's two releases read better as one set of notes; tightened
bullets; moved the Gemini 3 runtime-list update from Features to Fixes.

* docs(changelog): drop last 3 features from v0.2.15 entry per review
2026-04-22 20:02:42 +08:00
Bohan Jiang
6717db1fad feat(agents): surface task source on AgentTaskResponse + use it in Tasks tab (#1455)
Follow-up to #1453. That PR fixed the Tasks tab crash by filtering empty
issue_id out of the detail lookup and rendering a neutral "Task without
linked issue" label, but every issue-less task — chat-spawned or
autopilot-spawned — looked the same. The server already stores the
origin in `agent_task_queue.chat_session_id` / `autopilot_run_id`; only
the HTTP serializer was dropping them.

Server:
- `taskToResponse` now populates `ChatSessionID` and the new
  `AutopilotRunID` on `AgentTaskResponse`. Backward compatible: both
  omit when UUID is invalid, and existing clients ignore unknown
  fields.

Types:
- `AgentTask` (TS) gains `chat_session_id?` + `autopilot_run_id?` and a
  comment clarifying when `issue_id` is empty.

Tasks tab:
- Row label for issue-less tasks is picked from the populated source
  field: "Chat session" for chat tasks, "Autopilot run" for autopilot
  tasks, "Task without linked issue" as the neutral fallback. Rows stay
  inert (no anchor) in all three cases; existing issue-linked path is
  unchanged.

Tests:
- Two new regression tests assert the chat and autopilot labels render
  correctly and neither row becomes an anchor. Existing neutral-label
  test stays as the "neither source populated" case.
2026-04-22 19:26:57 +08:00
Dhruv-89
2a248b8548 fix(openclaw): raise agent discovery timeout to 30s (#1495)
'discoverOpenclawAgents' runs several 'openclaw' subprocesses under one
context; 5s was too short on cold starts or under load, causing empty
listings in the model picker. Increase the per-discovery cap to 30s.
2026-04-22 19:24:57 +08:00
Naiyuan Qing
f84d216794 fix(views): restore issue-mention class on <a> for mention card (#1516)
PR #1502's IssueChip extraction moved the `issue-mention` class from the
outer <a> into IssueChip's inner <span>, breaking three consumers that
select on `<a>.issue-mention` directly:

- `.rich-text-editor a.issue-mention` underline-exemption in
  content-editor.css (stopped matching -> mentions in editor gained a
  spurious underline).
- `link-hover-card.tsx` classList check that suppresses the URL preview
  on issue mentions (stopped matching -> hover card wrongly pops up
  over mention chips).
- Tailwind Typography prose (`prose a { text-decoration: underline }`)
  covers a separate path — markdown bubbles in chat. prose's specificity
  (0,1,1) beats `.no-underline` (0,1,0), so `not-prose` is the right
  escape hatch on the AppLink.

Put `issue-mention` back on the <a> in both wrappers (IssueMentionCard
and the editor's MentionView), and add `not-prose` only to the markdown
wrapper. IssueChip's BASE_CLASS keeps `issue-mention` too (inert on the
span; removing it is a separate scope that needs a full consumer audit).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 19:13:14 +08:00
13 changed files with 165 additions and 14 deletions

View File

@@ -123,15 +123,21 @@ function AppContent() {
// warning because `switchWorkspace` is a Zustand setState that the
// TabBar is subscribed to. useLayoutEffect flushes both renders before
// the user sees anything, so there's no visible flicker.
//
// Gate on `workspaceListFetched`: useQuery defaults `data` to `[]` before
// the first fetch, so without this guard we'd run validation against an
// empty slug set, wipe the persisted `activeWorkspaceSlug`, then fall
// back to `workspaces[0]` once the real list arrives — losing the user's
// last-opened workspace on every app start.
useLayoutEffect(() => {
if (!workspaces) return;
if (!workspaceListFetched) return;
const validSlugs = new Set(workspaces.map((w) => w.slug));
const tabStore = useTabStore.getState();
tabStore.validateWorkspaceSlugs(validSlugs);
if (!tabStore.activeWorkspaceSlug && workspaces.length > 0) {
tabStore.switchWorkspace(workspaces[0].slug);
useTabStore.getState().validateWorkspaceSlugs(validSlugs);
const { activeWorkspaceSlug, switchWorkspace } = useTabStore.getState();
if (!activeWorkspaceSlug && workspaces.length > 0) {
switchWorkspace(workspaces[0].slug);
}
}, [workspaces]);
}, [workspaces, workspaceListFetched]);
// null = undecided (pre-login or list hasn't settled yet)
// true = session started with zero workspaces; next transition to >=1 triggers restart

View File

@@ -281,6 +281,31 @@ export function createEnDict(allowSignup: boolean): LandingDict {
fixes: "Bug Fixes",
},
entries: [
{
version: "0.2.15",
date: "2026-04-22",
title: "Local Skills, LaTeX, Focus Mode & Orphan-Task Recovery",
changes: [],
features: [
"Import runtime local Skills into the workspace as first-class artifacts",
"Orphan-task recovery — abandoned agent runs auto-retry, with manual rerun as fallback",
"LaTeX rendering in issues, comments and chat",
"Chat Focus mode — share the page you're on as conversation context",
],
improvements: [
"Sub-issue `status_changed` events no longer spam parent-issue subscribers",
"Multi-arch Docker release images built natively per-arch (no QEMU)",
"Pin sidebar derives fields client-side for snappier reorders",
"Expanded reserved-slug list so new slugs can't collide with product routes",
],
fixes: [
"Gemini runtime model list now includes Gemini 3 and CLI aliases",
"Chat focus button disabled on pages without an anchor",
"Onboarding pin sync, welcome layout and runtime bootstrap state",
"`install.ps1` OS architecture detection hardened for more Windows setups",
"`/download` falls back to the previous release within a 1h freshness window",
],
},
{
version: "0.2.11",
date: "2026-04-21",

View File

@@ -281,6 +281,31 @@ export function createZhDict(allowSignup: boolean): LandingDict {
fixes: "问题修复",
},
entries: [
{
version: "0.2.15",
date: "2026-04-22",
title: "本地 Skills、LaTeX、Focus 模式与孤儿任务自恢复",
changes: [],
features: [
"支持将 Runtime 本地 Skills 导入工作区,成为一等工作区资产",
"孤儿任务自动恢复——意外中断的 Agent 执行会自动重试,必要时可手动重跑",
"Issue、评论与 Chat 支持 LaTeX 渲染",
"Chat Focus 模式——将当前页面作为上下文分享给对话",
],
improvements: [
"子 Issue 的 `status_changed` 事件不再向父 Issue 订阅者刷屏",
"Docker 发布镜像改为按架构原生构建,免 QEMU",
"侧边栏 Pin 字段在客户端派生,排序更跟手",
"扩充保留 slug 列表,新工作区 slug 不会再和产品路由冲突",
],
fixes: [
"Gemini Runtime 模型列表补上 Gemini 3 及若干 CLI 别名",
"没有锚点的页面上 Chat focus 按钮改为禁用",
"修复 Onboarding 中 Pin 同步、欢迎页布局与 Runtime bootstrap 状态",
"`install.ps1` 的系统架构探测更稳健,覆盖更多 Windows 环境",
"`/download` 在 1 小时新鲜度窗口内可回退到上一版本,避免撞上半发布状态",
],
},
{
version: "0.2.11",
date: "2026-04-21",

View File

@@ -27,6 +27,9 @@ export interface AgentTask {
id: string;
agent_id: string;
runtime_id: string;
// Empty string ("") when the task has no linked issue — either chat- or
// autopilot-spawned. Check chat_session_id / autopilot_run_id to tell
// which source produced it.
issue_id: string;
status: "queued" | "dispatched" | "running" | "completed" | "failed" | "cancelled";
priority: number;
@@ -36,6 +39,10 @@ export interface AgentTask {
result: unknown;
error: string | null;
created_at: string;
/** Non-empty when the task was spawned from a chat session. */
chat_session_id?: string;
/** Non-empty when the task was spawned by an autopilot run. */
autopilot_run_id?: string;
}
export interface Agent {

View File

@@ -202,4 +202,56 @@ describe("TasksTab", () => {
expect(label.closest("a")).toBeNull();
expect(mockGetIssue).not.toHaveBeenCalled();
});
it("labels chat-spawned tasks as 'Chat session'", async () => {
renderTasksTab(
[
{
id: "task-chat",
agent_id: "agent-1",
runtime_id: "runtime-1",
issue_id: "",
chat_session_id: "chat-42",
status: "running",
priority: 1,
dispatched_at: "2026-04-16T00:30:00Z",
started_at: "2026-04-16T00:31:00Z",
completed_at: null,
result: null,
error: null,
created_at: "2026-04-16T00:00:00Z",
},
],
[],
);
const label = await screen.findByText("Chat session");
expect(label.closest("a")).toBeNull();
});
it("labels autopilot-spawned tasks as 'Autopilot run'", async () => {
renderTasksTab(
[
{
id: "task-autopilot",
agent_id: "agent-1",
runtime_id: "runtime-1",
issue_id: "",
autopilot_run_id: "run-7",
status: "completed",
priority: 1,
dispatched_at: null,
started_at: null,
completed_at: "2026-04-16T01:00:00Z",
result: null,
error: null,
created_at: "2026-04-16T00:00:00Z",
},
],
[],
);
const label = await screen.findByText("Autopilot run");
expect(label.closest("a")).toBeNull();
});
});

View File

@@ -106,11 +106,18 @@ export function TasksTab({ agent }: { agent: Agent }) {
{sortedTasks.map((task) => {
const config = taskStatusConfig[task.status] ?? taskStatusConfig.queued!;
const Icon = config.icon;
// Tasks without a linked issue (autopilot run_only, chat-spawned,
// etc.) carry issue_id = "" — skip the lookup and render them
// as non-link rows.
// Tasks without a linked issue carry issue_id = "" — skip the
// detail lookup and render them as non-link rows. The source
// label is picked from chat_session_id / autopilot_run_id,
// which the server populates for chat- and autopilot-spawned
// tasks respectively.
const hasIssue = task.issue_id !== "";
const issue = hasIssue ? issueMap.get(task.issue_id) : undefined;
const sourcelessLabel = task.chat_session_id
? "Chat session"
: task.autopilot_run_id
? "Autopilot run"
: "Task without linked issue";
const isActive = task.status === "running" || task.status === "dispatched";
const isRunning = task.status === "running";
const rowClassName = `flex items-center gap-3 rounded-lg border px-4 py-3 transition-shadow hover:shadow-sm ${
@@ -136,7 +143,7 @@ export function TasksTab({ agent }: { agent: Agent }) {
</span>
)}
<span className={`text-sm truncate ${isActive ? "font-medium" : ""}`}>
{issue?.title ?? (hasIssue ? `Issue ${task.issue_id.slice(0, 8)}...` : "Task without linked issue")}
{issue?.title ?? (hasIssue ? `Issue ${task.issue_id.slice(0, 8)}...` : sourcelessLabel)}
</span>
</div>
<div className="mt-0.5 text-xs text-muted-foreground">

View File

@@ -60,7 +60,7 @@ function IssueMention({
};
return (
<a href={issuePath} onClick={handleClick} className="inline-flex">
<a href={issuePath} onClick={handleClick} className="issue-mention inline-flex">
<IssueChip
issueId={issueId}
fallbackLabel={fallbackLabel}

View File

@@ -18,7 +18,7 @@ interface IssueMentionCardProps {
export function IssueMentionCard({ issueId, fallbackLabel }: IssueMentionCardProps) {
const p = useWorkspacePaths();
return (
<AppLink href={p.issueDetail(issueId)} className="inline-flex">
<AppLink href={p.issueDetail(issueId)} className="issue-mention not-prose inline-flex">
<IssueChip
issueId={issueId}
fallbackLabel={fallbackLabel}

View File

@@ -222,6 +222,23 @@ func TestHealth(t *testing.T) {
}
}
func TestConfigRouteIsPublic(t *testing.T) {
resp, err := http.Get(testServer.URL + "/api/config")
if err != nil {
t.Fatalf("config request failed: %v", err)
}
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
t.Fatalf("expected 200, got %d: %s", resp.StatusCode, body)
}
var result struct {
CdnDomain string `json:"cdn_domain"`
}
readJSON(t, resp, &result)
}
// ---- Auth ----
func TestSendCodeAndVerify(t *testing.T) {

View File

@@ -134,6 +134,9 @@ func NewRouter(pool *pgxpool.Pool, hub *realtime.Hub, bus *events.Bus, analytics
r.Post("/auth/google", h.GoogleLogin)
r.Post("/auth/logout", h.Logout)
// Public API
r.Get("/api/config", h.GetConfig)
// Daemon API routes (require daemon token or valid user token)
r.Route("/api/daemon", func(r chi.Router) {
r.Use(middleware.DaemonAuth(queries))
@@ -172,7 +175,6 @@ func NewRouter(pool *pgxpool.Pool, hub *realtime.Hub, bus *events.Bus, analytics
r.Use(middleware.RefreshCloudFrontCookies(cfSigner))
// --- User-scoped routes (no workspace context required) ---
r.Get("/api/config", h.GetConfig)
r.Get("/api/me", h.GetMe)
r.Patch("/api/me", h.UpdateMe)
r.Patch("/api/me/onboarding", h.PatchOnboarding)

View File

@@ -139,6 +139,7 @@ type AgentTaskResponse struct {
TriggerCommentContent string `json:"trigger_comment_content,omitempty"` // content of the triggering comment
ChatSessionID string `json:"chat_session_id,omitempty"` // non-empty for chat tasks
ChatMessage string `json:"chat_message,omitempty"` // user message for chat tasks
AutopilotRunID string `json:"autopilot_run_id,omitempty"` // non-empty for autopilot-spawned tasks
}
// TaskAgentData holds agent info included in claim responses so the daemon
@@ -181,6 +182,11 @@ func taskToResponse(t db.AgentTaskQueue) AgentTaskResponse {
ParentTaskID: uuidToPtr(t.ParentTaskID),
CreatedAt: timestampToString(t.CreatedAt),
TriggerCommentID: uuidToPtr(t.TriggerCommentID),
// Surface task source so the UI can distinguish issue-linked tasks
// from chat-spawned or autopilot-spawned ones; all three may arrive
// with issue_id = "" once a task has no linked issue.
ChatSessionID: uuidToString(t.ChatSessionID),
AutopilotRunID: uuidToString(t.AutopilotRunID),
}
}

View File

@@ -22,6 +22,10 @@ type AppConfig struct {
PosthogHost string `json:"posthog_host"`
}
// GetConfig is mounted on the public (unauthenticated) route group because
// the web app calls it before login to decide whether to render the Google
// sign-in button and signup UI. Only add fields here that are safe to expose
// to anonymous callers — never user- or tenant-scoped data.
func (h *Handler) GetConfig(w http.ResponseWriter, r *http.Request) {
config := AppConfig{
AllowSignup: os.Getenv("ALLOW_SIGNUP") != "false",

View File

@@ -655,7 +655,7 @@ func discoverOpenclawAgents(ctx context.Context, executablePath string) ([]Model
if _, err := exec.LookPath(executablePath); err != nil {
return []Model{}, nil
}
runCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
runCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
// Try JSON modes first. Different openclaw builds expose the