From 07aabe6f6c62d01d963a7ecb5ba6aaf6742d64e1 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Fri, 16 Oct 2026 21:37:47 +0800 Subject: [PATCH] fix(dashboard): keep legacy completed tasks read-only Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> (cherry picked from commit f641857ec10f78402cb270b603f65ab0ef4d7fb9) --- .../dashboard/smoke/todo-ownership-smoke.mjs | 19 +++++++++++++++++++ .../personal-workspace/context-drawer.tsx | 12 ++++++------ 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/apps/presentation/dashboard/smoke/todo-ownership-smoke.mjs b/apps/presentation/dashboard/smoke/todo-ownership-smoke.mjs index 21951e39d7..c05641e689 100644 --- a/apps/presentation/dashboard/smoke/todo-ownership-smoke.mjs +++ b/apps/presentation/dashboard/smoke/todo-ownership-smoke.mjs @@ -44,6 +44,25 @@ try { } const notesLabel = locale === "en" ? "Notes" : "备注"; const evidenceLabel = locale === "en" ? "Evidence" : "证据"; + // Canonical deferred records may retain done=true; legacy completed records + // may omit status. The real adapter and inspector must distinguish them. + for (const [fields, completed, resumable] of [ + [{done: true}, true, false], + [{done: true, status: "done"}, true, false], + [{done: true, status: "deferred"}, false, true], + [{done: false, status: "blocked"}, false, true], + ]) { + const source = todoItemSchema.parse({todo_id: "retained-task", text: "Read retained work", ...fields}); + const todo = workspaceAgentTodoFromItem(source, "fallback-id"); + const drawer = render(ContextDrawer, {agents: [], callbacks: {}, onClose() {}, selection: { + kind: "todo", item: {...todo, goalId: "test-goal", goalTitle: "Test Goal"}, + }}); + assert.equal(drawer.includes("personal-task-completed-note"), completed, "Completed history remains read-only"); + assert.equal(drawer.includes("personal-task-inspector-actions"), !completed, "Completed records never expose new mutations"); + assert.equal(drawer.includes(locale === "en" ? "Resume task" : "恢复任务"), resumable, + "Only explicitly paused work offers resume"); + assert.equal(source.done, fields.done, "Presentation never rewrites the recorded lifecycle"); + } for (const invalid of [null, "dependency_cycle"]) { const source = todoItemSchema.parse({todo_id: "todo_cycle", text: "Review dependent work", done: false, status: "open", resume_when: "todo_done:todo_peer", resume_ready: false, 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 4ced7c7da0..bc4fbad8d3 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/context-drawer.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/context-drawer.tsx @@ -645,8 +645,8 @@ export function ContextDrawer({ agents, attentionHistory = [], onSelectAttention <>
- - {selection.item.status === "done" ? t("drawer.taskStatusCompleted") : selection.item.retryReadback ? t("drawer.resumePending") : selection.item.status === "deferred" ? t("drawer.taskStatusDeferred") : selection.item.status === "blocked" ? t("drawer.taskStatusBlocked") : t("drawer.taskStatusOpen")} + + {todoCompleted ? t("drawer.taskStatusCompleted") : selection.item.retryReadback ? t("drawer.resumePending") : selection.item.status === "deferred" ? t("drawer.taskStatusDeferred") : selection.item.status === "blocked" ? t("drawer.taskStatusBlocked") : t("drawer.taskStatusOpen")} {selection.item.priority ? {selection.item.priority} : null} {selection.item.taskClass === "advancement_task" ? t("drawer.taskAdvancement") : selection.item.taskClass ?? t("drawer.taskOrdinary")} @@ -662,7 +662,7 @@ export function ContextDrawer({ agents, attentionHistory = [], onSelectAttention
Goal
{selection.item.goalTitle}
{t("common.owner")}
{selection.item.claimedBy ? selection.item.ownerLabel ?? selection.item.claimedBy : t("drawer.notAssigned")}
-
{t("common.status")}
{selection.item.status === "done" ? t("drawer.taskStatusCompleted") : selection.item.retryReadback ? t("drawer.resumePending") : selection.item.status === "deferred" ? t("drawer.taskStatusDeferred") : selection.item.status === "blocked" ? t("drawer.taskStatusBlocked") : t("drawer.taskStatusOpen")}
+
{t("common.status")}
{todoCompleted ? t("drawer.taskStatusCompleted") : selection.item.retryReadback ? t("drawer.resumePending") : 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} {selection.item.dependencies ?
{t("drawer.dependencies")}
{selection.item.dependencies.join(" · ") || t("common.none")}
: null} @@ -679,10 +679,10 @@ export function ContextDrawer({ agents, attentionHistory = [], onSelectAttention
{t("drawer.validationDigest")}
{selection.item.validationDigest}
{selection.item.validationRevisionActor ?
{t("drawer.validationRevisionActor")}
{selection.item.validationRevisionActor}
: null} : null} -
{t("drawer.nextTransition")}
{selection.item.nextTransition ?? (selection.item.status === "done" ? t("drawer.taskNextCompleted") : selection.item.resumeInvalidReason === "dependency_cycle" ? t("drawer.taskNextCycle") : selection.item.retryReadback ? t("drawer.taskNextDeferred") : selection.item.resumeReady ? t("drawer.taskNextResumeReady") : selection.item.status === "deferred" ? t("drawer.taskNextDeferred") : selection.item.status === "blocked" ? t("drawer.taskNextBlocked") : t("drawer.taskNextOpen"))}
+
{t("drawer.nextTransition")}
{selection.item.nextTransition ?? (todoCompleted ? t("drawer.taskNextCompleted") : selection.item.resumeInvalidReason === "dependency_cycle" ? t("drawer.taskNextCycle") : selection.item.retryReadback ? t("drawer.taskNextDeferred") : selection.item.resumeReady ? t("drawer.taskNextResumeReady") : selection.item.status === "deferred" ? t("drawer.taskNextDeferred") : selection.item.status === "blocked" ? t("drawer.taskNextBlocked") : t("drawer.taskNextOpen"))}
- {!readOnly && selection.item.status !== "done" ?
+ {!readOnly && !todoCompleted ?
{t("drawer.taskManage")}
@@ -750,7 +750,7 @@ export function ContextDrawer({ agents, attentionHistory = [], onSelectAttention {t("drawer.notes")}
: null} - {selection.item.status === "done" ?
{t("drawer.taskCompletedTitle")}{t("drawer.taskCompletedNote")}
: null} + {todoCompleted ?
{t("drawer.taskCompletedTitle")}{t("drawer.taskCompletedNote")}
: null} ) : null}