The web host now composes the sandboxed product path (sandbox-local + sandbox-policy behind bash-sandbox/fs-sandbox, with user-approval and permission on top); BootHostOptions.sandbox carries the deployment defaults (workspace-write + ask). createApiProxy owns the approval pending registry: a ctx.approval ask becomes an answerable approval/requested mux frame with a stable rpcId, replayed verbatim on every mux open until settled; respond routes by the echoed rpcId, validates the ApprovalResponsePayload audit correlation, and broadcasts approval/resolved; the ask's abort signal withdraws the question as cancelled. session.permissions / session.setPermission project ctx.permission into a protocol-owned PermissionOption select; idle switches are held last-write-wins and flushed into the next prompted turn (the ACP bridge's anchoring pattern). The shared hasOpenTurn fold moved to dsh-session, deduplicating the private copies in user-approval, the ACP bridge, and the proxy. Client, per the designer draft: a pending approval takes over the composer (ApprovalPanel replaces the InputBar — amber strip, justification headline, paired command, one-shot refuse/allow, keyed by rpcId so a queued second approval remounts live; the resolved frame restores the composer); the sidebar session row shows an amber waiting-approval dot that outranks the running ring (manager-tracked approvalId set, idempotent under mux-open replays, cleared per connection generation, lit for uninstantiated sessions too); the permission selector is a composer bottom-row chip over an invisible native select, with a presentation-only title-case transform (workspace-write renders as Workspace Write; wire names untouched). Question placeholders stay in the message flow. The connection fixture mirrors the host behavior for keyless browser acceptance.
6.3 KiB
@deepseek-ai/dsh-client-ui-conversation
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), minimal details panel, scope-addressed ConversationService. Contract: api-contracts v3 §7 plus the slot terminal design (store seat / props shares).
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.
Approvals take over the composer through the chain this package declares: ApprovalPanel registers as a selector-routed 'conversation.composer' entry (the ui-question pattern) and occupies the composer in place of the InputBar while an approval wait is pending (amber strip, justification headline, paired command line from the running call's args, one-shot refuse/allow). The PendingApproval domain face in contract/slots.ts owns the wire encoding — the ApprovalResponsePayload value with the audit correlation — over the runtime's PendingWait carrier; the broadcast approval/resolved frame settles the wait and restores the composer. The sidebar mirrors the blocked state: the session row's amber warning dot (a manager-tracked waitingApproval list bit, lit for uninstantiated sessions too) outranks the running ring until the question resolves. Question placeholders remain in the message flow as display-only PendingCards while ui-question owns the answering takeover. The composer's bottom-row chip mounts the permission-preset select (PermissionSelect), fed by the injected permissions/setPermission callbacks over the object layer's session RPCs; empty options (a permission-less host) hide the control, and the derived custom value renders as current-only.
Generic tool rows classify the built-in bash, read, search, write, and edit names into dedicated visual variants. The filesystem variants render the edit icon and Write · <path> or Edit · <path> summary while retaining the shared row-to-details interaction.
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/openDetails) 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).
Per-session UI state (selection, composer draft, active view) lives in the declared chat store (stores.ts createChatStore): apply constructs one handle and passes it to the conversation, chat-view, and details registrations, so the session slots share one instance per session (selection written by the chat view, read by details) and the framework owns instance lifecycle and draft persistence. Components are pure — the framework standard kit (useSession/sessionId/useSessions) and the store faces (useStore/actions) arrive automatically from the registration declaration; the inject factories contribute plain data and callbacks only (send/stop choreography, tab read face, details/paging callbacks, startSession chain).
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
usagecarries 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.
- The permission select reads once per mount — a host-side preset change from another client surfaces only after a session re-select; live knob-event-driven refresh is deferred.
- The approval panel's "Always allow this type" is deferred — durable grants need a grant-storage design; only allow-once/reject answer today.