adapt todo display to the slash/input/session architecture

- TodoPanel mounts through a 'conversation.input.dock' list entry
  (todoDockEntry, QueueDock posture, order -1 above the queue rows) instead
  of a ConversationRoot hardcode; the inner component is unchanged and takes
  useSession from the dock entry's standard kit.
- The verify-todo-display.mjs chromium probe is replaced by an assembled
  keyless snapshot (apps/web/tests/todo-display.snapshot.ts, the
  code-mode-fixture idiom) pinning the TodoRow summary/state, the dock
  panel content, and the collapse round-trip over built bundles.
- Fake snapshots across specs gain the todos field; bilingual note/READMEs
  updated for the dock mount and the snapshot.
This commit is contained in:
imccyu
2026-07-27 10:09:42 +08:00
parent 9d63d75a6f
commit dadb92302f
14 changed files with 242 additions and 128 deletions
@@ -14,6 +14,7 @@ import { InputBar } from './skeleton/InputBar.tsx'
import { ChatView } from './chat/ChatView.tsx'
import { bashToolviewSample } from './toolviews/bash-sample.tsx'
import { todoToolview } from './toolviews/todo-row.tsx'
import { todoDockEntry } from './skeleton/TodoPanel.tsx'
import { queueDockEntry } from './queue/QueueDock.tsx'
import { ConversationRoot } from './skeleton/ConversationRoot.tsx'
import { ConversationSession } from './skeleton/ConversationSession.tsx'
@@ -186,6 +187,9 @@ export function apply(ctx: Context): void {
// The todo_write row rides the same seam (a product registration, not a sample).
ctx.plugin(todoToolview)
// The plan strip rides the input dock above the queue rows (same posture).
ctx.plugin(todoDockEntry)
// The read-only queue dock entry (T9 file territory) rides the same
// registration seam into the input dock declared above.
ctx.plugin(queueDockEntry)
@@ -1,10 +1,13 @@
// TodoPanel: persistent plan strip pinned above the composer (the web
// counterpart of the TUI plan panel; ACP maps the same event to its native
// plan). Renders the latest todo/write whole-list snapshot off the session
// snapshot — no data of its own, hidden while the list is empty. Zero
// framework imports: useSession arrives via props from ConversationRoot.
// TodoPanel: persistent plan strip above the composer (the web counterpart
// of the TUI plan panel; ACP maps the same event to its native plan). Renders
// the latest todo/write whole-list snapshot off the session snapshot — no
// data of its own, hidden while the list is empty. Mounted through the
// 'conversation.input.dock' slot (QueueDock posture): the standard session
// kit supplies useSession, so the inner component stays framework-free.
import { useState } from 'react'
import type { Context } from 'cordis'
import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
import type { TodoItem } from '@deepseek-ai/dsh-client-runtime/client'
import type { UseSession } from '@deepseek-ai/dsh-client-ui-slots'
import { IconChevronDownOutline14, IconChevronUpOutline14 } from '@deepseek-ai/dsh-client-ui-primitives'
@@ -57,3 +60,29 @@ export function TodoPanel({ useSession }: TodoPanelProps) {
</section>
)
}
/** Full props of a dock entry: InputZone owner share + session standard kit + global seat. */
export type TodoDockProps = PropsRuntime<'conversation.input.dock'>
/** Dock adapter: the standard kit's useSession feeds the strip. */
export function TodoDock({ useSession }: TodoDockProps) {
return <TodoPanel useSession={useSession} />
}
/**
* The plan strip as a plain registrant plugin (QueueDock posture).
* `inject: ['conversation']` is the ordering seam: the conversation service
* mounts after ui-conversation's slot registrations, so the
* 'conversation.input.dock' declaration is on the ledger by then.
*/
export const todoDockEntry = {
name: 'conversation-todo-dock',
inject: ['slots', 'conversation'],
/**
* Register the plan strip into the input dock (list entry, above the queue rows).
* @param ctx - registrant context (disposal rides ctx.effect inside slots.register).
*/
apply(ctx: Context): void {
ctx.slots.register({ name: 'conversation.input.dock', id: 'todo', order: -1 }, TodoDock)
},
}
@@ -56,7 +56,7 @@ function snapshotWith(
): ConversationSnapshot {
return {
sessionId: SID, nodes, foldDegraded: false, partial: null, runningCalls, codeDispatches,
pending: [], queue: [], running: runningCalls.length > 0, composerPhase: 'active', removed: false,
pending: [], queue: [], todos: [], running: runningCalls.length > 0, composerPhase: 'active', removed: false,
openState: 'open', openError: null,
hasMore: false, loadingOlder: false, promptError: null, blank: false, lastAgentError: null,
} as ConversationSnapshot
@@ -21,7 +21,7 @@ const SID = 's1' as SessionId
function snapshotOf(overrides: Partial<ConversationSnapshot> = {}): ConversationSnapshot {
return {
sessionId: SID, nodes: [], foldDegraded: false, partial: null, runningCalls: [], codeDispatches: new Map(),
pending: [], queue: [], running: false, composerPhase: 'active', removed: false,
pending: [], queue: [], todos: [], running: false, composerPhase: 'active', removed: false,
openState: 'open', openError: null, hasMore: false, loadingOlder: false,
promptError: null, blank: false, lastAgentError: null,
...overrides,
@@ -24,7 +24,7 @@ const SID = 's1' as SessionId
function mountBar(shell: SessionInputShell, over?: { running?: boolean; disabled?: boolean }) {
const session = createSnapshotStore<ConversationSnapshot>({
sessionId: SID, nodes: [], foldDegraded: false, partial: null, runningCalls: [], codeDispatches: new Map(),
pending: [], queue: [], running: over?.running ?? false, composerPhase: 'active',
pending: [], queue: [], todos: [], running: over?.running ?? false, composerPhase: 'active',
removed: over?.disabled ?? false, openState: 'open', openError: null, hasMore: false,
loadingOlder: false, promptError: null, blank: false, lastAgentError: null,
})
@@ -111,7 +111,7 @@ async function scopedBench(register?: (slash: SlashService) => void) {
const wiring = shell
const sessionStore = createSnapshotStore<ConversationSnapshot>({
sessionId, nodes: [], foldDegraded: false, partial: null, runningCalls: [], codeDispatches: new Map(),
pending: [], queue: [], running: false, composerPhase: 'active', removed: false,
pending: [], queue: [], todos: [], running: false, composerPhase: 'active', removed: false,
openState: 'open', openError: null, hasMore: false, loadingOlder: false,
promptError: null, blank: false, lastAgentError: null,
})
@@ -19,7 +19,7 @@ const SID = 's1' as SessionId
function snapshotWith(queue: QueuedMessage[]): ConversationSnapshot {
return {
sessionId: SID, nodes: [], foldDegraded: false, partial: null, runningCalls: [], codeDispatches: new Map(),
pending: [], queue, running: true, composerPhase: 'active', removed: false, openState: 'open', openError: null,
pending: [], queue, todos: [], running: true, composerPhase: 'active', removed: false, openState: 'open', openError: null,
hasMore: false, loadingOlder: false, promptError: null, blank: false, lastAgentError: null,
}
}