ryanyujazz-dev/dsh-plugins--packages-ui-conversation ↗★ 0
@ryanyujazz/dsh-client-ui-conversation
Conversation domain: skeleton, ordered chat flow, composer with the Host-backed busy-Enter preference, and details host
安装
此插件尚未提供可验证的 bundle,或兼容性检查未通过。请先阅读仓库说明。 阅读完整 README ↗
说明文档
阅读完整 README ↗@deepseek-ai/dsh-client-ui-conversation
English | 中文
Conversation domain: skeleton (header/tabs/composer/empty state), chat view (grouped step-summary flow, streaming tail isolation, and turn status), composer dock (session stats sticky with the input), input dock (queue rows plus the todo plan strip), details shell, and scope-addressed ConversationController. Tool presentation belongs to ui-tool.
Compaction renders as one collapsed row at the checkpoint's flow position without replacing the transcript above it. Automatic compaction uses the context-compacted title. Every completed marker with a loaded compaction/summary event shows the replaced-item and estimated-token counts and discloses the summary on click. Manual /compact starts as a running compact row; on successful settlement its explicit summary-event reference folds that command into the checkpoint row under the same React key. A completed checkpoint keeps the context-compaction icon at rest and replaces it with the collapsed or expanded disclosure only on hover or keyboard focus. Input rejection, no compactable history, cancellation, and failure retain the generic command row and its handler-authored text. Pairing never depends on adjacency because durable context may be injected while compaction is running. The framed checkpoint payload is model-facing and never renders; when the cited compaction/summary event is outside the loaded window, the checkpoint remains visible but non-expandable.
The resident conversation shell survives no-session and session transitions. Without a current session it locks message actions and presents the whole dashed composer card as a trigger for the root-scoped conversation.hero.workspace Workspace picker; the textarea remains read-only and keyboard-accessible. Selecting a Workspace connects or reuses its Host-owned blank session and opens that session without replacing the shell. The root always owns the same scrollport and Hero/composer subtree; separate strict-session header and body outlets fill their regions when the first Session arrives, so the Workspace picker, scroll body, composer seat, and textarea retain their React and DOM identity. 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. In the active phase the session header shows only the current session title and view tabs as ordinary column chrome; fork lineage remains session data and is not projected into the header. Beneath it the scrollport (data-conversation-scroll) holds the flowing views and the sticky composer stack (stats dock + input docks + bar). That scrollport reserves its scrollbar gutter unconditionally, and a view opting into a composer overlay leaves it a scroll container, so the input card keeps one horizontal position whether or not the transcript scrolls and whichever view tab is shown (decision). Wheel over the textarea chains: the capped draft scrolls locally until its edge, then forwards to that host.
Another plugin can make one session's composer inert through ctx.conversation.blocks: it sets a block carrying its own localized reason, and the bar renders the same disabled textarea with that reason as the placeholder — the no-workspace posture, reused. The push direction is the constraint, not a preference: the plugins that know a session cannot send (ui-model-selection, when no adapter serves its route) already depend on this package, so this package cannot read them. The model seat is the one control a block leaves live — every block this contract has is cleared by choosing a model, so locking it too would leave the composer asking for the only thing it prevents. A block is an affordance only; the Host refuses a prompt it cannot route regardless of what any client disables. The no-workspace state wins when both hold, because picking a workspace is the earlier prerequisite.
The view ring is a slot: the strict session-body registration declares the session-scoped 'conversation.view' list in its children table, that body renders the active entry through its renderSlot share (only: ), and view tabs project from registration options (id/order/label). The chat view is this package's own entry; plugins such as ui-trajectory contribute tabs through ctx.slots.register, and each view owns its chrome.
Chat business rows are independent registry contributions rather than a closed built-in union. A client plugin declaration-merges its typed ChatNodeDataMap key, registers a ConversationNodeDefinition on ctx.conversationEvents, and registers the matching keyed renderer on conversation.chat.node; it does not modify Session folds or a central renderer switch. The Conversation Node cookbook covers stable event ids, append/prepend replay, Location data, and renderer constraints.
Approvals take over the composer through the chain this package declares: ApprovalPanel registers as a selector-routed 'conversation.composer' entry (the ui-user-questions 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 runtime manager projects every approval or question wait through SessionSummary.pendingInteraction, including sessions never instantiated; ui-workspace owns its sidebar presentation. Pending waits leave the message flow entirely: questions (ui-user-questions) and approvals (ApprovalPanel) both answer through the composer takeover, so no display-only placeholder card remains. The composer's bottom-row Access seat mounts PermissionSelect, fed by the host-computed permissions projection through the standard-kit useProjection (key absence hides the chip); the chip opens a Menu-primitive dropdown whose kebab-case preset names render as title-case labels. Safe preset picks submit /permission immediately through the bar's injected command callback, while danger-full-access is presented as Full access and first opens an in-page Modal risk confirmation. The enabling action stays disabled until the user checks the acknowledgement; cancel, Escape, close, and mask click submit nothing.
The session header renders the session-scoped 'conversation.session.header.actions' list beside the title and the independent 'conversation.session.header.utilities' list at the right edge. Session context and lineage controls remain in actions; optional Session utilities cannot reorder or move them. The composer chain currency includes the current conversation session; ui-subagent selects one-shot or parent-unavailable addressed sessions for reason-specific read-only copy, while the ordinary InputBar keeps every addressed child Send-only because the continuation service exposes no public per-Activation cancellation operation and session.cancel would bypass its ownership.
Logged non-user messages render as a default-collapsed disclosure whose header names the role the runtime projected for the message — 上下文注入 for an injection, 跨会话召回 for a recalled session — followed by the producer name that projection read out of the durable source, so a reader distinguishes a skill catalog from a workspace instruction file or a recalled session without expanding. A source that names no producer shows the role alone. The shared DisclosureRow primitive gives this context surface the same compact geometry as other flow rows while retaining context semantics: the expanded body follows its content height up to a 141px scrolling cap and synthesizes no tool state or summary (historical disclosure decision, producer-label decision). That body follows the form the producer declared on its durable source: instructions names the reconciled files above their text, catalog lists the entries the source recorded instead of the model-facing prose, and every other value — absent, unknown to this version, or carrying no usable fields — renders the opaque body, which shows the model-facing text with its real line breaks and the remaining source fields. The opaque body is the documented default, not a leftover: a resumed, forked, or foreign log must render whether or not its producer is mounted here. A durable or pending steering bubble shares the user bubble's presentation unadorned; its mid-turn position in the flow is the only steering signal the transcript shows.
A Think row stays collapsed by default and exposes live reasoning throughput without expanding the chain of thought: while its reasoning block is the streaming tail, the summary switches from the settled first line to the latest non-blank line and its one-line scrollport follows each delta to the inline end. Expanding the row removes the moving summary and leaves the full reasoning in ordinary page flow, so page reading never fights an internal follower; settlement restores the stable first-line summary at the left edge (decision).
The chat view keeps Tool placement but delegates Tool presentation. Each ordered tool-call Conversation Node dispatches through the matching key of conversation.chat.node, while the details shell passes the selected call through conversation.details.tool. The assembled Web bundle registers ui-tool for that Chat Node key; it renders the Runtime-projected recursive root/child tree and owns per-name dispatch, generic rendering, and render-intent cards. The details seat alone retains a raw-result fallback when that renderer is absent.
The chat flow projects consecutive model-retry nodes across retry turns into one stable, muted status row updated to the latest attempt; every retry event remains in the runtime snapshot and session log. Its frontend countdown anchors the scheduled delay to client receipt, avoiding host/browser clock skew, rounds remaining time up to seconds, and has a one-second floor. The latest unresolved retry uses a left-to-right text shimmer. Subsequent turn facts distinguish an attempt that started from one cancelled during backoff, while the Host running bit only controls the live animation; the row then shows a static completed or cancelled label. Normal policy rows show the finite retry maximum; always policy rows show ∞. Activating the row reveals the latest exact retry delay and failure message. The client runtime removes each failed step's streaming tail before its retry node arrives, while the status remains visible after a later attempt succeeds. An unretried terminal failure renders as a persistent inline status at its turn boundary, showing the display-safe durable message and optional error code without offering an action the Host cannot fulfill; AUTH copy never echoes provider-supplied credential fragments.
TodoDock takes the 'conversation.input.dock' list slot at order: 0 — before Goal and Queue — 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 starts collapsed as a header of title plus its own ·-joined per-status counts (localized, 1 completed · 2 in progress · 1 pending, zero-count segments omitted). The dock adapter owns selection so the panel stays a pure function of its props. Anything the input-zone composer chain hides (a conversation.composer takeover such as ui-user-questions's) hides the whole dock, this strip included. The todo_write Tool row belongs to ui-tool.
QueueDock is the terminal input-dock entry at order: 20. It hides while empty, renders one pending row directly, and defaults two or more rows to a collapsed " 条排队消息" header whose button expands or collapses the complete list. The header exposes aria-expanded and aria-controls; the expanded list scrolls within a 180px height bound. An active edit or mutation keeps its rows visible, and emptying the queue restores the collapsed default for the next queue. Each visible ordinary-session row remains a single-line preview with its exact-occurrence edit, delete, and strict-steer actions; addressed subagents retain the rows as a read-only projection because their continuation transport does not expose queue mutation. If strict steer loses to a closed window, the original occurrence remains queued for normal delivery; if the driver already claimed it, normal delivery is already underway. Neither converged race displays a failure, while transport and unknown failures do.
The Host's placement-aware session/queue snapshot also carries pending steering. QueueDock filters it out, while ChatView projects it as a user-style bubble with Copy at the conversation tail; non-user next-step items (injected context) carry the context placement instead and render nowhere until claimed. Fork is absent here as on every user-style bubble. The Host delays steering retirement until the durable user/message carrying the steering has entered the mux stream. On that accepted live event, the client runtime retires the first matching current steering occurrence before publishing the snapshot; historical events cannot hide later occurrences that reuse the same MessageId. The bubble therefore hands off without a gap or duplicate, immediately restores Copy and the clock from the durable node — a steering bubble, like a user bubble, carries no branch action (decision) — and survives reconnect from the same authority.
Keyboard message submission resolves delivery from the addressed session's running state and steering capability. While idle, Enter and Cmd/Ctrl+Enter both perform an ordinary Queue send. While a primary session is running, the Host-backed ui-conversation.busyEnter General Settings preference assigns plain Enter to Queue (the default) or Steer, and Cmd/Ctrl+Enter performs the other behavior; the local settings provider stores it in $DSH_HOME/settings.yaml, so the choice follows the same user home across Web ports. Shift+Enter remains a newline. With an empty draft, Cmd/Ctrl+Enter instead steers every still-pending queued message into the running turn in FIFO order (the dock's per-row strict-steer action applied to the whole queue); plain Enter with an empty draft remains a no-op. While this whole-queue gesture is available, the textarea placeholder advertises it; a placeholder supplied by the owning surface still takes precedence. Addressed subagents keep both gestures on their Queue-only continuation transport even while running. The preference affects only the steer-capable busy-state gesture pair, and the send button and non-keyboard submit actions remain Queue. Composer Steer uses the existing best-effort session.prompt(mode: 'steer') contract: if the current next-step window closes before acceptance, AgentLoop admits the message as the next waking Queue turn without surfacing a failure or losing the draft transaction. The [Host-backed preferences decision](../../../.agents/notes/implemented/bug-fix/2026-08-06-host-backed-web-prefer