From e9dce2d2343eb03caa2f566e7846bb3bddd124d2 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 5 Oct 2026 01:27:13 +0800 Subject: [PATCH 1/4] fix(chat): expose rejected Todo previews in context Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../personal-workspace/context-drawer.tsx | 33 ++++++++++--- .../typed-actions.mjs | 14 ++++++ loopx/chat_server.py | 11 +++++ .../project_registry_io_manifest_v1.json | 8 +-- tests/test_chat_server_cors.py | 49 +++++++++++++++++++ 5 files changed, 104 insertions(+), 11 deletions(-) diff --git a/apps/presentation/dashboard/src/features/personal-workspace/context-drawer.tsx b/apps/presentation/dashboard/src/features/personal-workspace/context-drawer.tsx index 42c79930cd..6af940a1aa 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/context-drawer.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/context-drawer.tsx @@ -29,6 +29,7 @@ import { import type { PersonalWorkspaceCallbacks, + WorkspaceActionPreviewRequest, WorkspaceAgentOption, WorkspaceAttention, WorkspaceDrawerSelection, @@ -155,19 +156,23 @@ export function ContextDrawer({ agents, attentionHistory = [], onSelectAttention const [todoAgentId, setTodoAgentId] = useState(agents.find((agent) => agent.available)?.agentId ?? "codex"); const [todoPriority, setTodoPriority] = useState(""); const [todoResumeWhen, setTodoResumeWhen] = useState(""); + const [previewFailure, setPreviewFailure] = useState<{identity: string; message: string} | null>(null); + const previewAttemptRef = useRef(0); const closeRef = useRef(null); const drawerRef = useRef(null); const returnFocusRef = useRef(null); const titleRef = useRef(null); const selectionIdentity = selection.kind === "run" ? `run:${selection.item.runId}` : selection.kind === "proposal" ? `proposal:${selection.item.previewId}` - : selection.kind === "todo" ? `todo:${selection.item.todoId}` - : selection.kind === "attention" ? `attention:${selection.item.todoId}` + : selection.kind === "todo" ? `todo:${selection.item.goalId}:${selection.item.todoId}` + : selection.kind === "attention" ? `attention:${selection.item.goalId}:${selection.item.todoId}` : selection.kind === "output" ? `output:${selection.item.outputId}` : selection.kind === "schedule" ? `schedule:${selection.item.scheduleId}` : `goal:${selection.item.goalId}`; useEffect(() => { + previewAttemptRef.current += 1; + setPreviewFailure(null); setRepositoryCopyState("idle"); setDiagnosticsOpen(false); setRunDrawerTab("record"); @@ -370,9 +375,22 @@ export function ContextDrawer({ agents, attentionHistory = [], onSelectAttention if (await performRunAction(run, "correct", () => onCorrectRun(run, message))) setCorrection(""); } + async function previewAction(request: WorkspaceActionPreviewRequest) { + const attempt = ++previewAttemptRef.current; + setPreviewFailure(null); + try { + await callbacks.onPreviewAction?.(request); + } catch (error) { + if (attempt !== previewAttemptRef.current) return; + setPreviewFailure({identity: selectionIdentity, message: t("feedback.previewFailed", { + error: error instanceof Error ? error.message : String(error), + })}); + } + } + async function previewTodoTransition(todo: WorkspaceTodo, operation: TodoOperation, label: string, resumeWhen?: string) { if (operation === "successor_create") { - await callbacks.onPreviewAction?.({ + await previewAction({ actionKind: "todo.create", context: { goal_id: todo.goalId, kind: "todo", todo_id: todo.todoId }, idempotencyKey: `workspace-todo-successor-${todo.todoId}-${Date.now().toString(36)}`, @@ -381,7 +399,7 @@ export function ContextDrawer({ agents, attentionHistory = [], onSelectAttention }); return; } - await callbacks.onPreviewAction?.({ + await previewAction({ actionKind: "todo.update", context: { goal_id: todo.goalId, kind: "todo", todo_id: todo.todoId }, idempotencyKey: `workspace-todo-${todo.todoId}-${operation}-${Date.now().toString(36)}`, @@ -399,7 +417,7 @@ export function ContextDrawer({ agents, attentionHistory = [], onSelectAttention async function previewDecision(attention: WorkspaceAttention, decision: DecisionOutcome) { if (readOnly || !canDecideAttention(attention)) return; - await callbacks.onPreviewAction?.({ + await previewAction({ actionKind: "gate.resolve", context: { goal_id: attention.goalId, kind: "todo", todo_id: attention.todoId }, idempotencyKey: `workspace-decision-${attention.todoId}-${decision}-${Date.now().toString(36)}`, @@ -603,6 +621,7 @@ export function ContextDrawer({ agents, attentionHistory = [], onSelectAttention
+ {previewFailure?.identity === selectionIdentity ?

{previewFailure.message}

: null} {selection.kind === "attention" ? ( <> @@ -666,7 +685,7 @@ export function ContextDrawer({ agents, attentionHistory = [], onSelectAttention -
; })} diff --git a/apps/presentation/dashboard/src/features/personal-workspace/context-drawer.tsx b/apps/presentation/dashboard/src/features/personal-workspace/context-drawer.tsx index 6af940a1aa..94cbec247d 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/context-drawer.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/context-drawer.tsx @@ -660,7 +660,7 @@ export function ContextDrawer({ agents, attentionHistory = [], onSelectAttention

{t("drawer.taskInfo")}

Goal
{selection.item.goalTitle}
-
{t("common.owner")}
{selection.item.ownerLabel ?? selection.item.claimedBy ?? t("drawer.notAssigned")}
+
{t("common.owner")}
{selection.item.claimedBy ? selection.item.ownerLabel ?? selection.item.claimedBy : t("drawer.notAssigned")}
{t("common.status")}
{selection.item.done ? t("drawer.taskStatusCompleted") : selection.item.status === "deferred" ? t("drawer.taskStatusDeferred") : selection.item.status === "blocked" ? t("drawer.taskStatusBlocked") : t("drawer.taskStatusOpen")}
{t("drawer.priority")}
{selection.item.priority ?? t("drawer.notSet")}
{selection.item.completedAt ?
{t("drawer.completedAt")}
: null} diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-tasks-view.tsx b/apps/presentation/dashboard/src/features/personal-workspace/goal-tasks-view.tsx index e8f0faa781..5d101b91c7 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/goal-tasks-view.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-tasks-view.tsx @@ -240,7 +240,7 @@ export function GoalTasksView({ {openAgentTodos.map((todo) => { - const enriched = { ...todo, goalId: goal.goalId, goalTitle: goal.title, ownerLabel: todo.claimedBy ?? goal.agentLabel ?? goal.agentId }; + const enriched = { ...todo, goalId: goal.goalId, goalTitle: goal.title, ownerLabel: todo.claimedBy ?? t("drawer.notAssigned") }; const execution = executionRuns.find((item) => item.run.todoId === todo.todoId)?.run; return (
{ selectedTodoRef.current = element; } : undefined}> @@ -251,7 +251,7 @@ export function GoalTasksView({ {todo.status === "blocked" ? {t("tasks.blocked")} : null} {execution ? {execution.status === "running" || execution.status === "queued" ? t("runs.running") : execution.status === "failed" ? t("tasks.sessionError") : t("common.waiting")} : null} {todo.status === "deferred" ? {t("drawer.taskStatusDeferred")} : !execution ? {t("tasks.waiting")} : null} - {todo.claimedBy ?? goal.agentLabel ?? goal.agentId} + {enriched.ownerLabel}
diff --git a/examples/personal-workspace-browser/typed-actions.mjs b/examples/personal-workspace-browser/typed-actions.mjs index b0820e3a80..5c8df0e329 100644 --- a/examples/personal-workspace-browser/typed-actions.mjs +++ b/examples/personal-workspace-browser/typed-actions.mjs @@ -889,6 +889,12 @@ export const typedActionsScenario = { await page.locator(".personal-goal-link", { hasText: "Progress Projection" }).click(); await page.getByRole("heading", { name: "Progress Projection" }).waitFor({ state: "visible" }); await page.locator(".personal-task-card").getByText("Current Todo", {exact: true}).waitFor(); + const unclaimedCard = page.locator(".personal-task-card", {hasText: "Current Todo"}); + await unclaimedCard.getByText("未分配", {exact: true}).waitFor(); + await unclaimedCard.getByText("Current Todo", {exact: true}).click(); + const unclaimedOwner = page.getByRole("dialog", {name: "Todo 详情"}).locator("dl > div", {has: page.getByText("Owner", {exact: true})}); + await unclaimedOwner.getByText("未分配", {exact: true}).waitFor(); + await page.getByRole("button", {name: /关闭详情/}).click(); if ((await page.locator(".personal-channel-title").innerText()).includes("Current Todo")) throw new Error("Header repeated the task already shown in the workspace"); const progressColumn = page.locator(".personal-object-list", { hasText: "待执行 / 进行中" }); if ((await progressColumn.locator(".personal-task-card").count()) !== 5) throw new Error("Id-less long Todo was duplicated across compact and full projections"); From d2454029e4916540fc2a3c4e095bed7515483109 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 5 Oct 2026 02:35:53 +0800 Subject: [PATCH 3/4] fix(chat): reconcile confirmed Todo updates and report read failures Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../personal-workspace-contract.test.mjs | 4 ++-- .../personal-workspace/personal-workspace-page.tsx | 5 ++++- .../dashboard/src/views/dashboard-page.tsx | 6 +++++- examples/personal-workspace-browser/typed-actions.mjs | 11 ++++++++++- 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-contract.test.mjs b/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-contract.test.mjs index a2d89d2dd4..d7ef9066fc 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-contract.test.mjs +++ b/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-contract.test.mjs @@ -192,8 +192,8 @@ assert.match(tasks, /disabled=\{quickCompletingTodoIds\?\.has\(todo\.todoId\)\}/ assert.match(page, /callbacks\.onGoalActivationStateChange\?\.\(lifecycleChange\.goalId, lifecycleChange\.next\)/, "Goal lifecycle apply projects the requested state before the server responds"); assert.match(page, /model\.goals\.find\(\(goal\) => goal\.goalId === proposal\.goalId\)\?\.activationState/, "Goal lifecycle rollback captures the rendered state instead of assuming the operation inverse"); assert.match(page, /callbacks\.onGoalActivationStateChange\?\.\(lifecycleChange\.goalId, lifecycleChange\.previous\)/, "Rejected Goal lifecycle apply rolls back the optimistic projection"); -assert.match(page, /if \(applied\.actionKind === "goal\.lifecycle" \|\| applied\.actionKind === "gate\.resolve"\) \{\s*void reconcileStatus\(applied\.goalId \? \[applied\.goalId\] : undefined\)/, "Successful Goal lifecycle or decision apply reconciles the affected Goal without blocking the sidebar"); -assert.match(dashboard, /onReconcileStatus=\{\(options\) => loadFromUrl\([\s\S]*\{ background: true, invalidateGoalIds: options\?\.invalidateGoalIds, readScope: "missing" \}/, "Lifecycle reconciliation uses the non-fatal background status path"); +assert.match(page, /if \(applied\.actionKind === "goal\.lifecycle" \|\| applied\.actionKind === "gate\.resolve" \|\| applied\.actionKind === "todo\.update"\) \{\s*void reconcileStatus\(applied\.goalId \? \[applied\.goalId\] : undefined\)/, "Successful lifecycle, decision or Todo update reconciles the affected Goal without blocking the sidebar"); +assert.match(dashboard, /onReconcileStatus=\{\(options\) => loadFromUrl\([\s\S]*\{ rejectOnError: true, background: true, invalidateGoalIds: options\?\.invalidateGoalIds, readScope: "missing" \}/, "Action reconciliation preserves the background workspace and reports failed reads to its caller"); assert.match(dashboard, /statusRequestCanCommit\(statusRequestFenceRef\.current, request\)/, "A stale background response cannot overwrite a newer optimistic transition"); assert.match(sidebar, /Trash2/, "Stopped Goals expose a delete icon"); assert.match(sidebar, /onRequestGoalLifecycle\(goal, "delete"\)/, "Goal deletion stays behind the lifecycle request boundary"); diff --git a/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-page.tsx b/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-page.tsx index bf97621982..109052f7b1 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-page.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-page.tsx @@ -1448,6 +1448,9 @@ export function PersonalWorkspacePage({ } void reconcileStatus(proposal.goalId ? [proposal.goalId] : undefined); } + if (proposal.actionKind === "todo.update") { + void reconcileStatus(proposal.goalId ? [proposal.goalId] : undefined); + } return; } const result = await applyTypedAction(proposal.previewId); @@ -1487,7 +1490,7 @@ export function PersonalWorkspacePage({ if (applied.actionKind === "goal.lifecycle" && applied.lifecycleOperation === "delete" && applied.goalId) { callbacks.onGoalDeleted?.(applied.goalId); } - if (applied.actionKind === "goal.lifecycle" || applied.actionKind === "gate.resolve") { + if (applied.actionKind === "goal.lifecycle" || applied.actionKind === "gate.resolve" || applied.actionKind === "todo.update") { void reconcileStatus(applied.goalId ? [applied.goalId] : undefined); } } catch (error) { diff --git a/apps/presentation/dashboard/src/views/dashboard-page.tsx b/apps/presentation/dashboard/src/views/dashboard-page.tsx index 4d00cd8e9d..45a619e998 100644 --- a/apps/presentation/dashboard/src/views/dashboard-page.tsx +++ b/apps/presentation/dashboard/src/views/dashboard-page.tsx @@ -3301,6 +3301,7 @@ export function DashboardPage() { background?: boolean; readScope?: WorkspaceReadScope; invalidateGoalIds?: string[]; + rejectOnError?: boolean; resyncAttempt?: number; selectionRevision?: number; } = {}, @@ -3385,6 +3386,9 @@ export function DashboardPage() { } catch (error) { if (!statusRequestIsCurrent(statusRequestFenceRef.current, request)) return; if (!background) setLoadError(formatStatusError(error)); + // A confirmed action keeps its receipt while its caller reports that + // the independent status read needs retrying. Passive refreshes stay quiet. + if (options.rejectOnError) throw error; } finally { if (!background && statusRequestIsCurrent(statusRequestFenceRef.current, request)) { setIsLoading(false); @@ -3583,7 +3587,7 @@ export function DashboardPage() { onSelectWorkspace={selectWorkspace} onReconcileStatus={(options) => loadFromUrl( source.kind === "url" ? source.label : (statusUrl || defaultGlobalStatusUrl), - { background: true, invalidateGoalIds: options?.invalidateGoalIds, readScope: "missing" }, + { rejectOnError: true, background: true, invalidateGoalIds: options?.invalidateGoalIds, readScope: "missing" }, )} onRetryGoalArchive={retryGoalArchive} onRefresh={(readScope = "all") => loadFromUrl(source.kind === "url" ? source.label : (statusUrl || defaultGlobalStatusUrl), { readScope })} diff --git a/examples/personal-workspace-browser/typed-actions.mjs b/examples/personal-workspace-browser/typed-actions.mjs index 5c8df0e329..74211f8e3e 100644 --- a/examples/personal-workspace-browser/typed-actions.mjs +++ b/examples/personal-workspace-browser/typed-actions.mjs @@ -1791,7 +1791,16 @@ export const typedActionsScenario = { await page.getByText("确认执行").waitFor({state: "visible"}); const priorityEdit = api.actionPreviews.findLast(preview => preview.action_kind === "todo.update" && preview.normalized_parameters.priority === "P4"); if (!priorityEdit || priorityEdit.normalized_parameters.text !== undefined) throw new Error("Priority edit must be structured, without a text rewrite"); - await page.getByRole("button", {name: "关闭", exact: true}).click(); + const readsBeforePriorityApply = api.statusRequestCount; + const writesBeforePriorityApply = api.durableWriteCount; + await page.getByRole("button", {name: "确认并应用", exact: true}).click(); + await page.getByText("操作已完成,结果状态已通过读回验证。", {exact: true}).waitFor(); + await page.waitForTimeout(2_000); + if (api.statusRequestCount <= readsBeforePriorityApply) throw new Error("Verified Todo update left the workspace status unread"); + if (api.durableWriteCount !== writesBeforePriorityApply + 1) throw new Error("Todo status reconciliation repeated the confirmed write"); + // Applied receipts retain their Goal navigation; close the drawer itself + // so the next operation still begins from the existing task card. + await page.getByRole("button", {name: /关闭详情/}).click(); await taskRow.click(); taskManagement = page.locator("details.personal-task-management"); await taskManagement.locator("summary").click(); From ca1a04724c587e39794721c1901a501d1fd873af Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 5 Oct 2026 03:12:02 +0800 Subject: [PATCH 4/4] fix(workspace): keep Todo notes separate from evidence Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../dashboard/smoke/todo-ownership-smoke.mjs | 24 +++++++++++++++++++ .../personal-workspace/context-drawer.tsx | 4 ++++ .../src/features/personal-workspace/i18n.tsx | 2 ++ .../personal-workspace-contract.test.mjs | 2 +- .../personal-workspace-model.ts | 4 +++- .../personal-workspace/personal-workspace.css | 2 ++ 6 files changed, 36 insertions(+), 2 deletions(-) diff --git a/apps/presentation/dashboard/smoke/todo-ownership-smoke.mjs b/apps/presentation/dashboard/smoke/todo-ownership-smoke.mjs index db80af9736..270b0e2a30 100644 --- a/apps/presentation/dashboard/smoke/todo-ownership-smoke.mjs +++ b/apps/presentation/dashboard/smoke/todo-ownership-smoke.mjs @@ -3,6 +3,8 @@ import { createElement } from "react"; import { renderToStaticMarkup } from "react-dom/server"; import { GoalTasksView } from "../src/features/personal-workspace/goal-tasks-view.tsx"; import { CompletedTaskLane } from "../src/features/personal-workspace/completed-task-lane.tsx"; +import { todoItemSchema } from "../src/data/status.ts"; +import { workspaceAgentTodoFromItem } from "../src/features/personal-workspace/personal-workspace-model.ts"; import { ContextDrawer } from "../src/features/personal-workspace/context-drawer.tsx"; import { WorkspaceI18nProvider } from "../src/features/personal-workspace/i18n.tsx"; @@ -15,6 +17,28 @@ try { const render = (component, props) => renderToStaticMarkup(createElement( WorkspaceI18nProvider, null, createElement(component, props), )); + const notesLabel = locale === "en" ? "Notes" : "备注"; + const evidenceLabel = locale === "en" ? "Evidence" : "证据"; + const longNote = "Preserve separate notes. ".repeat(800) + "NOTE_END_SENTINEL"; + for (const fields of [{}, {note: null, evidence: null}, {note: "", evidence: ""}, + {note: longNote}, {evidence: "RECORDED_EVIDENCE"}, {note: longNote, evidence: "RECORDED_EVIDENCE"}]) { + for (const done of [false, true]) { + const source = todoItemSchema.parse({todo_id: "metadata-todo", text: "Read source metadata", done, + status: done ? "done" : "open", ...fields}); + const todo = workspaceAgentTodoFromItem(source, "fallback-id"); + assert.equal(todo.note, fields.note ?? null, "Notes remain an independent source field"); + assert.equal(todo.evidence, fields.evidence ?? null, "An absent evidence field cannot be inferred from notes"); + const drawer = render(ContextDrawer, {agents: [], callbacks: {}, onClose() {}, selection: { + kind: "todo", item: {...todo, goalId: "test-goal", goalTitle: "Test Goal"}, + }}); + assert.equal(drawer.includes(`${notesLabel}`), Boolean(fields.note)); + assert.equal(drawer.includes(`aria-label="${evidenceLabel}"`), Boolean(fields.evidence)); + assert.equal(drawer.includes("NOTE_END_SENTINEL"), Boolean(fields.note), "Both active and completed inspectors preserve the end of long notes"); + assert.equal(drawer.includes("RECORDED_EVIDENCE"), Boolean(fields.evidence)); + assert.equal(source.note, fields.note, "Reading never mutates canonical notes"); + assert.equal(source.evidence, fields.evidence, "Reading never mutates canonical evidence"); + } + } for (const claim of [undefined, null, "actual-worker"]) { const todo = {todoId: "test-todo", text: "Read claim identity", done: false, status: "open", claimedBy: claim}; const goal = {goalId: "test-goal", title: "Test Goal", agentId: "goal-default", agentLabel: "Goal default worker", agentTodos: [todo], state: "已安排", agentSentence: "", nextSentence: ""}; diff --git a/apps/presentation/dashboard/src/features/personal-workspace/context-drawer.tsx b/apps/presentation/dashboard/src/features/personal-workspace/context-drawer.tsx index 94cbec247d..8c5f8b77ab 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/context-drawer.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/context-drawer.tsx @@ -733,6 +733,10 @@ export function ContextDrawer({ agents, attentionHistory = [], onSelectAttention

{t("drawer.evidence")}

: null} + {selection.item.note ?
+ {t("drawer.notes")} + +
: null} {selection.item.done ?
{t("drawer.taskCompletedTitle")}{t("drawer.taskCompletedNote")}
: null} ) : null} diff --git a/apps/presentation/dashboard/src/features/personal-workspace/i18n.tsx b/apps/presentation/dashboard/src/features/personal-workspace/i18n.tsx index 3f7eaec907..d5a924c4a6 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/i18n.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/i18n.tsx @@ -219,6 +219,7 @@ const en = { "drawer.detailsAndActions": "Details & actions", "drawer.duration": "Duration 24h / 7d", "drawer.evidence": "Evidence", + "drawer.notes": "Notes", "drawer.executionHistory": "Execution history", "drawer.executionRecord": "Run record", "drawer.executionRecordAndResult": "Execution & result", @@ -1508,6 +1509,7 @@ const zhCN: Record = { "drawer.detailsAndActions": "详情与操作", "drawer.duration": "运行时长 24h / 7d", "drawer.evidence": "证据", + "drawer.notes": "备注", "drawer.executionHistory": "执行历史", "drawer.executionRecord": "运行记录", "drawer.executionRecordAndResult": "执行过程与结果", diff --git a/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-contract.test.mjs b/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-contract.test.mjs index d7ef9066fc..a95da6c407 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-contract.test.mjs +++ b/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-contract.test.mjs @@ -459,7 +459,7 @@ for (const capabilityId of [ const matches = capabilityLocalization.match(new RegExp(`${capabilityId}:`, "g")) ?? []; assert.equal(matches.length, 2, `${capabilityId} has English and Simplified Chinese metadata`); } -for (const fieldKey of ["allowed_domains", "coordinator_agent_id", "eligible_endpoints", "enabled", "executor_endpoint", "executor_model", "executor_reasoning_effort", "max_children", "profile", "profile_preset", "review_priority", "route_ref", "safe_fix", "selection_policy", "strict_receipt", "timezone"]) { +for (const fieldKey of ["allowed_domains", "coordinator_agent_id", "eligible_endpoints", "enabled", "executor_endpoint", "executor_model", "executor_reasoning_effort", "max_children", "profile", "profile_preset", "review_order", "route_ref", "safe_fix", "selection_policy", "strict_receipt", "timezone"]) { const matches = capabilityLocalization.match(new RegExp(`^\\s+${fieldKey}:`, "gm")) ?? []; assert.equal(matches.length, 2, `${fieldKey} has English and Simplified Chinese field copy`); } diff --git a/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-model.ts b/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-model.ts index a440cb7ed6..1dff20f6eb 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-model.ts +++ b/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-model.ts @@ -41,6 +41,7 @@ export type WorkspaceAgentTodo = { dependencies?: string[]; done: boolean; evidence?: string | null; + note?: string | null; nextTransition?: string | null; priority?: string | null; status?: string | null; @@ -69,7 +70,8 @@ export function workspaceAgentTodoFromItem(todo: Pick summary { min-height: 44px; align-content: center; cursor: pointer; font-size: 12px; font-weight: 600; } +.personal-detail-card > summary:focus-visible { outline: 2px solid var(--pw-blue-ink); outline-offset: 3px; } .personal-proposal-card { padding: 16px; border: 1px solid #c3d3f9; border-radius: 14px; background: #f5f8ff; } .personal-proposal-card > small { color: #5270bd; font-size: 11px; font-weight: 650; letter-spacing: 0.05em; } .personal-proposal-card h3 { margin: 8px 0; font-size: 15.5px; }