@deepseek-ai/dsh-client-ui-workspace 0.2.0-rc.2 → 0.2.1-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.
package/README.i18n.yaml CHANGED
@@ -18,8 +18,8 @@
18
18
  en: b164014ee565be16
19
19
  zh: 6a741647932b93e6
20
20
  /deepseek-ai-dsh-client-ui-workspace/use-this-package/reordering-and-view-options:
21
- en: 1fe0693758e0536b
22
- zh: ef30198bc767fc04
21
+ en: 8673afc1d72ad077
22
+ zh: 9fbb83a4029c5d10
23
23
  /deepseek-ai-dsh-client-ui-workspace/use-this-package/workspace-hierarchy:
24
24
  en: 5e049b90c55eddb3
25
25
  zh: e06dc8d0d25c013f
@@ -27,20 +27,20 @@
27
27
  en: 9eab3bfbaf98c987
28
28
  zh: 54e7e141dae5c9e8
29
29
  /deepseek-ai-dsh-client-ui-workspace/use-this-package/managing-sessions:
30
- en: 6b5c96c5b723c5f8
31
- zh: f8ae5f73860dce45
30
+ en: cafc87e8e0ac4058
31
+ zh: 4c35bc5bfa24ba6c
32
32
  /deepseek-ai-dsh-client-ui-workspace/use-this-package/pending-interactions:
33
33
  en: 9fb88260380bf093
34
34
  zh: 3bace2b96ecd4a82
35
35
  /deepseek-ai-dsh-client-ui-workspace/use-this-package/active-schedule-markers:
36
- en: dd0a7799c76c0f94
37
- zh: 48d1cdacc9168aa2
36
+ en: 04956ad6ba31cc86
37
+ zh: 0e5fb72cfa932d25
38
38
  /deepseek-ai-dsh-client-ui-workspace/understand-the-implementation:
39
- en: 21590da1f7488361
40
- zh: 95916475cced409e
39
+ en: 721d5a49fe231191
40
+ zh: 3ef1dd720219e00e
41
41
  /deepseek-ai-dsh-client-ui-workspace/understand-the-implementation/the-directory-flow-hole:
42
- en: 978415ec6b2e0cc0
43
- zh: 0d5c705ff34fb1da
42
+ en: 868904dbfb5713f8
43
+ zh: 225906bfef7398bf
44
44
  /deepseek-ai-dsh-client-ui-workspace/understand-the-implementation/session-row-actions:
45
45
  en: a3b80d0e844d2929
46
46
  zh: 85fe5dfb2968b491
@@ -69,5 +69,5 @@
69
69
  en: 00d7b1ddda7390f5
70
70
  zh: 1e5a1055d1be4d1d
71
71
  /deepseek-ai-dsh-client-ui-workspace/known-limitations-and-deferred-work/dev-note:
72
- en: 585813ff2c68c20c
73
- zh: 18b43c0d64fa8a72
72
+ en: e67f633dc6fe7773
73
+ zh: 3bb0c20c93f16f98
package/README.md CHANGED
@@ -37,7 +37,7 @@ Pinned Sessions lead ordinary Sessions in both grouped and flat views. **Last up
37
37
 
38
38
  Pinning moves the Session to the front of its complete saved sequence without changing the selected mode. It leads the pinned partition in Manual but need not lead it in Last updated. Unpinning leaves the saved position unchanged. Dragging either a pinned or ordinary row edits that same complete sequence and selects Manual; hidden archived members are retained. Missing pinned members prepend in pin-array order. New ordinary forks precede their sources in the complete sequence without inheriting pin membership; visible pins still lead ordinary rows. Other missing members append by recency with missing archives last. Supplementation stays in memory until a Session-order write records the complete result. Archive filtering alone changes neither saved positions nor account membership.
39
39
 
40
- The selected blank **New Session** retains its provisional first slot and cannot be dragged; after its first prompt it becomes an ordinary draggable row, retaining that position in Manual or following its current timestamp in Last updated. A collapsed-group drag uses the target Session identity and keeps the source visible. Session display orders for real Workspaces, Ungrouped, and the flat list are browser-local; Workspace group drag order remains Host-durable. The [Session pinning and archive decision](../../../.agents/notes/implemented/feature/2026-09-18-session-pin-and-sidebar-archive.md) records the ordering and recovery rules.
40
+ The selected blank **New Session** retains its provisional first slot and cannot be dragged; after its first prompt it becomes an ordinary draggable row, retaining that position in Manual or following its current timestamp in Last updated. A collapsed-group drag uses the target Session identity and keeps the source visible. Session display orders for real Workspaces, Ungrouped, and the flat list are browser-local; Workspace group drag order remains Host-durable.
41
41
 
42
42
  ### Workspace hierarchy
43
43
 
@@ -51,13 +51,13 @@ Collapsed search is one header action beside the view and add actions: activatin
51
51
 
52
52
  ### Managing sessions
53
53
 
54
- The Session row's Rename action opens a dialog prefilled with the row's display title; confirming an unchanged title is deliberately allowed — it pins the current automatic title against regeneration. Double-clicking a title also opens Rename; for an unarchived Session, the preceding clicks open its conversation. Rename uses a temporary `workspaceOperation` reference and awaits its initial history opening. The row's Fork action forks at the source's last completed turn and increments the inherited persisted title through Session Controller without retaining the child, opening its history, or changing selection. Workspace Delete opens a confirmation that states the retention boundary; success removes the group while its Sessions remain under Ungrouped. Pin, Rename, Fork, and Archive are themselves entries of the `sidebar.workspaces.session.menu.item` list (pin and archive also of `sidebar.workspaces.session.row.action`), so a client plugin's action takes whatever position its `order` gives it.
54
+ The Session row's Rename action opens a dialog prefilled with the row's display title; confirming an unchanged title is deliberately allowed — it pins the current automatic title against regeneration. Double-clicking a title also opens Rename; for an unarchived Session, the preceding clicks open its conversation. Rename uses a temporary `workspaceOperation` reference and awaits its initial history opening. The row's Fork action reads ordinary cold history normally. When the Host reports that source migration is required, an app-wide Toast offers to open that Session; the action only navigates and does not retry fork. A successful fork uses the source's last completed turn and increments the inherited persisted title through Session Controller without retaining the child, opening its history, or changing selection. Workspace Delete opens a confirmation that states the retention boundary; success removes the group while its Sessions remain under Ungrouped. Pin, Rename, Fork, and Archive are themselves entries of the `sidebar.workspaces.session.menu.item` list (pin and archive also of `sidebar.workspaces.session.row.action`), so a client plugin's action takes whatever position its `order` gives it.
55
55
 
56
56
  Archive commits without a confirmation dialog for a quiet Session and retains the Session's account position. A Session with running work is the one case that asks first: the Host refuses the plain archive and names that work, and the sidebar opens a stop-and-archive dialog listing it by family — the turn in progress, running subagents, background jobs, scheduled reminders, each with its labels — with the restore path stated; confirming asks the Host to stop the work the way the stop button does and archives once the archive set is durable, while the stops settle in the background, and cancelling leaves the Session running and visible. View options control visibility through one explicit three-way choice: Hide archived (the default) hides archived Sessions, All conversations (show archived) includes them, and Archived only hides ordinary Sessions and drops Workspaces without an archived Session; in tree grouping, children of a dropped Workspace nest under the nearest shown ancestor. Visible archived rows are grayed and carry an accessible explanation that they cannot be opened until restored; Rename, Fork, and Unarchive remain available. A successful archive shows a notice with an undo action and — while archived rows are hidden — a "filter archived sessions" action that switches the filter to All conversations (show archived); a stop-and-archive shows the same notice under its own wording, and undo restores the Session without resuming the stopped work. Unarchive removes the archive mark without restoring a pin or changing the saved position. An empty list shows a centered glyph-over-text placeholder; the Archived-only view names its own emptiness (No archived sessions yet) and adds a View other sessions text action that switches the filter back to Hide archived.
57
57
 
58
58
  Session update times use the tertiary label color, including on archived rows. A Session title wider than its row is clipped with an ellipsis at rest. Hovering the row scrolls the title to its far edge — the incremented title of a fork, for example — and reveals it without the ellipsis; leaving the row returns the title to its start.
59
59
 
60
- The keyboard reference exposes New Session, Search sessions, Add workspace, Rename session, Fork session, and Archive session. Desktop defaults use the platform's primary modifier, with Mod+K for search; Windows and macOS Web use the [shortcut service’s platform defaults](../shortcuts/README.md); Linux Web leaves these commands unbound until configured. Button tooltips and Session row menus display the effective bindings. Clicking a menu item targets that row; pressing its shortcut targets the main Session. Desktop and Windows/macOS Web rename use Mod+Alt+G, leaving Mod+Shift+R to browser reload. Rename requires a nonblank main Conversation with no modal obscuring it; a retained Session behind another main panel is unavailable. Other Windows and macOS Desktop bindings also execute from terminal input and modal dialogs; other environments follow the command's region and modal restrictions. Search and rename requests belong to this package; input drafts remain in the browser. The directory picker rejects another open while selection or Workspace adoption is pending. Fork captures the source Session and uses the row action's Host operation to select its last completed turn, without loading older Client history. Missing or blank Sessions are unavailable; the Host rejects a source with no completed turn. A rejected shortcut fork preserves the current selection, displays a localized notification, and can be retried; unexpected failures also retain diagnostic logging.
60
+ The keyboard reference exposes New Session, Search sessions, Add workspace, Rename session, Fork session, and Archive session. Desktop defaults use the platform's primary modifier, with Mod+K for search; Windows and macOS Web use the [shortcut service’s platform defaults](../shortcuts/README.md); Linux Web leaves these commands unbound until configured. Button tooltips and Session row menus display the effective bindings. Clicking a menu item targets that row; pressing its shortcut targets the main Session. Desktop and Windows/macOS Web rename use Mod+Alt+G, leaving Mod+Shift+R to browser reload. Rename requires a nonblank main Conversation with no modal obscuring it; a retained Session behind another main panel is unavailable. Other Windows and macOS Desktop bindings also execute from terminal input and modal dialogs; other environments follow the command's region and modal restrictions. Search and rename requests belong to this package; input drafts remain in the browser. The directory picker rejects another open while selection or Workspace adoption is pending. Fork captures the retained main-view Session, for which navigation has requested history opening, and keeps automatic migration. It uses the row action's Host operation to select its last completed turn, without loading older Client history. Missing or blank Sessions are unavailable; the Host rejects a source with no completed turn. A rejected shortcut fork preserves the current selection, displays a localized notification, and can be retried; unexpected failures also retain diagnostic logging.
61
61
 
62
62
  ### Pending interactions
63
63
 
@@ -69,11 +69,11 @@ Grouped and flat Session rows show a clock mark when the Session has active sche
69
69
 
70
70
  -----
71
71
 
72
- `ctx.uiWorkspace.openSession(target)` synchronously replaces the owned `mainView` reference and returns the main area to Conversation without waiting for `reference.ready`, so history loading renders inside the selected Session view. The target may be a known Session id or a durable direct-parent subagent address; an explicit address does not require a preloaded parent catalog. `openWorkspace(id, beforeOpen?)` opens its result only if no later navigation has superseded the request; New Session uses `openWorkspace`. `forkSession(id)` creates the child without navigating or superseding a pending navigation. The optional synchronous preparation callback runs after the target is retained and only for a current Workspace request, so superseded requests do not move composer drafts. Navigation or owner disposal suppresses the late UI commit, not the underlying Session creation. Startup restoration retains its main reference without changing the selected panel or cancelling a later navigation. Archiving the main Session releases its reference and clears the main selection. Selection failure leaves a global panel visible. Session rows read `usePanelInfo` to suppress their selected appearance while a global panel is active; search and directory-picker focus alone do not leave that panel.
72
+ `ctx.uiWorkspace.openSession(target)` synchronously replaces the owned `mainView` reference and returns the main area to Conversation without waiting for `reference.ready`, so history loading renders inside the selected Session view. The target may be a known Session id or a durable direct-parent subagent address; an explicit address does not require a preloaded parent catalog. `openWorkspace(id, beforeOpen?)` opens a Workspace blank only if no later navigation has superseded the request; an explicit New Session creates a fresh Session. `forkSession(id)` creates the child without navigating or superseding a pending navigation. The optional synchronous preparation callback runs after the target is retained and only for a current Workspace request, so superseded requests do not move composer drafts. Navigation or owner disposal suppresses the late UI commit, not the underlying Session creation. Startup restoration retains its main reference without changing the selected panel or cancelling a later navigation. Archiving the main Session releases its reference and clears the main selection. Selection failure leaves a global panel visible. Session rows read `usePanelInfo` to suppress their selected appearance while a global panel is active; search and directory-picker focus alone do not leave that panel.
73
73
 
74
74
  Navigation and startup restoration obtain the selected Session's projections from its `follow`; neither issues a separate projection refresh for that Session or its parent.
75
75
 
76
- New Session tries to acquire the first eligible blank in catalog order; startup restoration tries the saved blank. If that writer is held, navigation creates a new Session without trying other blanks. Other acquisition failures abort the request: an explicit New Session or hero Workspace pick shows the failure as a transient notice quoting the Host's code and message (for example a preset that fails to mount) or, for a failure that is not a Host refusal, that failure's own message; a request a later navigation or owner disposal superseded raises no notice, and startup restoration reports only to the console. Released blanks retain their slash-command state when reused. Later navigation cancels a pending startup selection.
76
+ Workspace reopening and explicit draft preparation try to acquire the first eligible blank in catalog order; startup restoration tries the saved blank. If that writer is held, navigation creates a new Session without trying other blanks. Other acquisition failures abort the request: an explicit New Session or hero Workspace pick shows the failure as a transient notice quoting the Host's code and message (for example a preset that fails to mount) or, for a failure that is not a Host refusal, that failure's own message; a request a later navigation or owner disposal superseded raises no notice, and startup restoration reports only to the console. Released blanks retain their slash-command state when reused. Later navigation cancels a pending startup selection.
77
77
 
78
78
  Once both Workspace and Session startup baselines are ready, an empty installation calls `workspaces.initializeDefault` and creates or reuses its blank Session. The composer becomes editable when that Session is selected; no message is submitted automatically. Later navigation or owner disposal prevents startup from selecting its result. Ineligible first use leaves the folder picker available without an error. Default Workspace creation failure shows a transient notice directing the user to Choose workspace, and is not retried until the next startup. Session creation failures use the ordinary restoration error handling. The registered Workspace remains available if Session creation or later submission fails.
79
79
 
@@ -89,9 +89,11 @@ The package is one composition: both target slots are declared by other plugins,
89
89
 
90
90
  The browser entry also declares two root-scoped `list` child seats on each Session row: `sidebar.session.row.leading`, rendered only while that row's primary status is idle and left blank on an archived row, and `sidebar.session.row.hover`, mounted only while that row's hover card is open. Both take the row's Session identity and nothing else, so an occupant reads its own data by that id; a Session-scoped seat would force a Session binding, which would activate and retain every listed Session.
91
91
 
92
+ Client callers can pass `prompt` and `clearPreviousDraft` in the second argument to `uiWorkspace.startSession`. A prompt is plain text; structured drafts are handled internally during restoration and Workspace transfer. Without clearing, existing text or attachments are preserved; explicit clearing replaces only the target Session's text and inline references, leaving attachments intact. Option values are captured before asynchronous creation and applied to the exact retained binding before navigation commits. Superseded navigation does not write content; a submission-blocked input refuses preparation and keeps the previous selection. Calls without options create a fresh Session using the current Agent preset definition. Workspace reconnection and explicit draft preparation may reuse an existing blank Session. The [draft initialization decision](../../../.agents/notes/implemented/architecture/2026-09-30-structured-draft-initialization.md) describes the input owner's data flow.
93
+
92
94
  ### The directory-flow hole
93
95
 
94
- Each registration declares a **directory-flow child hole** (`single` kind: `conversation.hero.workspace.directoryFlow` / `sidebar.workspaces.directoryFlow`) that the composed picker package's client half fills with its picking interaction — the `-native` backend's renderless OS-chooser driver, an in-app browsing dialog under a `-browse` composition. The flat **Add workspace...** action renders only while the surface's hole is occupied; an empty hole means the composition has no picking affordance. This package owns the trigger and the adoption: the occupant reports one picked path per open through the hole's owner conversation (`open`/`busy`/`onPicked`/`onCancel`/`onError`), and the owner adopts it through the object layer, selecting the committed Workspace only after its list projection has refreshed.
96
+ Each registration declares a **directory-flow child hole** (`single` kind: `conversation.hero.workspace.directoryFlow` / `sidebar.workspaces.directoryFlow`) that the composed picker package's client half fills with its picking interaction — the `-native` backend's renderless OS-chooser driver, an in-app browsing dialog under a `-browse` composition. The flat **Add workspace...** action renders only while the surface's hole is occupied; an empty hole means the composition has no picking affordance. The sidebar Add workspace action preserves an existing blank Session and its draft. This package owns the trigger and the adoption: the occupant reports one picked path per open through the hole's owner conversation (`open`/`busy`/`onPicked`/`onCancel`/`onError`), and the owner adopts it through the object layer, selecting the committed Workspace only after its list projection has refreshed.
95
97
 
96
98
  ### Session row actions
97
99
 
@@ -224,5 +226,3 @@ These limits define the search depth, the archive surface, and the picking carri
224
226
  None.
225
227
 
226
228
  </details>
227
-
228
- **Runtime invariant:** No companion is published. This is a pure-consumer plugin that registers presentational components into two host-declared slots and registers its locale dictionaries; its inject face consists of stateless RPC wrappers plus a create-and-open call. It emits no Cordis events and owns no cross-plugin mutable state.
package/README.zh.md CHANGED
@@ -37,7 +37,7 @@ kind: "package-reference"
37
37
 
38
38
  置顶把 Session 移到完整保存序列的首位,但不切换所选模式。它在手动模式下排在置顶区最前,在最近更新模式下则不一定。取消置顶不改变保存的位置。拖拽置顶行或普通行都修改同一条完整序列并选择手动模式,隐藏的归档成员始终保留。缺失的置顶成员按置顶数组顺序补到头部。新增的普通 fork 在完整序列中插在来源之前,不继承置顶成员关系;可见置顶行仍排在普通行之前。其他缺失成员按最近更新时间追加,缺失归档项放在最后。补齐仅在内存中完成,直到一次 Session 顺序写入保存完整结果。单纯切换归档筛选既不改变保存的位置,也不改变成员关系。
39
39
 
40
- 当前选中的空白**新会话**保留临时首位且无法拖拽;首条提示词落地后,它成为可拖拽的普通行,在手动排序中保留该位置,在最近更新中按当前时间戳排列。折叠分组的拖拽使用目标 Session 身份,并保持来源行可见。真实 Workspace、Ungrouped 与单列表的 Session 显示顺序都保留在浏览器本地;Workspace 分组的拖拽顺序仍由 Host 持久化。[会话置顶与归档决定](../../../.agents/notes/implemented/feature/2026-09-18-session-pin-and-sidebar-archive.zh.md)记录排序与恢复规则。
40
+ 当前选中的空白**新会话**保留临时首位且无法拖拽;首条提示词落地后,它成为可拖拽的普通行,在手动排序中保留该位置,在最近更新中按当前时间戳排列。折叠分组的拖拽使用目标 Session 身份,并保持来源行可见。真实 Workspace、Ungrouped 与单列表的 Session 显示顺序都保留在浏览器本地;Workspace 分组的拖拽顺序仍由 Host 持久化。
41
41
 
42
42
  ### 工作区层级
43
43
 
@@ -51,13 +51,13 @@ kind: "package-reference"
51
51
 
52
52
  ### 管理会话
53
53
 
54
- Session 行内的 Rename 操作打开一个以该行显示标题预填的对话框;确认未修改的标题是有意允许的——这正是把当前自动标题钉住、不再被重新生成覆盖的手势。双击标题也会打开 Rename;对于未归档 Session,先发生的点击会打开其对话。Rename 使用临时 `workspaceOperation` reference,并等待首次历史打开。行内 Fork 在源会话最后一个已完成轮次处 fork,通过 Session Controller 递增继承的持久化标题,不 retain 子会话、不打开其历史,也不改变选择。Workspace 行内的 Delete 操作会打开确认框,说明保留边界;成功后该分组被移除,其 Session 则留在 Ungrouped 下。Pin、Rename、Fork、Archive 本身就是 `sidebar.workspaces.session.menu.item` 列表的条目(pin 与 archive 同时也是 `sidebar.workspaces.session.row.action` 的条目),因此客户端插件的 action 由其 `order` 决定落在哪个位置。
54
+ Session 行内的 Rename 操作打开一个以该行显示标题预填的对话框;确认未修改的标题是有意允许的——这正是把当前自动标题钉住、不再被重新生成覆盖的手势。双击标题也会打开 Rename;对于未归档 Session,先发生的点击会打开其对话。Rename 使用临时 `workspaceOperation` reference,并等待首次历史打开。行内 Fork 正常读取普通冷历史。Host 报告源会话需要迁移时,应用级 Toast 提供打开该会话的操作;该操作仅导航,不重试 fork。成功的 fork 在源会话最后一个已完成轮次处创建分支,通过 Session Controller 递增继承的持久化标题,不 retain 子会话、不打开其历史,也不改变选择。Workspace 行内的 Delete 操作会打开确认框,说明保留边界;成功后该分组被移除,其 Session 则留在 Ungrouped 下。Pin、Rename、Fork、Archive 本身就是 `sidebar.workspaces.session.menu.item` 列表的条目(pin 与 archive 同时也是 `sidebar.workspaces.session.row.action` 的条目),因此客户端插件的 action 由其 `order` 决定落在哪个位置。
55
55
 
56
56
  对静止的 Session,Archive 不经确认对话框直接提交,并保留 Session 的记账位置。仍有工作在跑的 Session 是唯一会先询问的情形:Host 拒绝普通归档并列出这些工作,侧栏随即打开"停止并归档"对话框,按族列出——进行中的回合、运行中的子代理、后台任务、定时提醒,各带名称——并写明恢复路径;确认后请 Host 按停止按钮同样的方式停止这些工作,归档集合持久化后即完成归档,停止在后台收敛;取消则让 Session 继续运行并保持可见。视图选项以一组显式三选一控制显隐:隐藏已归档(默认项)隐藏已归档 Session,全部对话(显示已归档)将其纳入列表,仅显示已归档则隐藏普通 Session,并丢弃没有归档 Session 的 Workspace;树形分组下,被丢弃 Workspace 的子级挂到最近一个仍显示的祖先下。可见的归档行置灰,并提供无障碍说明,告知取消归档后才能打开;Rename、Fork 与取消归档仍然可用。归档成功后的提示提供"撤销"动作,并在归档行仍被隐藏时附带"筛选已归档会话"动作,后者直接把筛选切到全部对话(显示已归档);停止并归档显示同样的提示但措辞不同,撤销只恢复 Session,不会让被停止的工作继续。取消归档移除归档标记,但不恢复置顶,也不改变保存的位置。列表为空时显示居中的"图标在上、文字在下"占位;仅显示已归档视图用自己的文案(暂无已归档会话),并附"查看其他会话"文字按钮,点击把筛选切回隐藏已归档。
57
57
 
58
58
  会话更新时间使用 tertiary 文本色,包括已归档行。标题宽于所在行时,静止状态以省略号裁切。把指针停在行上,标题会滚动到远端——例如 fork 递增后的标题——并在揭示时不显示省略号;指针离开后标题回到开头。
59
59
 
60
- 快捷键速查提供新建会话、搜索会话、添加工作区、重命名会话、分叉会话和归档会话。桌面默认使用平台的主修饰键,搜索为 Mod+K;Windows 和 macOS Web 使用[快捷键服务的平台默认值](../shortcuts/README.zh.md);Linux Web 在用户配置前不绑定这些命令。按钮提示和会话行菜单显示当前有效绑定。点击菜单项操作该行,按快捷键操作主会话。Desktop 和 Windows/macOS Web 的重命名均使用 Mod+Alt+G,将 Mod+Shift+R 留给浏览器刷新。重命名要求主区域正在显示非空会话,且没有模态对话框遮挡;切换到其他主面板后,后台保留的会话不可作为重命名目标。其他 Windows 和 macOS Desktop 绑定也可从终端输入区域和模态对话框中执行;其他环境遵循命令的区域和模态限制。搜索和重命名的打开请求归本包管理,输入草稿保留在浏览器中。目录选择或工作区接纳尚未结束时,目录选择器拒绝重复打开。分叉捕获源会话,并使用会话行相同的 Host 操作选择最近已完成轮次,不读取更早的 Client 历史。没有会话或会话为空时不可用;没有已完成轮次的源会话由 Host 拒绝。快捷键分叉被拒绝时保留当前选择、显示本地化提示,并允许重试;非预期失败还会保留诊断日志。
60
+ 快捷键速查提供新建会话、搜索会话、添加工作区、重命名会话、分叉会话和归档会话。桌面默认使用平台的主修饰键,搜索为 Mod+K;Windows 和 macOS Web 使用[快捷键服务的平台默认值](../shortcuts/README.zh.md);Linux Web 在用户配置前不绑定这些命令。按钮提示和会话行菜单显示当前有效绑定。点击菜单项操作该行,按快捷键操作主会话。Desktop 和 Windows/macOS Web 的重命名均使用 Mod+Alt+G,将 Mod+Shift+R 留给浏览器刷新。重命名要求主区域正在显示非空会话,且没有模态对话框遮挡;切换到其他主面板后,后台保留的会话不可作为重命名目标。其他 Windows 和 macOS Desktop 绑定也可从终端输入区域和模态对话框中执行;其他环境遵循命令的区域和模态限制。搜索和重命名的打开请求归本包管理,输入草稿保留在浏览器中。目录选择或工作区接纳尚未结束时,目录选择器拒绝重复打开。分叉捕获主视图已持有并发起历史打开的会话,保留自动迁移;它使用与行菜单相同的 Host 操作选择最近已完成轮次,不读取更早的 Client 历史。没有会话或会话为空时不可用;没有已完成轮次的源会话由 Host 拒绝。快捷键分叉被拒绝时保留当前选择、显示本地化提示,并允许重试;非预期失败还会保留诊断日志。
61
61
 
62
62
  ### 待处理交互
63
63
 
@@ -69,11 +69,11 @@ Session 行渲染运行时的实时 `pendingInteraction` 分类:审批显示**
69
69
 
70
70
  -----
71
71
 
72
- `ctx.uiWorkspace.openSession(target)` 会同步替换其拥有的 `mainView` reference,并让主区域返回 Conversation,而不等待 `reference.ready`,因此历史加载会显示在已经选中的 Session 视图内。目标可以是已知 Session id,也可以是持久的直接父子 subagent 地址;显式地址不要求预先加载 parent catalog。`openWorkspace(id, beforeOpen?)` 仅在请求未被后续导航替代时打开结果;新会话使用 `openWorkspace`。`forkSession(id)` 创建子会话,不导航,也不替代尚未完成的导航。可选的同步准备回调在目标被 retain 后执行,并且仅对仍有效的 Workspace 请求执行,因此过期请求不会搬移 composer 草稿。后续导航或 owner 释放会阻止晚到的 UI 提交,但不取消底层 Session 创建。启动恢复会 retain 主 reference,不改变已选面板,也不取消后续导航。归档主 Session 会释放其 reference 并清除主选择。选择失败时保留当前全局面板。Session 行读取 `usePanelInfo`,在全局面板活跃时不显示 Session 选中样式;仅把焦点移到搜索框或目录选择器不会离开该面板。
72
+ `ctx.uiWorkspace.openSession(target)` 会同步替换其拥有的 `mainView` reference,并让主区域返回 Conversation,而不等待 `reference.ready`,因此历史加载会显示在已经选中的 Session 视图内。目标可以是已知 Session id,也可以是持久的直接父子 subagent 地址;显式地址不要求预先加载 parent catalog。`openWorkspace(id, beforeOpen?)` 仅在请求未被后续导航替代时打开工作区空白 Session;显式新会话操作会创建新的 Session。`forkSession(id)` 创建子会话,不导航,也不替代尚未完成的导航。可选的同步准备回调在目标被 retain 后执行,并且仅对仍有效的 Workspace 请求执行,因此过期请求不会搬移 composer 草稿。后续导航或 owner 释放会阻止晚到的 UI 提交,但不取消底层 Session 创建。启动恢复会 retain 主 reference,不改变已选面板,也不取消后续导航。归档主 Session 会释放其 reference 并清除主选择。选择失败时保留当前全局面板。Session 行读取 `usePanelInfo`,在全局面板活跃时不显示 Session 选中样式;仅把焦点移到搜索框或目录选择器不会离开该面板。
73
73
 
74
74
  导航和启动恢复通过所选 Session 的 `follow` 获取投影,不会另行刷新该 Session 或其父会话的投影。
75
75
 
76
- 新建会话尝试获取列表中第一个符合条件的空白会话;启动恢复尝试已保存的空白会话。若该写锁被占用,导航直接新建 Session,不再尝试其他空白会话。其他获取错误会中止请求:显式新建会话或在 hero 中选择工作区时,失败以短暂提示展示,引用 Host 的错误码和消息(例如 preset 挂载失败),非 Host 拒绝的失败则显示其自身消息;已被后续导航或 owner 销毁取代的请求不弹提示,启动恢复仍只报告到控制台。已释放的空白会话被复用时保留 slash 命令状态。后续导航会取消尚未完成的启动选择。
76
+ 重新打开工作区和显式准备草稿尝试获取列表中第一个符合条件的空白会话;启动恢复尝试已保存的空白会话。若该写锁被占用,导航直接新建 Session,不再尝试其他空白会话。其他获取错误会中止请求:显式新建会话或在 hero 中选择工作区时,失败以短暂提示展示,引用 Host 的错误码和消息(例如 preset 挂载失败),非 Host 拒绝的失败则显示其自身消息;已被后续导航或 owner 销毁取代的请求不弹提示,启动恢复仍只报告到控制台。已释放的空白会话被复用时保留 slash 命令状态。后续导航会取消尚未完成的启动选择。
77
77
 
78
78
  Workspace 和 Session 的启动基线均就绪后,空安装环境调用 `workspaces.initializeDefault`,创建或复用其空白 Session。选中该 Session 后输入框才可编辑,不会自动提交消息。后续导航或所属上下文销毁会阻止启动流程选中其结果。不符合首次使用条件时仍可选择文件夹,不显示错误。默认工作区创建失败时显示短暂提示,引导用户通过“选择工作区”选择文件夹,直到下次启动才重试。Session 创建失败沿用普通的恢复错误处理。登记成功的工作区在 Session 创建或后续提交失败时仍然保留。
79
79
 
@@ -89,9 +89,11 @@ Workspace 和 Session 的启动基线均就绪后,空安装环境调用 `works
89
89
 
90
90
  浏览器入口还为每个 Session 行声明两个 root 作用域的 `list` 子 slot:`sidebar.session.row.leading` 仅在该行主状态为 idle 时渲染、归档行留空,`sidebar.session.row.hover` 仅在该行的悬浮卡片打开时挂载。两者只接收行的 Session 身份,占用方据此读取自己的数据;Session 作用域的 slot 会强制建立 Session 绑定,从而激活并保留列表中每个 Session。
91
91
 
92
+ 客户端调用方可以在 `uiWorkspace.startSession` 的第二参数中传入 `prompt` 与 `clearPreviousDraft`。提示词仅接受普通文字;结构化草稿由内部恢复和工作区携带流程处理。不清空时保留已有文字或附件;显式清空只替换目标 Session 的文字和内联引用,不删除附件。在异步创建前捕获选项值,在导航提交前应用到准确保留的 binding。已被后续操作取代的导航不写内容;输入处于提交阻塞状态时拒绝准备并保留原选择。不传选项时按当前 Agent 预设定义创建新的 Session。工作区重连和显式草稿准备仍可复用已有空 Session。[草稿初始化决策](../../../.agents/notes/implemented/architecture/2026-09-30-structured-draft-initialization.zh.md)说明输入 owner 的数据流。
93
+
92
94
  ### 目录流子 slot
93
95
 
94
- 每个注册各自声明一个**目录流子 slot**(`single` kind:`conversation.hero.workspace.directoryFlow`/`sidebar.workspaces.directoryFlow`),由组合的选择器包 client half 填入其选取交互——`-native` 后端的无渲染 OS 选择器驱动,`-browse` 组合下则是应用内浏览对话框。平铺显示的**添加工作区…** 操作仅在当前界面的 slot 被占用时渲染;slot 为空意味着该组合没有目录选择能力。本包持有触发与接纳:占用方通过 slot 的属主交互约定(`open`/`busy`/`onPicked`/`onCancel`/`onError`)每次打开上报一个所选路径,owner 通过对象层接纳它,并等待 Workspace 列表投影刷新后才选中已提交的 Workspace。
96
+ 每个注册各自声明一个**目录流子 slot**(`single` kind:`conversation.hero.workspace.directoryFlow`/`sidebar.workspaces.directoryFlow`),由组合的选择器包 client half 填入其选取交互——`-native` 后端的无渲染 OS 选择器驱动,`-browse` 组合下则是应用内浏览对话框。平铺显示的**添加工作区…** 操作仅在当前界面的 slot 被占用时渲染;slot 为空意味着该组合没有目录选择能力。侧栏的添加工作区操作保留已有空白 Session 及其草稿。本包持有触发与接纳:占用方通过 slot 的属主交互约定(`open`/`busy`/`onPicked`/`onCancel`/`onError`)每次打开上报一个所选路径,owner 通过对象层接纳它,并等待 Workspace 列表投影刷新后才选中已提交的 Workspace。
95
97
 
96
98
  ### Session 行 action
97
99
 
@@ -224,5 +226,3 @@ Workspace 与 Session 悬浮卡片会复制对应行被截断的值:激活 Wor
224
226
  无。
225
227
 
226
228
  </details>
227
-
228
- **运行时不变式:** 不发布伴生入口。这是一个纯消费方插件,只向两个由宿主声明的 slot 注册展示组件,并注册自身的 locale dictionaries;inject face 由无状态 RPC 包装层和一次 create-and-open 调用组成;本插件不发出 Cordis 事件,也不持有跨插件可变状态。
package/lib/client.js CHANGED
@@ -204,6 +204,630 @@ window.__ModuleLoader__.load({
204
204
  });
205
205
  }
206
206
  //#endregion
207
+ //#region ../../util/values/src/partial-json.ts
208
+ /**
209
+ * Lazily scanned view of one JSON object's top-level fields, built from text
210
+ * that may still be streaming or from an already parsed object. Nothing is
211
+ * scanned until a reader asks; the view remembers every question it answered
212
+ * and reports changed answers when the owner refreshes for publication.
213
+ * Used for model tool-call arguments: a row reads the fields it
214
+ * cares about at whatever granularity it displays, at every stage of the call.
215
+ * @module @deepseek-ai/dsh-util-values/src/partial-json
216
+ */
217
+ const SIMPLE_ESCAPES = {
218
+ "\"": "\"",
219
+ "\\": "\\",
220
+ "/": "/",
221
+ b: "\b",
222
+ f: "\f",
223
+ n: "\n",
224
+ r: "\r",
225
+ t: " "
226
+ };
227
+ const CONTENT_ESCAPE = /[\\\u0000-\u001f]/u;
228
+ function isWhitespace(c) {
229
+ return c === " " || c === "\n" || c === "\r" || c === " ";
230
+ }
231
+ function isHex(c) {
232
+ return c >= "0" && c <= "9" || c >= "a" && c <= "f" || c >= "A" && c <= "F";
233
+ }
234
+ (class PartialArguments {
235
+ /** The view of a call with no arguments available. */
236
+ static EMPTY = PartialArguments.fromObject({});
237
+ /**
238
+ * View finished argument text without scanning it until a reader asks.
239
+ * @param text - the complete argument JSON text.
240
+ * @returns a sealed view.
241
+ */
242
+ static fromText(text) {
243
+ const view = new PartialArguments();
244
+ view.append(text);
245
+ view.sealed = true;
246
+ return view;
247
+ }
248
+ /**
249
+ * View an already parsed argument payload, such as a PTC dispatch object.
250
+ * @param value - the parsed argument value.
251
+ * @returns a sealed view; a non-object payload has no fields.
252
+ */
253
+ static fromObject(value) {
254
+ const view = new PartialArguments();
255
+ view.object = typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
256
+ view.sealed = true;
257
+ return view;
258
+ }
259
+ /**
260
+ * The source: text so far or a parsed object, plus whether it can still grow.
261
+ * These are the only enumerable fields, so two views over the same source
262
+ * compare equal structurally however far each has been read.
263
+ */
264
+ chunks = [];
265
+ object;
266
+ sealed = false;
267
+ #ends = [];
268
+ #size = 0;
269
+ #consumed = 0;
270
+ #mode = "root";
271
+ #escape = false;
272
+ #keyStart = 0;
273
+ #keyEscaped = false;
274
+ #key = "";
275
+ #current = null;
276
+ #nestedEnds = [];
277
+ #nestedInString = false;
278
+ #invalidAt;
279
+ #invalidValue = false;
280
+ #entries = /* @__PURE__ */ new Map();
281
+ #order = [];
282
+ #reads = /* @__PURE__ */ new Map();
283
+ /** Whether this view rejects further appends; does not scan text or register reads. */
284
+ get isSealed() {
285
+ return this.sealed;
286
+ }
287
+ /** Whether indexing or a content read found invalid JSON; unread value contents are not validated. */
288
+ get invalid() {
289
+ this.scan();
290
+ return this.#mode === "invalid" || this.#invalidValue;
291
+ }
292
+ /**
293
+ * Retain streamed argument text without scanning or comparing observed answers.
294
+ * @param fragment - the text following every fragment appended before.
295
+ */
296
+ append(fragment) {
297
+ if (this.sealed) throw new Error("PartialArguments: cannot append to a sealed view");
298
+ if (fragment.length === 0) return;
299
+ this.chunks.push(fragment);
300
+ this.#size += fragment.length;
301
+ this.#ends.push(this.#size);
302
+ }
303
+ /**
304
+ * Reconcile a streamed prefix with authoritative complete text without joining the fragments.
305
+ * @param text - the final argument text, which replaces missing or conflicting deltas.
306
+ * @returns this view sealed with its caches retained when every character matches; otherwise a new sealed view.
307
+ */
308
+ settle(text) {
309
+ if (this.object !== void 0 || text.length !== this.#size) return PartialArguments.fromText(text);
310
+ let offset = 0;
311
+ for (const chunk of this.chunks) {
312
+ if (!text.startsWith(chunk, offset)) return PartialArguments.fromText(text);
313
+ offset += chunk.length;
314
+ }
315
+ this.chunks = text.length === 0 ? [] : [text];
316
+ this.#ends = text.length === 0 ? [] : [text.length];
317
+ this.sealed = true;
318
+ return this;
319
+ }
320
+ /**
321
+ * Compare observed answers and advance their publication baseline. Unread views remain unscanned.
322
+ * @returns whether any observed answer changed since its first read or the preceding refresh.
323
+ */
324
+ refresh() {
325
+ if (this.#reads.size === 0) return false;
326
+ this.scan();
327
+ let changed = false;
328
+ let completions = false;
329
+ for (const read of this.#reads.values()) {
330
+ if (read.completion) {
331
+ completions = true;
332
+ continue;
333
+ }
334
+ changed = this.refreshRead(read) || changed;
335
+ }
336
+ if (completions) {
337
+ for (const read of this.#reads.values()) if (read.completion) changed = this.refreshRead(read) || changed;
338
+ }
339
+ if (this.sealed) this.#reads.clear();
340
+ return changed;
341
+ }
342
+ refreshRead(read) {
343
+ const now = read.answer();
344
+ if (Object.is(now, read.last)) return false;
345
+ read.last = now;
346
+ return true;
347
+ }
348
+ /**
349
+ * Check whether no further fields can arrive.
350
+ * @returns whether the outer object closed, indexing failed, or the view is sealed; unread values are not validated.
351
+ */
352
+ closed() {
353
+ return this.remember("closed", "", () => this.closedNow());
354
+ }
355
+ /**
356
+ * List discovered fields in first-appearance order.
357
+ * @returns top-level keys seen so far, in first-appearance order.
358
+ */
359
+ keys() {
360
+ return this.remember("keys", "", () => this.keysNow(), (keys) => keys.length);
361
+ }
362
+ /**
363
+ * Check whether a top-level field has appeared.
364
+ * @param key - argument name.
365
+ * @returns whether the field has appeared (a string opened or another value began).
366
+ */
367
+ has(key) {
368
+ return this.remember("has", key, () => this.hasNow(key));
369
+ }
370
+ /**
371
+ * Check whether a field's closing delimiter has arrived, without validating its contents.
372
+ * @param key - argument name.
373
+ * @returns whether its delimiter arrived and no content reader has reported an error for this value.
374
+ */
375
+ complete(key) {
376
+ return this.remember("complete", key, () => this.completeNow(key));
377
+ }
378
+ /**
379
+ * Read string length without materializing its text.
380
+ * @param key - argument name.
381
+ * @param options - change granularity for a streaming string.
382
+ * @returns decoded UTF-16 length of the string field so far; undefined when absent or not a string.
383
+ */
384
+ stringLength(key, options) {
385
+ const step = Math.max(1, Math.floor(options?.step ?? 1));
386
+ const offset = options?.offset ?? 0;
387
+ return this.remember(`length:${step}:${offset}`, key, () => this.lengthNow(key), (length) => length === void 0 ? void 0 : Math.ceil((length + offset) / step));
388
+ }
389
+ /**
390
+ * Check a string against a decoded UTF-16 length limit without materializing it.
391
+ * @param key - argument name.
392
+ * @param maxLength - decoded UTF-16 limit, floored to at least zero.
393
+ * @returns whether the string is longer than the limit; false when absent or not a string.
394
+ */
395
+ stringExceeds(key, maxLength) {
396
+ const limit = Math.max(0, Math.floor(maxLength));
397
+ return this.remember(`exceeds:${limit}`, key, () => (this.lengthNow(key, limit + 1) ?? 0) > limit);
398
+ }
399
+ /**
400
+ * Read a decoded string, including a streaming prefix.
401
+ * @param key - argument name.
402
+ * @returns the string field's decoded text so far; undefined when absent or not a string.
403
+ */
404
+ text(key) {
405
+ return this.remember("text", key, () => this.textNow(key));
406
+ }
407
+ /**
408
+ * Read at most the first decoded UTF-16 units of a string.
409
+ * @param key - argument name.
410
+ * @param maxLength - maximum decoded UTF-16 length, floored to at least one.
411
+ * @returns the bounded string prefix; undefined when absent or not a string.
412
+ */
413
+ textPrefix(key, maxLength) {
414
+ const limit = Math.max(1, Math.floor(maxLength));
415
+ return this.remember(`prefix:${limit}`, key, () => this.textPrefixNow(key, limit));
416
+ }
417
+ /**
418
+ * Read a completed non-string argument.
419
+ * @param key - argument name.
420
+ * @returns the parsed non-string value once it closed; undefined while open, absent, or a string.
421
+ */
422
+ value(key) {
423
+ return this.remember("value", key, () => this.valueNow(key));
424
+ }
425
+ /** Answer a question and, on a streaming view, remember it for change detection. */
426
+ remember(kind, key, read, comparison) {
427
+ this.scan();
428
+ const result = read();
429
+ if (!this.sealed) {
430
+ const id = `${kind}/${key}`;
431
+ if (!this.#reads.has(id)) this.#reads.set(id, {
432
+ completion: kind === "complete",
433
+ answer: comparison === void 0 ? read : () => comparison(read()),
434
+ last: comparison === void 0 ? result : comparison(result)
435
+ });
436
+ }
437
+ return result;
438
+ }
439
+ closedNow() {
440
+ return this.sealed || this.#mode === "closed" || this.#mode === "invalid";
441
+ }
442
+ keysNow() {
443
+ return this.object === void 0 ? this.#order : Object.keys(this.object);
444
+ }
445
+ hasNow(key) {
446
+ return this.object === void 0 ? this.#entries.has(key) : Object.hasOwn(this.object, key);
447
+ }
448
+ completeNow(key) {
449
+ if (this.object !== void 0) return Object.hasOwn(this.object, key);
450
+ const entry = this.#entries.get(key);
451
+ return entry !== void 0 && entry.end >= 0 && (entry.kind === "string" ? entry.invalidAt === void 0 : !entry.invalid);
452
+ }
453
+ lengthNow(key, limit = Number.POSITIVE_INFINITY) {
454
+ if (this.object !== void 0) {
455
+ const field = Object.hasOwn(this.object, key) ? this.object[key] : void 0;
456
+ return typeof field === "string" ? field.length : void 0;
457
+ }
458
+ const entry = this.#entries.get(key);
459
+ if (entry?.kind !== "string") return void 0;
460
+ if (entry.text !== void 0 && entry.text.at === entry.end) return entry.text.length;
461
+ const read = entry.length ??= {
462
+ at: entry.start,
463
+ length: 0,
464
+ text: ""
465
+ };
466
+ this.readString(entry, read, limit, false);
467
+ return read.length;
468
+ }
469
+ textNow(key) {
470
+ if (this.object !== void 0) {
471
+ const field = Object.hasOwn(this.object, key) ? this.object[key] : void 0;
472
+ return typeof field === "string" ? field : void 0;
473
+ }
474
+ const entry = this.#entries.get(key);
475
+ if (entry?.kind !== "string") return void 0;
476
+ if (entry.text === void 0 && entry.end >= 0 && entry.needsDecoding && entry.invalidAt === void 0) {
477
+ let text;
478
+ try {
479
+ text = JSON.parse(`"${this.slice(entry.start, entry.end)}"`);
480
+ } catch (_error) {}
481
+ if (text !== void 0) entry.text = {
482
+ at: entry.end,
483
+ length: text.length,
484
+ text
485
+ };
486
+ }
487
+ const read = entry.text ??= {
488
+ at: entry.start,
489
+ length: 0,
490
+ text: ""
491
+ };
492
+ this.readString(entry, read, Number.POSITIVE_INFINITY, true);
493
+ return read.text;
494
+ }
495
+ textPrefixNow(key, maxLength) {
496
+ if (this.object !== void 0) {
497
+ const field = Object.hasOwn(this.object, key) ? this.object[key] : void 0;
498
+ return typeof field === "string" ? field.slice(0, maxLength) : void 0;
499
+ }
500
+ const entry = this.#entries.get(key);
501
+ if (entry?.kind !== "string") return void 0;
502
+ const prefixes = entry.prefixes ??= /* @__PURE__ */ new Map();
503
+ let read = prefixes.get(maxLength);
504
+ if (read === void 0) {
505
+ read = {
506
+ at: entry.start,
507
+ length: 0,
508
+ text: ""
509
+ };
510
+ prefixes.set(maxLength, read);
511
+ }
512
+ this.readString(entry, read, maxLength, true);
513
+ return read.text;
514
+ }
515
+ valueNow(key) {
516
+ if (this.object !== void 0) {
517
+ if (!Object.hasOwn(this.object, key)) return void 0;
518
+ const field = this.object[key];
519
+ return typeof field === "string" ? void 0 : field;
520
+ }
521
+ const entry = this.#entries.get(key);
522
+ if (entry?.kind !== "value" || entry.end < 0 || entry.invalid) return void 0;
523
+ if (entry.parsed === void 0) try {
524
+ entry.parsed = JSON.parse(this.slice(entry.start, entry.end));
525
+ } catch (_error) {
526
+ entry.invalid = true;
527
+ this.#invalidValue = true;
528
+ }
529
+ return entry.parsed;
530
+ }
531
+ chunkAt(at) {
532
+ let low = 0;
533
+ let high = this.#ends.length;
534
+ while (low < high) {
535
+ const mid = low + high >>> 1;
536
+ if (this.#ends[mid] <= at) low = mid + 1;
537
+ else high = mid;
538
+ }
539
+ return low;
540
+ }
541
+ /** Materialize only a requested range, never the cumulative source. */
542
+ slice(start, end) {
543
+ if (start >= end) return "";
544
+ const first = this.chunkAt(start);
545
+ const last = this.chunkAt(end - 1);
546
+ const base = first === 0 ? 0 : this.#ends[first - 1];
547
+ if (first === last) return this.chunks[first].slice(start - base, end - base);
548
+ const parts = [this.chunks[first].slice(start - base)];
549
+ for (let i = first + 1; i < last; i++) parts.push(this.chunks[i]);
550
+ parts.push(this.chunks[last].slice(0, end - this.#ends[last - 1]));
551
+ return parts.join("");
552
+ }
553
+ readString(entry, read, limit, materialize) {
554
+ const end = Math.min(entry.end < 0 ? this.#consumed : entry.end, entry.invalidAt ?? Number.POSITIVE_INFINITY, this.#invalidAt ?? Number.POSITIVE_INFINITY);
555
+ if (!entry.needsDecoding) {
556
+ const length = Math.min(end - read.at, limit - read.length);
557
+ if (length <= 0) return;
558
+ if (materialize) read.text += this.slice(read.at, read.at + length);
559
+ read.at += length;
560
+ read.length += length;
561
+ return;
562
+ }
563
+ let chunkIndex = this.chunkAt(read.at);
564
+ while (read.at < end && read.length < limit) {
565
+ const base = chunkIndex === 0 ? 0 : this.#ends[chunkIndex - 1];
566
+ const chunk = this.chunks[chunkIndex];
567
+ const remaining = chunk.slice(read.at - base, Math.min(chunk.length, end - base));
568
+ const boundary = remaining.search(CONTENT_ESCAPE);
569
+ const length = Math.min(boundary < 0 ? remaining.length : boundary, limit - read.length);
570
+ if (length > 0) {
571
+ if (materialize) read.text += remaining.slice(0, length);
572
+ read.at += length;
573
+ read.length += length;
574
+ if (read.at === base + chunk.length) chunkIndex++;
575
+ continue;
576
+ }
577
+ const type = remaining.length > 1 ? remaining[1] : read.at + 1 < end ? this.chunks[chunkIndex + 1][0] : void 0;
578
+ let decoded;
579
+ let width = 2;
580
+ if (remaining[0] === "\\" && type === void 0 && entry.end < 0) return;
581
+ if (remaining[0] === "\\" && type === "u") {
582
+ const hex = this.slice(read.at + 2, Math.min(end, read.at + 6));
583
+ let valid = true;
584
+ for (let i = 0; i < hex.length; i++) if (!isHex(hex[i])) valid = false;
585
+ if (valid) {
586
+ if (hex.length < 4 && entry.end < 0) return;
587
+ if (hex.length === 4) decoded = String.fromCharCode(Number.parseInt(hex, 16));
588
+ }
589
+ width = 6;
590
+ } else if (remaining[0] === "\\" && type !== void 0) decoded = SIMPLE_ESCAPES[type];
591
+ if (decoded === void 0) {
592
+ entry.invalidAt = read.at;
593
+ this.#invalidValue = true;
594
+ return;
595
+ }
596
+ if (materialize) read.text += decoded;
597
+ read.length++;
598
+ read.at += width;
599
+ while (chunkIndex < this.chunks.length && read.at >= this.#ends[chunkIndex]) chunkIndex++;
600
+ }
601
+ }
602
+ /** Locate new field ranges without decoding or parsing their contents. */
603
+ scan() {
604
+ if (this.object !== void 0 || this.#consumed === this.#size) return;
605
+ for (let i = this.chunkAt(this.#consumed); i < this.chunks.length && this.#invalidAt === void 0; i++) {
606
+ const pending = this.chunks[i];
607
+ const base = i === 0 ? 0 : this.#ends[i - 1];
608
+ for (let index = this.#consumed - base; index < pending.length && this.#mode !== "invalid"; index++) {
609
+ if (this.#mode === "string" || this.#mode === "nested" && this.#nestedInString) {
610
+ const end = this.stringBoundary(pending, index);
611
+ this.#consumed += end - index;
612
+ index = end;
613
+ if (index === pending.length) break;
614
+ }
615
+ this.step(pending[index], this.#consumed);
616
+ this.#consumed++;
617
+ }
618
+ }
619
+ }
620
+ /** Only raw quotes and their preceding backslash runs can terminate a string. */
621
+ stringBoundary(fragment, start) {
622
+ let at = start;
623
+ while (true) {
624
+ const quote = fragment.indexOf("\"", at);
625
+ const end = quote < 0 ? fragment.length : quote;
626
+ if (this.#mode === "string") {
627
+ const entry = this.#current;
628
+ if (!entry.needsDecoding && CONTENT_ESCAPE.test(fragment.slice(at, end))) entry.needsDecoding = true;
629
+ }
630
+ let slashStart = end;
631
+ while (slashStart > at && fragment[slashStart - 1] === "\\") slashStart--;
632
+ const escaped = (end - slashStart) % 2 === 1 !== (slashStart === at && this.#escape);
633
+ this.#escape = quote < 0 && escaped;
634
+ if (quote < 0 || !escaped) return end;
635
+ at = quote + 1;
636
+ }
637
+ }
638
+ step(c, at) {
639
+ switch (this.#mode) {
640
+ case "root":
641
+ if (isWhitespace(c)) return;
642
+ if (c === "{") {
643
+ this.#mode = "key-or-end";
644
+ return;
645
+ }
646
+ this.fail();
647
+ return;
648
+ case "key-or-end":
649
+ if (isWhitespace(c)) return;
650
+ if (c === "}") {
651
+ this.#mode = "closed";
652
+ return;
653
+ }
654
+ if (c === "\"") {
655
+ this.beginKey(at);
656
+ return;
657
+ }
658
+ this.fail();
659
+ return;
660
+ case "key-only":
661
+ if (isWhitespace(c)) return;
662
+ if (c === "\"") {
663
+ this.beginKey(at);
664
+ return;
665
+ }
666
+ this.fail();
667
+ return;
668
+ case "key":
669
+ this.stepKey(c, at);
670
+ return;
671
+ case "colon":
672
+ if (isWhitespace(c)) return;
673
+ if (c === ":") {
674
+ this.#mode = "value";
675
+ return;
676
+ }
677
+ this.fail();
678
+ return;
679
+ case "value":
680
+ this.beginValue(c, at);
681
+ return;
682
+ case "string": {
683
+ const entry = this.#current;
684
+ entry.end = at;
685
+ this.#current = null;
686
+ this.#mode = "comma-or-end";
687
+ return;
688
+ }
689
+ case "scalar":
690
+ this.stepScalar(c, at);
691
+ return;
692
+ case "nested":
693
+ this.stepNested(c, at);
694
+ return;
695
+ case "comma-or-end":
696
+ if (isWhitespace(c)) return;
697
+ if (c === ",") {
698
+ this.#mode = "key-only";
699
+ return;
700
+ }
701
+ if (c === "}") {
702
+ this.#mode = "closed";
703
+ return;
704
+ }
705
+ this.fail();
706
+ return;
707
+ case "closed":
708
+ if (isWhitespace(c)) return;
709
+ this.fail();
710
+ return;
711
+ /* v8 ignore next 2 -- scan() stops stepping once the view is invalid. */
712
+ case "invalid": return;
713
+ /* v8 ignore next 2 -- Every scanner mode has a handler above. */
714
+ default: assertNever$1(this.#mode);
715
+ }
716
+ }
717
+ fail() {
718
+ this.#invalidAt = this.#consumed;
719
+ this.#mode = "invalid";
720
+ this.#current = null;
721
+ }
722
+ beginKey(at) {
723
+ this.#mode = "key";
724
+ this.#keyStart = at + 1;
725
+ this.#keyEscaped = false;
726
+ this.#escape = false;
727
+ }
728
+ stepKey(c, at) {
729
+ if (c < " ") {
730
+ this.fail();
731
+ return;
732
+ }
733
+ if (this.#escape) {
734
+ this.#escape = false;
735
+ return;
736
+ }
737
+ if (c === "\\") {
738
+ this.#escape = true;
739
+ this.#keyEscaped = true;
740
+ return;
741
+ }
742
+ if (c !== "\"") return;
743
+ const raw = this.slice(this.#keyStart, at);
744
+ if (this.#keyEscaped) try {
745
+ this.#key = JSON.parse(`"${raw}"`);
746
+ } catch (_error) {
747
+ this.fail();
748
+ return;
749
+ }
750
+ else this.#key = raw;
751
+ this.#mode = "colon";
752
+ }
753
+ open(entry) {
754
+ if (!this.#entries.has(this.#key)) this.#order.push(this.#key);
755
+ this.#entries.set(this.#key, entry);
756
+ this.#current = entry;
757
+ }
758
+ beginValue(c, at) {
759
+ if (isWhitespace(c)) return;
760
+ if (c === "\"") {
761
+ this.open({
762
+ kind: "string",
763
+ start: at + 1,
764
+ end: -1,
765
+ needsDecoding: false,
766
+ invalidAt: void 0,
767
+ length: void 0,
768
+ text: void 0,
769
+ prefixes: void 0
770
+ });
771
+ this.#escape = false;
772
+ this.#mode = "string";
773
+ return;
774
+ }
775
+ if (c === "}" || c === "," || c === ":" || c === "]") {
776
+ this.fail();
777
+ return;
778
+ }
779
+ this.open({
780
+ kind: "value",
781
+ start: at,
782
+ end: -1,
783
+ parsed: void 0,
784
+ invalid: false
785
+ });
786
+ if (c === "{" || c === "[") {
787
+ this.#mode = "nested";
788
+ this.#nestedEnds = [c === "{" ? "}" : "]"];
789
+ this.#nestedInString = false;
790
+ this.#escape = false;
791
+ return;
792
+ }
793
+ this.#mode = "scalar";
794
+ }
795
+ stepScalar(c, at) {
796
+ if (c !== "," && c !== "}" && !isWhitespace(c)) return;
797
+ this.closeValue(at);
798
+ this.#mode = c === "," ? "key-only" : c === "}" ? "closed" : "comma-or-end";
799
+ }
800
+ stepNested(c, at) {
801
+ if (this.#nestedInString) {
802
+ this.#nestedInString = false;
803
+ return;
804
+ }
805
+ if (c === "\"") {
806
+ this.#nestedInString = true;
807
+ return;
808
+ }
809
+ if (c === "{" || c === "[") {
810
+ this.#nestedEnds.push(c === "{" ? "}" : "]");
811
+ return;
812
+ }
813
+ if (c === "}" || c === "]") {
814
+ if (this.#nestedEnds.pop() !== c) {
815
+ this.fail();
816
+ return;
817
+ }
818
+ if (this.#nestedEnds.length === 0) {
819
+ this.closeValue(at + 1);
820
+ this.#mode = "comma-or-end";
821
+ }
822
+ }
823
+ }
824
+ closeValue(end) {
825
+ const entry = this.#current;
826
+ entry.end = end;
827
+ this.#current = null;
828
+ }
829
+ });
830
+ //#endregion
207
831
  //#region ../../util/values/src/index.ts
208
832
  /**
209
833
  * Mark an unreachable closed-union branch.
@@ -786,9 +1410,13 @@ window.__ModuleLoader__.load({
786
1410
  };
787
1411
  }, "ui-workspace: Workspace navigation policy");
788
1412
  }
789
- async connectWorkspace(workspaceId) {
1413
+ connectWorkspace(workspaceId) {
1414
+ return this.connectBlank(workspaceId, true);
1415
+ }
1416
+ async connectBlank(workspaceId, reuse) {
790
1417
  const workspace = this.workspaces.list.getSnapshot().items.find((item) => item.workspaceId === workspaceId);
791
1418
  if (workspace === void 0) throw new Error(`uiWorkspace.connectWorkspace: unknown workspace ${workspaceId}`);
1419
+ if (!reuse) return this.sessions.create({ workspaceId });
792
1420
  const inflight = this.connecting.get(workspaceId);
793
1421
  if (inflight !== void 0) return inflight;
794
1422
  const attempt = this.reuseOrCreateBlank(workspace).finally(() => {
@@ -821,11 +1449,14 @@ window.__ModuleLoader__.load({
821
1449
  openSession(target) {
822
1450
  this.replaceMain(target, this.lifetime.signal, "reveal");
823
1451
  }
824
- async openWorkspace(workspaceId, beforeOpen) {
1452
+ openWorkspace(workspaceId, beforeOpen) {
1453
+ return this.openWorkspaceSession(workspaceId, beforeOpen, true);
1454
+ }
1455
+ async openWorkspaceSession(workspaceId, beforeOpen, reuse) {
825
1456
  const navigation = AbortSignal.any([this.ctx.layout.beginNavigation(), this.lifetime.signal]);
826
1457
  let sessionId;
827
1458
  try {
828
- sessionId = await this.connectWorkspace(workspaceId);
1459
+ sessionId = await this.connectBlank(workspaceId, reuse);
829
1460
  } catch (error) {
830
1461
  if (!navigation.aborted) this.notify({
831
1462
  kind: "createFailed",
@@ -836,14 +1467,17 @@ window.__ModuleLoader__.load({
836
1467
  if (navigation.aborted) return;
837
1468
  this.replaceMain(sessionId, navigation, "reveal", beforeOpen);
838
1469
  }
839
- async forkSession(sessionId, onCreated) {
1470
+ async forkSession(sessionId, onCreated, options) {
840
1471
  return this.sessions.fork({
841
1472
  sessionId,
842
1473
  increaseTitle: true,
843
- ...onCreated === void 0 ? {} : { onCreated }
1474
+ ...onCreated === void 0 ? {} : { onCreated },
1475
+ ...options?.allowMigration === void 0 ? {} : { allowMigration: options.allowMigration }
844
1476
  });
845
1477
  }
846
- startSession(workspaceId) {
1478
+ startSession(workspaceId, options) {
1479
+ const draftOptions = options === void 0 ? void 0 : { ...options };
1480
+ const initializeDraft = draftOptions !== void 0 && (draftOptions.prompt !== void 0 || draftOptions.clearPreviousDraft === true);
847
1481
  const workspace = this.workspaces.list.getSnapshot();
848
1482
  const sessions = this.sessions.list.getSnapshot();
849
1483
  const current = this.mainReference?.sessionId;
@@ -851,13 +1485,37 @@ window.__ModuleLoader__.load({
851
1485
  const recent = workspace.phase === "ready" && sessions.phase === "ready" ? recentWorkspace(workspace.items, sessions.byId) : void 0;
852
1486
  const target = workspaceId ?? currentWorkspaceId ?? recent;
853
1487
  if (target === void 0) {
1488
+ if (initializeDraft) {
1489
+ this.notify({
1490
+ kind: "createFailed",
1491
+ message: this.ctx.locale.bind("workspace")("draft.workspaceRequired")
1492
+ });
1493
+ return;
1494
+ }
854
1495
  this.clearMain();
855
1496
  return;
856
1497
  }
857
- this.openWorkspace(target).catch((reason) => {
1498
+ (draftOptions === void 0 ? this.openWorkspaceSession(target, void 0, false) : this.openWorkspace(target, initializeDraft ? (id) => {
1499
+ const binding = this.sessions.binding(id);
1500
+ if (binding === void 0) this.draftPreparationFailed();
1501
+ this.prepareDraft(binding, draftOptions);
1502
+ } : void 0)).catch((reason) => {
858
1503
  console.warn("new session failed:", reason);
859
1504
  });
860
1505
  }
1506
+ prepareDraft(binding, options) {
1507
+ const conversation = this.ctx.get("conversation");
1508
+ if (conversation === void 0) this.draftPreparationFailed();
1509
+ if (conversation.input.requestDraftInitialization(binding, options) === "blocked") this.draftPreparationFailed();
1510
+ }
1511
+ draftPreparationFailed() {
1512
+ const message = this.ctx.locale.bind("workspace")("draft.initializationFailed");
1513
+ this.notify({
1514
+ kind: "createFailed",
1515
+ message
1516
+ });
1517
+ throw new Error(message);
1518
+ }
861
1519
  async archiveSession(sessionId, options = {}) {
862
1520
  await this.workspaces.archiveSession(sessionId, options);
863
1521
  if (this.mainReference?.sessionId === sessionId) this.clearMain();
@@ -3219,7 +3877,7 @@ window.__ModuleLoader__.load({
3219
3877
  side: "right",
3220
3878
  onPick: (workspaceId) => {
3221
3879
  closeAddWorkspace();
3222
- startSession(workspaceId);
3880
+ startSession(workspaceId, { clearPreviousDraft: false });
3223
3881
  },
3224
3882
  onClose: () => {
3225
3883
  closeAddWorkspace();
@@ -3793,23 +4451,31 @@ window.__ModuleLoader__.load({
3793
4451
  */
3794
4452
  /**
3795
4453
  * Hold for the notices that take longer to read than a one-line warning: the
3796
- * actionable archived notice (two buttons to react to) and a refused Session
3797
- * creation, which quotes the Host's reason.
4454
+ * archive and fork recovery actions, and a refused Session creation quoting the Host's reason.
3798
4455
  */
3799
4456
  const LONG_TOAST_HOLD_MS = 6e3;
3800
4457
  /**
3801
- * Render the current notice: the archived and stopped-and-archived notices
3802
- * with their undo action — plus the show-archived action while archived rows
3803
- * are hidden — on a 6 s hold, a refused Session creation with the Host's
3804
- * reason on the same hold, or a plain warning for a failed pin, an archived
3805
- * row that was clicked, or default Workspace creation.
3806
- * @param props - the notice hook, the shared viewing store, the notice dismissal, the two archived-notice actions, and the locale seat.
4458
+ * Render an operation notice, retaining recovery actions for six seconds.
4459
+ * @param props - notice and viewing sources, dismissal and recovery callbacks, and localized copy.
3807
4460
  * @returns the notice on display, or null.
3808
4461
  */
3809
- function RowActionToast({ useToast, useStore, dismissToast, undoArchive, showArchived, t }) {
4462
+ function RowActionToast({ useToast, useStore, dismissToast, undoArchive, showArchived, openForkSource, t }) {
3810
4463
  const toast = useToast((current) => current);
3811
4464
  const archivedRowsVisible = useStore((state) => (state.archivedFilter ?? "default") !== "default");
3812
4465
  if (toast === null) return null;
4466
+ if (toast.kind === "forkRequiresOpen") return (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Toast, {
4467
+ text: t("toast.forkRequiresOpen"),
4468
+ icon: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconWarningOutlineRegular, {}),
4469
+ holdMs: LONG_TOAST_HOLD_MS,
4470
+ actions: [{
4471
+ label: t("toast.openForkSource"),
4472
+ onClick: () => {
4473
+ dismissToast();
4474
+ openForkSource(toast.sessionId);
4475
+ }
4476
+ }],
4477
+ onDone: dismissToast
4478
+ }, `toast-${String(toast.seq)}`);
3813
4479
  if (toast.kind === "archived" || toast.kind === "stoppedAndArchived") {
3814
4480
  const { sessionId } = toast;
3815
4481
  return (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Toast, {
@@ -3866,6 +4532,8 @@ window.__ModuleLoader__.load({
3866
4532
  /** Simplified Chinese dictionary (the key-set source of truth). */
3867
4533
  const zh = {
3868
4534
  "defaultWorkspace.failed": "无法创建默认工作区,请通过“选择工作区”选择文件夹",
4535
+ "draft.workspaceRequired": "请先选择工作区",
4536
+ "draft.initializationFailed": "暂时无法填入草稿,请稍后重试",
3869
4537
  "group.ungrouped": "未分组",
3870
4538
  "session.new": "新会话",
3871
4539
  "session.untitled": "未命名",
@@ -3946,6 +4614,8 @@ window.__ModuleLoader__.load({
3946
4614
  "toast.pinFailed": "置顶失败,请稍后重试",
3947
4615
  "toast.unpinFailed": "取消置顶失败,请稍后重试",
3948
4616
  "toast.createFailed": "新建会话失败:{message}",
4617
+ "toast.forkRequiresOpen": "请先打开原会话完成迁移,再创建分支",
4618
+ "toast.openForkSource": "打开原会话",
3949
4619
  "sessions.count.one": "{n} 个会话",
3950
4620
  "sessions.count.other": "{n} 个会话",
3951
4621
  "actions.workspace.aria": "工作区“{name}”的操作",
@@ -3981,6 +4651,8 @@ window.__ModuleLoader__.load({
3981
4651
  /** English dictionary, checked complete against the zh key set. */
3982
4652
  const en = {
3983
4653
  "defaultWorkspace.failed": "Unable to create default workspace. Use Choose workspace to select a folder.",
4654
+ "draft.workspaceRequired": "Choose a workspace first.",
4655
+ "draft.initializationFailed": "Could not fill the draft. Please try again shortly.",
3984
4656
  "group.ungrouped": "Ungrouped",
3985
4657
  "session.new": "New Session",
3986
4658
  "session.untitled": "Untitled",
@@ -4061,6 +4733,8 @@ window.__ModuleLoader__.load({
4061
4733
  "toast.pinFailed": "Pin failed. Try again later.",
4062
4734
  "toast.unpinFailed": "Unpin failed. Try again later.",
4063
4735
  "toast.createFailed": "New session failed: {message}",
4736
+ "toast.forkRequiresOpen": "Open the original session to complete migration before creating a branch.",
4737
+ "toast.openForkSource": "Open original session",
4064
4738
  "sessions.count.one": "{n} session",
4065
4739
  "sessions.count.other": "{n} sessions",
4066
4740
  "actions.workspace.aria": "Workspace actions for {name}",
@@ -4238,7 +4912,12 @@ window.__ModuleLoader__.load({
4238
4912
  parent_session_id: sessionId,
4239
4913
  click_position: "sidebar"
4240
4914
  });
4241
- }).catch(() => {});
4915
+ }, { allowMigration: false }).catch((error) => {
4916
+ if (error instanceof Error && error.name === "SessionForkError" && error.rpcError.code === "session/migration-required") notify({
4917
+ kind: "forkRequiresOpen",
4918
+ sessionId
4919
+ });
4920
+ });
4242
4921
  } });
4243
4922
  const renameInjected = () => ({ requestSessionRename });
4244
4923
  const renameDialogInjected = () => ({
@@ -4254,11 +4933,18 @@ window.__ModuleLoader__.load({
4254
4933
  undoArchive: unarchiveSession,
4255
4934
  showArchived: () => {
4256
4935
  viewInstance.actions.setArchivedFilter("show");
4936
+ },
4937
+ openForkSource: (sessionId) => {
4938
+ if (workspaces.list.getSnapshot().archivedSessionIds.includes(sessionId)) {
4939
+ notify({ kind: "archivedNotOpenable" });
4940
+ return;
4941
+ }
4942
+ openSession(sessionId);
4257
4943
  }
4258
4944
  });
4259
4945
  const browserInjected = () => ({
4260
- startSession: (workspaceId) => {
4261
- uiWorkspace.startSession(workspaceId);
4946
+ startSession: (workspaceId, options) => {
4947
+ uiWorkspace.startSession(workspaceId, options);
4262
4948
  },
4263
4949
  open: openSession,
4264
4950
  searchSessions,
@@ -45,6 +45,7 @@ import type { SessionActivity, WorkspaceId, WorkspaceView } from '@deepseek-ai/d
45
45
  import type { SessionId } from '@deepseek-ai/dsh-session/types';
46
46
  import type { ShortcutCatalogEntry } from '@deepseek-ai/dsh-client-shortcuts/client';
47
47
  import type { WorkspaceShortcutState } from '../shortcuts.ts';
48
+ import type { StartSessionOptions } from '../navigation.ts';
48
49
  import type { createWorkspaceViewStore } from '../stores.ts';
49
50
  /**
50
51
  * Owner share of the directory-flow holes: the complete conversation between
@@ -236,11 +237,11 @@ export type WorkspaceBrowserInjected = {
236
237
  /** Dismiss the shortcut's fork-failure notification. */
237
238
  dismissForkError: () => void;
238
239
  /**
239
- * Start a New Session in a Workspace: reuse-or-create its blank session and
240
- * open it; without an explicit workspace, inherit the current Session
241
- * Workspace, then the recent Workspace, or clear into the New Session view.
240
+ * Create a fresh Session, or preserve a reusable blank with explicit draft options.
241
+ * Without an explicit Workspace, inherit the current Session Workspace,
242
+ * then the recent Workspace, or clear into the New Session view.
242
243
  */
243
- startSession: (workspaceId?: WorkspaceId) => void;
244
+ startSession: (workspaceId?: WorkspaceId, options?: StartSessionOptions) => void;
244
245
  /** Open a real Session. */
245
246
  open: (sessionId: SessionId) => void;
246
247
  /**
@@ -294,6 +295,9 @@ export type RowToast = {
294
295
  kind: 'archivedNotOpenable';
295
296
  } | {
296
297
  kind: 'defaultWorkspaceFailed';
298
+ } | {
299
+ kind: 'forkRequiresOpen';
300
+ sessionId: SessionId;
297
301
  }
298
302
  /**
299
303
  * An explicit New Session request that failed. `message` is untranslated:
@@ -406,7 +410,7 @@ export interface SessionRenameDialogInjected {
406
410
  /** Rename a Session (explicit user title; resolves on host acceptance). */
407
411
  renameSession: (sessionId: SessionId, title: string) => Promise<void>;
408
412
  }
409
- /** Row toast share: the notice on display, its dismissal, and the two actions the archived notice offers. */
413
+ /** Row toast share: the notice, dismissal, archive recovery, and explicit source-history opening. */
410
414
  export interface RowToastInjected {
411
415
  hooks: {
412
416
  /** The notice on display, or none. */
@@ -418,6 +422,8 @@ export interface RowToastInjected {
418
422
  undoArchive: (sessionId: SessionId) => void;
419
423
  /** Switch the archived filter to "show" so the archived row is back in view. */
420
424
  showArchived: () => void;
425
+ /** Open the fork source without retrying the fork. */
426
+ openForkSource: (sessionId: SessionId) => void;
421
427
  }
422
428
  /** Props of the rename dialog entry in `shell.overlay`. */
423
429
  export type SessionRenameDialogProps = PropsRuntime<'shell.overlay'> & PropsLocale<'workspace'> & Omit<SessionRenameDialogInjected, 'hooks'> & PropsHooks<SessionRenameDialogInjected['hooks']>;
@@ -2,7 +2,7 @@ import type { Context } from '@deepseek-ai/cordis';
2
2
  import type { WorkspaceSnapshot } from '@deepseek-ai/dsh-api-workspace-controller/client';
3
3
  import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots';
4
4
  import { type WorkspaceKey } from './locales.ts';
5
- export type { UiWorkspace } from './navigation.ts';
5
+ export type { StartSessionOptions, UiWorkspace } from './navigation.ts';
6
6
  export type { DirectoryFlowOwnerProps, DirectoryFlowSlotName, DirectoryPickingHooks, DirectoryPickingInjected, MenuOpenState, RowToast, SessionRenameTarget, SessionRowOwnerProps, UseMenuOpenState, WorkspaceBrowserInjected, SessionRowScheduleOwnerProps, WorkspaceBrowserProps, WorkspacePickerInjected, WorkspacePickerProps, } from './contract/slots.ts';
7
7
  export type { WorkspaceKey } from './locales.ts';
8
8
  declare module '@deepseek-ai/dsh-client-ui-slots' {
@@ -6,6 +6,8 @@
6
6
  /** Simplified Chinese dictionary (the key-set source of truth). */
7
7
  export declare const zh: {
8
8
  'defaultWorkspace.failed': string;
9
+ 'draft.workspaceRequired': string;
10
+ 'draft.initializationFailed': string;
9
11
  'group.ungrouped': string;
10
12
  'session.new': string;
11
13
  'session.untitled': string;
@@ -86,6 +88,8 @@ export declare const zh: {
86
88
  'toast.pinFailed': string;
87
89
  'toast.unpinFailed': string;
88
90
  'toast.createFailed': string;
91
+ 'toast.forkRequiresOpen': string;
92
+ 'toast.openForkSource': string;
89
93
  'sessions.count.one': string;
90
94
  'sessions.count.other': string;
91
95
  'actions.workspace.aria': string;
@@ -123,6 +127,8 @@ export type WorkspaceKey = keyof typeof zh;
123
127
  /** English dictionary, checked complete against the zh key set. */
124
128
  export declare const en: {
125
129
  'defaultWorkspace.failed': string;
130
+ 'draft.workspaceRequired': string;
131
+ 'draft.initializationFailed': string;
126
132
  'group.ungrouped': string;
127
133
  'session.new': string;
128
134
  'session.untitled': string;
@@ -203,6 +209,8 @@ export declare const en: {
203
209
  'toast.pinFailed': string;
204
210
  'toast.unpinFailed': string;
205
211
  'toast.createFailed': string;
212
+ 'toast.forkRequiresOpen': string;
213
+ 'toast.openForkSource': string;
206
214
  'sessions.count.one': string;
207
215
  'sessions.count.other': string;
208
216
  'actions.workspace.aria': string;
@@ -4,8 +4,11 @@ import type { ClientRemote, DirectoryListing, RemoteFailure } from '@deepseek-ai
4
4
  import type { ISessions, SessionTarget } from '@deepseek-ai/dsh-api-session-controller/client';
5
5
  import type { IWorkspaces, WorkspaceId } from '@deepseek-ai/dsh-api-workspace-controller/client';
6
6
  import type { SessionId } from '@deepseek-ai/dsh-session/types';
7
+ import type { DraftInitializationOptions } from '@deepseek-ai/dsh-client-ui-conversation/client';
7
8
  import type { RowToast } from './contract/slots.ts';
8
9
  import type { WorkspaceViewStoreActions } from './stores.ts';
10
+ /** Optional content preparation for the resolved target Session. */
11
+ export type StartSessionOptions = DraftInitializationOptions;
9
12
  /** Workspace archive and directory operations consumed by Client UI domains. */
10
13
  export interface UiWorkspace {
11
14
  /**
@@ -27,9 +30,10 @@ export interface UiWorkspace {
27
30
  * Fork a Session without changing the current selection.
28
31
  * @param sessionId - source Session.
29
32
  * @param onCreated - observer before the optional child-title update.
33
+ * @param options - optional permission to start source migration; omission preserves the Host default.
30
34
  * @returns the child SessionId after creation and inherited-title increment.
31
35
  */
32
- forkSession(sessionId: SessionId, onCreated?: (childId: SessionId) => void): Promise<SessionId>;
36
+ forkSession(sessionId: SessionId, onCreated?: (childId: SessionId) => void, options?: Pick<Parameters<ISessions['fork']>[0], 'allowMigration'>): Promise<SessionId>;
33
37
  /**
34
38
  * Resolve the reusable or newly created blank Session for a Workspace.
35
39
  * @param workspaceId - target Workspace.
@@ -37,11 +41,12 @@ export interface UiWorkspace {
37
41
  */
38
42
  connectWorkspace(workspaceId: WorkspaceId): Promise<SessionId>;
39
43
  /**
40
- * Start a New Session flow and navigate to its Session; a creation the Host
44
+ * Create a fresh Session, or reuse a blank when preparing an explicit draft. A creation the Host
41
45
  * refuses is shown through the Workspace notice and leaves the selection as it was.
42
46
  * @param workspaceId - explicit target; absent inherits the current or most recent Workspace.
47
+ * @param options - initial content; existing text or attachments are preserved unless clearPreviousDraft is true.
43
48
  */
44
- startSession(workspaceId?: WorkspaceId): void;
49
+ startSession(workspaceId?: WorkspaceId, options?: StartSessionOptions): void;
45
50
  /**
46
51
  * Archive a Session and clear it when it is the current selection.
47
52
  * @param sessionId - Session to archive.
@@ -122,12 +127,16 @@ declare class UiWorkspaceService extends Service implements UiWorkspace {
122
127
  */
123
128
  constructor(ctx: Context, directoryPicker: ClientRemote['directoryPicker'], workspaces: IWorkspaces, sessions: ISessions, view: Pick<WorkspaceViewStoreActions, 'pinSessionOrder'>, notify: (toast: RowToast) => void);
124
129
  connectWorkspace(workspaceId: WorkspaceId): Promise<SessionId>;
130
+ private connectBlank;
125
131
  private reuseOrCreateBlank;
126
132
  private reuseBlank;
127
133
  openSession(target: SessionTarget): void;
128
134
  openWorkspace(workspaceId: WorkspaceId, beforeOpen?: (sessionId: SessionId) => void): Promise<void>;
129
- forkSession(sessionId: SessionId, onCreated?: (childId: SessionId) => void): Promise<SessionId>;
130
- startSession(workspaceId?: WorkspaceId): void;
135
+ private openWorkspaceSession;
136
+ forkSession(sessionId: SessionId, onCreated?: (childId: SessionId) => void, options?: Pick<Parameters<ISessions['fork']>[0], 'allowMigration'>): Promise<SessionId>;
137
+ startSession(workspaceId?: WorkspaceId, options?: StartSessionOptions): void;
138
+ private prepareDraft;
139
+ private draftPreparationFailed;
131
140
  archiveSession(sessionId: SessionId, options?: {
132
141
  readonly stopActivity?: boolean;
133
142
  }): Promise<void>;
@@ -1,12 +1,8 @@
1
1
  import type { RowToastProps } from '../contract/slots.ts';
2
2
  /**
3
- * Render the current notice: the archived and stopped-and-archived notices
4
- * with their undo action — plus the show-archived action while archived rows
5
- * are hidden — on a 6 s hold, a refused Session creation with the Host's
6
- * reason on the same hold, or a plain warning for a failed pin, an archived
7
- * row that was clicked, or default Workspace creation.
8
- * @param props - the notice hook, the shared viewing store, the notice dismissal, the two archived-notice actions, and the locale seat.
3
+ * Render an operation notice, retaining recovery actions for six seconds.
4
+ * @param props - notice and viewing sources, dismissal and recovery callbacks, and localized copy.
9
5
  * @returns the notice on display, or null.
10
6
  */
11
- export declare function RowActionToast({ useToast, useStore, dismissToast, undoArchive, showArchived, t }: RowToastProps): import("react").JSX.Element | null;
7
+ export declare function RowActionToast({ useToast, useStore, dismissToast, undoArchive, showArchived, openForkSource, t }: RowToastProps): import("react").JSX.Element | null;
12
8
  //# sourceMappingURL=RowActionToast.d.ts.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-client-ui-workspace",
3
3
  "description": "Workspace picker plugin: one WorkspacePicker registered into the sidebar and empty-state workspace slots",
4
- "version": "0.2.0-rc.2",
4
+ "version": "0.2.1-alpha.2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -45,37 +45,37 @@
45
45
  },
46
46
  "license": "MIT",
47
47
  "peerDependencies": {
48
- "@deepseek-ai/cordis": "~4.0.4"
48
+ "@deepseek-ai/cordis": "~4.0.5-alpha.1"
49
49
  },
50
50
  "devDependencies": {
51
+ "@deepseek-ai/cordis": "~4.0.5-alpha.1",
52
+ "@deepseek-ai/dsh-api-remotes": "0.2.1-alpha.2",
53
+ "@deepseek-ai/dsh-agent": "0.2.1-alpha.2",
54
+ "@deepseek-ai/dsh-api-session-controller": "0.2.1-alpha.2",
55
+ "@deepseek-ai/dsh-api-workspace-controller": "0.2.1-alpha.2",
56
+ "@deepseek-ai/dsh-client-connection": "0.2.1-alpha.2",
57
+ "@deepseek-ai/dsh-client-locale": "0.2.1-alpha.2",
58
+ "@deepseek-ai/dsh-client-store": "0.2.1-alpha.2",
59
+ "@deepseek-ai/dsh-client-test-runtime": "0.2.1-alpha.2",
60
+ "@deepseek-ai/dsh-client-ui-conversation": "0.2.1-alpha.2",
61
+ "@deepseek-ai/dsh-client-ui-layout": "0.2.1-alpha.2",
62
+ "@deepseek-ai/dsh-client-ui-primitives": "0.2.1-alpha.2",
63
+ "@deepseek-ai/dsh-client-ui-renderer": "0.2.1-alpha.2",
64
+ "@deepseek-ai/dsh-client-ui-session": "0.2.1-alpha.2",
65
+ "@deepseek-ai/dsh-client-ui-sidebar": "0.2.1-alpha.2",
66
+ "@deepseek-ai/dsh-client-ui-slots": "0.2.1-alpha.2",
67
+ "@deepseek-ai/dsh-jobs": "0.2.1-alpha.2",
68
+ "@deepseek-ai/dsh-schedule": "0.2.1-alpha.2",
69
+ "@deepseek-ai/dsh-session": "0.2.1-alpha.2",
70
+ "@deepseek-ai/dsh-subagent": "0.2.1-alpha.2",
71
+ "@deepseek-ai/dsh-typert-protocol": "0.2.1-alpha.2",
72
+ "@deepseek-ai/dsh-util-values": "0.2.1-alpha.2",
73
+ "@deepseek-ai/dsh-util-workspace-path": "0.2.1-alpha.2",
51
74
  "@types/react": "~18.3.1",
52
75
  "clsx": "^2.0.0",
53
76
  "react": "^18.2.0",
54
- "@deepseek-ai/dsh-agent": "0.2.0-rc.2",
55
- "@deepseek-ai/dsh-api-session-controller": "0.2.0-rc.2",
56
- "@deepseek-ai/dsh-api-workspace-controller": "0.2.0-rc.2",
57
- "@deepseek-ai/dsh-client-connection": "0.2.0-rc.2",
58
- "@deepseek-ai/cordis": "~4.0.4",
59
- "@deepseek-ai/dsh-client-locale": "0.2.0-rc.2",
60
- "@deepseek-ai/dsh-client-store": "0.2.0-rc.2",
61
- "@deepseek-ai/dsh-client-test-runtime": "0.2.0-rc.2",
62
- "@deepseek-ai/dsh-client-ui-layout": "0.2.0-rc.2",
63
- "@deepseek-ai/dsh-client-ui-primitives": "0.2.0-rc.2",
64
- "@deepseek-ai/dsh-client-ui-session": "0.2.0-rc.2",
65
- "@deepseek-ai/dsh-client-ui-renderer": "0.2.0-rc.2",
66
- "@deepseek-ai/dsh-client-ui-conversation": "0.2.0-rc.2",
67
- "@deepseek-ai/dsh-client-ui-sidebar": "0.2.0-rc.2",
68
- "@deepseek-ai/dsh-client-ui-slots": "0.2.0-rc.2",
69
- "@deepseek-ai/dsh-session": "0.2.0-rc.2",
70
- "@deepseek-ai/dsh-subagent": "0.2.0-rc.2",
71
- "@deepseek-ai/dsh-typert-protocol": "0.2.0-rc.2",
72
- "@deepseek-ai/dsh-schedule": "0.2.0-rc.2",
73
- "@deepseek-ai/dsh-util-values": "0.2.0-rc.2",
74
- "@deepseek-ai/dsh-jobs": "0.2.0-rc.2",
75
- "@deepseek-ai/dsh-util-workspace-path": "0.2.0-rc.2",
76
- "@deepseek-ai/dsh-client-shortcuts": "0.2.0-rc.2",
77
- "@deepseek-ai/dsh-client-product-analytics": "0.2.0-rc.2",
78
- "@deepseek-ai/dsh-api-remotes": "0.2.0-rc.2"
77
+ "@deepseek-ai/dsh-client-shortcuts": "0.2.1-alpha.2",
78
+ "@deepseek-ai/dsh-client-product-analytics": "0.2.1-alpha.2"
79
79
  },
80
80
  "files": [
81
81
  "lib/index.js",