@deepseek-ai/dsh-client-ui-conversation 0.1.6-alpha.2 → 0.1.7-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +20 -6
  3. package/README.zh.md +20 -6
  4. package/lib/client.js +1374 -370
  5. package/lib/index.js +9 -8
  6. package/lib/types/client/contract/conversation.d.ts +19 -0
  7. package/lib/types/client/contract/groups.d.ts +116 -0
  8. package/lib/types/client/contract/input.d.ts +9 -0
  9. package/lib/types/client/contract/records.d.ts +3 -7
  10. package/lib/types/client/contract/slots.d.ts +51 -17
  11. package/lib/types/client/conversation/assembler.d.ts +16 -1
  12. package/lib/types/client/conversation/assembly.d.ts +3 -0
  13. package/lib/types/client/conversation/group-registry.d.ts +30 -0
  14. package/lib/types/client/conversation/group-store.d.ts +29 -0
  15. package/lib/types/client/conversation/location-index.d.ts +6 -0
  16. package/lib/types/client/index.d.ts +3 -1
  17. package/lib/types/client/input/editor/claim-decor.d.ts +2 -2
  18. package/lib/types/client/input/editor/keymap.d.ts +6 -2
  19. package/lib/types/client/input/editor/runtime.d.ts +13 -0
  20. package/lib/types/client/input/editor/view-binding.d.ts +1 -1
  21. package/lib/types/client/input/facade.d.ts +7 -0
  22. package/lib/types/client/input/submission-policy.d.ts +6 -6
  23. package/lib/types/client/locales.d.ts +384 -4
  24. package/lib/types/client/queue/QueueDock.d.ts +1 -1
  25. package/lib/types/client/service.d.ts +6 -0
  26. package/lib/types/client/skeleton/ConversationHeader.d.ts +8 -0
  27. package/lib/types/client/skeleton/ConversationSession.d.ts +1 -1
  28. package/lib/types/client/skeleton/DefaultConversationViews.d.ts +1 -1
  29. package/lib/types/client/skeleton/control-row-layout.d.ts +10 -0
  30. package/lib/types/client/view-selection.d.ts +4 -0
  31. package/lib/types/index.d.ts +16 -5
  32. package/lib/types/submission-settings.d.ts +9 -1
  33. package/package.json +35 -34
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/client/ui-conversation/README.md
5
- README.md: bc3b582f9d80e97c29e27dcaa680dc064ec30deb
6
- README.zh.md: 21881e7933cdf46cf9e27a66f076fc40c48e4b1a
5
+ README.md: 4823aa0a1ba5975c4efc8b6bdddd7ca8c57c068f
6
+ README.zh.md: 5ef8a1b6621bf8bab7e98cb18722a9aedda29101
package/README.md CHANGED
@@ -25,12 +25,16 @@ English | [中文](README.zh.md)
25
25
  <a id="conversation-assembly"></a>
26
26
  ## Conversation assembly
27
27
 
28
- `UiConversation.events` is the single registry for event Definitions, and `UiConversation.views` is the single registry for target snapshot builders. Both registries reject duplicate keys, preserve registration order, return idempotent disposers, and rebuild existing bindings when their contribution roster changes. `UiConversation.binding(bindingOrSessionId)` returns one identity-stable Conversation binding for the current Session Controller binding. It does not open another event source.
28
+ `UiConversation.events` is the single registry for event Definitions, and `UiConversation.views` is the single registry for target snapshot builders. Both registries reject duplicate keys, preserve registration order, return idempotent disposers, and rebuild existing bindings when their contribution roster changes. `UiConversation.binding(bindingOrSessionId)` returns one identity-stable Conversation binding for the current Session Controller binding. It does not open another event source. A View Definition may declare `toolCallFocus` to translate a tool-call id into its focus identity. Conversation supplies an Inspect callback only while such a target has a visible View entry; Chat consumes this callback without selecting a target.
29
29
 
30
30
  The adapter passes each `SessionEventLikeEntry` directly to the assembler. Its outer `type` distinguishes durable events from Client-only transient events, while its inner `event` always exposes `type`, `seq`, `time`, and `data`; Definitions receive that inner `SessionEventLike`. Replacement windows may include both entry variants, while historical prepends carry durable entries and live appends may carry either. Every Definition uses the same `match` and `update` methods for both event forms, while `start` receives only a durable event and the assembler rejects a transient start. Definitions that do not consume Assistant deltas return `null` for `assistant/live-chunk`. Replacement windows and revision gaps rebuild from the complete loaded window; contiguous append, prepend, and Assistant-settlement revisions use incremental assembly. Settlement removes only the named attempt's transient matches, applies its optional durable entry, and replays the affected Contexts and dependents without replacing unrelated target nodes. The assembler owns Context matching, Turn/Step locations, target node materialization, target activity, and stable target sources. `ConversationSnapshot` contains only target-neutral views and active-target facts; Session lifecycle state remains in `SessionSnapshot`.
31
31
 
32
32
  A target becomes active when shell selection resolves it or when its source receives a first subscriber. The assembler replaces that target from current Contexts once and keeps it active for later incremental flushes; creating a source does not activate it and unsubscription does not deactivate it.
33
33
 
34
+ `UiConversation.groups` registers one optional business Group Definition per existing target. It consumes projected Node changes, changed Turns, and indexed target positions after Node materialization, including first activation, and owns all segmentation and group data. Indexed Turn reads preserve interruptions from neighbouring Nodes, allowing business updates to stay within affected Turns and groups. The assembler validates and installs root references and keyed group snapshots before publishing Node, Group, and Location data sources. Ungrouped targets retain their original path. [Grouping](../../../docs/subsystems/conversation.md#group-definitions) defines input lifetime, typed registration, atomic updates, and renderer responsibilities.
35
+
36
+ Group registration rejects a missing View target. Removing an already registered View Definition pauses its grouping and clears its published results without removing the Group Definition; registering that View again rebuilds from the current loaded timeline through the existing replacement flow. Switching View tabs does not remove a View Definition.
37
+
34
38
  Target packages declaration-merge their snapshot and Location data maps, then register with `ctx.uiConversation.events.register(...)` and `ctx.uiConversation.views.register(...)`. A target reads its Session-owned source with `ctx.uiConversation.binding(binding).target(targetId)`. Registrations are Cordis effects and their returned disposers remove the contribution from the same registry. The shared request inspection serves every target: `ctx.uiConversation.inspectSystemPrompt(previous, event)` interprets system messages and positional replacements as immutable loaded-surface state. It selects the last nonempty surviving system node in surface order, retains only surviving replacement positions for chained rewrites, and withholds the prompt after an unindexed older endpoint until prepend replay supplies its order. Target-owned Definitions retain historical cards independently. `ctx.uiConversation.inspectRequestPrompt(previous, header, system)` classifies request changes against that effective prompt; ordinary messages and stream chunks require no system-state work.
35
39
 
36
40
  <a id="shell-and-standard-props"></a>
@@ -38,8 +42,12 @@ Target packages declaration-merge their snapshot and Location data maps, then re
38
42
 
39
43
  The shared image slot props keep display choices separate from durable references: `thumbnail` requests a contained attachment-list thumbnail, while `compact` requests a cropped gallery tile. An optional per-image `label` supplies the accessible display name; loading and cache identity still use the original attachment reference. [ui-attachment](../ui-attachment/README.md) owns rendering and the lightbox.
40
44
 
45
+ The composer measures its expanded control groups after size, content, visibility, or font-loading changes. If they cannot share a line, the row sets `--dsh-composer-model-text-display: none` and `--dsh-composer-model-icon-display: block` for the model seat; their defaults are `block` and `none`. Wrapping remains available if even the icon cannot fit.
46
+
41
47
  The context-occupancy button shows a ring and percentage below the input card, after the Session statistics. Clicking it opens the token breakdown in a panel kept inside the viewport, including when no statistics are shown; the button stays hidden until context usage and capacity are available.
42
48
 
49
+ With Developer tools off, the shell hides only Trajectory; other plugin-contributed Views remain available. The View tab bar is hidden when fewer than two Views are available. Turning Developer tools off while Trajectory is active returns to Chat; stored View preferences and recorded Session data remain intact. Turning it on makes Trajectory available again. View owners receive the available roster so their navigation actions can follow the same visibility.
50
+
43
51
  The composer registers the File command action and owns its label, availability, and native file-dialog callback. Menu availability and invocation both consult the mounted composer's current attachment-intake policy. Unmounting or locking the composer disables that action; disposing the plugin removes its registration. The callback binding stays inside the input module.
44
52
 
45
53
  `SessionInputShell` owns one Lexical editor per Session through its private [DraftEditorRuntime](src/client/input/editor/runtime.ts), while retaining submission, attachment selection, and recovery decisions. [DraftEditor](src/client/input/editor/DraftEditor.tsx) renders the borrowed editor; InputBar retains its Hooks and refs and installs DOM behavior through [view-binding](src/client/input/editor/view-binding.ts). Editor-facing types live in [draft-editor.ts](src/client/contract/draft-editor.ts), with shared input and submission types in [input.ts](src/client/contract/input.ts). This separation does not support simultaneous editable roots for one Session; [the two-stage isolation proposal](../../../.agents/notes/proposed/architecture/2026-09-14-composer-model-and-draft-editor.md) defines the remaining work.
@@ -48,19 +56,21 @@ Claimed commands retain their identity and highlight when only their arguments a
48
56
 
49
57
  Workspace selection uses `uiWorkspace.openWorkspace` to prepare the target and commit navigation. Draft text and attachments move in its synchronous preparation callback only while that request is current; later navigation or owner disposal leaves the original draft intact.
50
58
 
51
- The package occupies the root-scoped `main` key `conversation`. Its `main.conversation` shell keeps the strict Session Header outside the optional-Session `conversation.content` Component Factory. The Factory owns the shared body and Composer, reads the current Session through its standard Hook, and exposes strict-Session `views` plus root-scoped `widthControls` local positions. Its default adapter renders the existing `conversation.session` entry, while the main occurrence selects the width handles; an embedded occurrence can replace `views` and omit those handles without rendering the main Header. `ctx.uiSession.provide()` materializes the Conversation and input sources from the same Session binding and supplies `inputActions` as a stable standard prop.
59
+ The package occupies the root-scoped `main` key `conversation`. Its `main.conversation` shell keeps the resident `conversation.header` outside the optional-Session `conversation.content` Component Factory. The header hosts root-scoped navigation without a selected Session; titles, actions, and View tabs remain in its strict Session child. The Factory owns the shared body and Composer, reads the current Session through its standard Hook, and exposes strict-Session `views` plus root-scoped `widthControls` local positions. Its default adapter renders the existing `conversation.session` entry, while the main occurrence selects the width handles; an embedded occurrence can replace `views` and omit those handles without rendering the main Header. The shared body and Composer register the queue and Todo docks. The Todo dock uses the shared panel elevation above the composer; its rows use the shared idle, ongoing, and done markers for pending, in-progress, and completed items. `ctx.uiSession.provide()` materializes the Conversation and input sources from the same Session binding and supplies `inputActions` as a stable standard prop.
52
60
 
53
- A blank Session retains the header's leading and corner controls, including the right-sidebar opener, while hiding its title, actions, utilities, and View tabs. Selecting a Workspace creates the Session needed by these controls; the first message is not required. Without a selected Session, the strict header is absent. Sidebar entries retain their own data and execution prerequisites.
61
+ A blank Session retains the header's leading and corner controls, including the right-sidebar opener, while hiding its title, actions, utilities, and View tabs. Selecting a Workspace creates the Session needed by these controls; the first message is not required. Without a selected Session, the strict header is absent; the resident container reserves a 40px drag band on macOS desktop and no empty vertical space on Web, Windows, or Linux. Sidebar entries retain their own data and execution prerequisites. A started Session header uses one row when fewer than two Views are available; the tab row reserves space only while rendered.
54
62
 
55
63
  View selection is deterministic: a registered persisted selection wins, otherwise registered `chat` wins, otherwise no View renders. It never chooses the first registered View. Shell phase combines Session lifecycle with the active-target set; no target-specific snapshot is read by the shell.
56
64
 
57
65
  The shell reads the persisted View preference before rendering when a Session first binds or a cached Session becomes current, activates the registered preferred View or Chat fallback, and activates later tab or focus selections before committing them to the store. A blank Session still omits the `conversation.view` slot; no unselected target is activated.
58
66
 
59
- Active transcripts in the main occurrence expose content-width drag handles in their uncovered side gutters; embedded occurrences omit them. A View that paints into a gutter raises only its concrete painted element above the handle; transparent full-width wrappers stay below so they do not claim empty gutter. This requires the path between that element and the Conversation body to remain outside an intermediate stacking context; the shipped Chromium behavior is pinned by the browser scenario. Chat applies the rule to table elements, while its column-bounded tool cards need no raise. Wheel motion over a handle still scrolls the transcript, while Ctrl+wheel remains a browser zoom gesture. The sticky composer intentionally owns its full footer band, which is not a resize target; an already-captured drag lifts its indicator until release ([decision](../../../.agents/notes/implemented/bug-fix/2026-09-14-transcript-width-handle-layering.md)).
67
+ Active transcripts in the main occurrence expose content-width drag handles in their uncovered side gutters; embedded occurrences omit them. A View that paints into a gutter raises only its concrete painted element above the handle; transparent full-width wrappers stay below so they do not claim empty gutter. This requires the path between that element and the Conversation body to remain outside an intermediate stacking context; the shipped Chromium behavior is pinned by the browser scenario. Chat applies the rule to table elements, while its column-bounded tool cards need no raise. Wheel motion over a handle still scrolls the transcript, while Ctrl+wheel remains a browser zoom gesture. The sticky composer intentionally owns its full footer band, which is not a resize target; an already-captured drag lifts its indicator until release ([handle styles](src/client/skeleton/ConversationRoot.module.css)).
60
68
 
61
- The resident composer survives no-Session and Session transitions. Whitespace hides its placeholder; a whitespace-only draft without attachments cannot be sent. The no-Session state keeps the same composer surface mounted but inert while the Workspace picker connects a blank Session. The surface is a shell-owned Lexical editor: reference chips are atomic decorator nodes carrying the owner's serialization identity (submission expands them through the owner codec), claimed slash commands stay styled leading text, folder text references carry the folder glyph as an icon prefix, and the draft's clipboard projection is mirrored into the per-Session Conversation store. QueueDock reads `next-turn` directly from the Session `inbox` projection, including cold recovered messages. Queue operations address exact queue occurrences through the scoped `ctx.conversation` service; queue previews render sent text through the shared inline reference projection from `ui-primitives` (wire session forms fold to their label) and show local or durable images and files in original attachment order. Images use thumbnails; files use compact name-and-size cards. An edit exposes the literal sent text, and durable thumbnails resolve through the session image URL cache. Busy Enter behavior is stored in the Host-backed `ui-conversation` settings namespace. The composer keymap arbitrates the trigger menu's keys through the slash pipeline — Tab settles the highlighted completion (or drills a drillable one), Escape and Shift+Tab leave the menu without settling — and leaves every other key to the editor. An overlay that takes the keyboard hands it back through `SessionInput.focus()`, which rides Lexical's own focus so the caret returns where the draft left it rather than at the start.
69
+ The width-handle indicator follows the pointer only during a captured drag; ordinary hover leaves its position unchanged.
62
70
 
63
- Default sends commit optimistically: Enter clears the draft, occurrence table, and undo history in the same transaction, keeps the composer in `plain`, and runs the send as a detached attempt, so typing and further sends continue during the flight. `sendSession` registers a Session submission echo (`session.beginSubmission`) with the delivery mode before serializing, preserving selected image and file order in `pendingSubmissions`; Session derives the placement from that mode and its current running state, so idle sends use the transcript, busy Queue sends use QueueDock, and busy Steer sends use the pending-steering surface. It then yields one paint, encodes images through the browser's native `FileReader` data-URL path, and cites staged file receipts. Command submissions use the same receipts for generic files, so sending `/goal` or `/plan` never reads those browser files again. The prompt reuses the submission `requestId`; queue and history observation by that `rpcId` retires the echo once. Concurrent failures are restored together in submission order until the user edits the restored content; command submissions keep the frozen `submitting` phase. Detached attempts retain their attachment ids through admission and Session scope disposal. An observed retirement immediately exposes each image preview through the durable cache, replaces it with the canonical URL after fetching the admitted attachment, revokes each URL after its use ends, and releases file cards. Selected generic files enter one FIFO background-upload queue; `maxConcurrentFileUploads` defaults to two active Worker transports, the Conversation service retains queued and active operations plus byte progress across Session navigation, and removing a draft skips its queued transfer or aborts its active transport. Continuable subagents disable attachment intake and skip local echoes because their transport does not preserve the browser request id.
71
+ The resident composer survives no-Session and Session transitions. Whitespace hides its placeholder; a whitespace-only draft without attachments cannot be sent. The no-Session state keeps the same composer surface mounted but inert while the Workspace picker connects a blank Session. The surface is a shell-owned Lexical editor: reference chips are atomic decorator nodes carrying the owner's serialization identity (submission expands them through the owner codec), claimed slash commands stay styled leading text, folder text references carry the folder glyph as an icon prefix, and the draft's clipboard projection is mirrored into the per-Session Conversation store. QueueDock reads `next-turn` from the Session `inbox` projection, including cold recovered messages, and excludes only requestIds still owned by local transcript submissions. Other queued rows retain their normal display and actions. Queue operations address exact queue occurrences through the scoped `ctx.conversation` service; queue previews render sent text through the shared inline reference projection from `ui-primitives` (wire session forms fold to their label) and show local or durable images and files in original attachment order. Images use thumbnails; files use compact name-and-size cards. An edit exposes the literal sent text in a textarea that grows with its content, so line breaks survive re-editing; Enter saves, Shift+Enter breaks the line, and Escape cancels. Durable thumbnails resolve through the session image URL cache. Busy Enter behavior is stored in the Host-backed `ui-conversation` settings namespace. The composer keymap arbitrates the trigger menu's keys through the slash pipeline Tab settles the highlighted completion (or drills a drillable one), Escape and Shift+Tab leave the menu without settling and leaves every other key to the editor. An overlay that takes the keyboard hands it back through `SessionInput.focus()`, which rides Lexical's own focus so the caret returns where the draft left it rather than at the start.
72
+
73
+ Default sends commit optimistically: Enter clears the draft, occurrence table, and undo history in the same transaction, keeps the composer in `plain`, and runs the send as a detached attempt, so typing and further sends continue during the flight. `sendSession` registers a Session submission echo (`session.beginSubmission`) with the delivery mode before serializing, preserving selected image and file order in `pendingSubmissions`; Session derives the placement from that mode and its current running state, so idle sends use the transcript, busy Queue sends use QueueDock, and busy Steer sends use the pending-steering surface. It then yields one paint, encodes images through the browser's native `FileReader` data-URL path, and cites staged file receipts. Command submissions use the same receipts for generic files, so sending `/goal` or `/plan` never reads those browser files again. The prompt reuses the submission `requestId`; Session retires the echo once at its display handoff, correlated by that `rpcId`. Concurrent failures are restored together in submission order until the user edits the restored content; command submissions keep the frozen `submitting` phase. Detached attempts retain their attachment ids through admission and Session scope disposal. An observed retirement immediately exposes each image preview through the durable cache, replaces it with the canonical URL after fetching the admitted attachment, revokes each URL after its use ends, and releases file cards. Selected generic files enter one FIFO background-upload queue; `maxConcurrentFileUploads` defaults to two active Worker transports, the Conversation service retains queued and active operations plus byte progress across Session navigation, and removing a draft skips its queued transfer or aborts its active transport. Where the browser shell exposes `__DSH_HOST_PATHS__` (the Desktop application), dropped or pasted folders and dropped, picked, or pasted non-image files with a real path become `@path` chips; images keep uploading. Drag-and-drop and paste identify directories through the browser entry API; when that API is absent or returns no entry, pasted items retain ordinary file handling. The file picker cannot select directories. References require the `ui-reference` plugin and the original paths to remain readable by the model's file tools. Paths inside the workspace are relative; others remain absolute. The whole batch is validated before insertion, retains source order and selected text, and uses whitespace-separated, closed-quote mentions. A browser without the bridge refuses dropped or pasted folders; a Desktop folder without a reported path is rejected separately. Continuable subagents disable attachment intake and skip local echoes because their transport does not preserve the browser request id.
64
74
 
65
75
  Queued submission echoes show “Sending…” beside disabled edit, remove, and steer buttons; a collapsed dock keeps the sending status in its header. A matching Host queue row replaces the echo and enables each action according to its normal text-content and running-state requirements. Prompt acknowledgement alone does not enable queue actions. A failed submission removes its echo and displays an error; the composer restores the failed draft when it is empty or still contains the previous automatic restoration, preserving subsequently typed text.
66
76
 
@@ -118,6 +128,10 @@ try {
118
128
 
119
129
  The selector must be a pure function of the owner currency. Its non-null return is delivered to the component as `matched`; `PropsRuntime<'conversation.composer'>` supplies the standard Session and global props. Chain order remains ascending `priority`, then registration order, and the first non-null selector wins. The shell keeps the default composer mounted beneath a takeover. Request state, listeners, response encoding, and any request-specific child slots belong to the business package; they are not carried by `SessionSnapshot` or declared by this core package.
120
130
 
131
+ `InputActions.captureInsertion()` captures the draft selection and revision; `insertText(text, span)` inserts one undoable plain-text edit only while that revision is current and the editor permits editing. Asynchronous consumers retain rejected results for user action.
132
+
133
+ `conversation.input.activity` hosts one control between the model selector and Send. Its `onActiveChange` callback expands that control across the toolbar and hides ordinary accessory controls and the context meter while preserving the editor and submit action. Closing the activity restores those controls with context details closed. The empty hero dock remains collapsed when it has no content. The occupant releases expansion on unmount and owns any activity-specific feedback.
134
+
121
135
  <a id="model-experience"></a>
122
136
  ## Model Experience
123
137
 
package/README.zh.md CHANGED
@@ -25,12 +25,16 @@ kind: "package-reference"
25
25
  <a id="conversation-assembly"></a>
26
26
  ## Conversation 组装
27
27
 
28
- `UiConversation.events` 是 event Definition 的唯一 registry,`UiConversation.views` 是 target snapshot builder 的唯一 registry。两者都拒绝重复 key、保持注册顺序、返回幂等 disposer,并在 contribution roster 变化时重建现有 binding。`UiConversation.binding(bindingOrSessionId)` 为当前 Session Controller binding 返回 identity 稳定的 Conversation binding,不会另开事件源。
28
+ `UiConversation.events` 是 event Definition 的唯一 registry,`UiConversation.views` 是 target snapshot builder 的唯一 registry。两者都拒绝重复 key、保持注册顺序、返回幂等 disposer,并在 contribution roster 变化时重建现有 binding。`UiConversation.binding(bindingOrSessionId)` 为当前 Session Controller binding 返回 identity 稳定的 Conversation binding,不会另开事件源。 View Definition 可以声明 `toolCallFocus`,将工具调用 id 转换为自身的焦点标识。仅当此目标拥有可见的 View 条目时,Conversation 才提供 Inspect 回调;Chat 直接使用回调,不选择目标。
29
29
 
30
30
  适配器把每个 `SessionEventLikeEntry` 直接交给 assembler。外层 `type` 区分持久事件与 Client-only transient event,内部 `event` 则统一公开 `type`、`seq`、`time` 与 `data`;Definition 接收这个内部 `SessionEventLike`。replacement window 可以包含两种 entry,历史 prepend 携带持久 entry,实时 append 则可以携带任一种。两种事件都使用 Definition 的同一组 `match` 与 `update` 方法,`start` 只接收持久 event,assembler 会拒绝 transient start。不消费 Assistant delta 的 Definition 对 `assistant/live-chunk` 返回 `null`。replace window 或 revision 断档从完整已加载窗口重建;连续 revision 的 append、prepend 与 Assistant settlement 使用增量组装。settlement 只删除具名 attempt 的 transient match,应用可选持久 entry,并重放受影响的 Context 及其 dependent,不替换无关 target node。assembler 拥有 Context 匹配、Turn/Step location、target node 物化、target activity 和稳定 target source。`ConversationSnapshot` 只包含与 target 无关的 View 与 active-target 事实;Session lifecycle 状态仍属于 `SessionSnapshot`。
31
31
 
32
32
  shell 选择解析出 target 或 target source 收到首个 subscriber 时,该 target 进入 active 状态。assembler 从当前 Context 对它执行一次 replace,并使它参与后续增量 flush;创建 source 不会激活 target,取消订阅也不会停用 target。
33
33
 
34
+ `UiConversation.groups` 为每个已注册目标注册一个可选的业务 Group Definition。它在节点物化后消费投影后的节点变化、变化轮次及已索引的目标位置,覆盖首次激活,并拥有全部分段规则与组数据。按索引读取 Turn 时保留相邻 Node 造成的分隔,使业务更新可以限制在受影响轮次和组内。assembler 先校验并安装根引用和按键索引的组快照,再发布 Node、Group 和 Location 数据来源。未分组目标保持原有路径。[分组](../../../docs/subsystems/conversation.zh.md#group-definitions)定义输入有效期、类型化注册、原子更新及渲染器职责。
35
+
36
+ Group 注册时缺少 View 目标会报错。已注册的 View Definition 被移除后,其分组计算暂停并清空已发布结果,但保留 Group Definition;重新注册该 View 后,沿现有替换流程从当前已加载时间线重建。切换 View 页签不会移除 View Definition。
37
+
34
38
  target package 通过 declaration merge 扩展 snapshot 与 Location data map,再调用 `ctx.uiConversation.events.register(...)` 和 `ctx.uiConversation.views.register(...)`。target 通过 `ctx.uiConversation.binding(binding).target(targetId)` 读取其 Session-owned source。注册属于 Cordis effect,返回的 disposer 从同一个 registry 移除 contribution。共享的请求检查服务于每个 target:`ctx.uiConversation.inspectSystemPrompt(previous, event)` 将系统消息与位置替换解释为不可变的已加载 surface 状态。它按 surface 顺序选择最后一个非空的存活系统节点,为连续重写只保留存活的替换位置;遇到未建立索引的更早端点后,提示词保持不可用,直到向前补页回放提供其顺序。target 自有的 Definition 独立保留历史卡片。`ctx.uiConversation.inspectRequestPrompt(previous, header, system)` 根据该有效提示词分类请求变更;普通消息与流式分片无需处理系统状态。
35
39
 
36
40
  <a id="shell-and-standard-props"></a>
@@ -38,8 +42,12 @@ target package 通过 declaration merge 扩展 snapshot 与 Location data map,
38
42
 
39
43
  共享图片插槽属性将展示选择与持久化引用分开:`thumbnail` 请求完整缩放的附件列表缩略图,`compact` 请求裁剪的图片方块。每张图片可通过可选的 `label` 提供无障碍展示名称;加载和缓存标识仍使用原始附件引用。[ui-attachment](../ui-attachment/README.zh.md) 负责渲染与灯箱。
40
44
 
45
+ 控件组的尺寸、内容、可见性或字体加载状态变化后,composer 测量展开状态下的控件组。若无法排在同一行,控制栏为模型位设置 `--dsh-composer-model-text-display: none` 和 `--dsh-composer-model-icon-display: block`;两者默认值分别为 `block` 和 `none`。若连图标也放不下,仍允许换行。
46
+
41
47
  上下文占用按钮在输入卡片下方、会话统计右侧显示圆环和百分比。点击按钮可在视口内的面板查看 token 构成,没有统计项时面板也不会越界;上下文用量和容量尚不可用时,按钮保持隐藏。
42
48
 
49
+ 关闭开发者工具时,外壳仅隐藏 Trajectory;其他插件贡献的 View 仍然可用。可用 View 少于两个时隐藏 View 标签栏。在 Trajectory 激活时关闭开发者工具会返回 Chat;已保存的 View 偏好和 Session 记录保持不变。开启后,Trajectory 恢复可用。View 所有者接收可用列表,使导航操作跟随相同的可见性。
50
+
43
51
  输入框注册「文件」命令动作,负责其标题、可用性和原生文件选择器回调。菜单可用性与实际调用都读取已挂载输入框当前的附件接收策略。输入框卸载或锁定后该动作不可用,插件 dispose(资源释放)时移除注册。回调绑定留在输入模块内部。
44
52
 
45
53
  `SessionInputShell` 通过私有 [DraftEditorRuntime](src/client/input/editor/runtime.ts) 为每个 Session 持有一个 Lexical editor,同时保留提交、附件选择和恢复决策。[DraftEditor](src/client/input/editor/DraftEditor.tsx) 呈现借用的 editor;InputBar 保留钩子与 refs,并通过 [view-binding](src/client/input/editor/view-binding.ts) 安装 DOM 行为。编辑器类型位于 [draft-editor.ts](src/client/contract/draft-editor.ts),共享输入和提交类型位于 [input.ts](src/client/contract/input.ts)。这一拆分不支持同一 Session 同时挂载多个可编辑 root;[两阶段隔离提案](../../../.agents/notes/proposed/architecture/2026-09-14-composer-model-and-draft-editor.zh.md) 定义剩余工作。
@@ -48,19 +56,21 @@ target package 通过 declaration merge 扩展 snapshot 与 Location data map,
48
56
 
49
57
  工作区选择使用 `uiWorkspace.openWorkspace` 准备目标并提交导航。草稿文字和附件仅在该请求仍为当前请求时,通过它的同步准备回调搬移;后续导航或所有者释放会保留原草稿。
50
58
 
51
- 本包占据 root 作用域 `main` 中的 `conversation` key。其 `main.conversation` shell strict Session Header 保留在 optional-Session `conversation.content` Component Factory 外。Factory 拥有共享正文与 Composer,通过其标准 Hook 读取当前 Session,并公开 strict-Session `views` 与 root-scoped `widthControls` 两个局部位置。默认 adapter 渲染现有 `conversation.session` entry,主 occurrence 选择宽度拖拽条;嵌入式 occurrence 可以替换 `views`、省略拖拽条,且不渲染主 Header。`ctx.uiSession.provide()` 从同一个 Session binding 物化 Conversation 与 input source,并将 `inputActions` 作为稳定标准 prop 提供。
59
+ 本包占据 root 作用域 `main` 中的 `conversation` key。其 `main.conversation` 外壳将常驻的 `conversation.header` 放在可选 Session `conversation.content` Component Factory 外。未选中 Session 时,头部仍承载根作用域导航;标题、操作和 View 标签保留在严格 Session 子组件中。Factory 拥有共享正文与 Composer,通过其标准 Hook 读取当前 Session,并公开 strict-Session `views` 与 root-scoped `widthControls` 两个局部位置。默认 adapter 渲染现有 `conversation.session` entry,主 occurrence 选择宽度拖拽条;嵌入式 occurrence 可以替换 `views`、省略拖拽条,且不渲染主 Header。共享正文与 Composer 注册 queue dock 和 Todo dock。Todo dock 在 composer 上方使用共享面板 elevation;其中的行分别以共享 idle、ongoing 与 done 标记表示待处理、进行中与已完成。`ctx.uiSession.provide()` 从同一个 Session binding 物化 Conversation 与 input source,并将 `inputActions` 作为稳定标准 prop 提供。
52
60
 
53
- blank Session 保留 header 的 leading 与 corner 控件,包括右侧栏展开入口,同时隐藏标题、actions、utilities 和 View tabs。选择 Workspace 会创建这些控件所需的 Session,无需先发送消息。没有选中 Session 时,strict header 不挂载。侧栏各入口仍遵循自身的数据与执行环境要求。
61
+ blank Session 保留 header 的 leading 与 corner 控件,包括右侧栏展开入口,同时隐藏标题、actions、utilities 和 View tabs。选择 Workspace 会创建这些控件所需的 Session,无需先发送消息。没有选中 Session 时,strict header 不挂载;常驻容器在 macOS 桌面保留 40px 拖拽区域,在 Web、Windows 或 Linux 上不预留空白高度。侧栏各入口仍遵循自身的数据与执行环境要求。 已开始的 Session 在可用 View 少于两个时使用单行标题栏,仅在渲染标签行时保留其高度。
54
62
 
55
63
  View 选择规则固定:有效且已注册的持久化选择优先,其次是已注册的 `chat`,否则不渲染 View;绝不选择第一个已注册 View。Shell phase 只组合 Session lifecycle 与 active-target set,不读取任何 target-specific 快照。
56
64
 
57
65
  Session 首次绑定或缓存的 Session 成为 current 时,shell 会在渲染前读取持久化 View 偏好,激活已注册的偏好 View 或 Chat fallback,并在后续 tab 或 focus 选择写入 store 前先激活对应 target。blank Session 仍不渲染 `conversation.view` slot;未选中的 target 不会激活。
58
66
 
59
- 主 occurrence 的活跃 transcript 只在未被内容覆盖的两侧沟槽中提供正文宽度拖拽条;嵌入式 occurrence 省略这些拖拽条。View 如果绘制进沟槽,只将具体的可见元素提到拖拽条上方;透明的全宽包装层保持在下方,不会占用空白沟槽。该规则要求此元素与 Conversation body 之间不能引入中间堆叠上下文;浏览器场景固定了交付 Chromium 的行为。Chat 将该规则用于表格元素,其限定在阅读列内的工具卡片无需提高层级。指针位于拖拽条上时,滚轮仍会滚动 transcript,Ctrl+滚轮则保留为浏览器缩放手势。粘滞 composer 刻意拥有完整的底部区带,该区域不是宽度调整目标;已捕获的拖拽会将指示线提高到松开为止([决策](../../../.agents/notes/implemented/bug-fix/2026-09-14-transcript-width-handle-layering.zh.md))。
67
+ 主 occurrence 的活跃 transcript 只在未被内容覆盖的两侧沟槽中提供正文宽度拖拽条;嵌入式 occurrence 省略这些拖拽条。View 如果绘制进沟槽,只将具体的可见元素提到拖拽条上方;透明的全宽包装层保持在下方,不会占用空白沟槽。该规则要求此元素与 Conversation body 之间不能引入中间堆叠上下文;浏览器场景固定了交付 Chromium 的行为。Chat 将该规则用于表格元素,其限定在阅读列内的工具卡片无需提高层级。指针位于拖拽条上时,滚轮仍会滚动 transcript,Ctrl+滚轮则保留为浏览器缩放手势。粘滞 composer 刻意拥有完整的底部区带,该区域不是宽度调整目标;已捕获的拖拽会将指示线提高到松开为止([拖动手柄样式](src/client/skeleton/ConversationRoot.module.css))。
60
68
 
61
- 常驻 composer 在无 Session 与有 Session 之间保持挂载。输入空白字符会隐藏占位提示;没有附件的纯空白草稿无法发送。无 Session 时,同一个编辑器表面保持 inert,Workspace picker 连接 blank Session。该表面是 shell 所有的 Lexical 编辑器:引用 chip 是携带 owner 序列化身份的原子 decorator 节点(提交时经 owner codec 展开),已认领的 slash command 保持为带样式的行首文本,文件夹文本引用以图标前缀携带文件夹图形,草稿的剪贴板投影镜像到逐 Session Conversation store。QueueDock 直接从 Session 的 `inbox` 投影读取 `next-turn`,包含从冷状态恢复的消息。Queue 操作通过 scoped `ctx.conversation` service 寻址准确的 queue occurrence;queue 预览经 `ui-primitives` 的共享行内引用投影渲染已发送文本(wire 会话形式折叠为其标签),并按原始附件顺序展示本地或持久化的图片和文件。图片使用缩略图,文件使用紧凑的名称与大小卡片。编辑态展示字面发送文本,持久化缩略图通过会话图片 URL 缓存解析。繁忙时 Enter 行为保存在 Host-backed `ui-conversation` settings namespace。 composer 键盘映射经斜杠流水线裁决触发菜单的按键——Tab 确认高亮补全项(可下钻项则下钻),Escape 与 Shift+Tab 离开菜单且不选定——其余按键交给编辑器自身。 接管键盘的浮层通过 `SessionInput.focus()` 把键盘还回来,该路径走 Lexical 自己的 focus,因此光标回到草稿原来的位置而不是开头。
69
+ 宽度拖拽条的指示线只在已捕获指针的拖拽期间跟随指针,普通悬停不改变其位置。
62
70
 
63
- 默认发送采用乐观提交:Enter 在同一事务里清空草稿、occurrence 表和撤销历史,composer 保持 `plain`,发送作为 detached attempt 运行,发送期间可以继续输入和提交。`sendSession` 在序列化之前用投递模式注册 Session 提交回显(`session.beginSubmission`),并在 `pendingSubmissions` 中保留图片与文件的选择顺序;Session 根据该模式与当前运行状态推导位置,因此空闲发送进入 transcript(文本记录),繁忙时 Queue 进入 QueueDock,繁忙时 Steer 进入 pending-steering 区域。随后让出一帧,图片经浏览器原生 `FileReader` data-URL 路径编码,文件则引用已暂存凭证。命令提交也用同一凭证表示通用文件,因此发送 `/goal` `/plan` 时不会再次读取这些浏览器文件。提示词复用提交 `requestId`;queue 或历史以同一 `rpcId` 被观察后,回显只退休一次。多个并发发送失败时,在用户编辑还原内容之前按提交顺序合并还原;命令提交保持冻结的 `submitting` 阶段。Detached attempt 持有附件 id,直到 admission 完成或 Session scope 销毁。回显以 observed 退休时,durable 图片缓存立即公开每个预览 URL,读取 admitted 附件后用规范化 URL 替换预览,并在各 URL 停止使用后撤销,同时释放文件卡。选中的通用文件进入同一个先进先出的后台上传队列;`maxConcurrentFileUploads` 默认允许两个 Worker transport 同时运行,Conversation 服务在切换 Session 时继续持有排队和运行中的传输操作及字节进度,移除草稿会跳过排队中的传输或中止正在运行的传输。continuable 子代理禁用附件入口,也不创建本地回显,因为其 transport 不保留浏览器 request id。
71
+ 常驻 composer 在无 Session 与有 Session 之间保持挂载。输入空白字符会隐藏占位提示;没有附件的纯空白草稿无法发送。无 Session 时,同一个编辑器表面保持 inert,Workspace picker 连接 blank Session。该表面是 shell 所有的 Lexical 编辑器:引用 chip 是携带 owner 序列化身份的原子 decorator 节点(提交时经 owner codec 展开),已认领的 slash command 保持为带样式的行首文本,文件夹文本引用以图标前缀携带文件夹图形,草稿的剪贴板投影镜像到逐 Session Conversation store。QueueDock 从 Session 的 `inbox` 投影读取 `next-turn`,包含从冷状态恢复的消息,仅排除仍由本地 transcript 提交承接的 requestId。其他排队行保留正常展示和操作。Queue 操作通过 scoped `ctx.conversation` service 寻址准确的 queue occurrence;queue 预览经 `ui-primitives` 的共享行内引用投影渲染已发送文本(wire 会话形式折叠为其标签),并按原始附件顺序展示本地或持久化的图片和文件。图片使用缩略图,文件使用紧凑的名称与大小卡片。编辑态在可随内容增高的 textarea 中展示字面发送文本,因此重新编辑不会丢失换行;Enter 保存,Shift+Enter 换行,Escape 取消。持久化缩略图通过会话图片 URL 缓存解析。繁忙时 Enter 行为保存在 Host-backed `ui-conversation` settings namespace。 composer 键盘映射经斜杠流水线裁决触发菜单的按键——Tab 确认高亮补全项(可下钻项则下钻),Escape Shift+Tab 离开菜单且不选定——其余按键交给编辑器自身。 接管键盘的浮层通过 `SessionInput.focus()` 把键盘还回来,该路径走 Lexical 自己的 focus,因此光标回到草稿原来的位置而不是开头。
72
+
73
+ 默认发送采用乐观提交:Enter 在同一事务里清空草稿、occurrence 表和撤销历史,composer 保持 `plain`,发送作为 detached attempt 运行,发送期间可以继续输入和提交。`sendSession` 在序列化之前用投递模式注册 Session 提交回显(`session.beginSubmission`),并在 `pendingSubmissions` 中保留图片与文件的选择顺序;Session 根据该模式与当前运行状态推导位置,因此空闲发送进入 transcript(文本记录),繁忙时 Queue 进入 QueueDock,繁忙时 Steer 进入 pending-steering 区域。随后让出一帧,图片经浏览器原生 `FileReader` data-URL 路径编码,文件则引用已暂存凭证。命令提交也用同一凭证表示通用文件,因此发送 `/goal` 或 `/plan` 时不会再次读取这些浏览器文件。提示词复用提交 `requestId`;Session 按同一 `rpcId` 关联展示接管,并仅退休回显一次。多个并发发送失败时,在用户编辑还原内容之前按提交顺序合并还原;命令提交保持冻结的 `submitting` 阶段。Detached attempt 持有附件 id,直到 admission 完成或 Session scope 销毁。回显以 observed 退休时,durable 图片缓存立即公开每个预览 URL,读取 admitted 附件后用规范化 URL 替换预览,并在各 URL 停止使用后撤销,同时释放文件卡。选中的通用文件进入同一个先进先出的后台上传队列;`maxConcurrentFileUploads` 默认允许两个 Worker transport 同时运行,Conversation 服务在切换 Session 时继续持有排队和运行中的传输操作及字节进度,移除草稿会跳过排队中的传输或中止正在运行的传输。浏览器 shell 暴露 `__DSH_HOST_PATHS__` 时(桌面应用),拖入或粘贴的文件夹以及拖入、选择或粘贴的带真实路径的非图片文件会成为 `@路径` chip;图片仍然上传。拖放和粘贴通过浏览器 entry API 识别目录;该 API 不可用或没有返回 entry 时,粘贴项沿用普通文件处理。文件选择器不能选择目录。引用需要启用 `ui-reference` 插件,原路径也必须仍可由模型的文件工具读取。工作区内的路径使用相对形式,其他路径保留绝对形式。整批文件先校验再插入,保留来源顺序和已选中的文字,引用之间有空白分隔,含空格的路径使用闭合引号。没有该桥的浏览器会拒绝拖入或粘贴的文件夹,桌面端无法获取文件夹路径时单独报错。continuable 子代理禁用附件入口,也不创建本地回显,因为其 transport 不保留浏览器 request id。
64
74
 
65
75
  排队提交的本地回显在禁用的编辑、删除、插话按钮旁显示“发送中…”;折叠后的队列在标题栏保留发送状态。匹配的 Host 队列行替换回显后,各操作按原有的纯文本内容和运行状态要求启用。仅收到提示词确认不会启用队列操作。提交失败会移除回显并显示错误;输入框为空或仍保留上一次自动恢复的内容时,composer 恢复失败草稿,保留用户随后输入的文字。
66
76
 
@@ -118,6 +128,10 @@ try {
118
128
 
119
129
  selector 必须是 owner currency 的纯函数。非 null 返回值作为 `matched` 传给组件;`PropsRuntime<'conversation.composer'>` 提供标准 Session 与 global props。Chain 顺序仍按 `priority` 升序,再按注册顺序;首个返回非 null 的 selector 获选。Shell 会在 takeover 下保持默认 composer 挂载。Request 状态、listener、response encoding 和任何 request-specific child slot 都属于业务 package,不进入 `SessionSnapshot`,也不由 core 包声明。
120
130
 
131
+ `InputActions.captureInsertion()` 捕获草稿选区与版本;`insertText(text, span)` 仅在版本未变且编辑器允许编辑时,插入一次可撤销的纯文本编辑。异步消费者在插入被拒绝后负责保留结果,等待用户操作。
132
+
133
+ `conversation.input.activity` 在模型选择器与发送按钮之间承载一个控件。其 `onActiveChange` 回调将控件展开至整条工具栏并隐藏普通辅助控件和上下文用量按钮,同时保留编辑器与提交按钮。关闭活动后恢复这些控件,上下文详情保持关闭。首页输入框下方没有内容时,该区域保持收起。占用者在卸载时释放展开状态,并拥有活动专属反馈。
134
+
121
135
  <a id="model-experience"></a>
122
136
  ## 模型体验
123
137