dsh-group-status 0.3.0 → 0.5.0

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 (126) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/README.en.md +12 -10
  3. package/README.md +14 -10
  4. package/lib/client.js +1294 -281
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +7 -4
  7. package/lib/types/client/BadgeSettingsCard.d.ts.map +1 -1
  8. package/lib/types/client/BadgeSettingsCard.js +6 -6
  9. package/lib/types/client/BadgeSettingsCard.js.map +1 -1
  10. package/lib/types/client/badge-field.d.ts +33 -10
  11. package/lib/types/client/badge-field.d.ts.map +1 -1
  12. package/lib/types/client/badge-field.js +35 -16
  13. package/lib/types/client/badge-field.js.map +1 -1
  14. package/lib/types/client/badge-labels.d.ts +3 -3
  15. package/lib/types/client/badge-labels.js +3 -3
  16. package/lib/types/client/badge-settings.d.ts +0 -25
  17. package/lib/types/client/badge-settings.d.ts.map +1 -1
  18. package/lib/types/client/badge-settings.js +0 -18
  19. package/lib/types/client/badge-settings.js.map +1 -1
  20. package/lib/types/client/contract/slots.d.ts +39 -21
  21. package/lib/types/client/contract/slots.d.ts.map +1 -1
  22. package/lib/types/client/contract/slots.js.map +1 -1
  23. package/lib/types/client/contract/toast.d.ts +67 -0
  24. package/lib/types/client/contract/toast.d.ts.map +1 -0
  25. package/lib/types/client/contract/toast.js +2 -0
  26. package/lib/types/client/contract/toast.js.map +1 -0
  27. package/lib/types/client/facets.d.ts +39 -5
  28. package/lib/types/client/facets.d.ts.map +1 -1
  29. package/lib/types/client/facets.js +48 -4
  30. package/lib/types/client/facets.js.map +1 -1
  31. package/lib/types/client/index.d.ts +6 -3
  32. package/lib/types/client/index.d.ts.map +1 -1
  33. package/lib/types/client/index.js +48 -13
  34. package/lib/types/client/index.js.map +1 -1
  35. package/lib/types/client/locales.d.ts +14 -8
  36. package/lib/types/client/locales.d.ts.map +1 -1
  37. package/lib/types/client/locales.js +14 -8
  38. package/lib/types/client/locales.js.map +1 -1
  39. package/lib/types/client/navigation.d.ts +45 -12
  40. package/lib/types/client/navigation.d.ts.map +1 -1
  41. package/lib/types/client/navigation.js +102 -26
  42. package/lib/types/client/navigation.js.map +1 -1
  43. package/lib/types/client/rows/GroupBadge.d.ts +4 -4
  44. package/lib/types/client/rows/GroupBadge.d.ts.map +1 -1
  45. package/lib/types/client/rows/GroupBadge.js +10 -7
  46. package/lib/types/client/rows/GroupBadge.js.map +1 -1
  47. package/lib/types/client/rows/Rows.d.ts.map +1 -1
  48. package/lib/types/client/rows/Rows.js +4 -4
  49. package/lib/types/client/rows/Rows.js.map +1 -1
  50. package/lib/types/client/rows/WorkspaceBrowser.d.ts +1 -1
  51. package/lib/types/client/rows/WorkspaceBrowser.d.ts.map +1 -1
  52. package/lib/types/client/rows/WorkspaceBrowser.js +66 -61
  53. package/lib/types/client/rows/WorkspaceBrowser.js.map +1 -1
  54. package/lib/types/client/session-actions/ArchiveSession.js +1 -1
  55. package/lib/types/client/session-actions/ArchiveSession.js.map +1 -1
  56. package/lib/types/client/session-actions/ForkSession.d.ts +5 -4
  57. package/lib/types/client/session-actions/ForkSession.d.ts.map +1 -1
  58. package/lib/types/client/session-actions/ForkSession.js +7 -4
  59. package/lib/types/client/session-actions/ForkSession.js.map +1 -1
  60. package/lib/types/client/session-actions/PinSession.js +1 -1
  61. package/lib/types/client/session-actions/PinSession.js.map +1 -1
  62. package/lib/types/client/session-actions/RowActionToast.d.ts +5 -8
  63. package/lib/types/client/session-actions/RowActionToast.d.ts.map +1 -1
  64. package/lib/types/client/session-actions/RowActionToast.js +10 -49
  65. package/lib/types/client/session-actions/RowActionToast.js.map +1 -1
  66. package/lib/types/client/session-actions/SessionId.d.ts +8 -0
  67. package/lib/types/client/session-actions/SessionId.d.ts.map +1 -0
  68. package/lib/types/client/session-actions/SessionId.js +16 -0
  69. package/lib/types/client/session-actions/SessionId.js.map +1 -0
  70. package/lib/types/client/session-watermarks.d.ts +30 -0
  71. package/lib/types/client/session-watermarks.d.ts.map +1 -0
  72. package/lib/types/client/session-watermarks.js +54 -0
  73. package/lib/types/client/session-watermarks.js.map +1 -0
  74. package/lib/types/client/shortcuts.d.ts +2 -7
  75. package/lib/types/client/shortcuts.d.ts.map +1 -1
  76. package/lib/types/client/shortcuts.js +7 -11
  77. package/lib/types/client/shortcuts.js.map +1 -1
  78. package/lib/types/client/toast/Toast.d.ts +41 -0
  79. package/lib/types/client/toast/Toast.d.ts.map +1 -0
  80. package/lib/types/client/toast/Toast.js +44 -0
  81. package/lib/types/client/toast/Toast.js.map +1 -0
  82. package/lib/types/client/toast/register.d.ts +11 -0
  83. package/lib/types/client/toast/register.d.ts.map +1 -0
  84. package/lib/types/client/toast/register.js +162 -0
  85. package/lib/types/client/toast/register.js.map +1 -0
  86. package/lib/types/client/tree.d.ts +16 -1
  87. package/lib/types/client/tree.d.ts.map +1 -1
  88. package/lib/types/client/tree.js +20 -8
  89. package/lib/types/client/tree.js.map +1 -1
  90. package/lib/types/client/use-registered-facets.d.ts +11 -0
  91. package/lib/types/client/use-registered-facets.d.ts.map +1 -0
  92. package/lib/types/client/use-registered-facets.js +48 -0
  93. package/lib/types/client/use-registered-facets.js.map +1 -0
  94. package/lib/types/host-contract.d.ts +57 -0
  95. package/lib/types/host-contract.d.ts.map +1 -0
  96. package/lib/types/host-contract.js +84 -0
  97. package/lib/types/host-contract.js.map +1 -0
  98. package/lib/types/index.d.ts +7 -10
  99. package/lib/types/index.d.ts.map +1 -1
  100. package/lib/types/index.js +7 -4
  101. package/lib/types/index.js.map +1 -1
  102. package/package.json +34 -30
  103. package/lib/types/client/facet-visibility.d.ts +0 -17
  104. package/lib/types/client/facet-visibility.d.ts.map +0 -1
  105. package/lib/types/client/facet-visibility.js +0 -18
  106. package/lib/types/client/facet-visibility.js.map +0 -1
  107. package/lib/types/client/legacy/bridge.d.ts +0 -30
  108. package/lib/types/client/legacy/bridge.d.ts.map +0 -1
  109. package/lib/types/client/legacy/bridge.js +0 -58
  110. package/lib/types/client/legacy/bridge.js.map +0 -1
  111. package/lib/types/client/legacy/register-legacy-facets.d.ts +0 -14
  112. package/lib/types/client/legacy/register-legacy-facets.d.ts.map +0 -1
  113. package/lib/types/client/legacy/register-legacy-facets.js +0 -77
  114. package/lib/types/client/legacy/register-legacy-facets.js.map +0 -1
  115. package/lib/types/client/legacy/schedule.d.ts +0 -49
  116. package/lib/types/client/legacy/schedule.d.ts.map +0 -1
  117. package/lib/types/client/legacy/schedule.js +0 -73
  118. package/lib/types/client/legacy/schedule.js.map +0 -1
  119. package/lib/types/client/legacy/store-source.d.ts +0 -78
  120. package/lib/types/client/legacy/store-source.d.ts.map +0 -1
  121. package/lib/types/client/legacy/store-source.js +0 -82
  122. package/lib/types/client/legacy/store-source.js.map +0 -1
  123. package/lib/types/client/legacy/waiting-review.d.ts +0 -85
  124. package/lib/types/client/legacy/waiting-review.d.ts.map +0 -1
  125. package/lib/types/client/legacy/waiting-review.js +0 -179
  126. package/lib/types/client/legacy/waiting-review.js.map +0 -1
package/lib/client.js CHANGED
@@ -5,8 +5,8 @@ window.__ModuleLoader__.load({
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  let _deepseek_ai_dsh_client_store = require("@deepseek-ai/dsh-client-store");
8
- let react = require("react");
9
8
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
9
+ let react = require("react");
10
10
  let react_jsx_runtime = require("react/jsx-runtime");
11
11
  let _deepseek_ai_cordis = require("@deepseek-ai/cordis");
12
12
  //#region src/client/contract/slots.ts
@@ -30,10 +30,8 @@ window.__ModuleLoader__.load({
30
30
  searchRequest: 0,
31
31
  addRequested: false,
32
32
  directoryBusy: false,
33
- renameTarget: null,
34
- forkError: null
33
+ renameTarget: null
35
34
  });
36
- let forkErrorSeq = 0;
37
35
  return {
38
36
  state,
39
37
  search: () => {
@@ -74,22 +72,6 @@ window.__ModuleLoader__.load({
74
72
  ...state.getSnapshot(),
75
73
  renameTarget: null
76
74
  });
77
- },
78
- forkFailed: (reason) => {
79
- forkErrorSeq += 1;
80
- state.set({
81
- ...state.getSnapshot(),
82
- forkError: {
83
- reason,
84
- seq: forkErrorSeq
85
- }
86
- });
87
- },
88
- dismissForkError: () => {
89
- state.set({
90
- ...state.getSnapshot(),
91
- forkError: null
92
- });
93
75
  }
94
76
  };
95
77
  }
@@ -100,8 +82,9 @@ window.__ModuleLoader__.load({
100
82
  * @param navigation - session creation and forking from the pointer controls' navigation service.
101
83
  * @param controls - browser-owned opening requests.
102
84
  * @param archiveSession - shared archive action, including running-work confirmation and notices.
85
+ * @param reportForkFailure - publish a classified failure through the frame-wide notice channel.
103
86
  */
104
- function installWorkspaceShortcuts(ctx, navigation, controls, archiveSession) {
87
+ function installWorkspaceShortcuts(ctx, navigation, controls, archiveSession, reportForkFailure) {
105
88
  const t = ctx.locale.bind("workspace");
106
89
  const current = () => Object.values(ctx.sessions.list.getSnapshot().byId).find((row) => (row.retainedBy.mainView ?? 0) > 0);
107
90
  const addReason = () => ctx.slots.entries("sidebar.workspaces.directoryFlow").length === 0 ? t("shortcut.noPicker") : controls.state.getSnapshot().directoryBusy ? t("shortcut.directoryBusy") : null;
@@ -183,8 +166,8 @@ window.__ModuleLoader__.load({
183
166
  status: "handled",
184
167
  run: () => {
185
168
  navigation.forkSession(target.id).catch((error) => {
186
- const unavailable = error instanceof Error && error.name === "SessionForkError" && error.rpcError.code === "session/fork-unavailable";
187
- controls.forkFailed(unavailable ? "unavailable" : "failed");
169
+ const unavailable = (error instanceof Error && error.name === "SessionForkError" ? error : void 0)?.rpcError.code === "session/fork-unavailable";
170
+ reportForkFailure(unavailable ? "unavailable" : "failed");
188
171
  if (!unavailable) console.warn("session fork rejected:", error);
189
172
  });
190
173
  }
@@ -228,30 +211,17 @@ window.__ModuleLoader__.load({
228
211
  */
229
212
  /** Loader 条目 id —— 设置页的寻址键,也是宿主 `describe()` 的 ns。 */
230
213
  const ENTRY_ID = "dsh-group-status";
231
- /**
232
- * 一个 facet 是否被设置关掉。
233
- *
234
- * - 没有 `settingKey` → 始终参与(缺省是"显示",与其余开关的默认值同源)。
235
- * - 声明了但不是布尔开关键 → 同样按显示处理:拼错的键不该让一个状态整块消失。
236
- */
237
- function facetVisibleBySettings(settings, settingKey) {
238
- if (settingKey === void 0) return true;
239
- const value = settings[settingKey];
240
- return typeof value === "boolean" ? value : true;
241
- }
242
214
  /** 默认值:与 `Config` 各字段的 schema 默认同源(全部显示,数字阈值 2 即 count=1 时省略)。 */
243
215
  const DEFAULT_BADGE_SETTINGS = Object.freeze({
244
216
  showCompletedInBadge: true,
245
217
  showRunningInBadge: true,
246
218
  showPendingInBadge: true,
247
- showScheduledInBadge: true,
248
- showWaitingReviewInBadge: true,
249
219
  showHoverDetails: true,
250
220
  countThreshold: 2
251
221
  });
252
222
  /** 两个设置对象是否逐字段相等(用于发布前比较,避免无谓通知)。 */
253
223
  function settingsEqual(a, b) {
254
- return a.showCompletedInBadge === b.showCompletedInBadge && a.showRunningInBadge === b.showRunningInBadge && a.showPendingInBadge === b.showPendingInBadge && a.showScheduledInBadge === b.showScheduledInBadge && a.showWaitingReviewInBadge === b.showWaitingReviewInBadge && a.showHoverDetails === b.showHoverDetails && a.countThreshold === b.countThreshold;
224
+ return a.showCompletedInBadge === b.showCompletedInBadge && a.showRunningInBadge === b.showRunningInBadge && a.showPendingInBadge === b.showPendingInBadge && a.showHoverDetails === b.showHoverDetails && a.countThreshold === b.countThreshold;
255
225
  }
256
226
  /** 从原始快照派生:只有**显式 false** 才隐藏,缺失/非法一律显示(countThreshold 默认 2)。 */
257
227
  function resolveFromSnapshot(snap) {
@@ -270,8 +240,6 @@ window.__ModuleLoader__.load({
270
240
  showCompletedInBadge: getBool("showCompletedInBadge", true),
271
241
  showRunningInBadge: getBool("showRunningInBadge", true),
272
242
  showPendingInBadge: getBool("showPendingInBadge", true),
273
- showScheduledInBadge: getBool("showScheduledInBadge", true),
274
- showWaitingReviewInBadge: getBool("showWaitingReviewInBadge", true),
275
243
  showHoverDetails: getBool("showHoverDetails", true),
276
244
  countThreshold: getNumber("countThreshold", 2)
277
245
  };
@@ -451,12 +419,52 @@ window.__ModuleLoader__.load({
451
419
  };
452
420
  }
453
421
  /**
454
- * 生产用的模块级单例登记表面。
422
+ * 挂在 `globalThis` 上的登记表面的键名。
423
+ *
424
+ * 它同时是**现场诊断入口**:宿主里可以直接
425
+ * `globalThis.__dshGroupStatusFacetRegistry__.list().map(e => e.facet.id)` 读出"此刻到底
426
+ * 有哪些状态源登记上了"——「我注册了为什么没显示」不该只能靠猜(协议文档点名的那类
427
+ * 不可诊断失败)。
428
+ */
429
+ const SHARED_REGISTRY_KEY = "__dshGroupStatusFacetRegistry__";
430
+ /**
431
+ * 取(并首次创建)**进程级**共享的登记表面。
432
+ *
433
+ * ## 为什么不能是模块级
434
+ *
435
+ * 提供方把 facet 注册进来是一次性的:它在自己的 `apply` 里 `require` 本插件的 client
436
+ * 入口、登记完就把注销函数挂在 effect 上。而宿主的热替换(HMR、`reload(id, rev)`)
437
+ * **只换代被替换的那个模块**——`dsh-client-modules` 的 `invalidate()` 只删自己那一行的
438
+ * factory 与 loadCache,不级联到"谁 require 过它"(依赖方不会重跑 apply)。
439
+ *
440
+ * 于是模块级单例在这里必然失效:本插件被换成新实例 → 新实例建一张**空表** → 提供方
441
+ * 那份注册留在被丢弃的旧表上 → 徽标从此少一家,且**只有刷新页面才好**。开发环境天天
442
+ * 重建 `lib/`,这条几乎必然发生。
443
+ *
444
+ * 挂到 `globalThis` 上之后,换代只是"再拿一次同一张表":注册跨换代存活,`facetStore()`
445
+ * 在新实例里 `rewire()` 时照样订阅到那些来源。提供方被换代时按同 id 覆盖登记(见
446
+ * {@link FacetRegistry.register} 的来源决胜),不会重复计数。
447
+ *
448
+ * @returns 全进程唯一的登记表面。
449
+ */
450
+ function sharedFacetRegistry() {
451
+ const host = globalThis;
452
+ const existing = host[SHARED_REGISTRY_KEY];
453
+ if (existing !== void 0) return existing;
454
+ const created = createFacetRegistry();
455
+ host[SHARED_REGISTRY_KEY] = created;
456
+ return created;
457
+ }
458
+ /**
459
+ * 生产用的单例登记表面。
455
460
  *
456
461
  * 与 {@link createFacetRegistry} 分开,是为了让测试能对着一张干净的表断言协议行为,
457
462
  * 而不会被别的用例或别的插件登记的 facet 污染。
463
+ *
464
+ * ⚠️ 它只是 {@link sharedFacetRegistry} 在**本模块首次加载时**取到的那份引用;跨模块实例
465
+ * 存活的是 globalThis 上那一张,所以运行期一律经 {@link sharedFacetRegistry} 现取。
458
466
  */
459
- const registry = createFacetRegistry();
467
+ const registry = sharedFacetRegistry();
460
468
  /**
461
469
  * 本插件 client 入口对外的登记函数(provider 侧唯一需要认识的符号)。
462
470
  *
@@ -465,13 +473,14 @@ window.__ModuleLoader__.load({
465
473
  * 返回值形状保持稳定,provider 的 dispose 路径不必区分成败。
466
474
  */
467
475
  function registerGroupStatusFacet(facet) {
468
- const result = registry.register(facet);
476
+ const target = sharedFacetRegistry();
477
+ const result = target.register(facet);
469
478
  if (!result.ok) {
470
479
  if (!(result.reason === "superseded" && facetOrigin(facet) === "legacy")) reportRejection(result);
471
480
  return () => void 0;
472
481
  }
473
482
  return () => {
474
- registry.remove(result.id, facet);
483
+ target.remove(result.id, facet);
475
484
  };
476
485
  }
477
486
  /**
@@ -592,19 +601,39 @@ window.__ModuleLoader__.load({
592
601
  }
593
602
  //#endregion
594
603
  //#region src/client/badge-field.ts
595
- /** 卡片字段名(与 `src/index.ts` 的 `Config` 同名同义)。 */
604
+ /**
605
+ * 每个开关控制的是徽标上的哪一枚点。
606
+ *
607
+ * 单一事实来源:设置卡片里给每行画的**预览点**与折叠徽标实际画的那三枚点,都从这里取。
608
+ * 两处各写一套的话迟早会漂,而漂了的后果不是"多一个点少一个点",是**卡片预览与徽标说的
609
+ * 不是一回事**(同一族毛病:#25① 修过点行与明细的顺序,#37 又发现配置面里混着别人家的语义)。
610
+ *
611
+ * 对应关系与 `rows/GroupBadge.tsx` 一致:待审批 = warning 黄点、进行中 = ongoing 转圈、
612
+ * 已完成 = done 绿点。外部来源的点**不在这里** —— 它们由来源自己画(`renderIcon`),
613
+ * 也不在本插件的卡片上(显隐归来源自己)。
614
+ */
615
+ const BADGE_FIELD_DOT_STATE = {
616
+ showPendingInBadge: "warning",
617
+ showRunningInBadge: "ongoing",
618
+ showCompletedInBadge: "done"
619
+ };
620
+ /**
621
+ * 卡片字段名(与 `src/index.ts` 的 `Config` 同名同义)。
622
+ *
623
+ * ⚠️ 只有本插件自己的语义。曾有 `showScheduledInBadge` / `showWaitingReviewInBadge`
624
+ * 两个字段替 dsh-later / dsh-gitea-dispatch 写死名字与图标形状,现已删除:外部来源的
625
+ * 显隐归来源自己(不注册就是不显示)。
626
+ */
596
627
  const BADGE_FIELDS = {
597
628
  showCompletedInBadge: "showCompletedInBadge",
598
629
  showRunningInBadge: "showRunningInBadge",
599
630
  showPendingInBadge: "showPendingInBadge",
600
- showScheduledInBadge: "showScheduledInBadge",
601
- showWaitingReviewInBadge: "showWaitingReviewInBadge",
602
631
  showHoverDetails: "showHoverDetails",
603
632
  countThreshold: "countThreshold"
604
633
  };
605
634
  /**
606
- * 草稿文本解析成开关状态:与 {@link isFieldEnabled} 不同,这里比较的是模型给的
607
- * **文本**,别把字符串喂给上面那个函数(`'false' !== false` 恒真)。
635
+ * 草稿文本解析成开关状态:比较的是模型给的**文本**,不是存储值 ——
636
+ * 别把字符串 `'false'` 当成布尔 `false`(`'false' !== false` 恒真)。
608
637
  */
609
638
  function isDraftEnabled(text) {
610
639
  return text !== "false";
@@ -633,13 +662,21 @@ window.__ModuleLoader__.load({
633
662
  } : void 0
634
663
  };
635
664
  }
636
- /** 所有字段规格(按卡片渲染顺序)。 */
665
+ /** 各字段的字段转换规格。 */
666
+ const showCompletedField = makeBooleanField(BADGE_FIELDS.showCompletedInBadge, true);
667
+ const showRunningField = makeBooleanField(BADGE_FIELDS.showRunningInBadge, true);
668
+ /**
669
+ * 所有字段规格。
670
+ *
671
+ * ⚠️ 顺序**必须**与徽标画点的顺序一致(`rows/GroupBadge.tsx`:待审批 → 进行中 → 已完成;
672
+ * 外部来源排在已完成之前,但它们不在本插件的卡片上)。用户操作场景就是"我看到徽标第 N
673
+ * 个图标,想关掉它",卡片顺序错位会让人怀疑两处是两套事实 —— 同一族毛病 #25① 已经在
674
+ * 点行与明细之间修过一次。回归用例:`tests/badge-settings-card.client.spec.tsx`。
675
+ */
637
676
  const ALL_BADGE_FIELDS = [
638
- makeBooleanField(BADGE_FIELDS.showCompletedInBadge, true),
639
- makeBooleanField(BADGE_FIELDS.showRunningInBadge, true),
640
677
  makeBooleanField(BADGE_FIELDS.showPendingInBadge, true),
641
- makeBooleanField(BADGE_FIELDS.showScheduledInBadge, true),
642
- makeBooleanField(BADGE_FIELDS.showWaitingReviewInBadge, true),
678
+ showRunningField,
679
+ showCompletedField,
643
680
  makeBooleanField(BADGE_FIELDS.showHoverDetails, true),
644
681
  {
645
682
  field: BADGE_FIELDS.countThreshold,
@@ -659,7 +696,7 @@ window.__ModuleLoader__.load({
659
696
  ];
660
697
  //#endregion
661
698
  //#region \0dsh-css:src/client/BadgeSettingsCard.module.css.mjs
662
- const css$4 = ".dhp54G_section{min-width:0;padding:16px 0}.dhp54G_section:last-child{padding-bottom:0}.dhp54G_heading{color:var(--dsw-alias-label-primary);margin:0;font-size:13px;font-weight:600;line-height:1.5}.dhp54G_badgeField{gap:6px;padding:12px 0;display:grid}.dhp54G_badgeToggleRow{color:var(--dsw-alias-label-primary);justify-content:space-between;align-items:flex-start;gap:16px;font-size:13px;line-height:1.5;display:flex}.dhp54G_badgeToggleLabel{flex:1;min-width:0}.dhp54G_badgeNumberControl{align-items:center;gap:4px;display:flex}.dhp54G_badgeNumberButton{width:28px;height:28px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-elevated,#2a2a2a);border:1px solid var(--dsw-alias-border-primary,#555);cursor:pointer;user-select:none;border-radius:6px;justify-content:center;align-items:center;padding:0;font-size:16px;font-weight:600;line-height:1;display:flex}.dhp54G_badgeNumberButton:hover:not(:disabled){border-color:var(--dsw-alias-border-focus,#0078d4);background:var(--dsw-alias-bg-hover,#3a3a3a)}.dhp54G_badgeNumberButton:active:not(:disabled){background:var(--dsw-alias-bg-active,#4a4a4a)}.dhp54G_badgeNumberButton:disabled{opacity:.3;cursor:not-allowed}.dhp54G_badgeNumberValue{text-align:center;min-width:24px;color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500}.dhp54G_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}";
699
+ const css$4 = ".dhp54G_section{min-width:0;padding:16px 0}.dhp54G_section:last-child{padding-bottom:0}.dhp54G_heading{color:var(--dsw-alias-label-primary);margin:0;font-size:13px;font-weight:600;line-height:1.5}.dhp54G_badgeField{gap:6px;padding:12px 0;display:grid}.dhp54G_badgeToggleRow{color:var(--dsw-alias-label-primary);justify-content:space-between;align-items:flex-start;gap:16px;font-size:13px;line-height:1.5;display:flex}.dhp54G_badgeSpecimen{vertical-align:-2px;align-items:center;margin-right:6px;display:inline-flex}.dhp54G_badgeToggleLabel{flex:1;min-width:0}.dhp54G_badgeNumberControl{align-items:center;gap:4px;display:flex}.dhp54G_badgeNumberButton{width:28px;height:28px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-elevated,#2a2a2a);border:1px solid var(--dsw-alias-border-primary,#555);cursor:pointer;user-select:none;border-radius:6px;justify-content:center;align-items:center;padding:0;font-size:16px;font-weight:600;line-height:1;display:flex}.dhp54G_badgeNumberButton:hover:not(:disabled){border-color:var(--dsw-alias-border-focus,#0078d4);background:var(--dsw-alias-bg-hover,#3a3a3a)}.dhp54G_badgeNumberButton:active:not(:disabled){background:var(--dsw-alias-bg-active,#4a4a4a)}.dhp54G_badgeNumberButton:disabled{opacity:.3;cursor:not-allowed}.dhp54G_badgeNumberValue{text-align:center;min-width:24px;color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500}.dhp54G_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}";
663
700
  const tagId$4 = "dsh-group-status/BadgeSettingsCard.module.css";
664
701
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$4) + "]") === null) {
665
702
  const tag = document.createElement("style");
@@ -673,6 +710,7 @@ window.__ModuleLoader__.load({
673
710
  "badgeNumberButton": "dhp54G_badgeNumberButton",
674
711
  "badgeNumberControl": "dhp54G_badgeNumberControl",
675
712
  "badgeNumberValue": "dhp54G_badgeNumberValue",
713
+ "badgeSpecimen": "dhp54G_badgeSpecimen",
676
714
  "badgeToggleLabel": "dhp54G_badgeToggleLabel",
677
715
  "badgeToggleRow": "dhp54G_badgeToggleRow",
678
716
  "heading": "dhp54G_heading",
@@ -753,21 +791,29 @@ window.__ModuleLoader__.load({
753
791
  id: badgeSectionId,
754
792
  children: t("settings.badge.section")
755
793
  }),
756
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
757
- field: BADGE_FIELDS.showRunningInBadge,
758
- text: fieldText(BADGE_FIELDS.showRunningInBadge),
759
- label: t("settings.badge.showRunning"),
760
- hint: t("settings.badge.showRunningHint"),
761
- state,
762
- actions
763
- }),
764
794
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
765
795
  field: BADGE_FIELDS.showPendingInBadge,
766
796
  text: fieldText(BADGE_FIELDS.showPendingInBadge),
767
797
  label: t("settings.badge.showPending"),
768
798
  hint: t("settings.badge.showPendingHint"),
769
799
  state,
770
- actions
800
+ actions,
801
+ specimen: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, {
802
+ state: BADGE_FIELD_DOT_STATE.showPendingInBadge,
803
+ size: 12
804
+ })
805
+ }),
806
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
807
+ field: BADGE_FIELDS.showRunningInBadge,
808
+ text: fieldText(BADGE_FIELDS.showRunningInBadge),
809
+ label: t("settings.badge.showRunning"),
810
+ hint: t("settings.badge.showRunningHint"),
811
+ state,
812
+ actions,
813
+ specimen: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, {
814
+ state: BADGE_FIELD_DOT_STATE.showRunningInBadge,
815
+ size: 12
816
+ })
771
817
  }),
772
818
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
773
819
  field: BADGE_FIELDS.showCompletedInBadge,
@@ -775,23 +821,11 @@ window.__ModuleLoader__.load({
775
821
  label: t("settings.badge.showCompleted"),
776
822
  hint: t("settings.badge.showCompletedHint"),
777
823
  state,
778
- actions
779
- }),
780
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
781
- field: BADGE_FIELDS.showScheduledInBadge,
782
- text: fieldText(BADGE_FIELDS.showScheduledInBadge),
783
- label: t("settings.badge.showScheduled"),
784
- hint: t("settings.badge.showScheduledHint"),
785
- state,
786
- actions
787
- }),
788
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
789
- field: BADGE_FIELDS.showWaitingReviewInBadge,
790
- text: fieldText(BADGE_FIELDS.showWaitingReviewInBadge),
791
- label: t("settings.badge.showWaitingReview"),
792
- hint: t("settings.badge.showWaitingReviewHint"),
793
- state,
794
- actions
824
+ actions,
825
+ specimen: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, {
826
+ state: BADGE_FIELD_DOT_STATE.showCompletedInBadge,
827
+ size: 12
828
+ })
795
829
  })
796
830
  ]
797
831
  }),
@@ -830,15 +864,18 @@ window.__ModuleLoader__.load({
830
864
  ]
831
865
  });
832
866
  }
833
- /** 单行开关:label + Switch + hint。 */
834
- function ToggleRow({ field, text, label, hint, state, actions }) {
867
+ /** 单行开关:label + Switch + hint。`specimen` 是行首那枚预览点(可选)。 */
868
+ function ToggleRow({ field, text, label, hint, state, actions, specimen }) {
835
869
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
836
870
  className: BadgeSettingsCard_module_css_default.badgeField,
837
871
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
838
872
  className: BadgeSettingsCard_module_css_default.badgeToggleRow,
839
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
873
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
840
874
  className: BadgeSettingsCard_module_css_default.badgeToggleLabel,
841
- children: label
875
+ children: [specimen !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
876
+ className: BadgeSettingsCard_module_css_default.badgeSpecimen,
877
+ children: specimen
878
+ }), label]
842
879
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
843
880
  checked: isDraftEnabled(text),
844
881
  label,
@@ -902,7 +939,838 @@ window.__ModuleLoader__.load({
902
939
  });
903
940
  }
904
941
  //#endregion
905
- //#region node_modules/.pnpm/@deepseek-ai+dsh-util-values@0.2.0-rc.2_@deepseek-ai+cordis@4.0.4/node_modules/@deepseek-ai/dsh-util-values/lib/index.js
942
+ //#region src/client/toast/Toast.tsx
943
+ /** Shared Workspace notice presentation and keyed business adapters. */
944
+ /**
945
+ * Render one notice using the shared Factory.
946
+ * @param props - keyed payload, injected display mapping, locale, and Factory renderer.
947
+ * @returns the shared Toast occurrence.
948
+ */
949
+ function ToastEntry(props) {
950
+ const { dismissToast, display, t, renderFactorySlot, seq } = props;
951
+ return renderFactorySlot("workspace.toast.content", {
952
+ ...display({ ...props }, t, dismissToast),
953
+ seq,
954
+ onDone: dismissToast
955
+ });
956
+ }
957
+ /**
958
+ * Keep the archive filter action reactive without subscribing unrelated notices.
959
+ * @param props - archive notice, display mapping, and the existing viewing store.
960
+ * @returns the shared Toast occurrence with its current actions.
961
+ */
962
+ function ArchiveToastEntry(props) {
963
+ const { dismissToast, display, filterAction, useStore, t, renderFactorySlot, seq } = props;
964
+ const archivedVisible = useStore((state) => (state.archivedFilter ?? "default") !== "default");
965
+ const fields = display({ ...props }, t, dismissToast);
966
+ return renderFactorySlot("workspace.toast.content", {
967
+ ...fields,
968
+ actions: [...fields.actions ?? [], ...archivedVisible ? [] : [filterAction(t, dismissToast)]],
969
+ seq,
970
+ onDone: dismissToast
971
+ });
972
+ }
973
+ /**
974
+ * Present plain display data with the shared primitive's timing and portal.
975
+ * @param props - localized display fields, display identity, and dismissal.
976
+ * @returns one Toast, remounted only for a new display identity.
977
+ */
978
+ function WorkspaceToast({ seq, text, tone, holdMs, actions, onDone }) {
979
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Toast, {
980
+ text,
981
+ onDone,
982
+ ...tone === "success" ? { tone: "success" } : {},
983
+ ...tone === "warning" ? { icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconWarningOutlineRegular, {}) } : {},
984
+ ...holdMs === void 0 ? {} : { holdMs },
985
+ ...actions === void 0 ? {} : { actions }
986
+ }, seq);
987
+ }
988
+ //#endregion
989
+ //#region src/client/toast/register.tsx
990
+ /** Wrap a business display mapping without making the slot component generic. */
991
+ function plainToast(display) {
992
+ return () => ({ display });
993
+ }
994
+ /**
995
+ * Register notice presentations with their business-owned display fields and actions.
996
+ * @param ctx - workspace plugin context.
997
+ * @param viewStore - viewing store, read only by archive notices.
998
+ * @param actions - archive and source-opening callbacks.
999
+ */
1000
+ function registerToasts(ctx, viewStore, actions) {
1001
+ const toastEntry = ToastEntry;
1002
+ const archiveToastEntry = ArchiveToastEntry;
1003
+ const archiveFilterAction = (t, dismiss) => ({
1004
+ prefix: t("toast.archivedOr"),
1005
+ label: t("toast.archivedFilter"),
1006
+ onClick: () => {
1007
+ dismiss();
1008
+ actions.showArchived();
1009
+ }
1010
+ });
1011
+ ctx.slots.registerFactory({
1012
+ name: "workspace.toast.content",
1013
+ scope: "root"
1014
+ }, WorkspaceToast);
1015
+ ctx.slots.inject("workspace.toast", function* () {
1016
+ yield ctx.slots.register({
1017
+ name: "workspace.toast",
1018
+ key: "sessionIdCopied",
1019
+ locale: "workspace",
1020
+ inject: plainToast((_notice, t) => ({
1021
+ text: t("toast.sessionIdCopied"),
1022
+ tone: "success"
1023
+ }))
1024
+ }, toastEntry);
1025
+ yield ctx.slots.register({
1026
+ name: "workspace.toast",
1027
+ key: "sessionIdCopyFailed",
1028
+ locale: "workspace",
1029
+ inject: plainToast((_notice, t) => ({
1030
+ text: t("toast.sessionIdCopyFailed"),
1031
+ tone: "warning"
1032
+ }))
1033
+ }, toastEntry);
1034
+ yield ctx.slots.register({
1035
+ name: "workspace.toast",
1036
+ key: "pinFailed",
1037
+ locale: "workspace",
1038
+ inject: plainToast((_notice, t) => ({
1039
+ text: t("toast.pinFailed"),
1040
+ tone: "warning"
1041
+ }))
1042
+ }, toastEntry);
1043
+ yield ctx.slots.register({
1044
+ name: "workspace.toast",
1045
+ key: "unpinFailed",
1046
+ locale: "workspace",
1047
+ inject: plainToast((_notice, t) => ({
1048
+ text: t("toast.unpinFailed"),
1049
+ tone: "warning"
1050
+ }))
1051
+ }, toastEntry);
1052
+ yield ctx.slots.register({
1053
+ name: "workspace.toast",
1054
+ key: "archivedNotOpenable",
1055
+ locale: "workspace",
1056
+ inject: plainToast((_notice, t) => ({
1057
+ text: t("toast.archivedNotOpenable"),
1058
+ tone: "warning"
1059
+ }))
1060
+ }, toastEntry);
1061
+ yield ctx.slots.register({
1062
+ name: "workspace.toast",
1063
+ key: "defaultWorkspaceFailed",
1064
+ locale: "workspace",
1065
+ inject: plainToast((_notice, t) => ({
1066
+ text: t("defaultWorkspace.failed"),
1067
+ tone: "warning"
1068
+ }))
1069
+ }, toastEntry);
1070
+ yield ctx.slots.register({
1071
+ name: "workspace.toast",
1072
+ key: "shortcutForkFailed",
1073
+ locale: "workspace",
1074
+ inject: plainToast((notice, t) => ({
1075
+ text: t(notice.reason === "unavailable" ? "shortcut.noCompletedTurn" : "shortcut.forkFailed"),
1076
+ tone: "plain"
1077
+ }))
1078
+ }, toastEntry);
1079
+ yield ctx.slots.register({
1080
+ name: "workspace.toast",
1081
+ key: "createFailed",
1082
+ locale: "workspace",
1083
+ inject: plainToast((notice, t) => ({
1084
+ text: t("toast.createFailed", { message: notice.message }),
1085
+ tone: "warning",
1086
+ holdMs: 6e3
1087
+ }))
1088
+ }, toastEntry);
1089
+ yield ctx.slots.register({
1090
+ name: "workspace.toast",
1091
+ key: "forkRequiresOpen",
1092
+ locale: "workspace",
1093
+ inject: plainToast((notice, t, dismiss) => ({
1094
+ text: t("toast.forkRequiresOpen"),
1095
+ tone: "warning",
1096
+ holdMs: 6e3,
1097
+ actions: [{
1098
+ label: t("toast.openForkSource"),
1099
+ onClick: () => {
1100
+ dismiss();
1101
+ actions.openForkSource(notice.sessionId);
1102
+ }
1103
+ }]
1104
+ }))
1105
+ }, toastEntry);
1106
+ yield ctx.slots.register({
1107
+ name: "workspace.toast",
1108
+ key: "archived",
1109
+ locale: "workspace",
1110
+ store: viewStore,
1111
+ inject: () => ({
1112
+ display: (notice, t, dismiss) => ({
1113
+ text: t("toast.archived"),
1114
+ tone: "success",
1115
+ holdMs: 6e3,
1116
+ actions: [{
1117
+ label: t("toast.archivedUndo"),
1118
+ onClick: () => {
1119
+ dismiss();
1120
+ actions.undoArchive(notice.sessionId);
1121
+ }
1122
+ }]
1123
+ }),
1124
+ filterAction: archiveFilterAction
1125
+ })
1126
+ }, archiveToastEntry);
1127
+ yield ctx.slots.register({
1128
+ name: "workspace.toast",
1129
+ key: "stoppedAndArchived",
1130
+ locale: "workspace",
1131
+ store: viewStore,
1132
+ inject: () => ({
1133
+ display: (notice, t, dismiss) => ({
1134
+ text: t("toast.stoppedAndArchived"),
1135
+ tone: "success",
1136
+ holdMs: 6e3,
1137
+ actions: [{
1138
+ label: t("toast.archivedUndo"),
1139
+ onClick: () => {
1140
+ dismiss();
1141
+ actions.undoArchive(notice.sessionId);
1142
+ }
1143
+ }]
1144
+ }),
1145
+ filterAction: archiveFilterAction
1146
+ })
1147
+ }, archiveToastEntry);
1148
+ });
1149
+ }
1150
+ //#endregion
1151
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-util-values@0.2.1-rc.2_@deepseek-ai+cordis@4.0.5/node_modules/@deepseek-ai/dsh-util-values/lib/index.js
1152
+ /**
1153
+ * Lazily scanned view of one JSON object's top-level fields, built from text
1154
+ * that may still be streaming or from an already parsed object. Nothing is
1155
+ * scanned until a reader asks; the view remembers every question it answered
1156
+ * and reports changed answers when the owner refreshes for publication.
1157
+ * Used for model tool-call arguments: a row reads the fields it
1158
+ * cares about at whatever granularity it displays, at every stage of the call.
1159
+ * @module @deepseek-ai/dsh-util-values/src/partial-json
1160
+ */
1161
+ const SIMPLE_ESCAPES = {
1162
+ "\"": "\"",
1163
+ "\\": "\\",
1164
+ "/": "/",
1165
+ b: "\b",
1166
+ f: "\f",
1167
+ n: "\n",
1168
+ r: "\r",
1169
+ t: " "
1170
+ };
1171
+ const CONTENT_ESCAPE = /[\\\u0000-\u001f]/u;
1172
+ function isWhitespace(c) {
1173
+ return c === " " || c === "\n" || c === "\r" || c === " ";
1174
+ }
1175
+ function isHex(c) {
1176
+ return c >= "0" && c <= "9" || c >= "a" && c <= "f" || c >= "A" && c <= "F";
1177
+ }
1178
+ (class PartialArguments {
1179
+ /** The view of a call with no arguments available. */
1180
+ static EMPTY = PartialArguments.fromObject({});
1181
+ /**
1182
+ * View finished argument text without scanning it until a reader asks.
1183
+ * @param text - the complete argument JSON text.
1184
+ * @returns a sealed view.
1185
+ */
1186
+ static fromText(text) {
1187
+ const view = new PartialArguments();
1188
+ view.append(text);
1189
+ view.sealed = true;
1190
+ return view;
1191
+ }
1192
+ /**
1193
+ * View an already parsed argument payload, such as a PTC dispatch object.
1194
+ * @param value - the parsed argument value.
1195
+ * @returns a sealed view; a non-object payload has no fields.
1196
+ */
1197
+ static fromObject(value) {
1198
+ const view = new PartialArguments();
1199
+ view.object = typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
1200
+ view.sealed = true;
1201
+ return view;
1202
+ }
1203
+ /**
1204
+ * The source: text so far or a parsed object, plus whether it can still grow.
1205
+ * These are the only enumerable fields, so two views over the same source
1206
+ * compare equal structurally however far each has been read.
1207
+ */
1208
+ chunks = [];
1209
+ object;
1210
+ sealed = false;
1211
+ #ends = [];
1212
+ #size = 0;
1213
+ #consumed = 0;
1214
+ #mode = "root";
1215
+ #escape = false;
1216
+ #keyStart = 0;
1217
+ #keyEscaped = false;
1218
+ #key = "";
1219
+ #current = null;
1220
+ #nestedEnds = [];
1221
+ #nestedInString = false;
1222
+ #invalidAt;
1223
+ #invalidValue = false;
1224
+ #entries = /* @__PURE__ */ new Map();
1225
+ #order = [];
1226
+ #reads = /* @__PURE__ */ new Map();
1227
+ /** Whether this view rejects further appends; does not scan text or register reads. */
1228
+ get isSealed() {
1229
+ return this.sealed;
1230
+ }
1231
+ /** Whether indexing or a content read found invalid JSON; unread value contents are not validated. */
1232
+ get invalid() {
1233
+ this.scan();
1234
+ return this.#mode === "invalid" || this.#invalidValue;
1235
+ }
1236
+ /**
1237
+ * Retain streamed argument text without scanning or comparing observed answers.
1238
+ * @param fragment - the text following every fragment appended before.
1239
+ */
1240
+ append(fragment) {
1241
+ if (this.sealed) throw new Error("PartialArguments: cannot append to a sealed view");
1242
+ if (fragment.length === 0) return;
1243
+ this.chunks.push(fragment);
1244
+ this.#size += fragment.length;
1245
+ this.#ends.push(this.#size);
1246
+ }
1247
+ /**
1248
+ * Reconcile a streamed prefix with authoritative complete text without joining the fragments.
1249
+ * @param text - the final argument text, which replaces missing or conflicting deltas.
1250
+ * @returns this view sealed with its caches retained when every character matches; otherwise a new sealed view.
1251
+ */
1252
+ settle(text) {
1253
+ if (this.object !== void 0 || text.length !== this.#size) return PartialArguments.fromText(text);
1254
+ let offset = 0;
1255
+ for (const chunk of this.chunks) {
1256
+ if (!text.startsWith(chunk, offset)) return PartialArguments.fromText(text);
1257
+ offset += chunk.length;
1258
+ }
1259
+ this.chunks = text.length === 0 ? [] : [text];
1260
+ this.#ends = text.length === 0 ? [] : [text.length];
1261
+ this.sealed = true;
1262
+ return this;
1263
+ }
1264
+ /**
1265
+ * Compare observed answers and advance their publication baseline. Unread views remain unscanned.
1266
+ * @returns whether any observed answer changed since its first read or the preceding refresh.
1267
+ */
1268
+ refresh() {
1269
+ if (this.#reads.size === 0) return false;
1270
+ this.scan();
1271
+ let changed = false;
1272
+ let completions = false;
1273
+ for (const read of this.#reads.values()) {
1274
+ if (read.completion) {
1275
+ completions = true;
1276
+ continue;
1277
+ }
1278
+ changed = this.refreshRead(read) || changed;
1279
+ }
1280
+ if (completions) {
1281
+ for (const read of this.#reads.values()) if (read.completion) changed = this.refreshRead(read) || changed;
1282
+ }
1283
+ if (this.sealed) this.#reads.clear();
1284
+ return changed;
1285
+ }
1286
+ refreshRead(read) {
1287
+ const now = read.answer();
1288
+ if (Object.is(now, read.last)) return false;
1289
+ read.last = now;
1290
+ return true;
1291
+ }
1292
+ /**
1293
+ * Check whether no further fields can arrive.
1294
+ * @returns whether the outer object closed, indexing failed, or the view is sealed; unread values are not validated.
1295
+ */
1296
+ closed() {
1297
+ return this.remember("closed", "", () => this.closedNow());
1298
+ }
1299
+ /**
1300
+ * List discovered fields in first-appearance order.
1301
+ * @returns top-level keys seen so far, in first-appearance order.
1302
+ */
1303
+ keys() {
1304
+ return this.remember("keys", "", () => this.keysNow(), (keys) => keys.length);
1305
+ }
1306
+ /**
1307
+ * Check whether a top-level field has appeared.
1308
+ * @param key - argument name.
1309
+ * @returns whether the field has appeared (a string opened or another value began).
1310
+ */
1311
+ has(key) {
1312
+ return this.remember("has", key, () => this.hasNow(key));
1313
+ }
1314
+ /**
1315
+ * Check whether a field's closing delimiter has arrived, without validating its contents.
1316
+ * @param key - argument name.
1317
+ * @returns whether its delimiter arrived and no content reader has reported an error for this value.
1318
+ */
1319
+ complete(key) {
1320
+ return this.remember("complete", key, () => this.completeNow(key));
1321
+ }
1322
+ /**
1323
+ * Read string length without materializing its text.
1324
+ * @param key - argument name.
1325
+ * @param options - change granularity for a streaming string.
1326
+ * @returns decoded UTF-16 length of the string field so far; undefined when absent or not a string.
1327
+ */
1328
+ stringLength(key, options) {
1329
+ const step = Math.max(1, Math.floor(options?.step ?? 1));
1330
+ const offset = options?.offset ?? 0;
1331
+ return this.remember(`length:${step}:${offset}`, key, () => this.lengthNow(key), (length) => length === void 0 ? void 0 : Math.ceil((length + offset) / step));
1332
+ }
1333
+ /**
1334
+ * Check a string against a decoded UTF-16 length limit without materializing it.
1335
+ * @param key - argument name.
1336
+ * @param maxLength - decoded UTF-16 limit, floored to at least zero.
1337
+ * @returns whether the string is longer than the limit; false when absent or not a string.
1338
+ */
1339
+ stringExceeds(key, maxLength) {
1340
+ const limit = Math.max(0, Math.floor(maxLength));
1341
+ return this.remember(`exceeds:${limit}`, key, () => (this.lengthNow(key, limit + 1) ?? 0) > limit);
1342
+ }
1343
+ /**
1344
+ * Read a decoded string, including a streaming prefix.
1345
+ * @param key - argument name.
1346
+ * @returns the string field's decoded text so far; undefined when absent or not a string.
1347
+ */
1348
+ text(key) {
1349
+ return this.remember("text", key, () => this.textNow(key));
1350
+ }
1351
+ /**
1352
+ * Read at most the first decoded UTF-16 units of a string.
1353
+ * @param key - argument name.
1354
+ * @param maxLength - maximum decoded UTF-16 length, floored to at least one.
1355
+ * @returns the bounded string prefix; undefined when absent or not a string.
1356
+ */
1357
+ textPrefix(key, maxLength) {
1358
+ const limit = Math.max(1, Math.floor(maxLength));
1359
+ return this.remember(`prefix:${limit}`, key, () => this.textPrefixNow(key, limit));
1360
+ }
1361
+ /**
1362
+ * Read a completed non-string argument.
1363
+ * @param key - argument name.
1364
+ * @returns the parsed non-string value once it closed; undefined while open, absent, or a string.
1365
+ */
1366
+ value(key) {
1367
+ return this.remember("value", key, () => this.valueNow(key));
1368
+ }
1369
+ /** Answer a question and, on a streaming view, remember it for change detection. */
1370
+ remember(kind, key, read, comparison) {
1371
+ this.scan();
1372
+ const result = read();
1373
+ if (!this.sealed) {
1374
+ const id = `${kind}/${key}`;
1375
+ if (!this.#reads.has(id)) this.#reads.set(id, {
1376
+ completion: kind === "complete",
1377
+ answer: comparison === void 0 ? read : () => comparison(read()),
1378
+ last: comparison === void 0 ? result : comparison(result)
1379
+ });
1380
+ }
1381
+ return result;
1382
+ }
1383
+ closedNow() {
1384
+ return this.sealed || this.#mode === "closed" || this.#mode === "invalid";
1385
+ }
1386
+ keysNow() {
1387
+ return this.object === void 0 ? this.#order : Object.keys(this.object);
1388
+ }
1389
+ hasNow(key) {
1390
+ return this.object === void 0 ? this.#entries.has(key) : Object.hasOwn(this.object, key);
1391
+ }
1392
+ completeNow(key) {
1393
+ if (this.object !== void 0) return Object.hasOwn(this.object, key);
1394
+ const entry = this.#entries.get(key);
1395
+ return entry !== void 0 && entry.end >= 0 && (entry.kind === "string" ? entry.invalidAt === void 0 : !entry.invalid);
1396
+ }
1397
+ lengthNow(key, limit = Number.POSITIVE_INFINITY) {
1398
+ if (this.object !== void 0) {
1399
+ const field = Object.hasOwn(this.object, key) ? this.object[key] : void 0;
1400
+ return typeof field === "string" ? field.length : void 0;
1401
+ }
1402
+ const entry = this.#entries.get(key);
1403
+ if (entry?.kind !== "string") return void 0;
1404
+ if (entry.text !== void 0 && entry.text.at === entry.end) return entry.text.length;
1405
+ const read = entry.length ??= {
1406
+ at: entry.start,
1407
+ length: 0,
1408
+ text: ""
1409
+ };
1410
+ this.readString(entry, read, limit, false);
1411
+ return read.length;
1412
+ }
1413
+ textNow(key) {
1414
+ if (this.object !== void 0) {
1415
+ const field = Object.hasOwn(this.object, key) ? this.object[key] : void 0;
1416
+ return typeof field === "string" ? field : void 0;
1417
+ }
1418
+ const entry = this.#entries.get(key);
1419
+ if (entry?.kind !== "string") return void 0;
1420
+ if (entry.text === void 0 && entry.end >= 0 && entry.needsDecoding && entry.invalidAt === void 0) {
1421
+ let text;
1422
+ try {
1423
+ text = JSON.parse(`"${this.slice(entry.start, entry.end)}"`);
1424
+ } catch (_error) {}
1425
+ if (text !== void 0) entry.text = {
1426
+ at: entry.end,
1427
+ length: text.length,
1428
+ text
1429
+ };
1430
+ }
1431
+ const read = entry.text ??= {
1432
+ at: entry.start,
1433
+ length: 0,
1434
+ text: ""
1435
+ };
1436
+ this.readString(entry, read, Number.POSITIVE_INFINITY, true);
1437
+ return read.text;
1438
+ }
1439
+ textPrefixNow(key, maxLength) {
1440
+ if (this.object !== void 0) {
1441
+ const field = Object.hasOwn(this.object, key) ? this.object[key] : void 0;
1442
+ return typeof field === "string" ? field.slice(0, maxLength) : void 0;
1443
+ }
1444
+ const entry = this.#entries.get(key);
1445
+ if (entry?.kind !== "string") return void 0;
1446
+ const prefixes = entry.prefixes ??= /* @__PURE__ */ new Map();
1447
+ let read = prefixes.get(maxLength);
1448
+ if (read === void 0) {
1449
+ read = {
1450
+ at: entry.start,
1451
+ length: 0,
1452
+ text: ""
1453
+ };
1454
+ prefixes.set(maxLength, read);
1455
+ }
1456
+ this.readString(entry, read, maxLength, true);
1457
+ return read.text;
1458
+ }
1459
+ valueNow(key) {
1460
+ if (this.object !== void 0) {
1461
+ if (!Object.hasOwn(this.object, key)) return void 0;
1462
+ const field = this.object[key];
1463
+ return typeof field === "string" ? void 0 : field;
1464
+ }
1465
+ const entry = this.#entries.get(key);
1466
+ if (entry?.kind !== "value" || entry.end < 0 || entry.invalid) return void 0;
1467
+ if (entry.parsed === void 0) try {
1468
+ entry.parsed = JSON.parse(this.slice(entry.start, entry.end));
1469
+ } catch (_error) {
1470
+ entry.invalid = true;
1471
+ this.#invalidValue = true;
1472
+ }
1473
+ return entry.parsed;
1474
+ }
1475
+ chunkAt(at) {
1476
+ let low = 0;
1477
+ let high = this.#ends.length;
1478
+ while (low < high) {
1479
+ const mid = low + high >>> 1;
1480
+ if (this.#ends[mid] <= at) low = mid + 1;
1481
+ else high = mid;
1482
+ }
1483
+ return low;
1484
+ }
1485
+ /** Materialize only a requested range, never the cumulative source. */
1486
+ slice(start, end) {
1487
+ if (start >= end) return "";
1488
+ const first = this.chunkAt(start);
1489
+ const last = this.chunkAt(end - 1);
1490
+ const base = first === 0 ? 0 : this.#ends[first - 1];
1491
+ if (first === last) return this.chunks[first].slice(start - base, end - base);
1492
+ const parts = [this.chunks[first].slice(start - base)];
1493
+ for (let i = first + 1; i < last; i++) parts.push(this.chunks[i]);
1494
+ parts.push(this.chunks[last].slice(0, end - this.#ends[last - 1]));
1495
+ return parts.join("");
1496
+ }
1497
+ readString(entry, read, limit, materialize) {
1498
+ const end = Math.min(entry.end < 0 ? this.#consumed : entry.end, entry.invalidAt ?? Number.POSITIVE_INFINITY, this.#invalidAt ?? Number.POSITIVE_INFINITY);
1499
+ if (!entry.needsDecoding) {
1500
+ const length = Math.min(end - read.at, limit - read.length);
1501
+ if (length <= 0) return;
1502
+ if (materialize) read.text += this.slice(read.at, read.at + length);
1503
+ read.at += length;
1504
+ read.length += length;
1505
+ return;
1506
+ }
1507
+ let chunkIndex = this.chunkAt(read.at);
1508
+ while (read.at < end && read.length < limit) {
1509
+ const base = chunkIndex === 0 ? 0 : this.#ends[chunkIndex - 1];
1510
+ const chunk = this.chunks[chunkIndex];
1511
+ const remaining = chunk.slice(read.at - base, Math.min(chunk.length, end - base));
1512
+ const boundary = remaining.search(CONTENT_ESCAPE);
1513
+ const length = Math.min(boundary < 0 ? remaining.length : boundary, limit - read.length);
1514
+ if (length > 0) {
1515
+ if (materialize) read.text += remaining.slice(0, length);
1516
+ read.at += length;
1517
+ read.length += length;
1518
+ if (read.at === base + chunk.length) chunkIndex++;
1519
+ continue;
1520
+ }
1521
+ const type = remaining.length > 1 ? remaining[1] : read.at + 1 < end ? this.chunks[chunkIndex + 1][0] : void 0;
1522
+ let decoded;
1523
+ let width = 2;
1524
+ if (remaining[0] === "\\" && type === void 0 && entry.end < 0) return;
1525
+ if (remaining[0] === "\\" && type === "u") {
1526
+ const hex = this.slice(read.at + 2, Math.min(end, read.at + 6));
1527
+ let valid = true;
1528
+ for (let i = 0; i < hex.length; i++) if (!isHex(hex[i])) valid = false;
1529
+ if (valid) {
1530
+ if (hex.length < 4 && entry.end < 0) return;
1531
+ if (hex.length === 4) decoded = String.fromCharCode(Number.parseInt(hex, 16));
1532
+ }
1533
+ width = 6;
1534
+ } else if (remaining[0] === "\\" && type !== void 0) decoded = SIMPLE_ESCAPES[type];
1535
+ if (decoded === void 0) {
1536
+ entry.invalidAt = read.at;
1537
+ this.#invalidValue = true;
1538
+ return;
1539
+ }
1540
+ if (materialize) read.text += decoded;
1541
+ read.length++;
1542
+ read.at += width;
1543
+ while (chunkIndex < this.chunks.length && read.at >= this.#ends[chunkIndex]) chunkIndex++;
1544
+ }
1545
+ }
1546
+ /** Locate new field ranges without decoding or parsing their contents. */
1547
+ scan() {
1548
+ if (this.object !== void 0 || this.#consumed === this.#size) return;
1549
+ for (let i = this.chunkAt(this.#consumed); i < this.chunks.length && this.#invalidAt === void 0; i++) {
1550
+ const pending = this.chunks[i];
1551
+ const base = i === 0 ? 0 : this.#ends[i - 1];
1552
+ for (let index = this.#consumed - base; index < pending.length && this.#mode !== "invalid"; index++) {
1553
+ if (this.#mode === "string" || this.#mode === "nested" && this.#nestedInString) {
1554
+ const end = this.stringBoundary(pending, index);
1555
+ this.#consumed += end - index;
1556
+ index = end;
1557
+ if (index === pending.length) break;
1558
+ }
1559
+ this.step(pending[index], this.#consumed);
1560
+ this.#consumed++;
1561
+ }
1562
+ }
1563
+ }
1564
+ /** Only raw quotes and their preceding backslash runs can terminate a string. */
1565
+ stringBoundary(fragment, start) {
1566
+ let at = start;
1567
+ while (true) {
1568
+ const quote = fragment.indexOf("\"", at);
1569
+ const end = quote < 0 ? fragment.length : quote;
1570
+ if (this.#mode === "string") {
1571
+ const entry = this.#current;
1572
+ if (!entry.needsDecoding && CONTENT_ESCAPE.test(fragment.slice(at, end))) entry.needsDecoding = true;
1573
+ }
1574
+ let slashStart = end;
1575
+ while (slashStart > at && fragment[slashStart - 1] === "\\") slashStart--;
1576
+ const escaped = (end - slashStart) % 2 === 1 !== (slashStart === at && this.#escape);
1577
+ this.#escape = quote < 0 && escaped;
1578
+ if (quote < 0 || !escaped) return end;
1579
+ at = quote + 1;
1580
+ }
1581
+ }
1582
+ step(c, at) {
1583
+ switch (this.#mode) {
1584
+ case "root":
1585
+ if (isWhitespace(c)) return;
1586
+ if (c === "{") {
1587
+ this.#mode = "key-or-end";
1588
+ return;
1589
+ }
1590
+ this.fail();
1591
+ return;
1592
+ case "key-or-end":
1593
+ if (isWhitespace(c)) return;
1594
+ if (c === "}") {
1595
+ this.#mode = "closed";
1596
+ return;
1597
+ }
1598
+ if (c === "\"") {
1599
+ this.beginKey(at);
1600
+ return;
1601
+ }
1602
+ this.fail();
1603
+ return;
1604
+ case "key-only":
1605
+ if (isWhitespace(c)) return;
1606
+ if (c === "\"") {
1607
+ this.beginKey(at);
1608
+ return;
1609
+ }
1610
+ this.fail();
1611
+ return;
1612
+ case "key":
1613
+ this.stepKey(c, at);
1614
+ return;
1615
+ case "colon":
1616
+ if (isWhitespace(c)) return;
1617
+ if (c === ":") {
1618
+ this.#mode = "value";
1619
+ return;
1620
+ }
1621
+ this.fail();
1622
+ return;
1623
+ case "value":
1624
+ this.beginValue(c, at);
1625
+ return;
1626
+ case "string": {
1627
+ const entry = this.#current;
1628
+ entry.end = at;
1629
+ this.#current = null;
1630
+ this.#mode = "comma-or-end";
1631
+ return;
1632
+ }
1633
+ case "scalar":
1634
+ this.stepScalar(c, at);
1635
+ return;
1636
+ case "nested":
1637
+ this.stepNested(c, at);
1638
+ return;
1639
+ case "comma-or-end":
1640
+ if (isWhitespace(c)) return;
1641
+ if (c === ",") {
1642
+ this.#mode = "key-only";
1643
+ return;
1644
+ }
1645
+ if (c === "}") {
1646
+ this.#mode = "closed";
1647
+ return;
1648
+ }
1649
+ this.fail();
1650
+ return;
1651
+ case "closed":
1652
+ if (isWhitespace(c)) return;
1653
+ this.fail();
1654
+ return;
1655
+ /* v8 ignore next 2 -- scan() stops stepping once the view is invalid. */
1656
+ case "invalid": return;
1657
+ /* v8 ignore next 2 -- Every scanner mode has a handler above. */
1658
+ default: assertNever$1(this.#mode);
1659
+ }
1660
+ }
1661
+ fail() {
1662
+ this.#invalidAt = this.#consumed;
1663
+ this.#mode = "invalid";
1664
+ this.#current = null;
1665
+ }
1666
+ beginKey(at) {
1667
+ this.#mode = "key";
1668
+ this.#keyStart = at + 1;
1669
+ this.#keyEscaped = false;
1670
+ this.#escape = false;
1671
+ }
1672
+ stepKey(c, at) {
1673
+ if (c < " ") {
1674
+ this.fail();
1675
+ return;
1676
+ }
1677
+ if (this.#escape) {
1678
+ this.#escape = false;
1679
+ return;
1680
+ }
1681
+ if (c === "\\") {
1682
+ this.#escape = true;
1683
+ this.#keyEscaped = true;
1684
+ return;
1685
+ }
1686
+ if (c !== "\"") return;
1687
+ const raw = this.slice(this.#keyStart, at);
1688
+ if (this.#keyEscaped) try {
1689
+ this.#key = JSON.parse(`"${raw}"`);
1690
+ } catch (_error) {
1691
+ this.fail();
1692
+ return;
1693
+ }
1694
+ else this.#key = raw;
1695
+ this.#mode = "colon";
1696
+ }
1697
+ open(entry) {
1698
+ if (!this.#entries.has(this.#key)) this.#order.push(this.#key);
1699
+ this.#entries.set(this.#key, entry);
1700
+ this.#current = entry;
1701
+ }
1702
+ beginValue(c, at) {
1703
+ if (isWhitespace(c)) return;
1704
+ if (c === "\"") {
1705
+ this.open({
1706
+ kind: "string",
1707
+ start: at + 1,
1708
+ end: -1,
1709
+ needsDecoding: false,
1710
+ invalidAt: void 0,
1711
+ length: void 0,
1712
+ text: void 0,
1713
+ prefixes: void 0
1714
+ });
1715
+ this.#escape = false;
1716
+ this.#mode = "string";
1717
+ return;
1718
+ }
1719
+ if (c === "}" || c === "," || c === ":" || c === "]") {
1720
+ this.fail();
1721
+ return;
1722
+ }
1723
+ this.open({
1724
+ kind: "value",
1725
+ start: at,
1726
+ end: -1,
1727
+ parsed: void 0,
1728
+ invalid: false
1729
+ });
1730
+ if (c === "{" || c === "[") {
1731
+ this.#mode = "nested";
1732
+ this.#nestedEnds = [c === "{" ? "}" : "]"];
1733
+ this.#nestedInString = false;
1734
+ this.#escape = false;
1735
+ return;
1736
+ }
1737
+ this.#mode = "scalar";
1738
+ }
1739
+ stepScalar(c, at) {
1740
+ if (c !== "," && c !== "}" && !isWhitespace(c)) return;
1741
+ this.closeValue(at);
1742
+ this.#mode = c === "," ? "key-only" : c === "}" ? "closed" : "comma-or-end";
1743
+ }
1744
+ stepNested(c, at) {
1745
+ if (this.#nestedInString) {
1746
+ this.#nestedInString = false;
1747
+ return;
1748
+ }
1749
+ if (c === "\"") {
1750
+ this.#nestedInString = true;
1751
+ return;
1752
+ }
1753
+ if (c === "{" || c === "[") {
1754
+ this.#nestedEnds.push(c === "{" ? "}" : "]");
1755
+ return;
1756
+ }
1757
+ if (c === "}" || c === "]") {
1758
+ if (this.#nestedEnds.pop() !== c) {
1759
+ this.fail();
1760
+ return;
1761
+ }
1762
+ if (this.#nestedEnds.length === 0) {
1763
+ this.closeValue(at + 1);
1764
+ this.#mode = "comma-or-end";
1765
+ }
1766
+ }
1767
+ }
1768
+ closeValue(end) {
1769
+ const entry = this.#current;
1770
+ entry.end = end;
1771
+ this.#current = null;
1772
+ }
1773
+ });
906
1774
  /** Duplicate-install-safe JSON and immutable-value helpers. @module @deepseek-ai/dsh-util-values */
907
1775
  /**
908
1776
  * Mark an unreachable closed-union branch.
@@ -915,7 +1783,7 @@ window.__ModuleLoader__.load({
915
1783
  throw new Error(`unreachable variant${context ? ` in ${context}` : ""}: ${rendered}`);
916
1784
  }
917
1785
  //#endregion
918
- //#region node_modules/.pnpm/@deepseek-ai+dsh-util-workspace-path@0.2.0-rc.2_@deepseek-ai+cordis@4.0.4/node_modules/@deepseek-ai/dsh-util-workspace-path/lib/index.js
1786
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-util-workspace-path@0.2.1-rc.2_@deepseek-ai+cordis@4.0.5/node_modules/@deepseek-ai/dsh-util-workspace-path/lib/index.js
919
1787
  /**
920
1788
  * Browser-safe Workspace path and display helpers.
921
1789
  * @module @deepseek-ai/dsh-util-workspace-path
@@ -959,6 +1827,12 @@ window.__ModuleLoader__.load({
959
1827
  function owningGroupKey(workspaces, sessionId) {
960
1828
  return workspaces.find((workspace) => workspace.sessionIds.includes(sessionId))?.workspaceId ?? "";
961
1829
  }
1830
+ /** 宿主认的 kind 集合(查表用)。 */
1831
+ const PENDING_INTERACTION_KIND_SET = /* @__PURE__ */ new Set([
1832
+ "approval",
1833
+ "plan-review",
1834
+ "question"
1835
+ ]);
962
1836
  function mainSessionId(list) {
963
1837
  return Object.values(list.byId).find((session) => (session.retainedBy.mainView ?? 0) > 0)?.id;
964
1838
  }
@@ -1142,12 +2016,7 @@ window.__ModuleLoader__.load({
1142
2016
  }
1143
2017
  /** Keep navigation presentation independent from domain-owned interaction objects. */
1144
2018
  function visiblePendingKind(kind) {
1145
- switch (kind) {
1146
- case "approval":
1147
- case "plan-review":
1148
- case "question": return kind;
1149
- default: return;
1150
- }
2019
+ return kind !== void 0 && PENDING_INTERACTION_KIND_SET.has(kind) ? kind : void 0;
1151
2020
  }
1152
2021
  function runningChildCount(list, parentId, statuses) {
1153
2022
  return list.projectionsBySession[parentId]?.values.subagentCatalog?.reduce((count, child) => count + ((statuses.get(child.id)?.running ?? list.byId[child.id]?.running) === true ? 1 : 0), 0) ?? 0;
@@ -1490,46 +2359,90 @@ window.__ModuleLoader__.load({
1490
2359
  };
1491
2360
  }, "ui-workspace: Workspace navigation policy");
1492
2361
  }
1493
- async connectWorkspace(workspaceId) {
2362
+ connectWorkspace(workspaceId, options = {}) {
2363
+ return this.connectBlank(workspaceId, options);
2364
+ }
2365
+ /**
2366
+ * @param reuse - preset for the reused or created blank; absent creates a fresh Session.
2367
+ */
2368
+ async connectBlank(workspaceId, reuse) {
1494
2369
  const workspace = this.workspaces.list.getSnapshot().items.find((item) => item.workspaceId === workspaceId);
1495
2370
  if (workspace === void 0) throw new Error(`uiWorkspace.connectWorkspace: unknown workspace ${workspaceId}`);
2371
+ if (reuse === void 0) return this.sessions.create({ workspaceId });
1496
2372
  const inflight = this.connecting.get(workspaceId);
1497
- if (inflight !== void 0) return inflight;
1498
- const attempt = this.reuseOrCreateBlank(workspace).finally(() => {
1499
- this.connecting.delete(workspaceId);
2373
+ if (inflight !== void 0 && inflight.agentPreset === reuse.agentPreset) return inflight.attempt;
2374
+ const attempt = this.createInWorkspace(workspace, reuse).finally(() => {
2375
+ if (this.connecting.get(workspaceId)?.attempt === attempt) this.connecting.delete(workspaceId);
2376
+ });
2377
+ this.connecting.set(workspaceId, {
2378
+ agentPreset: reuse.agentPreset,
2379
+ attempt
1500
2380
  });
1501
- this.connecting.set(workspaceId, attempt);
1502
2381
  return attempt;
1503
2382
  }
1504
- reuseOrCreateBlank(workspace) {
2383
+ createInWorkspace(workspace, options) {
1505
2384
  const archived = this.workspaces.list.getSnapshot().archivedSessionIds;
1506
2385
  const sessions = this.sessions.list.getSnapshot();
1507
- for (const id of sessions.ids) {
2386
+ const blank = sessions.ids.find((id) => {
1508
2387
  const summary = sessions.byId[id];
1509
- if (summary === void 0 || !summary.blank || summary.cwd !== workspace.path || !workspace.sessionIds.includes(id) || archived.includes(id)) continue;
1510
- return this.reuseBlank(workspace.workspaceId, id);
1511
- }
1512
- return this.sessions.create({ workspaceId: workspace.workspaceId });
2388
+ return summary !== void 0 && summary.blank && summary.cwd === workspace.path && workspace.sessionIds.includes(id) && !archived.includes(id);
2389
+ });
2390
+ return this.createNamed({
2391
+ workspaceId: workspace.workspaceId,
2392
+ ...blank === void 0 ? {} : { sessionId: blank },
2393
+ ...options.agentPreset === void 0 ? {} : { agentPreset: options.agentPreset }
2394
+ });
1513
2395
  }
1514
- async reuseBlank(workspaceId, sessionId) {
2396
+ /**
2397
+ * Create through the Host. A named blank the Host declined may still read as
2398
+ * blank from stale cached list metadata, which hides it from the Session tree;
2399
+ * its projections are reloaded so the row reflects its log.
2400
+ */
2401
+ async createNamed(request) {
2402
+ const sessionId = await this.sessions.create(request);
2403
+ const named = request.sessionId;
2404
+ if (named !== void 0 && named !== sessionId) this.sessions.refreshProjections(named).catch((reason) => {
2405
+ console.warn("declined blank Session projection reload failed:", reason);
2406
+ });
2407
+ return sessionId;
2408
+ }
2409
+ /**
2410
+ * Reopen a saved blank Session under the preset its log recorded, or under
2411
+ * the Host default once that preset no longer activates. Listed values are
2412
+ * cached hints that can lag the log after an unclean Host exit, so the
2413
+ * Session's projections are reloaded before the preset is read; a Session
2414
+ * that still awaits a format migration reloads only those cached hints.
2415
+ */
2416
+ async restoreBlank(workspaceId, sessionId) {
2417
+ await this.sessions.refreshProjections(sessionId);
2418
+ const recorded = this.sessions.list.getSnapshot().byId[sessionId]?.projectionValues?.agentPreset;
2419
+ const request = {
2420
+ workspaceId,
2421
+ sessionId
2422
+ };
2423
+ if (typeof recorded !== "string") return this.createNamed(request);
1515
2424
  try {
1516
- return await this.sessions.create({
1517
- workspaceId,
1518
- sessionId
2425
+ return await this.createNamed({
2426
+ ...request,
2427
+ agentPreset: recorded
1519
2428
  });
1520
2429
  } catch (error) {
1521
- if (sessionCreateErrorOf(error)?.rpcError.code !== "session/writer-held") throw error;
1522
- return this.sessions.create({ workspaceId });
2430
+ const code = sessionCreateErrorOf(error)?.rpcError.code;
2431
+ if (code !== "agent-preset/not-found" && code !== "agent-preset/invalid") throw error;
2432
+ return this.createNamed(request);
1523
2433
  }
1524
2434
  }
1525
2435
  openSession(target) {
1526
2436
  this.replaceMain(target, this.lifetime.signal, "reveal");
1527
2437
  }
1528
- async openWorkspace(workspaceId, beforeOpen) {
2438
+ openWorkspace(workspaceId, beforeOpen, options = {}) {
2439
+ return this.openWorkspaceSession(workspaceId, beforeOpen, options);
2440
+ }
2441
+ async openWorkspaceSession(workspaceId, beforeOpen, reuse) {
1529
2442
  const navigation = AbortSignal.any([this.ctx.layout.beginNavigation(), this.lifetime.signal]);
1530
2443
  let sessionId;
1531
2444
  try {
1532
- sessionId = await this.connectWorkspace(workspaceId);
2445
+ sessionId = await this.connectBlank(workspaceId, reuse);
1533
2446
  } catch (error) {
1534
2447
  if (!navigation.aborted) this.notify({
1535
2448
  kind: "createFailed",
@@ -1540,14 +2453,18 @@ window.__ModuleLoader__.load({
1540
2453
  if (navigation.aborted) return;
1541
2454
  this.replaceMain(sessionId, navigation, "reveal", beforeOpen);
1542
2455
  }
1543
- async forkSession(sessionId, onCreated) {
2456
+ forkSession(sessionId, onCreated, options) {
1544
2457
  return this.sessions.fork({
1545
2458
  sessionId,
1546
2459
  increaseTitle: true,
1547
- ...onCreated === void 0 ? {} : { onCreated }
2460
+ ...onCreated === void 0 ? {} : { onCreated },
2461
+ ...options?.allowMigration === void 0 ? {} : { allowMigration: options.allowMigration }
1548
2462
  });
1549
2463
  }
1550
- startSession(workspaceId) {
2464
+ startSession(workspaceId, options) {
2465
+ const { agentPreset, ...draft } = options ?? {};
2466
+ const draftOptions = options === void 0 ? void 0 : draft;
2467
+ const initializeDraft = draftOptions !== void 0 && (draftOptions.prompt !== void 0 || draftOptions.clearPreviousDraft === true);
1551
2468
  const workspace = this.workspaces.list.getSnapshot();
1552
2469
  const sessions = this.sessions.list.getSnapshot();
1553
2470
  const current = this.mainReference?.sessionId;
@@ -1555,13 +2472,34 @@ window.__ModuleLoader__.load({
1555
2472
  const recent = workspace.phase === "ready" && sessions.phase === "ready" ? recentWorkspace(workspace.items, sessions.byId) : void 0;
1556
2473
  const target = workspaceId ?? currentWorkspaceId ?? recent;
1557
2474
  if (target === void 0) {
2475
+ if (initializeDraft) {
2476
+ this.notify({
2477
+ kind: "createFailed",
2478
+ message: this.ctx.locale.bind("workspace")("draft.workspaceRequired")
2479
+ });
2480
+ return;
2481
+ }
1558
2482
  this.clearMain();
1559
2483
  return;
1560
2484
  }
1561
- this.openWorkspace(target).catch((reason) => {
2485
+ (draftOptions === void 0 ? this.openWorkspaceSession(target, void 0, void 0) : this.openWorkspace(target, initializeDraft ? (sessionId) => {
2486
+ const binding = this.sessions.binding(sessionId) ?? this.draftPreparationFailed();
2487
+ this.prepareDraft(binding, draftOptions);
2488
+ } : void 0, agentPreset === void 0 ? {} : { agentPreset })).catch((reason) => {
1562
2489
  console.warn("new session failed:", reason);
1563
2490
  });
1564
2491
  }
2492
+ prepareDraft(binding, options) {
2493
+ if ((this.ctx.get("conversation") ?? this.draftPreparationFailed()).input.requestDraftInitialization(binding, options) === "blocked") this.draftPreparationFailed();
2494
+ }
2495
+ draftPreparationFailed() {
2496
+ const message = this.ctx.locale.bind("workspace")("draft.initializationFailed");
2497
+ this.notify({
2498
+ kind: "createFailed",
2499
+ message
2500
+ });
2501
+ throw new Error(message);
2502
+ }
1565
2503
  async archiveSession(sessionId, options = {}) {
1566
2504
  await this.workspaces.archiveSession(sessionId, options);
1567
2505
  if (this.mainReference?.sessionId === sessionId) this.clearMain();
@@ -1640,7 +2578,7 @@ window.__ModuleLoader__.load({
1640
2578
  }
1641
2579
  const navigation = AbortSignal.any([this.ctx.layout.beginNavigation(), this.lifetime.signal]);
1642
2580
  let sessionId;
1643
- if (summary !== void 0 && workspace !== void 0 && summary.cwd === workspace.path && !workspaces.archivedSessionIds.includes(summary.id)) sessionId = await this.reuseBlank(workspace.workspaceId, summary.id);
2581
+ if (summary !== void 0 && workspace !== void 0 && summary.cwd === workspace.path && !workspaces.archivedSessionIds.includes(summary.id)) sessionId = await this.restoreBlank(workspace.workspaceId, summary.id);
1644
2582
  let target = workspace?.workspaceId ?? recentWorkspace(workspaces.items, sessions.byId);
1645
2583
  if (target === void 0 && workspaces.items.length === 0 && sessions.ids.length === 0) {
1646
2584
  const prepared = await this.initializeDefaultWorkspace(navigation);
@@ -1946,23 +2884,96 @@ window.__ModuleLoader__.load({
1946
2884
  liveStatus: aggregated.get(group.key) ?? EMPTY_LIVE_STATUS
1947
2885
  }));
1948
2886
  }
2887
+ /** The blank row plus the first `watermark` non-blank rows, in list order. */
2888
+ function watermarkRows(sessions, watermark) {
2889
+ let count = 0;
2890
+ const rows = sessions.filter((session) => {
2891
+ if (session.blank) return true;
2892
+ count += 1;
2893
+ return count <= watermark;
2894
+ });
2895
+ return {
2896
+ rows,
2897
+ hiddenCount: sessions.length - rows.length
2898
+ };
2899
+ }
2900
+ /**
2901
+ * Smallest watermark that covers every non-blank row within the automatic maximum that is running, has a running
2902
+ * child, awaits the user, carries an unread completion, or is current, and the reveal target at any position.
2903
+ * @param sessions - one group's ordered Session rows.
2904
+ * @param current - Session retained by the main view, if the sidebar is the active surface.
2905
+ * @param revealSessionId - Session selected from search and required to be visible.
2906
+ * @returns the group's baseline non-blank row count.
2907
+ */
2908
+ function requiredWatermark(sessions, current, revealSessionId) {
2909
+ let index = 0;
2910
+ let required = 0;
2911
+ for (const session of sessions) {
2912
+ if (session.blank) continue;
2913
+ index += 1;
2914
+ if (index <= 50 && (session.running || session.runningSubagentCount > 0 || session.pendingInteraction !== void 0 || session.completed || session.id === current) || session.id === revealSessionId) required = index;
2915
+ }
2916
+ return required;
2917
+ }
2918
+ /**
2919
+ * Raise each listed group's watermark to its requirement; returns `marks` itself when nothing rises.
2920
+ * @param marks - watermarks stored by the current browser tree.
2921
+ * @param required - required baselines, in group order.
2922
+ * @returns either the unchanged input or a new object containing only raised groups.
2923
+ */
2924
+ function raiseWatermarks(marks, required) {
2925
+ let raised = marks;
2926
+ for (const [key, watermark] of required) if (watermark > (raised[key] ?? 5)) raised = {
2927
+ ...raised,
2928
+ [key]: watermark
2929
+ };
2930
+ return raised;
2931
+ }
1949
2932
  //#endregion
1950
- //#region src/client/facet-visibility.ts
2933
+ //#region src/client/use-registered-facets.ts
1951
2934
  /**
1952
- * 按用户设置筛掉不参与折叠的状态来源。
2935
+ * 把登记面读成「这一轮渲染该折哪些来源」的**唯一**一处接线。
2936
+ *
2937
+ * ## 为什么单独成文
2938
+ *
2939
+ * 这一步此前直接写在 `rows/WorkspaceBrowser.tsx` 里,两行:
2940
+ *
2941
+ * ```ts
2942
+ * useSyncExternalStore(facetsStore.subscribe, facetsStore.getVersion, …) // 返回值丢掉
2943
+ * const facets = useMemo(() => visibleFacets(facetsStore.list(), settings), [settings])
2944
+ * ```
2945
+ *
2946
+ * 看着没问题——订阅了、也 memo 了。实际是**订阅通知只重渲染、不重算**:
2947
+ * `useSyncExternalStore` 的返回值被丢掉,`useMemo` 的依赖里就没有任何东西随登记面变化,
2948
+ * 于是那次重渲染原样拿回**上一次**的登记面。提供方在自己的 `apply` 里注册,而宿主不保证
2949
+ * `apply` 与本组件首渲染的先后:注册**晚于**首渲染时,facet 明明登记上了,却永远进不了
2950
+ * 徽标——不报错、不红测试(能渲染这条路径的那份上游 spec 在本仓库是跳过的,见
2951
+ * `vitest.config.ts` 的 `MONOREPO_ONLY_SPECS`),只能靠刷新页面碰运气。
1953
2952
  *
1954
- * 单独成文是因为它属于**徽标自身**的可见性规则,而不是某个来源的适配:来源只在
1955
- * `facet.settingKey` 上声明"我受哪个开关管",开关的语义与过滤时机归本插件。
2953
+ * 版本号必须**同时**当两件事用:① 触发重渲染的信号,② memo 的依赖。这里把它收进一个
2954
+ * hook,让「只用①、丢掉②」在结构上写不出来,并且让这条接缝能被 `@testing-library/react`
2955
+ * 直接覆盖(本文件零宿主 import,宿主依赖闭包进不来)。
1956
2956
  *
1957
- * 过滤发生在**折叠之前**而不是渲染阶段:徽标数字、图标、`aria-label`、悬停明细与
1958
- * "要不要画徽标"因此天然同源。放到渲染阶段会出现"label 里有、计数里没有",而那正是
1959
- * 徽标最容易被怀疑撒谎的地方。
2957
+ * ## 为什么现在不再按设置筛
1960
2958
  *
1961
- * @module dsh-group-status/client/facet-visibility
2959
+ * 旧版本这里会过一道 `visibleFacets(list, settings)`,按 `facet.settingKey` 查本插件的
2960
+ * 布尔开关。那意味着**本插件的配置面里写着别人家的语义**(`showScheduledInBadge` /
2961
+ * `showWaitingReviewInBadge`,连图标形状都替对方写死),与"本插件不认识任何来源"直接
2962
+ * 矛盾。现在外部来源的显隐归**来源自己**:它想被关掉就不注册 facet,登记面即所见。
2963
+ * 本插件的开关只管自己那三档会话状态(见 `badge-settings.ts`)。
2964
+ *
2965
+ * @module dsh-group-status/client/use-registered-facets
2966
+ */
2967
+ /**
2968
+ * 当前登记的来源,按登记面顺序(已按 `order` 排好)。
2969
+ *
2970
+ * 引用在版本号不变时保持稳定:登记面是低频事件,重算成本 = O(facet × 组内会话)。
2971
+ *
2972
+ * @param store - facet 消费侧存储(`facetStore()` 单例)。
1962
2973
  */
1963
- /** 筛出设置允许显示的登记项;顺序保持登记面的顺序(已按 `order` 排好)。 */
1964
- function visibleFacets(entries, settings) {
1965
- return entries.filter((entry) => facetVisibleBySettings(settings, entry.facet.settingKey));
2974
+ function useRegisteredFacets(store) {
2975
+ const version = (0, react.useSyncExternalStore)(store.subscribe, store.getVersion, store.getServerVersion);
2976
+ return (0, react.useMemo)(() => [...store.list()], [version]);
1966
2977
  }
1967
2978
  //#endregion
1968
2979
  //#region src/client/badge-labels.ts
@@ -2005,9 +3016,9 @@ window.__ModuleLoader__.load({
2005
3016
  * Accessible label for a folded group's aggregated status, reusing the
2006
3017
  * session-row locale keys so both surfaces read identically.
2007
3018
  *
2008
- * 按设置过滤:**关掉的外部来源根本不在折果里**(`WorkspaceBrowser` 折叠前就按
2009
- * `facet.settingKey` 过滤了登记面),所以这里不必再判设置,`aria-label` 与徽标视觉
2010
- * 天然一致;宿主的行级状态仍按设置逐项过滤。
3019
+ * 按设置过滤:本插件那三档会话状态按设置逐项过滤;**外部来源不参与过滤** —— 登记了就
3020
+ * 一定出现在这里,没登记就不在折果里,所以 `aria-label` 与徽标视觉天然一致,不必在这里
3021
+ * 再判一次设置(本插件也没有替别人家存的开关)。
2011
3022
  */
2012
3023
  function groupLiveLabel(s, t, settings, now = Date.now()) {
2013
3024
  const parts = [];
@@ -2187,10 +3198,10 @@ window.__ModuleLoader__.load({
2187
3198
  *
2188
3199
  * Count matches the dots: host-side sessions plus every registered facet's own count.
2189
3200
  *
2190
- * `settings` 按来源过滤:关掉的来源不画点、不计数、不进 aria-label、不参与「是否显示
2191
- * 徽标」的判断。会话行自身的状态点不受影响:那是行级状态,不是分组提示。**外部**来源的
2192
- * 过滤发生在折叠之前(`WorkspaceBrowser` 按 `facet.settingKey` 筛登记面),所以到这里
2193
- * 的折果已经是"该显示的",label 与视觉不可能各说一套。
3201
+ * `settings` 只过滤**本插件自己的**三档会话状态:关掉的档位不画点、不计数、不进
3202
+ * aria-label、不参与「是否显示徽标」的判断。会话行自身的状态点不受影响:那是行级状态,
3203
+ * 不是分组提示。**外部来源在这里没有开关可关** —— 它们要么注册了(一定显示),要么没
3204
+ * 注册(根本不在登记面上)。要关某个来源,去那个插件自己的设置卡片:显隐是来源的决定。
2194
3205
  *
2195
3206
  * `now` 缺省取渲染时刻(与 `WorkspaceBrowser` 为相对时间读的时钟同一姿态):文案里的
2196
3207
  * 剩余时长以此计算,只在提供方 store 通知时随渲染更新。
@@ -2205,8 +3216,8 @@ window.__ModuleLoader__.load({
2205
3216
  if (pending === 0 && active === 0 && completed === 0 && facetTotal === 0) return null;
2206
3217
  const label = groupLiveLabel(s, t, settings, now);
2207
3218
  const dots = [];
2208
- if (pending > 0) dots.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: "warning" }, "warn"));
2209
- if (active > 0) dots.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: "ongoing" }, "run"));
3219
+ if (pending > 0) dots.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: BADGE_FIELD_DOT_STATE.showPendingInBadge }, "warn"));
3220
+ if (active > 0) dots.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: BADGE_FIELD_DOT_STATE.showRunningInBadge }, "run"));
2210
3221
  const urgentClass = Rows_module_css_default.groupBadgeUrgent ?? "";
2211
3222
  for (const entry of s.facets.values()) {
2212
3223
  const glyph = facetGlyph(entry, t, now);
@@ -2216,7 +3227,7 @@ window.__ModuleLoader__.load({
2216
3227
  children: glyph
2217
3228
  }, entry.facet.id));
2218
3229
  }
2219
- if (completed > 0) dots.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: "done" }, "done"));
3230
+ if (completed > 0) dots.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: BADGE_FIELD_DOT_STATE.showCompletedInBadge }, "done"));
2220
3231
  const count = pending + active + completed + facetTotal;
2221
3232
  const countLabel = count > 99 ? "99+" : count >= settings.countThreshold ? String(count) : "";
2222
3233
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
@@ -2476,6 +3487,7 @@ window.__ModuleLoader__.load({
2476
3487
  side: "bottom",
2477
3488
  align: "end",
2478
3489
  delayMs: 500,
3490
+ portal: true,
2479
3491
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2480
3492
  type: "button",
2481
3493
  className: Rows_module_css_default.iconButton,
@@ -2504,6 +3516,7 @@ window.__ModuleLoader__.load({
2504
3516
  t
2505
3517
  }),
2506
3518
  openDelayMs: 800,
3519
+ grouped: true,
2507
3520
  disabled: menuOpen,
2508
3521
  copyText: row.cwd,
2509
3522
  copyLabel: t("copy"),
@@ -2658,6 +3671,7 @@ window.__ModuleLoader__.load({
2658
3671
  side: "bottom",
2659
3672
  align: "end",
2660
3673
  delayMs: 500,
3674
+ portal: true,
2661
3675
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2662
3676
  type: "button",
2663
3677
  className: Rows_module_css_default.iconButton,
@@ -2812,6 +3826,7 @@ window.__ModuleLoader__.load({
2812
3826
  t
2813
3827
  }),
2814
3828
  openDelayMs: 800,
3829
+ grouped: true,
2815
3830
  disabled: menuOpen || drag?.active === true,
2816
3831
  copyText: row.blank || row.title === "" ? void 0 : row.title,
2817
3832
  copyLabel: t("copy"),
@@ -3253,22 +4268,6 @@ window.__ModuleLoader__.load({
3253
4268
  const SEARCH_DEBOUNCE_MS = 250;
3254
4269
  /** `session.search` wire bound, measured in JavaScript UTF-16 code units. */
3255
4270
  const SEARCH_QUERY_MAX_CODE_UNITS = 500;
3256
- /** Idle Session rows visible per Workspace before the local overflow control. */
3257
- const COLLAPSED_SESSION_LIMIT = 5;
3258
- /** Keep provisional and running rows outside the idle-session quota, including parents with running children. */
3259
- function collapsedSessionRows(sessions, limit = COLLAPSED_SESSION_LIMIT) {
3260
- let idleCount = 0;
3261
- const rows = sessions.filter((session) => {
3262
- if (session.blank || session.running || session.runningSubagentCount > 0) return true;
3263
- if (idleCount >= limit) return false;
3264
- idleCount += 1;
3265
- return true;
3266
- });
3267
- return {
3268
- rows,
3269
- hiddenCount: sessions.length - rows.length
3270
- };
3271
- }
3272
4271
  /** Keep controlled input and RPC payload inside the session.search wire contract. */
3273
4272
  function sanitizeSearchQuery(value) {
3274
4273
  const withoutNul = value.replaceAll("\0", "");
@@ -3454,11 +4453,11 @@ window.__ModuleLoader__.load({
3454
4453
  const panelActive = usePanelInfo((info) => info.activePanelId !== null);
3455
4454
  const badgeSettings = useBadgeSettings((settings) => settings);
3456
4455
  const statuses = useSessionStatus((s) => s);
3457
- (0, react.useSyncExternalStore)(facetsStore.subscribe, facetsStore.getVersion, facetsStore.getServerVersion);
3458
- const facets = (0, react.useMemo)(() => visibleFacets(facetsStore.list(), badgeSettings), [badgeSettings]);
4456
+ const facets = useRegisteredFacets(facetsStore);
3459
4457
  const current = panelActive ? void 0 : Object.values(list.byId).find((session) => (session.retainedBy.mainView ?? 0) > 0)?.id;
3460
4458
  const revealGroup = revealSessionId === void 0 || !workspaceReady ? void 0 : owningGroupKey(workspaces, revealSessionId);
3461
- const [sessionLimits, setSessionLimits] = (0, react.useState)({});
4459
+ const [storedWatermarks, setStoredWatermarks] = (0, react.useState)({});
4460
+ const [watermarkMotionKey, setWatermarkMotionKey] = (0, react.useState)(0);
3462
4461
  const [drag, setDrag] = (0, react.useState)(null);
3463
4462
  const sessionDropCommitted = (0, react.useRef)(false);
3464
4463
  const [workspaceDrag, setWorkspaceDrag] = (0, react.useState)(null);
@@ -3516,20 +4515,24 @@ window.__ModuleLoader__.load({
3516
4515
  revealGroup,
3517
4516
  setGroupExpanded
3518
4517
  ]);
3519
- (0, react.useEffect)(() => {
3520
- if (revealSessionId === void 0 || revealGroup === void 0) return;
3521
- const group = groups.find((candidate) => candidate.key === revealGroup);
3522
- if (group === void 0 || !group.expanded || !group.sessions.some((row) => row.id === revealSessionId)) return;
3523
- if (collapsedSessionRows(group.sessions).rows.some((row) => row.id === revealSessionId)) return;
3524
- setSessionLimits((limits) => limits[revealGroup] === Infinity ? limits : {
3525
- ...limits,
3526
- [revealGroup]: Infinity
3527
- });
3528
- }, [
3529
- groups,
3530
- revealGroup,
3531
- revealSessionId
3532
- ]);
4518
+ const baselines = new Map(groups.map((group) => [group.key, Math.max(5, requiredWatermark(group.expanded ? group.sessions : [], current, group.key === revealGroup ? revealSessionId : void 0))]));
4519
+ const foldedKeys = new Set(groups.filter((group) => !group.expanded).map((group) => group.key));
4520
+ const underFoldedAncestor = (key) => {
4521
+ for (let owner = parents.get(key); owner !== void 0; owner = parents.get(owner)) if (foldedKeys.has(owner)) return true;
4522
+ return false;
4523
+ };
4524
+ const requiredWatermarks = new Map([...baselines].filter(([key]) => !underFoldedAncestor(key)));
4525
+ const sessionWatermarks = raiseWatermarks(storedWatermarks, requiredWatermarks);
4526
+ if (sessionWatermarks !== storedWatermarks) setStoredWatermarks((marks) => raiseWatermarks(marks, requiredWatermarks));
4527
+ const dropWatermarks = (drop) => {
4528
+ if (!Object.keys(storedWatermarks).some(drop)) return;
4529
+ setStoredWatermarks((marks) => Object.fromEntries(Object.entries(marks).filter(([key]) => !drop(key))));
4530
+ setWatermarkMotionKey((key) => key + 1);
4531
+ };
4532
+ const withinGroup = (key, root) => {
4533
+ for (let owner = key; owner !== void 0; owner = parents.get(owner)) if (owner === root) return true;
4534
+ return false;
4535
+ };
3533
4536
  const now = Date.now();
3534
4537
  const commitSessionDrag = (activeDrag, over) => {
3535
4538
  if (sessionDropCommitted.current) return;
@@ -3540,7 +4543,7 @@ window.__ModuleLoader__.load({
3540
4543
  if (over.id === activeDrag.sessionId) return;
3541
4544
  const accountSessionIds = activeDrag.accountKey === "" ? ungroupedSessionIds : workspaces.find((workspace) => workspace.workspaceId === activeDrag.accountKey)?.sessionIds;
3542
4545
  if (accountSessionIds === void 0) return;
3543
- const renderedSessions = collapsedSessionRows(group.sessions, sessionLimits[group.key]).rows;
4546
+ const renderedSessions = watermarkRows(group.sessions, sessionWatermarks[group.key] ?? 5).rows;
3544
4547
  const nextOrder = sessionDragOrder(accountSessionIds, renderedSessions, activeDrag, over);
3545
4548
  if (nextOrder !== void 0) setSessionOrder(activeDrag.accountKey, nextOrder);
3546
4549
  };
@@ -3598,8 +4601,9 @@ window.__ModuleLoader__.load({
3598
4601
  const workspaceId = group.workspaceId;
3599
4602
  const children = childrenByParent.get(group.key) ?? [];
3600
4603
  const compatibleDrag = workspaceDrag !== null && parents.get(workspaceDrag.workspaceId) === parents.get(group.key);
3601
- const collapsed = collapsedSessionRows(group.sessions);
3602
- const visible = collapsedSessionRows(group.sessions, sessionLimits[group.key]);
4604
+ const watermark = sessionWatermarks[group.key] ?? 5;
4605
+ const collapsed = watermarkRows(group.sessions, baselines.get(group.key) ?? 5);
4606
+ const visible = watermarkRows(group.sessions, watermark);
3603
4607
  const sessionsExpanded = visible.hiddenCount === 0;
3604
4608
  rowKeys.push(`workspace:${group.key}`);
3605
4609
  const childRows = group.expanded ? children.map((child) => renderGroup(child, depth + 1)) : [];
@@ -3673,10 +4677,7 @@ window.__ModuleLoader__.load({
3673
4677
  now,
3674
4678
  t,
3675
4679
  onToggle: () => {
3676
- if (group.expanded) setSessionLimits((limits) => ({
3677
- ...limits,
3678
- [group.key]: COLLAPSED_SESSION_LIMIT
3679
- }));
4680
+ if (group.expanded) dropWatermarks((key) => withinGroup(key, group.key));
3680
4681
  setGroupExpanded(group.key, !group.expanded);
3681
4682
  },
3682
4683
  onCreate: () => {
@@ -3761,10 +4762,15 @@ window.__ModuleLoader__.load({
3761
4762
  "data-row-key": `overflow:${group.key}`,
3762
4763
  "aria-expanded": sessionsExpanded,
3763
4764
  onClick: () => {
3764
- setSessionLimits((limits) => ({
3765
- ...limits,
3766
- [group.key]: sessionsExpanded ? COLLAPSED_SESSION_LIMIT : visible.hiddenCount <= COLLAPSED_SESSION_LIMIT ? Infinity : (limits[group.key] ?? COLLAPSED_SESSION_LIMIT) + COLLAPSED_SESSION_LIMIT
4765
+ if (sessionsExpanded) {
4766
+ dropWatermarks((key) => key === group.key);
4767
+ return;
4768
+ }
4769
+ setStoredWatermarks((marks) => ({
4770
+ ...marks,
4771
+ [group.key]: visible.hiddenCount <= 5 ? Infinity : watermark + 5
3767
4772
  }));
4773
+ setWatermarkMotionKey((key) => key + 1);
3768
4774
  },
3769
4775
  children: sessionsExpanded ? t("sessions.collapse") : t("sessions.expand", { n: visible.hiddenCount })
3770
4776
  })
@@ -3772,7 +4778,7 @@ window.__ModuleLoader__.load({
3772
4778
  }, group.key);
3773
4779
  };
3774
4780
  const groupRows = rootGroups.map((group) => renderGroup(group, 0));
3775
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4781
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.HoverCardGroup, {
3776
4782
  className: clsx(WorkspaceBrowser_module_css_default.treeBody, WorkspaceBrowser_module_css_default.wide),
3777
4783
  children: [
3778
4784
  workspaceDropAtListStart && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -3784,7 +4790,7 @@ window.__ModuleLoader__.load({
3784
4790
  label: t("section.sessions"),
3785
4791
  rowKeys,
3786
4792
  ready: list.phase === "ready" && workspaceReady && !nativeDragActive,
3787
- resetKey: animationResetKey,
4793
+ resetKey: JSON.stringify([animationResetKey, watermarkMotionKey]),
3788
4794
  children: [groupsWithAggregatedStatus.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmptySessions, {
3789
4795
  rowState,
3790
4796
  onLeaveArchivedOnly,
@@ -3817,7 +4823,7 @@ window.__ModuleLoader__.load({
3817
4823
  if (nextOrder !== void 0) setSessionOrder(FLAT_SESSION_ORDER_KEY, nextOrder);
3818
4824
  };
3819
4825
  const now = Date.now();
3820
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4826
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.HoverCardGroup, {
3821
4827
  className: clsx(WorkspaceBrowser_module_css_default.treeBody, WorkspaceBrowser_module_css_default.wide),
3822
4828
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(AnimatedRows, {
3823
4829
  className: clsx(WorkspaceBrowser_module_css_default.list, WorkspaceBrowser_module_css_default.flatList),
@@ -3950,7 +4956,7 @@ window.__ModuleLoader__.load({
3950
4956
  * @param props - composed slot props (shell owner share + store + injected actions).
3951
4957
  * @returns the region element tree.
3952
4958
  */
3953
- function WorkspaceBrowser({ wide, usePanelInfo, expandSidebar, useSessions, useSessionStatus, useWorkspaces, useStore, actions, startSession, open, requestSessionRename, notifyArchivedNotOpenable, renameWorkspace, deleteWorkspace, insertWorkspaceBefore, unarchiveSession, createWorkspace, searchSessions, searchResultLimit, useDirectoryFlow, useHostInfo, useShortcuts, useWorkspaceShortcuts, useBadgeSettings, requestSearch, requestAddWorkspace, closeAddWorkspace, setDirectoryBusy, dismissForkError, renderSlot, t }) {
4959
+ function WorkspaceBrowser({ wide, usePanelInfo, expandSidebar, useSessions, useSessionStatus, useWorkspaces, useStore, actions, startSession, open, requestSessionRename, notifyArchivedNotOpenable, renameWorkspace, deleteWorkspace, insertWorkspaceBefore, unarchiveSession, createWorkspace, searchSessions, searchResultLimit, useDirectoryFlow, useHostInfo, useShortcuts, useWorkspaceShortcuts, useBadgeSettings, requestSearch, requestAddWorkspace, closeAddWorkspace, setDirectoryBusy, renderSlot, t }) {
3954
4960
  const home = useHostInfo((info) => info.home);
3955
4961
  const shortcuts = useShortcuts((rows) => rows);
3956
4962
  const searchShortcut = shortcuts.find((row) => row.id === "session.search");
@@ -4379,7 +5385,7 @@ window.__ModuleLoader__.load({
4379
5385
  side: "right",
4380
5386
  onPick: (workspaceId) => {
4381
5387
  closeAddWorkspace();
4382
- startSession(workspaceId);
5388
+ startSession(workspaceId, { clearPreviousDraft: false });
4383
5389
  },
4384
5390
  onClose: () => {
4385
5391
  closeAddWorkspace();
@@ -4559,11 +5565,7 @@ window.__ModuleLoader__.load({
4559
5565
  role: "alert",
4560
5566
  children: deleteError
4561
5567
  })]
4562
- }),
4563
- shortcutState.forkError !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Toast, {
4564
- text: t(shortcutState.forkError.reason === "unavailable" ? "shortcut.noCompletedTurn" : "shortcut.forkFailed"),
4565
- onDone: dismissForkError
4566
- }, shortcutState.forkError.seq)
5568
+ })
4567
5569
  ]
4568
5570
  });
4569
5571
  }
@@ -4608,6 +5610,7 @@ window.__ModuleLoader__.load({
4608
5610
  side: "bottom",
4609
5611
  align: "end",
4610
5612
  delayMs: 500,
5613
+ portal: true,
4611
5614
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4612
5615
  type: "button",
4613
5616
  className: Rows_module_css_default.iconButton,
@@ -4753,13 +5756,15 @@ window.__ModuleLoader__.load({
4753
5756
  /** The fork action: one `sidebar.workspaces.session.menu.item` row. */
4754
5757
  /**
4755
5758
  * Menu row (order 300): fork at the Session's last completed turn; the child
4756
- * arrives through the Host list beside its source.
4757
- * @param props - owner share, menu open state, and the fork share.
4758
- * @returns the row.
5759
+ * arrives through the Host list beside its source. Absent when the access
5760
+ * snapshot excludes `session/fork` for this Session.
5761
+ * @param props - owner share, menu open state, access Hook, and the fork share.
5762
+ * @returns the row, or nothing when the caller may not fork this Session.
4759
5763
  */
4760
- function ForkSessionMenuItem({ sessionId, useMenuOpenState, useShortcuts, forkSession, t }) {
5764
+ function ForkSessionMenuItem({ sessionId, useMenuOpenState, useShortcuts, useSessionAccess, forkSession, t }) {
4761
5765
  const [, setMenuOpen] = useMenuOpenState();
4762
5766
  const shortcut = useShortcuts((rows) => rows.find((row) => row.id === "session.fork"));
5767
+ if (!useSessionAccess(sessionId, "session/fork")) return null;
4763
5768
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MenuItemButton, {
4764
5769
  shortcut,
4765
5770
  icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconBranchOutlineRegular, {}),
@@ -4820,6 +5825,7 @@ window.__ModuleLoader__.load({
4820
5825
  side: "bottom",
4821
5826
  align: "end",
4822
5827
  delayMs: 500,
5828
+ portal: true,
4823
5829
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4824
5830
  type: "button",
4825
5831
  className: Rows_module_css_default.iconButton,
@@ -4949,75 +5955,38 @@ window.__ModuleLoader__.load({
4949
5955
  });
4950
5956
  }
4951
5957
  //#endregion
4952
- //#region src/client/session-actions/RowActionToast.tsx
4953
- /**
4954
- * The `shell.overlay` entry for Workspace and Session notices.
4955
- * One notice is visible at a time; a parent rerender does not extend its hold.
4956
- */
5958
+ //#region src/client/session-actions/SessionId.tsx
5959
+ /** The copy Session ID action: one `sidebar.workspaces.session.menu.item` row. */
4957
5960
  /**
4958
- * Hold for the notices that take longer to read than a one-line warning: the
4959
- * actionable archived notice (two buttons to react to) and a refused Session
4960
- * creation, which quotes the Host's reason.
5961
+ * Copy the complete ID after dismissing the row menu.
5962
+ * @param props - row identity, menu dismissal, clipboard action, and locale.
5963
+ * @returns the final built-in menu item.
4961
5964
  */
4962
- const LONG_TOAST_HOLD_MS = 6e3;
5965
+ function SessionIdMenuItem({ sessionId, useMenuOpenState, copySessionId, t }) {
5966
+ const [, setMenuOpen] = useMenuOpenState();
5967
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MenuItemButton, {
5968
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCopyOutlineRegular, {}),
5969
+ onSelect: () => {
5970
+ setMenuOpen(false);
5971
+ copySessionId(sessionId);
5972
+ },
5973
+ children: t("menu.sessionId")
5974
+ });
5975
+ }
5976
+ //#endregion
5977
+ //#region src/client/session-actions/RowActionToast.tsx
4963
5978
  /**
4964
- * Render the current notice: the archived and stopped-and-archived notices
4965
- * with their undo action — plus the show-archived action while archived rows
4966
- * are hidden — on a 6 s hold, a refused Session creation with the Host's
4967
- * reason on the same hold, or a plain warning for a failed pin, an archived
4968
- * row that was clicked, or default Workspace creation.
4969
- * @param props - the notice hook, the shared viewing store, the notice dismissal, the two archived-notice actions, and the locale seat.
4970
- * @returns the notice on display, or null.
5979
+ * Dispatch the current notice without interpreting its business payload.
5980
+ * @param props - Current notice, dismissal, and the declared keyed renderer.
5981
+ * @returns the selected notice presentation, or nothing.
4971
5982
  */
4972
- function RowActionToast({ useToast, useStore, dismissToast, undoArchive, showArchived, t }) {
4973
- const toast = useToast((current) => current);
4974
- const archivedRowsVisible = useStore((state) => (state.archivedFilter ?? "default") !== "default");
4975
- if (toast === null) return null;
4976
- if (toast.kind === "archived" || toast.kind === "stoppedAndArchived") {
4977
- const { sessionId } = toast;
4978
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Toast, {
4979
- text: t(toast.kind === "archived" ? "toast.archived" : "toast.stoppedAndArchived"),
4980
- tone: "success",
4981
- holdMs: LONG_TOAST_HOLD_MS,
4982
- actions: [{
4983
- label: t("toast.archivedUndo"),
4984
- onClick: () => {
4985
- dismissToast();
4986
- undoArchive(sessionId);
4987
- }
4988
- }, ...archivedRowsVisible ? [] : [{
4989
- prefix: t("toast.archivedOr"),
4990
- label: t("toast.archivedFilter"),
4991
- onClick: () => {
4992
- dismissToast();
4993
- showArchived();
4994
- }
4995
- }]],
4996
- onDone: dismissToast
4997
- }, `toast-${String(toast.seq)}`);
4998
- }
4999
- if (toast.kind === "createFailed") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Toast, {
5000
- text: t("toast.createFailed", { message: toast.message }),
5001
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconWarningOutlineRegular, {}),
5002
- holdMs: LONG_TOAST_HOLD_MS,
5003
- onDone: dismissToast
5004
- }, `toast-${String(toast.seq)}`);
5005
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Toast, {
5006
- text: plainNoticeText(toast, t),
5007
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconWarningOutlineRegular, {}),
5008
- onDone: dismissToast
5009
- }, `toast-${String(toast.seq)}`);
5010
- }
5011
- /** The copy of one plain warning, keyed by the notice kind the union closes over. */
5012
- function plainNoticeText(toast, t) {
5013
- switch (toast.kind) {
5014
- case "pinFailed": return t("toast.pinFailed");
5015
- case "unpinFailed": return t("toast.unpinFailed");
5016
- case "defaultWorkspaceFailed": return t("defaultWorkspace.failed");
5017
- case "archivedNotOpenable": return t("toast.archivedNotOpenable");
5018
- /* v8 ignore next 2 -- closed-union backstop; only reached if a notice kind is forged */
5019
- default: return assertNever$1(toast);
5020
- }
5983
+ function RowActionToast({ useToast, dismissToast, renderSlot }) {
5984
+ const notice = useToast((current) => current);
5985
+ if (notice === null) return null;
5986
+ return renderSlot("workspace.toast", {
5987
+ ...notice,
5988
+ dismissToast
5989
+ }, { entryKey: notice.kind });
5021
5990
  }
5022
5991
  //#endregion
5023
5992
  //#region src/client/locales.ts
@@ -5029,6 +5998,8 @@ window.__ModuleLoader__.load({
5029
5998
  /** Simplified Chinese dictionary (the key-set source of truth). */
5030
5999
  const zh = {
5031
6000
  "defaultWorkspace.failed": "无法创建默认工作区,请通过“选择工作区”选择文件夹",
6001
+ "draft.workspaceRequired": "请先选择工作区",
6002
+ "draft.initializationFailed": "暂时无法填入草稿,请稍后重试",
5032
6003
  "group.ungrouped": "未分组",
5033
6004
  "session.new": "新会话",
5034
6005
  "session.untitled": "未命名",
@@ -5083,6 +6054,9 @@ window.__ModuleLoader__.load({
5083
6054
  "menu.unarchiveSession": "取消归档",
5084
6055
  "menu.pinSession": "置顶会话",
5085
6056
  "menu.unpinSession": "取消置顶",
6057
+ "menu.sessionId": "复制 Session ID",
6058
+ "toast.sessionIdCopied": "Session ID 已复制",
6059
+ "toast.sessionIdCopyFailed": "复制失败,请重试",
5086
6060
  "row.archived": "已归档",
5087
6061
  "row.pinned": "已置顶",
5088
6062
  "toast.archivedNotOpenable": "已归档对话暂时无法查看,请取消归档后查看",
@@ -5109,6 +6083,8 @@ window.__ModuleLoader__.load({
5109
6083
  "toast.pinFailed": "置顶失败,请稍后重试",
5110
6084
  "toast.unpinFailed": "取消置顶失败,请稍后重试",
5111
6085
  "toast.createFailed": "新建会话失败:{message}",
6086
+ "toast.forkRequiresOpen": "请先打开原会话完成迁移,再创建分支",
6087
+ "toast.openForkSource": "打开原会话",
5112
6088
  "sessions.count.one": "{n} 个会话",
5113
6089
  "sessions.count.other": "{n} 个会话",
5114
6090
  "actions.workspace.aria": "工作区“{name}”的操作",
@@ -5148,10 +6124,6 @@ window.__ModuleLoader__.load({
5148
6124
  "settings.badge.showPendingHint": "关闭后,等待审批 / 计划评审 / 等待回答的会话不再画黄点,也不再计入折叠徽标的数字与悬停明细。",
5149
6125
  "settings.badge.showCompleted": "显示「已完成」绿点",
5150
6126
  "settings.badge.showCompletedHint": "关闭后,「已完成」的会话不再画绿点,也不再计入折叠徽标的数字与悬停明细。会话行自身的绿点不受影响。",
5151
- "settings.badge.showScheduled": "显示「定时任务」时钟",
5152
- "settings.badge.showScheduledHint": "关闭后,带定时提醒的会话(来自 dsh-later)不再画时钟图标,也不再计入折叠徽标的数字与悬停明细。",
5153
- "settings.badge.showWaitingReview": "显示「等待评审」沙漏",
5154
- "settings.badge.showWaitingReviewHint": "关闭后,等待代码评审的会话(来自 dsh-gitea-dispatch)不再画沙漏图标,也不再计入折叠徽标的数字与悬停明细。",
5155
6127
  "settings.hover.section": "悬停明细",
5156
6128
  "settings.hover.showDetails": "显示状态明细行",
5157
6129
  "settings.hover.showDetailsHint": "关闭后,悬停卡只显示工作区标题、路径和创建时间——不再显示聚合状态明细行。",
@@ -5167,6 +6139,8 @@ window.__ModuleLoader__.load({
5167
6139
  /** English dictionary, checked complete against the zh key set. */
5168
6140
  const en = {
5169
6141
  "defaultWorkspace.failed": "Unable to create default workspace. Use Choose workspace to select a folder.",
6142
+ "draft.workspaceRequired": "Choose a workspace first.",
6143
+ "draft.initializationFailed": "Could not fill the draft. Please try again shortly.",
5170
6144
  "group.ungrouped": "Ungrouped",
5171
6145
  "session.new": "New Session",
5172
6146
  "session.untitled": "Untitled",
@@ -5221,6 +6195,9 @@ window.__ModuleLoader__.load({
5221
6195
  "menu.unarchiveSession": "Unarchive session",
5222
6196
  "menu.pinSession": "Pin session",
5223
6197
  "menu.unpinSession": "Unpin session",
6198
+ "menu.sessionId": "Copy Session ID",
6199
+ "toast.sessionIdCopied": "Session ID copied",
6200
+ "toast.sessionIdCopyFailed": "Copy failed. Try again.",
5224
6201
  "row.archived": "Archived",
5225
6202
  "row.pinned": "Pinned",
5226
6203
  "toast.archivedNotOpenable": "Archived sessions cannot be opened. Unarchive it to view.",
@@ -5247,6 +6224,8 @@ window.__ModuleLoader__.load({
5247
6224
  "toast.pinFailed": "Pin failed. Try again later.",
5248
6225
  "toast.unpinFailed": "Unpin failed. Try again later.",
5249
6226
  "toast.createFailed": "New session failed: {message}",
6227
+ "toast.forkRequiresOpen": "Open the original session to complete migration before creating a branch.",
6228
+ "toast.openForkSource": "Open original session",
5250
6229
  "sessions.count.one": "{n} session",
5251
6230
  "sessions.count.other": "{n} sessions",
5252
6231
  "actions.workspace.aria": "Workspace actions for {name}",
@@ -5286,10 +6265,6 @@ window.__ModuleLoader__.load({
5286
6265
  "settings.badge.showPendingHint": "When off, Sessions waiting for approval / plan review / answer stop drawing the yellow dot and drop out of the collapsed badge count and its hover details.",
5287
6266
  "settings.badge.showCompleted": "Show the completed dot",
5288
6267
  "settings.badge.showCompletedHint": "When off, completed Sessions stop drawing the dot and drop out of the collapsed badge count and its hover details. The Session row's own dot is unaffected.",
5289
- "settings.badge.showScheduled": "Show the scheduled icon",
5290
- "settings.badge.showScheduledHint": "When off, Sessions with scheduled reminders (from dsh-later) stop drawing the clock icon and drop out of the collapsed badge count and its hover details.",
5291
- "settings.badge.showWaitingReview": "Show the waiting-for-review icon",
5292
- "settings.badge.showWaitingReviewHint": "When off, Sessions waiting for code review (from dsh-gitea-dispatch) stop drawing the hourglass icon and drop out of the collapsed badge count and its hover details.",
5293
6268
  "settings.hover.section": "Hover details",
5294
6269
  "settings.hover.showDetails": "Show status detail lines",
5295
6270
  "settings.hover.showDetailsHint": "When off, the hover card shows only the workspace title, path, and creation time — no aggregated status lines.",
@@ -5321,7 +6296,8 @@ window.__ModuleLoader__.load({
5321
6296
  "remote",
5322
6297
  "remote.directoryPicker",
5323
6298
  "layout",
5324
- "shortcuts"
6299
+ "shortcuts",
6300
+ "remoteAccess"
5325
6301
  ];
5326
6302
  /**
5327
6303
  * Register the browser and picker once their slot declarations are on the
@@ -5347,7 +6323,14 @@ window.__ModuleLoader__.load({
5347
6323
  });
5348
6324
  };
5349
6325
  const uiWorkspace = new UiWorkspaceService(ctx, ctx.remote.directoryPicker, workspaces, sessions, viewInstance.actions, notify);
5350
- ctx.slots.provideRoot({ hooks: { workspaces: workspaces.list } });
6326
+ const denyWorkspaceAccess = {
6327
+ getSnapshot: () => false,
6328
+ subscribe: () => () => {}
6329
+ };
6330
+ ctx.slots.provideRoot({
6331
+ hooks: { workspaces: workspaces.list },
6332
+ keyedHooks: { workspaceAccess: (workspaceId, action) => action === void 0 ? denyWorkspaceAccess : ctx.remoteAccess.source("workspace", workspaceId, action) }
6333
+ });
5351
6334
  ctx.effect(() => ctx.locale.register(NS, {
5352
6335
  zh,
5353
6336
  en
@@ -5446,7 +6429,12 @@ window.__ModuleLoader__.load({
5446
6429
  },
5447
6430
  unarchiveSession
5448
6431
  });
5449
- installWorkspaceShortcuts(ctx, uiWorkspace, shortcutControls, archiveInjected().archiveSession);
6432
+ installWorkspaceShortcuts(ctx, uiWorkspace, shortcutControls, archiveInjected().archiveSession, (reason) => {
6433
+ notify({
6434
+ kind: "shortcutForkFailed",
6435
+ reason
6436
+ });
6437
+ });
5450
6438
  const archiveConfirmInjected = () => ({
5451
6439
  hooks: { archiveRequest },
5452
6440
  settleSessionArchive: () => {
@@ -5467,7 +6455,12 @@ window.__ModuleLoader__.load({
5467
6455
  parent_session_id: sessionId,
5468
6456
  click_position: "sidebar"
5469
6457
  });
5470
- }).catch(() => {});
6458
+ }, { allowMigration: false }).catch((reason) => {
6459
+ if ((reason instanceof Error && reason.name === "SessionForkError" ? reason : void 0)?.rpcError?.code === "session/migration-required") notify({
6460
+ kind: "forkRequiresOpen",
6461
+ sessionId
6462
+ });
6463
+ });
5471
6464
  } });
5472
6465
  const renameInjected = () => ({ requestSessionRename });
5473
6466
  const renameDialogInjected = () => ({
@@ -5475,19 +6468,31 @@ window.__ModuleLoader__.load({
5475
6468
  settleSessionRename: shortcutControls.closeRename,
5476
6469
  renameSession
5477
6470
  });
6471
+ const sessionIdInjected = () => ({ copySessionId: async (id) => {
6472
+ notify({ kind: await (0, _deepseek_ai_dsh_client_ui_primitives.writeClipboard)(id) ? "sessionIdCopied" : "sessionIdCopyFailed" });
6473
+ } });
6474
+ registerToasts(ctx, viewStore, {
6475
+ undoArchive: unarchiveSession,
6476
+ showArchived: () => {
6477
+ viewInstance.actions.setArchivedFilter("show");
6478
+ },
6479
+ openForkSource: (id) => {
6480
+ if (workspaces.list.getSnapshot().archivedSessionIds.includes(id)) {
6481
+ notify({ kind: "archivedNotOpenable" });
6482
+ return;
6483
+ }
6484
+ openSession(id);
6485
+ }
6486
+ });
5478
6487
  const rowToastInjected = () => ({
5479
6488
  hooks: { toast: rowToast },
5480
6489
  dismissToast: () => {
5481
6490
  rowToast.set(null);
5482
- },
5483
- undoArchive: unarchiveSession,
5484
- showArchived: () => {
5485
- viewInstance.actions.setArchivedFilter("show");
5486
6491
  }
5487
6492
  });
5488
6493
  const browserInjected = () => ({
5489
- startSession: (workspaceId) => {
5490
- uiWorkspace.startSession(workspaceId);
6494
+ startSession: (workspaceId, options) => {
6495
+ uiWorkspace.startSession(workspaceId, options);
5491
6496
  },
5492
6497
  open: openSession,
5493
6498
  searchSessions,
@@ -5513,7 +6518,6 @@ window.__ModuleLoader__.load({
5513
6518
  requestAddWorkspace: shortcutControls.add,
5514
6519
  closeAddWorkspace: shortcutControls.closeAdd,
5515
6520
  setDirectoryBusy: shortcutControls.directoryBusy,
5516
- dismissForkError: shortcutControls.dismissForkError,
5517
6521
  hooks: {
5518
6522
  directoryFlow: browserFlowSource,
5519
6523
  hostInfo,
@@ -5586,6 +6590,13 @@ window.__ModuleLoader__.load({
5586
6590
  locale: NS,
5587
6591
  inject: archiveInjected
5588
6592
  }, ArchiveSessionMenuItem);
6593
+ yield ctx.slots.register({
6594
+ name: "sidebar.workspaces.session.menu.item",
6595
+ id: "session-id",
6596
+ order: 450,
6597
+ locale: NS,
6598
+ inject: sessionIdInjected
6599
+ }, SessionIdMenuItem);
5589
6600
  });
5590
6601
  ctx.slots.inject("sidebar.workspaces.session.row.action", function* () {
5591
6602
  yield ctx.slots.register({
@@ -5619,9 +6630,11 @@ window.__ModuleLoader__.load({
5619
6630
  yield ctx.slots.register({
5620
6631
  name: "shell.overlay",
5621
6632
  id: "workspace.row-toast",
5622
- locale: NS,
5623
- store: viewStore,
5624
- inject: rowToastInjected
6633
+ inject: rowToastInjected,
6634
+ children: { "workspace.toast": {
6635
+ kind: "keyed",
6636
+ scope: "root"
6637
+ } }
5625
6638
  }, RowActionToast);
5626
6639
  });
5627
6640
  ctx.slots.inject("conversation.hero.workspace", () => ctx.slots.register({