From 6978dbfb13fab55c9f04071855b8ddd2168e83de Mon Sep 17 00:00:00 2001 From: 07akioni <07akioni2@gmail.com> Date: Tue, 28 Jul 2026 19:16:39 +0800 Subject: [PATCH] fix: cr --- ...28-todo-plan-clears-on-next-turn.i18n.yaml | 4 +- ...026-07-28-todo-plan-clears-on-next-turn.md | 2 +- ...-07-28-todo-plan-clears-on-next-turn.zh.md | 2 +- apps/web/tests/todo-display.snapshot.ts | 28 ++++++++++++++ packages/client/runtime/README.i18n.yaml | 4 +- packages/client/runtime/README.md | 2 +- packages/client/runtime/README.zh.md | 2 +- .../src/client/sessions/conversation.ts | 5 ++- .../host/apiproxy/src/api/sessions.schema.ts | 2 +- packages/host/apiproxy/src/api/sessions.ts | 9 +++-- .../snapshots/todo-plan-cleared.expected.txt | 38 +++++++++++++++++++ packages/ui/tui/tests/tui.snapshot.ts | 29 ++++++++++++++ 12 files changed, 112 insertions(+), 15 deletions(-) create mode 100644 packages/ui/tui/tests/snapshots/todo-plan-cleared.expected.txt diff --git a/.agents/notes/implemented/feature/2026-07-28-todo-plan-clears-on-next-turn.i18n.yaml b/.agents/notes/implemented/feature/2026-07-28-todo-plan-clears-on-next-turn.i18n.yaml index 887a761ca8..9507c166d4 100644 --- a/.agents/notes/implemented/feature/2026-07-28-todo-plan-clears-on-next-turn.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-28-todo-plan-clears-on-next-turn.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-28-todo-plan-clears-on-next-turn.md -2026-07-28-todo-plan-clears-on-next-turn.md: 64c7207d334689817c3a392a0866158e4c1b927d -2026-07-28-todo-plan-clears-on-next-turn.zh.md: 9df4d952888df9e83f84deb3f2d587b5f9e2ff77 +2026-07-28-todo-plan-clears-on-next-turn.md: daf25fd92eae4c6429a07c6864a20a828fa9dbcd +2026-07-28-todo-plan-clears-on-next-turn.zh.md: 10b1c1c6b15d54b5cc30f4bbe36bb5e23befa3b6 diff --git a/.agents/notes/implemented/feature/2026-07-28-todo-plan-clears-on-next-turn.md b/.agents/notes/implemented/feature/2026-07-28-todo-plan-clears-on-next-turn.md index 64c7207d33..daf25fd92e 100644 --- a/.agents/notes/implemented/feature/2026-07-28-todo-plan-clears-on-next-turn.md +++ b/.agents/notes/implemented/feature/2026-07-28-todo-plan-clears-on-next-turn.md @@ -28,4 +28,4 @@ Host `backscanTodos` (and the fixture parallel) walks the full log from the tail ## Consequences -Interactive hosts and the history projection share one lifetime rule; reopening a session restores a plan only when no later turn has started. Partial supersession of the session-long standing-plan wording in [web todo display](2026-07-23-web-todo-display.md) and [`todo_write` tool](2026-06-29-todo-write-tool.md): event-sourcing, last-write-wins replacement, and the two render surfaces stay there; this note owns turn-boundary clearance. Coverage: client session specs for live clear + replay empty, host history projection after a post-write `turn/start`, and the existing web todo-display snapshot (fixture turn 65 remains the log tip, so the strip still pins). +Interactive hosts and the history projection share one lifetime rule; reopening a session restores a plan only when no later turn has started. Partial supersession of the session-long standing-plan wording in [web todo display](2026-07-23-web-todo-display.md) and [`todo_write` tool](2026-06-29-todo-write-tool.md): event-sourcing, last-write-wins replacement, and the two render surfaces stay there; this note owns turn-boundary clearance. Coverage: client session specs for live clear + replay empty, host history projection after a post-write `turn/start`, the standing-plan web todo-display snapshot (fixture turn 65 remains the log tip), plus assembled web/TUI snapshots that start the next turn and pin the strip gone. diff --git a/.agents/notes/implemented/feature/2026-07-28-todo-plan-clears-on-next-turn.zh.md b/.agents/notes/implemented/feature/2026-07-28-todo-plan-clears-on-next-turn.zh.md index 9df4d95288..10b1c1c6b1 100644 --- a/.agents/notes/implemented/feature/2026-07-28-todo-plan-clears-on-next-turn.zh.md +++ b/.agents/notes/implemented/feature/2026-07-28-todo-plan-clears-on-next-turn.zh.md @@ -28,4 +28,4 @@ Host 的 `backscanTodos`(以及 fixture 平行实现)从日志尾部向前 ## Consequences -交互式宿主与 history 投影共用同一生命周期规则;重开会话仅在其后没有更新的 turn 启动时恢复计划。对 [web todo 展示](2026-07-23-web-todo-display.md) 与 [`todo_write` 工具](2026-06-29-todo-write-tool.md) 中「会话级站立计划」表述构成部分取代:事件溯源、后写覆盖与两个渲染面仍归那两份 Note;本 Note 拥有 turn 边界清空。覆盖:客户端 session 对实时清空 + 回放为空的用例、host 在写入后的 `turn/start` 之后的 history 投影,以及既有的 web todo-display 快照(fixture 第 65 轮仍是日志末端,计划条仍被钉住)。 +交互式宿主与 history 投影共用同一生命周期规则;重开会话仅在其后没有更新的 turn 启动时恢复计划。对 [web todo 展示](2026-07-23-web-todo-display.md) 与 [`todo_write` 工具](2026-06-29-todo-write-tool.md) 中「会话级站立计划」表述构成部分取代:事件溯源、后写覆盖与两个渲染面仍归那两份 Note;本 Note 拥有 turn 边界清空。覆盖:客户端 session 对实时清空 + 回放为空的用例、host 在写入后的 `turn/start` 之后的 history 投影、站立计划的 web todo-display 快照(fixture 第 65 轮仍是日志末端),以及开启下一 turn 并钉住计划条消失的 assembled web/TUI 快照。 diff --git a/apps/web/tests/todo-display.snapshot.ts b/apps/web/tests/todo-display.snapshot.ts index 4083e5b1d2..9b86f2ccea 100644 --- a/apps/web/tests/todo-display.snapshot.ts +++ b/apps/web/tests/todo-display.snapshot.ts @@ -189,3 +189,31 @@ it('collapses the plan strip to the count summary and restores it', async () => fireEvent.click(header) expect(panel.querySelectorAll('li')).toHaveLength(3) }) + +it('hides the plan strip when the next turn starts', async () => { + boot() + await openFixtureSession() + expect(document.querySelector('[data-testid="todo-panel"]')).not.toBeNull() + + const composer = await screen.findByPlaceholderText('Message the agent', {}, { timeout: 10_000 }) + fireEvent.change(composer, { target: { value: '下一轮清空计划' } }) + fireEvent.keyDown(composer, { key: 'Enter' }) + + await screen.findByText('下一轮清空计划', { exact: true }, { timeout: 10_000 }) + await waitFor(() => { + expect(document.querySelector('[data-testid="todo-panel"]')).toBeNull() + }, { timeout: 10_000 }) + + expect({ + promptVisible: screen.getByText('下一轮清空计划', { exact: true }).textContent, + panelGone: document.querySelector('[data-testid="todo-panel"]') === null, + // Historical todo_write row stays in the flow; only the dock strip clears. + rowStillPresent: document.querySelector('[data-sample="todo-row"]') !== null, + }).toMatchInlineSnapshot(` + { + "panelGone": true, + "promptVisible": "下一轮清空计划", + "rowStillPresent": true, + } + `) +}) diff --git a/packages/client/runtime/README.i18n.yaml b/packages/client/runtime/README.i18n.yaml index 32d4fdf417..9a146335be 100644 --- a/packages/client/runtime/README.i18n.yaml +++ b/packages/client/runtime/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/runtime/README.md -README.md: 16c1124ec812b9f030ce8266a16cdb8f5db0e6cc -README.zh.md: a3d2a2dfdd1662afee65ec45e26b1ef1029f44b5 +README.md: acbe04a9710250618623d47c175baccc9c312a2c +README.zh.md: 002f0678bc8b339fba66f543eab1a06a8ee123f8 diff --git a/packages/client/runtime/README.md b/packages/client/runtime/README.md index 16c1124ec8..acbe04a971 100644 --- a/packages/client/runtime/README.md +++ b/packages/client/runtime/README.md @@ -2,7 +2,7 @@ English | [中文](README.zh.md) -Client cordis boot and React-free object services: SlotsService wraps SlotCore and supplies renderer data sources; SessionsService owns Session objects, list/scope/history state; WorkspacesService depends on SessionsService and owns Workspace objects, list/actions, default-target derivation, and the New Session blank-reuse entry (`connectWorkspace`). The runtime fans the shared Host stream into both managers. Client sessions are always Host-born (Session+Agent+cwd in one `session.create`); the client holds no pre-entity session state — a session's Agent scope (the client mirror of host dsh-scope, keyed by the shared agent/session id) is born when its row enters the list mirror and dies with the prune. Contract: api-contracts v3 §4. `ConversationSnapshot` carries `todos` — the session's current todo projection: taken from the tail history page's full-log value (host-computed, independent of the page window), preserved across an older-page prepend, and overwritten by each live `todo/write` (last write wins). A tail response that omits the field means the log holds no `todo/write`, so the list resets to empty — a plan the log never kept (a write lost to a host crash) disappears on the next open or resync. +Client cordis boot and React-free object services: SlotsService wraps SlotCore and supplies renderer data sources; SessionsService owns Session objects, list/scope/history state; WorkspacesService depends on SessionsService and owns Workspace objects, list/actions, default-target derivation, and the New Session blank-reuse entry (`connectWorkspace`). The runtime fans the shared Host stream into both managers. Client sessions are always Host-born (Session+Agent+cwd in one `session.create`); the client holds no pre-entity session state — a session's Agent scope (the client mirror of host dsh-scope, keyed by the shared agent/session id) is born when its row enters the list mirror and dies with the prune. Contract: api-contracts v3 §4. `ConversationSnapshot` carries `todos` — the standing plan (latest `todo/write` with no later `turn/start`): taken from the tail history page's full-log projection (host-computed, independent of the page window), preserved across an older-page prepend when the window never re-determines it, overwritten by each live `todo/write`, and cleared on each live `turn/start`. A tail response that omits the field means no plan stands (never wrote one, or a later turn retired it), so the list resets to empty — including a plan the log never kept (a write lost to a host crash) on the next open or resync. ## Workspace and Session lists diff --git a/packages/client/runtime/README.zh.md b/packages/client/runtime/README.zh.md index a3d2a2dfdd..002f0678bc 100644 --- a/packages/client/runtime/README.zh.md +++ b/packages/client/runtime/README.zh.md @@ -2,7 +2,7 @@ [English](README.md) | 中文 -客户端 cordis 启动与不依赖 React 的对象服务:SlotsService 包装 SlotCore 并提供 renderer 数据源;SessionsService 拥有 Session 对象、列表/scope/history 状态;WorkspacesService 依赖 SessionsService,拥有 Workspace 对象、列表/操作、默认目标派生,以及 New Session 空会话复用入口(`connectWorkspace`)。运行时把共享 Host 流分发给两个 manager。客户端 Session 一律由 Host 出生(一次 `session.create` 同瞬产出 Session+Agent+cwd);客户端不持有任何实体化之前的会话状态——Agent scope(host dsh-scope 的客户端镜像,以 agent/session 共用 id 为键)在会话行进入列表镜像时出生,随 prune 死亡。契约:api-contracts v3 §4。`ConversationSnapshot` 携带 `todos`——会话当前的 todo 投影:取自尾页 history 携带的全量 log 值(host 计算,独立于分页窗口),跨往前翻页保留,并被每次实时 `todo/write` 覆盖(后写胜出)。尾页响应省略该字段即表示 log 中没有任何 `todo/write`,因此列表复位为空——log 从未留下的计划(写入因 host 崩溃丢失)会在下一次打开或 resync 时消失。 +客户端 cordis 启动与不依赖 React 的对象服务:SlotsService 包装 SlotCore 并提供 renderer 数据源;SessionsService 拥有 Session 对象、列表/scope/history 状态;WorkspacesService 依赖 SessionsService,拥有 Workspace 对象、列表/操作、默认目标派生,以及 New Session 空会话复用入口(`connectWorkspace`)。运行时把共享 Host 流分发给两个 manager。客户端 Session 一律由 Host 出生(一次 `session.create` 同瞬产出 Session+Agent+cwd);客户端不持有任何实体化之前的会话状态——Agent scope(host dsh-scope 的客户端镜像,以 agent/session 共用 id 为键)在会话行进入列表镜像时出生,随 prune 死亡。契约:api-contracts v3 §4。`ConversationSnapshot` 携带 `todos`——站立计划(其后没有更晚 `turn/start` 的最近一次 `todo/write`):取自尾页 history 携带的全量 log 投影(host 计算,独立于分页窗口);窗口本身未再判定计划时跨往前翻页保留;被每次实时 `todo/write` 覆盖,并在每次实时 `turn/start` 清空。尾页响应省略该字段即表示当前没有站立计划(从未写入,或更晚的 turn 已退役),因此列表复位为空——也包括 log 从未留下的计划(写入因 host 崩溃丢失)在下一次打开或 resync 时消失。 ## Workspace 与 Session 列表 diff --git a/packages/client/runtime/src/client/sessions/conversation.ts b/packages/client/runtime/src/client/sessions/conversation.ts index 8cd57c4eb3..b3b434af49 100644 --- a/packages/client/runtime/src/client/sessions/conversation.ts +++ b/packages/client/runtime/src/client/sessions/conversation.ts @@ -243,7 +243,8 @@ export interface ConversationSnapshot { */ blank: boolean lastAgentError: string | null - /** Current whole-list `todo/write` projection — the tail page's full-log value, then each live - * write (last write wins); empty = the log holds no plan. */ + /** Standing plan strip: the latest `todo/write` with no later `turn/start`. Seeded from the + * tail history page, overwritten by each live write, cleared on each live `turn/start`; + * empty when no plan stands. */ todos: readonly TodoItem[] } diff --git a/packages/host/apiproxy/src/api/sessions.schema.ts b/packages/host/apiproxy/src/api/sessions.schema.ts index 02efe769af..81d32d37ca 100644 --- a/packages/host/apiproxy/src/api/sessions.schema.ts +++ b/packages/host/apiproxy/src/api/sessions.schema.ts @@ -139,7 +139,7 @@ export const historyEntrySchema = z.object({ view: toolEventViewSchema.optional(), }) satisfies z.ZodType> -/** One todo item of the tail page's session-level projection (the todo/write payload shape). */ +/** One todo item of the tail page's standing-plan projection (the todo/write payload shape). */ export const todoItemSchema = z.object({ content: z.string(), status: z.union([z.literal('pending'), z.literal('in_progress'), z.literal('completed')]), diff --git a/packages/host/apiproxy/src/api/sessions.ts b/packages/host/apiproxy/src/api/sessions.ts index 88308c829b..c2367b5110 100644 --- a/packages/host/apiproxy/src/api/sessions.ts +++ b/packages/host/apiproxy/src/api/sessions.ts @@ -149,10 +149,11 @@ export interface SessionsApi { * Each entry pairs the raw SessionEvent with the host-computed view (tool events whose * presenter produced one, evaluated against the registry at pagination time); the client * rebuilds the surface from the events with the shared fold. - * The tail page (beforeSeq absent) also carries `todos` — the session's current todo - * projection (latest `todo/write` over the FULL log, independent of the page window) — - * so a paged client restores the plan without walking history; absent when the session - * never wrote one. Older pages omit it (the projection is session-level, not per-page). + * The tail page (beforeSeq absent) also carries `todos` — the standing plan over the + * FULL log (latest `todo/write` with no later `turn/start`), independent of the page + * window — so a paged client restores the plan without walking history. Absent when no + * plan stands (never wrote one, or a later `turn/start` retired it). Older pages omit + * it (the projection is session-level, not per-page). */ history(request: RpcRequest<{ sessionId: SessionId; beforeSeq?: number; maxMessages?: number }>): Promise> diff --git a/packages/ui/tui/tests/snapshots/todo-plan-cleared.expected.txt b/packages/ui/tui/tests/snapshots/todo-plan-cleared.expected.txt new file mode 100644 index 0000000000..0f4c9ea0ab --- /dev/null +++ b/packages/ui/tui/tests/snapshots/todo-plan-cleared.expected.txt @@ -0,0 +1,38 @@ +terminal 96x36 buffer=normal length=36 base=0 viewport=0 +lifecycle started=1 stopped=0 progress=inactive +title "DSH snapshot" +cursor hidden column=7 viewportRow=15 bufferRow=15 +viewport +0| " DEEPSEEK HARNESS" + style 1-8 fg=bright-blue bold + style 10-16 bold +1| " Snapshot agent ready." + style 1-21 fg=bright-black +2| " main-session" + style 1-12 dim +3| +4| "Assistant " + style 0-8 fg=bright-magenta bold underline +5| "Tracking the steps. " +6| "Model wait 0.0s · Completed 2026-07-21 14:45:00 " + style 0-46 dim +7| +8| "You " + style 0-2 fg=bright-blue bold underline +9| "Plan the work. " +10| +11| "You " + style 0-2 fg=bright-blue bold underline +12| "Next question. " +13| +14| "/workspace/project (tui-staging) deepseek-v4-flash ↑0 ↓0 0% context" + style 0-17 fg=bright-blue bold + style 18-31 fg=bright-black + style 34-50 fg=bright-black + style 53-57 fg=bright-black + style 60-69 fg=bright-black +15| " dsh > " + style 1-3 fg=bright-blue bold + style 5-6 fg=bright-black + style 7-7 inverse +16-35| diff --git a/packages/ui/tui/tests/tui.snapshot.ts b/packages/ui/tui/tests/tui.snapshot.ts index 8192c28044..f96cafafe8 100644 --- a/packages/ui/tui/tests/tui.snapshot.ts +++ b/packages/ui/tui/tests/tui.snapshot.ts @@ -57,6 +57,7 @@ const CHECKPOINTS = [ 'resume-sessions', 'status-diagnostics', 'status-diagnostics-narrow', + 'todo-plan-cleared', ] as const // Real-loop scenarios own their assertions in separate snapshot suites but @@ -299,6 +300,34 @@ describe('TUI terminal-state snapshots', () => { await disposeSnapshot(harness) }) + it('clears the plan strip when the next turn starts', async () => { + // Freeze Completed-at formatting: the first turn ends before the next starts, + // so the assistant timing line still appears without a Plan strip below it. + const nowSpy = vi.spyOn(Date, 'now').mockReturnValue(new Date(2026, 6, 21, 14, 45, 0).getTime()) + const harness = await setupSnapshot({ + beforeMount(session) { + appendUser(session, 'Plan the work.') + appendAssistant(session, [{ type: 'text', text: 'Tracking the steps.' }]) + session.append('todo/write', { + todos: [ + { content: 'read code', status: 'completed' }, + { content: 'write tests', status: 'in_progress' }, + ], + }) + session.append('step/end', { turn: 1, step: 1 }) + session.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) + session.append('turn/start', { + turn: 2, + trigger: { kind: 'message', source: { kind: 'user' } }, + }) + appendUser(session, 'Next question.') + }, + }) + await checkpoint('todo-plan-cleared', harness.terminal) + nowSpy.mockRestore() + await disposeSnapshot(harness) + }) + it('pins failed-stream retraction, scheduled retry, and eventual success', async () => { const harness = await setupSnapshot() await renderAfter(harness, () => {