Files
deepseek-harness/packages/client/ui-conversation

@deepseek-ai/dsh-client-ui-conversation

English | 中文

Conversation domain: skeleton (header/tabs/composer/empty state), chat view (grouped step-summary flow, streaming tail isolation, stats line, per-tool row slot with a bash sample registrant and the todo row), input dock (queue rows plus the todo plan strip), minimal details panel, scope-addressed ConversationService. Contract: api-contracts v3 §7 plus the slot terminal design (store seat / props shares).

The resident conversation shell survives no-session and session transitions. Without a current session it renders a disabled input bar; its root-scoped conversation.hero.workspace slot hosts the Workspace picker. Selecting a Workspace connects or reuses its Host-owned blank session and opens that session without replacing the shell. Blank sessions render the same composer body as active sessions, while the InputHub carries drafts across Workspace switches and mirrors them into the session store.

The view ring IS a slot: the conversation registration declares the 'conversation.view' list slot (session scope) in its children table, ConversationRoot renders the active entry through its renderSlot share (only: <active id>), and view tabs project from the ring ledger's registration options (id/order/label). The chat view is this package's own ring entry; other plugins (ui-trajectory) contribute tabs through plain ctx.slots.register — the former package-local view registry (registerView/ViewEntry/ConversationViewMap and the chrome attachment table) is retired, with per-view chrome dissolved into the view components themselves.

Generic tool rows classify the built-in bash, read, search, write, edit, and run_code names into dedicated visual variants. The filesystem variants render the edit icon and a path summary; that path is a hover-underline link that opens the file with the host OS default application (host.openPath, relative paths resolve against the session cwd). Tool rows are not whole-row click targets and do not open the details panel. The code variant summarizes with the model-authored description and expands to the program itself; its logged sub-dispatches render as always-visible nested rows through the SAME keyed toolview hole (custom registrations and the GenericToolCard fallback apply to sub-rows unchanged). Cordis lifecycle tools reuse those generic variants while presenting Inspect, Mount temporary Plugin, and Unmount temporary Plugin with a shared Cordis accent; mount keeps the code variant's expandable source rendering.

Tool rows are slots too — the standalone tool ring (ToolViewRegistry/ctx.toolviews/outlet) is retired. The chat entry declares the keyed 'conversation.chat.toolview' hole (session scope; the key space is runtime-open); its render site dispatches per row via entryKey: toolName with GenericToolCard as the call-site fallback. The owner payload is the uniform ToolRowOwnerProps (callId/toolName/block/openFile) and ToolRowProps pre-composes it with the session standard kit. A registrant is a plain plugin: ctx.slots.register({ name: 'conversation.chat.toolview', key: '<tool>', inject? }, Row) with inject: ['slots', 'conversation'] as the load-order seam (apply mounts ConversationService after the chat registration, so the service being present guarantees the slot is declared); session differentiation happens inside the component (useSessions reading parentId — the bash sample is the third-party-posture exemplar). Trajectory/waterfall toolview slots share this shape and land with their own render sites (RendersCheck rejects a declaration nobody renders).

The todo surfaces are two registrations over that shape, both plain registrant plugins with inject: ['slots', 'conversation']. TodoRow takes the 'conversation.chat.toolview' key todo_write and summarizes what the call attempted (<done>/<total> 已完成 · <active item> parsed from its args, falling back to the generic summary on malformed or wrongly-shaped model JSON, and keeping the generic dot for non-ok execution states so a cancelled call never reads as a completed update). TodoDock takes the 'conversation.input.dock' list slot at order: -1 — above the queue rows — and is the plan strip: it reads the host-computed todos projection via useProjection (standing plan: latest todo/write with no later turn/start) and renders TodoPanel, which takes the plain list, hides itself while the list is empty, and collapses to a header of title plus "<done>/<total> tasks · <n> in progress" (status glyphs are the figma check / progress / dashed-pending set). The dock adapter owns the selection so the panel stays a pure function of its props; the standing list lives here rather than in the row so the row stays one line. Anything the input-zone composer chain hides (a conversation.composer takeover such as ui-question's) hides the whole dock, this strip included.

Per-session UI state for selection and the active view lives in the declared chat store (stores.ts createChatStore); the InputHub owns the composer state machine and mirrors its draft into that store for persistence. Apply passes one store handle to the strict session subtree, chat view, and details registrations, so each session shares one instance and the framework owns its lifecycle. Components are pure: the framework standard kit supplies useSession/sessionId, global useSessions/useWorkspaces, and the input machine's useInput/inputActions; store faces and inject factories supply the remaining state and callbacks.

The composer bar declares session-scoped single seats for 'conversation.input.plan' (right of the local access-mode control) and 'conversation.input.model' (immediately before the pending indicator and send/stop button), plus list slots for overlay, dock, left, and right input extensions. Feature packages own each control and its state; ui-conversation supplies placement, the locked owner prop, and the standard slot shares. While the plan projection's effective target is plan mode, InputBar swaps its textarea placeholder to the plan-task wording (a host-folded value read through the standard-kit useProjection; owner-supplied placeholders win). The resident no-session shell uses DisabledInputBar and therefore dispatches no session-scoped control seats.

src/client/ is organized for the future package split: contract/ is the sole inter-domain shared face (slots.ts slot declarations + composed slot props including the tool-row contract, views.ts shared primitives, tool-call-model.ts); the skeleton/, chat/, and toolviews/ (sample registrants) domain directories import contract files and never each other; apply.ts is the only assembly point allowed to import all three domains. The /client export surface is the contract only — apply/inject, the two service classes, and the contract/ type families; implementation components (skeleton, chat rows) and the store factory stay internal and reach the page exclusively through apply's slot registrations (tests take them via the ./src/* subpath).

Model Experience

None, as the conversation UI renders session history and streams in the browser; nothing here reaches a model request.

KV Cache effect

None; this package neither assembles nor sends a provider request.

Known Limitations and Deferred Work

  • The stats line has no duration segment — assistant usage carries token accounting only; elapsed-time needs a host data source.
  • Details panel is the minimal form — selected call args/result raw display; the Input/Output/Metadata switch, Prev/Next stepping, and See-in-trajectory deep link are deferred.
  • Assistant footer extensions (IconActions row, per-message paging) are reserved slots — drawn in the design, not implemented.
  • The sparkle icon for the others tool row is a hand-drawn approximation — the design glyph's vector geometry is not exportable locally; promotion into ui-primitives waits on an exact export.
  • Approval cards are display-only placeholders — question requests answer through the composer chain (ui-question), while web-side approval answering is the P-II approvals project.
  • TodoPanel truncates long item text to one ellipsized line — the figma strip has no wrap or expand affordance; full text is not readable inline.