Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/presentation/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"license": "Apache-2.0",
"type": "module",
"scripts": {
"smoke:todo-ownership": "vite build --ssr smoke/todo-ownership-smoke.mjs --outDir node_modules/.cache/todo-ownership --emptyOutDir && node node_modules/.cache/todo-ownership/todo-ownership-smoke.js",
"smoke:workspace-progressive-loader": "node smoke/workspace-progressive-status-smoke.mjs",
"smoke:workspace-progressive": "node ../../../examples/workspace-progressive-loading-browser-smoke.mjs",
"build": "tsc --noEmit && vite build && npm run build:chat",
Expand Down
64 changes: 64 additions & 0 deletions apps/presentation/dashboard/smoke/todo-ownership-smoke.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import assert from "node:assert/strict";
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";

// A Goal's default executor is not evidence that a Todo has been claimed.
// Exercise the real consumers, including retained history and stale labels.
const originalWindow = globalThis.window;
try {
for (const [locale, unassigned, owner] of [["en", "Unassigned", "Owner"], ["zh-CN", "未分配", "Owner"]]) {
globalThis.window = { localStorage: { getItem: () => locale } };
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(`<summary>${notesLabel}</summary>`), 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: ""};
const expected = claim ?? unassigned;
const active = render(GoalTasksView, {goal, items: [], userTodos: [], onSelect() {}});
assert.ok(active.includes(expected), "Active cards must identify only the recorded claim");
assert.ok(!active.includes("Goal default worker"), "Goal defaults cannot substitute for a task claim");
const completed = render(CompletedTaskLane, {goal, agentId: "all", seed: [{...todo, done: true, status: "done"}], enabled: false, onSelect() {}});
assert.ok(completed.includes(expected), "Completed history preserves the same claim semantics");
assert.ok(!completed.includes("Goal default worker"));
const drawer = render(ContextDrawer, {agents: [], callbacks: {}, onClose() {}, selection: {
kind: "todo", item: {...todo, goalId: goal.goalId, goalTitle: goal.title,
ownerLabel: claim ? claim : "Goal default worker"},
}});
assert.ok(drawer.includes(`<dt>${owner}</dt><dd>${expected}</dd>`), "A stale display label cannot invent an absent claim");
assert.equal(todo.claimedBy, claim, "Rendering never mutates the source claim");
}
}
} finally {
if (originalWindow === undefined) delete globalThis.window;
else globalThis.window = originalWindow;
}
console.log("todo-ownership-smoke: ok");
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,8 @@ export function CompletedTaskLane({ goal, agentId, seed, enabled, refreshRevisio
{indices.map((index) => {
const todo = rows[index]!;
return <div className="personal-task-card personal-completed-row" key={todo.todoId} style={{ top: index * rowHeight, height: rowHeight }}>
<button type="button" onFocus={() => setFocused(index)} onBlur={() => setFocused(null)} onClick={() => onSelect({ kind: "todo", item: { ...todo, goalId: goal.goalId, goalTitle: goal.title, ownerLabel: todo.claimedBy ?? goal.agentLabel ?? goal.agentId } })}>
<span className="is-done">✓</span><strong>{compactWorkspaceText(todo.text, 112)}</strong><small>{todo.claimedBy ?? goal.agentLabel ?? goal.agentId}</small>
<button type="button" onFocus={() => setFocused(index)} onBlur={() => setFocused(null)} onClick={() => onSelect({ kind: "todo", item: { ...todo, goalId: goal.goalId, goalTitle: goal.title, ownerLabel: todo.claimedBy } })}>
<span className="is-done">✓</span><strong>{compactWorkspaceText(todo.text, 112)}</strong><small>{todo.claimedBy ?? t("drawer.notAssigned")}</small>
</button>
</div>;
})}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {

import type {
PersonalWorkspaceCallbacks,
WorkspaceActionPreviewRequest,
WorkspaceAgentOption,
WorkspaceAttention,
WorkspaceDrawerSelection,
Expand Down Expand Up @@ -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<HTMLButtonElement>(null);
const drawerRef = useRef<HTMLDivElement>(null);
const returnFocusRef = useRef<HTMLElement | null>(null);
const titleRef = useRef<HTMLHeadingElement>(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");
Expand Down Expand Up @@ -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)}`,
Expand All @@ -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)}`,
Expand All @@ -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)}`,
Expand Down Expand Up @@ -603,6 +621,7 @@ export function ContextDrawer({ agents, attentionHistory = [], onSelectAttention
</header>

<div className="personal-drawer-body">
{previewFailure?.identity === selectionIdentity ? <p className="personal-proposal-explainer" role="alert">{previewFailure.message}</p> : null}
{selection.kind === "attention" ? (
<>
<AttentionDetailCard item={selection.item} onSelect={onSelectAttention} successor={attentionSuccessor(selection.item, attentionHistory)} />
Expand Down Expand Up @@ -641,7 +660,7 @@ export function ContextDrawer({ agents, attentionHistory = [], onSelectAttention
<h4>{t("drawer.taskInfo")}</h4>
<dl>
<div><dt>Goal</dt><dd>{selection.item.goalTitle}</dd></div>
<div><dt>{t("common.owner")}</dt><dd>{selection.item.ownerLabel ?? selection.item.claimedBy ?? t("drawer.notAssigned")}</dd></div>
<div><dt>{t("common.owner")}</dt><dd>{selection.item.claimedBy ? selection.item.ownerLabel ?? selection.item.claimedBy : t("drawer.notAssigned")}</dd></div>
<div><dt>{t("common.status")}</dt><dd>{selection.item.done ? t("drawer.taskStatusCompleted") : selection.item.status === "deferred" ? t("drawer.taskStatusDeferred") : selection.item.status === "blocked" ? t("drawer.taskStatusBlocked") : t("drawer.taskStatusOpen")}</dd></div>
<div><dt>{t("drawer.priority")}</dt><dd>{selection.item.priority ?? t("drawer.notSet")}</dd></div>
{selection.item.completedAt ? <div><dt>{t("drawer.completedAt")}</dt><dd><time dateTime={selection.item.completedAt}>{selection.item.completedAt}</time></dd></div> : null}
Expand All @@ -666,7 +685,7 @@ export function ContextDrawer({ agents, attentionHistory = [], onSelectAttention
<select aria-label={t("drawer.reassign")} onChange={(event) => setTodoAgentId(event.target.value)} value={todoAgentId}>
{agents.filter((agent) => agent.available).map((agent) => <option key={agent.agentId} value={agent.agentId}>{agent.label}</option>)}
</select>
<button className="personal-secondary-action" onClick={() => void callbacks.onPreviewAction?.({
<button className="personal-secondary-action" onClick={() => void previewAction({
actionKind: "todo.update",
context: { goal_id: selection.item.goalId, kind: "todo", todo_id: selection.item.todoId },
idempotencyKey: `workspace-todo-${selection.item.todoId}-reassign-${todoAgentId}-${Date.now().toString(36)}`,
Expand All @@ -680,7 +699,7 @@ export function ContextDrawer({ agents, attentionHistory = [], onSelectAttention
{["P0", "P1", "P2", "P3", "P4"].map((priority) => <option key={priority} value={priority}>{priority}</option>)}
<option value="clear">{t("drawer.taskPriorityClear")}</option>
</select>
<button className="personal-secondary-action" disabled={!todoPriority} onClick={() => void callbacks.onPreviewAction?.({
<button className="personal-secondary-action" disabled={!todoPriority} onClick={() => void previewAction({
actionKind: "todo.update",
context: {goal_id: selection.item.goalId, kind: "todo", todo_id: selection.item.todoId},
idempotencyKey: `workspace-todo-${selection.item.todoId}-priority-${todoPriority}-${Date.now().toString(36)}`,
Expand Down Expand Up @@ -714,6 +733,10 @@ export function ContextDrawer({ agents, attentionHistory = [], onSelectAttention
<h4>{t("drawer.evidence")}</h4>
<MarkdownText text={selection.item.evidence} />
</section> : null}
{selection.item.note ? <details className="personal-detail-card" aria-label={t("drawer.notes")}>
<summary>{t("drawer.notes")}</summary>
<MarkdownText text={selection.item.note} />
</details> : null}
{selection.item.done ? <div className="personal-task-completed-note"><Check size={16} /><span><strong>{t("drawer.taskCompletedTitle")}</strong><small>{t("drawer.taskCompletedNote")}</small></span></div> : null}
</>
) : null}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ export function GoalTasksView({
</TaskLane>
<TaskLane listView={listView} count={openAgentTodos.length} label={t("tasks.pendingAndRunning")} tone="progress">
{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 (
<div className={`personal-task-card${execution ? " has-session" : ""}${selectedTodoId === todo.todoId ? " is-selected" : ""}`} key={todo.todoId} ref={selectedTodoId === todo.todoId ? (element) => { selectedTodoRef.current = element; } : undefined}>
Expand All @@ -251,7 +251,7 @@ export function GoalTasksView({
{todo.status === "blocked" ? <span className="personal-priority-badge is-blocked">{t("tasks.blocked")}</span> : null}
{execution ? <span className="personal-task-session-status">{execution.status === "running" || execution.status === "queued" ? t("runs.running") : execution.status === "failed" ? t("tasks.sessionError") : t("common.waiting")}</span> : null}
{todo.status === "deferred" ? <span className="personal-task-session-status">{t("drawer.taskStatusDeferred")}</span> : !execution ? <span className="personal-task-session-status">{t("tasks.waiting")}</span> : null}
{todo.claimedBy ?? goal.agentLabel ?? goal.agentId}
{enriched.ownerLabel}
</small>
</button>
<div className="personal-task-card-actions">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -1508,6 +1509,7 @@ const zhCN: Record<WorkspaceMessageKey, string> = {
"drawer.detailsAndActions": "详情与操作",
"drawer.duration": "运行时长 24h / 7d",
"drawer.evidence": "证据",
"drawer.notes": "备注",
"drawer.executionHistory": "执行历史",
"drawer.executionRecord": "运行记录",
"drawer.executionRecordAndResult": "执行过程与结果",
Expand Down
Loading
Loading