dsh-group-status 0.4.0 → 0.5.1

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 (94) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/README.en.md +10 -6
  3. package/README.md +12 -6
  4. package/lib/client.js +1913 -1804
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +38 -8
  7. package/lib/types/build-stamp.d.ts +4 -0
  8. package/lib/types/build-stamp.d.ts.map +1 -0
  9. package/lib/types/build-stamp.js +23 -0
  10. package/lib/types/build-stamp.js.map +1 -0
  11. package/lib/types/client/BadgeSettingsCard.d.ts.map +1 -1
  12. package/lib/types/client/BadgeSettingsCard.js +8 -8
  13. package/lib/types/client/BadgeSettingsCard.js.map +1 -1
  14. package/lib/types/client/badge-field.d.ts +33 -10
  15. package/lib/types/client/badge-field.d.ts.map +1 -1
  16. package/lib/types/client/badge-field.js +35 -16
  17. package/lib/types/client/badge-field.js.map +1 -1
  18. package/lib/types/client/badge-labels.d.ts +3 -3
  19. package/lib/types/client/badge-labels.js +3 -3
  20. package/lib/types/client/badge-settings.d.ts +0 -25
  21. package/lib/types/client/badge-settings.d.ts.map +1 -1
  22. package/lib/types/client/badge-settings.js +0 -18
  23. package/lib/types/client/badge-settings.js.map +1 -1
  24. package/lib/types/client/facets.d.ts +39 -5
  25. package/lib/types/client/facets.d.ts.map +1 -1
  26. package/lib/types/client/facets.js +48 -4
  27. package/lib/types/client/facets.js.map +1 -1
  28. package/lib/types/client/group-status.d.ts +5 -0
  29. package/lib/types/client/group-status.d.ts.map +1 -1
  30. package/lib/types/client/group-status.js +25 -0
  31. package/lib/types/client/group-status.js.map +1 -1
  32. package/lib/types/client/locales.d.ts +4 -8
  33. package/lib/types/client/locales.d.ts.map +1 -1
  34. package/lib/types/client/locales.js +4 -8
  35. package/lib/types/client/locales.js.map +1 -1
  36. package/lib/types/client/rows/EmptySessions.d.ts +4 -0
  37. package/lib/types/client/rows/EmptySessions.d.ts.map +1 -0
  38. package/lib/types/client/rows/EmptySessions.js +10 -0
  39. package/lib/types/client/rows/EmptySessions.js.map +1 -0
  40. package/lib/types/client/rows/GroupBadge.d.ts +4 -4
  41. package/lib/types/client/rows/GroupBadge.d.ts.map +1 -1
  42. package/lib/types/client/rows/GroupBadge.js +10 -7
  43. package/lib/types/client/rows/GroupBadge.js.map +1 -1
  44. package/lib/types/client/rows/ListView.d.ts +24 -0
  45. package/lib/types/client/rows/ListView.d.ts.map +1 -0
  46. package/lib/types/client/rows/ListView.js +81 -0
  47. package/lib/types/client/rows/ListView.js.map +1 -0
  48. package/lib/types/client/rows/SessionTree.d.ts +4 -0
  49. package/lib/types/client/rows/SessionTree.d.ts.map +1 -0
  50. package/lib/types/client/rows/SessionTree.js +381 -0
  51. package/lib/types/client/rows/SessionTree.js.map +1 -0
  52. package/lib/types/client/rows/ViewOptionsMenu.d.ts +14 -0
  53. package/lib/types/client/rows/ViewOptionsMenu.d.ts.map +1 -0
  54. package/lib/types/client/rows/ViewOptionsMenu.js +50 -0
  55. package/lib/types/client/rows/ViewOptionsMenu.js.map +1 -0
  56. package/lib/types/client/rows/WorkspaceBrowser.d.ts +0 -5
  57. package/lib/types/client/rows/WorkspaceBrowser.d.ts.map +1 -1
  58. package/lib/types/client/rows/WorkspaceBrowser.js +8 -557
  59. package/lib/types/client/rows/WorkspaceBrowser.js.map +1 -1
  60. package/lib/types/client/rows/browser-shared.d.ts +105 -0
  61. package/lib/types/client/rows/browser-shared.d.ts.map +1 -0
  62. package/lib/types/client/rows/browser-shared.js +93 -0
  63. package/lib/types/client/rows/browser-shared.js.map +1 -0
  64. package/lib/types/client/session-actions/derived.d.ts.map +1 -1
  65. package/lib/types/client/session-actions/derived.js +13 -2
  66. package/lib/types/client/session-actions/derived.js.map +1 -1
  67. package/lib/types/client/session-watermarks.d.ts +6 -1
  68. package/lib/types/client/session-watermarks.d.ts.map +1 -1
  69. package/lib/types/client/session-watermarks.js +31 -11
  70. package/lib/types/client/session-watermarks.js.map +1 -1
  71. package/lib/types/client/toast/register.d.ts.map +1 -1
  72. package/lib/types/client/toast/register.js +102 -132
  73. package/lib/types/client/toast/register.js.map +1 -1
  74. package/lib/types/client/tree.d.ts +16 -1
  75. package/lib/types/client/tree.d.ts.map +1 -1
  76. package/lib/types/client/tree.js +20 -8
  77. package/lib/types/client/tree.js.map +1 -1
  78. package/lib/types/client/use-registered-facets.d.ts +11 -0
  79. package/lib/types/client/use-registered-facets.d.ts.map +1 -0
  80. package/lib/types/client/use-registered-facets.js +48 -0
  81. package/lib/types/client/use-registered-facets.js.map +1 -0
  82. package/lib/types/host-contract.d.ts +71 -0
  83. package/lib/types/host-contract.d.ts.map +1 -0
  84. package/lib/types/host-contract.js +127 -0
  85. package/lib/types/host-contract.js.map +1 -0
  86. package/lib/types/index.d.ts +13 -13
  87. package/lib/types/index.d.ts.map +1 -1
  88. package/lib/types/index.js +17 -8
  89. package/lib/types/index.js.map +1 -1
  90. package/package.json +16 -2
  91. package/lib/types/client/facet-visibility.d.ts +0 -17
  92. package/lib/types/client/facet-visibility.d.ts.map +0 -1
  93. package/lib/types/client/facet-visibility.js +0 -18
  94. package/lib/types/client/facet-visibility.js.map +0 -1
package/lib/client.js CHANGED
@@ -211,30 +211,17 @@ window.__ModuleLoader__.load({
211
211
  */
212
212
  /** Loader 条目 id —— 设置页的寻址键,也是宿主 `describe()` 的 ns。 */
213
213
  const ENTRY_ID = "dsh-group-status";
214
- /**
215
- * 一个 facet 是否被设置关掉。
216
- *
217
- * - 没有 `settingKey` → 始终参与(缺省是"显示",与其余开关的默认值同源)。
218
- * - 声明了但不是布尔开关键 → 同样按显示处理:拼错的键不该让一个状态整块消失。
219
- */
220
- function facetVisibleBySettings(settings, settingKey) {
221
- if (settingKey === void 0) return true;
222
- const value = settings[settingKey];
223
- return typeof value === "boolean" ? value : true;
224
- }
225
214
  /** 默认值:与 `Config` 各字段的 schema 默认同源(全部显示,数字阈值 2 即 count=1 时省略)。 */
226
215
  const DEFAULT_BADGE_SETTINGS = Object.freeze({
227
216
  showCompletedInBadge: true,
228
217
  showRunningInBadge: true,
229
218
  showPendingInBadge: true,
230
- showScheduledInBadge: true,
231
- showWaitingReviewInBadge: true,
232
219
  showHoverDetails: true,
233
220
  countThreshold: 2
234
221
  });
235
222
  /** 两个设置对象是否逐字段相等(用于发布前比较,避免无谓通知)。 */
236
223
  function settingsEqual(a, b) {
237
- 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;
238
225
  }
239
226
  /** 从原始快照派生:只有**显式 false** 才隐藏,缺失/非法一律显示(countThreshold 默认 2)。 */
240
227
  function resolveFromSnapshot(snap) {
@@ -253,8 +240,6 @@ window.__ModuleLoader__.load({
253
240
  showCompletedInBadge: getBool("showCompletedInBadge", true),
254
241
  showRunningInBadge: getBool("showRunningInBadge", true),
255
242
  showPendingInBadge: getBool("showPendingInBadge", true),
256
- showScheduledInBadge: getBool("showScheduledInBadge", true),
257
- showWaitingReviewInBadge: getBool("showWaitingReviewInBadge", true),
258
243
  showHoverDetails: getBool("showHoverDetails", true),
259
244
  countThreshold: getNumber("countThreshold", 2)
260
245
  };
@@ -434,12 +419,52 @@ window.__ModuleLoader__.load({
434
419
  };
435
420
  }
436
421
  /**
437
- * 生产用的模块级单例登记表面。
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
+ * 生产用的单例登记表面。
438
460
  *
439
461
  * 与 {@link createFacetRegistry} 分开,是为了让测试能对着一张干净的表断言协议行为,
440
462
  * 而不会被别的用例或别的插件登记的 facet 污染。
463
+ *
464
+ * ⚠️ 它只是 {@link sharedFacetRegistry} 在**本模块首次加载时**取到的那份引用;跨模块实例
465
+ * 存活的是 globalThis 上那一张,所以运行期一律经 {@link sharedFacetRegistry} 现取。
441
466
  */
442
- const registry = createFacetRegistry();
467
+ const registry = sharedFacetRegistry();
443
468
  /**
444
469
  * 本插件 client 入口对外的登记函数(provider 侧唯一需要认识的符号)。
445
470
  *
@@ -448,13 +473,14 @@ window.__ModuleLoader__.load({
448
473
  * 返回值形状保持稳定,provider 的 dispose 路径不必区分成败。
449
474
  */
450
475
  function registerGroupStatusFacet(facet) {
451
- const result = registry.register(facet);
476
+ const target = sharedFacetRegistry();
477
+ const result = target.register(facet);
452
478
  if (!result.ok) {
453
479
  if (!(result.reason === "superseded" && facetOrigin(facet) === "legacy")) reportRejection(result);
454
480
  return () => void 0;
455
481
  }
456
482
  return () => {
457
- registry.remove(result.id, facet);
483
+ target.remove(result.id, facet);
458
484
  };
459
485
  }
460
486
  /**
@@ -575,19 +601,39 @@ window.__ModuleLoader__.load({
575
601
  }
576
602
  //#endregion
577
603
  //#region src/client/badge-field.ts
578
- /** 卡片字段名(与 `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
+ */
579
627
  const BADGE_FIELDS = {
580
628
  showCompletedInBadge: "showCompletedInBadge",
581
629
  showRunningInBadge: "showRunningInBadge",
582
630
  showPendingInBadge: "showPendingInBadge",
583
- showScheduledInBadge: "showScheduledInBadge",
584
- showWaitingReviewInBadge: "showWaitingReviewInBadge",
585
631
  showHoverDetails: "showHoverDetails",
586
632
  countThreshold: "countThreshold"
587
633
  };
588
634
  /**
589
- * 草稿文本解析成开关状态:与 {@link isFieldEnabled} 不同,这里比较的是模型给的
590
- * **文本**,别把字符串喂给上面那个函数(`'false' !== false` 恒真)。
635
+ * 草稿文本解析成开关状态:比较的是模型给的**文本**,不是存储值 ——
636
+ * 别把字符串 `'false'` 当成布尔 `false`(`'false' !== false` 恒真)。
591
637
  */
592
638
  function isDraftEnabled(text) {
593
639
  return text !== "false";
@@ -616,13 +662,21 @@ window.__ModuleLoader__.load({
616
662
  } : void 0
617
663
  };
618
664
  }
619
- /** 所有字段规格(按卡片渲染顺序)。 */
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
+ */
620
676
  const ALL_BADGE_FIELDS = [
621
- makeBooleanField(BADGE_FIELDS.showCompletedInBadge, true),
622
- makeBooleanField(BADGE_FIELDS.showRunningInBadge, true),
623
677
  makeBooleanField(BADGE_FIELDS.showPendingInBadge, true),
624
- makeBooleanField(BADGE_FIELDS.showScheduledInBadge, true),
625
- makeBooleanField(BADGE_FIELDS.showWaitingReviewInBadge, true),
678
+ showRunningField,
679
+ showCompletedField,
626
680
  makeBooleanField(BADGE_FIELDS.showHoverDetails, true),
627
681
  {
628
682
  field: BADGE_FIELDS.countThreshold,
@@ -642,7 +696,7 @@ window.__ModuleLoader__.load({
642
696
  ];
643
697
  //#endregion
644
698
  //#region \0dsh-css:src/client/BadgeSettingsCard.module.css.mjs
645
- 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}";
646
700
  const tagId$4 = "dsh-group-status/BadgeSettingsCard.module.css";
647
701
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$4) + "]") === null) {
648
702
  const tag = document.createElement("style");
@@ -656,6 +710,7 @@ window.__ModuleLoader__.load({
656
710
  "badgeNumberButton": "dhp54G_badgeNumberButton",
657
711
  "badgeNumberControl": "dhp54G_badgeNumberControl",
658
712
  "badgeNumberValue": "dhp54G_badgeNumberValue",
713
+ "badgeSpecimen": "dhp54G_badgeSpecimen",
659
714
  "badgeToggleLabel": "dhp54G_badgeToggleLabel",
660
715
  "badgeToggleRow": "dhp54G_badgeToggleRow",
661
716
  "heading": "dhp54G_heading",
@@ -736,21 +791,29 @@ window.__ModuleLoader__.load({
736
791
  id: badgeSectionId,
737
792
  children: t("settings.badge.section")
738
793
  }),
739
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
740
- field: BADGE_FIELDS.showRunningInBadge,
741
- text: fieldText(BADGE_FIELDS.showRunningInBadge),
742
- label: t("settings.badge.showRunning"),
743
- hint: t("settings.badge.showRunningHint"),
744
- state,
745
- actions
746
- }),
747
794
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
748
795
  field: BADGE_FIELDS.showPendingInBadge,
749
796
  text: fieldText(BADGE_FIELDS.showPendingInBadge),
750
797
  label: t("settings.badge.showPending"),
751
798
  hint: t("settings.badge.showPendingHint"),
752
799
  state,
753
- 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
+ })
754
817
  }),
755
818
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
756
819
  field: BADGE_FIELDS.showCompletedInBadge,
@@ -758,23 +821,11 @@ window.__ModuleLoader__.load({
758
821
  label: t("settings.badge.showCompleted"),
759
822
  hint: t("settings.badge.showCompletedHint"),
760
823
  state,
761
- actions
762
- }),
763
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
764
- field: BADGE_FIELDS.showScheduledInBadge,
765
- text: fieldText(BADGE_FIELDS.showScheduledInBadge),
766
- label: t("settings.badge.showScheduled"),
767
- hint: t("settings.badge.showScheduledHint"),
768
- state,
769
- actions
770
- }),
771
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
772
- field: BADGE_FIELDS.showWaitingReviewInBadge,
773
- text: fieldText(BADGE_FIELDS.showWaitingReviewInBadge),
774
- label: t("settings.badge.showWaitingReview"),
775
- hint: t("settings.badge.showWaitingReviewHint"),
776
- state,
777
- 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
+ })
778
829
  })
779
830
  ]
780
831
  }),
@@ -806,6 +857,8 @@ window.__ModuleLoader__.load({
806
857
  text: fieldText(BADGE_FIELDS.countThreshold),
807
858
  label: t("settings.count.threshold"),
808
859
  hint: t("settings.count.thresholdHint"),
860
+ decreaseLabel: t("settings.count.decrease"),
861
+ increaseLabel: t("settings.count.increase"),
809
862
  state,
810
863
  actions
811
864
  })]
@@ -813,15 +866,18 @@ window.__ModuleLoader__.load({
813
866
  ]
814
867
  });
815
868
  }
816
- /** 单行开关:label + Switch + hint。 */
817
- function ToggleRow({ field, text, label, hint, state, actions }) {
869
+ /** 单行开关:label + Switch + hint。`specimen` 是行首那枚预览点(可选)。 */
870
+ function ToggleRow({ field, text, label, hint, state, actions, specimen }) {
818
871
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
819
872
  className: BadgeSettingsCard_module_css_default.badgeField,
820
873
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
821
874
  className: BadgeSettingsCard_module_css_default.badgeToggleRow,
822
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
875
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
823
876
  className: BadgeSettingsCard_module_css_default.badgeToggleLabel,
824
- children: label
877
+ children: [specimen !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
878
+ className: BadgeSettingsCard_module_css_default.badgeSpecimen,
879
+ children: specimen
880
+ }), label]
825
881
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
826
882
  checked: isDraftEnabled(text),
827
883
  label,
@@ -837,7 +893,7 @@ window.__ModuleLoader__.load({
837
893
  });
838
894
  }
839
895
  /** 单行数字输入:label + 带加减按钮的数字输入 + hint。 */
840
- function NumberRow({ field, text, label, hint, state, actions }) {
896
+ function NumberRow({ field, text, label, hint, decreaseLabel, increaseLabel, state, actions }) {
841
897
  const disabled = !state.writable || state.saving;
842
898
  const currentValue = Number(text) || 0;
843
899
  const decrement = () => {
@@ -861,7 +917,7 @@ window.__ModuleLoader__.load({
861
917
  className: BadgeSettingsCard_module_css_default.badgeNumberButton,
862
918
  disabled: disabled || currentValue <= 0,
863
919
  onClick: decrement,
864
- "aria-label": "减少",
920
+ "aria-label": decreaseLabel,
865
921
  children: "−"
866
922
  }),
867
923
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -873,7 +929,7 @@ window.__ModuleLoader__.load({
873
929
  className: BadgeSettingsCard_module_css_default.badgeNumberButton,
874
930
  disabled,
875
931
  onClick: increment,
876
- "aria-label": "增加",
932
+ "aria-label": increaseLabel,
877
933
  children: "+"
878
934
  })
879
935
  ]
@@ -933,11 +989,34 @@ window.__ModuleLoader__.load({
933
989
  }
934
990
  //#endregion
935
991
  //#region src/client/toast/register.tsx
936
- /** Wrap a business display mapping without making the slot component generic. */
992
+ /**
993
+ * Wrap a business display mapping without making the slot component generic.
994
+ * @param display - 该键的纯文案映射;`notice` 的类型由键决定,写错字段即编译失败。
995
+ */
937
996
  function plainToast(display) {
938
997
  return () => ({ display });
939
998
  }
940
999
  /**
1000
+ * 一条普通通知的登记数据:键与它自己的 display 成对。
1001
+ *
1002
+ * 键必须作为泛型实参先写出来,`display` 的 `notice` 才有类型——单看 display 推断不出
1003
+ * K,只能退回全部通知的联合。存下来的 `inject` 按 {@link KeyedToastInjected} 抹掉键
1004
+ * 负载:键 → 负载 的对应关系在入参这一侧已经校验过。
1005
+ */
1006
+ function plainToastRow(key, display) {
1007
+ return {
1008
+ key,
1009
+ inject: plainToast(display)
1010
+ };
1011
+ }
1012
+ const ARCHIVE_TOASTS = [{
1013
+ key: "archived",
1014
+ text: "toast.archived"
1015
+ }, {
1016
+ key: "stoppedAndArchived",
1017
+ text: "toast.stoppedAndArchived"
1018
+ }];
1019
+ /**
941
1020
  * Register notice presentations with their business-owned display fields and actions.
942
1021
  * @param ctx - workspace plugin context.
943
1022
  * @param viewStore - viewing store, read only by archive notices.
@@ -954,130 +1033,72 @@ window.__ModuleLoader__.load({
954
1033
  actions.showArchived();
955
1034
  }
956
1035
  });
1036
+ const plainToasts = [
1037
+ plainToastRow("sessionIdCopied", (_notice, t) => ({
1038
+ text: t("toast.sessionIdCopied"),
1039
+ tone: "success"
1040
+ })),
1041
+ plainToastRow("sessionIdCopyFailed", (_notice, t) => ({
1042
+ text: t("toast.sessionIdCopyFailed"),
1043
+ tone: "warning"
1044
+ })),
1045
+ plainToastRow("pinFailed", (_notice, t) => ({
1046
+ text: t("toast.pinFailed"),
1047
+ tone: "warning"
1048
+ })),
1049
+ plainToastRow("unpinFailed", (_notice, t) => ({
1050
+ text: t("toast.unpinFailed"),
1051
+ tone: "warning"
1052
+ })),
1053
+ plainToastRow("archivedNotOpenable", (_notice, t) => ({
1054
+ text: t("toast.archivedNotOpenable"),
1055
+ tone: "warning"
1056
+ })),
1057
+ plainToastRow("defaultWorkspaceFailed", (_notice, t) => ({
1058
+ text: t("defaultWorkspace.failed"),
1059
+ tone: "warning"
1060
+ })),
1061
+ plainToastRow("shortcutForkFailed", (notice, t) => ({
1062
+ text: t(notice.reason === "unavailable" ? "shortcut.noCompletedTurn" : "shortcut.forkFailed"),
1063
+ tone: "plain"
1064
+ })),
1065
+ plainToastRow("createFailed", (notice, t) => ({
1066
+ text: t("toast.createFailed", { message: notice.message }),
1067
+ tone: "warning",
1068
+ holdMs: 6e3
1069
+ })),
1070
+ plainToastRow("forkRequiresOpen", (notice, t, dismiss) => ({
1071
+ text: t("toast.forkRequiresOpen"),
1072
+ tone: "warning",
1073
+ holdMs: 6e3,
1074
+ actions: [{
1075
+ label: t("toast.openForkSource"),
1076
+ onClick: () => {
1077
+ dismiss();
1078
+ actions.openForkSource(notice.sessionId);
1079
+ }
1080
+ }]
1081
+ }))
1082
+ ];
957
1083
  ctx.slots.registerFactory({
958
1084
  name: "workspace.toast.content",
959
1085
  scope: "root"
960
1086
  }, WorkspaceToast);
961
1087
  ctx.slots.inject("workspace.toast", function* () {
962
- yield ctx.slots.register({
963
- name: "workspace.toast",
964
- key: "sessionIdCopied",
965
- locale: "workspace",
966
- inject: plainToast((_notice, t) => ({
967
- text: t("toast.sessionIdCopied"),
968
- tone: "success"
969
- }))
970
- }, toastEntry);
971
- yield ctx.slots.register({
972
- name: "workspace.toast",
973
- key: "sessionIdCopyFailed",
974
- locale: "workspace",
975
- inject: plainToast((_notice, t) => ({
976
- text: t("toast.sessionIdCopyFailed"),
977
- tone: "warning"
978
- }))
979
- }, toastEntry);
980
- yield ctx.slots.register({
981
- name: "workspace.toast",
982
- key: "pinFailed",
983
- locale: "workspace",
984
- inject: plainToast((_notice, t) => ({
985
- text: t("toast.pinFailed"),
986
- tone: "warning"
987
- }))
988
- }, toastEntry);
989
- yield ctx.slots.register({
990
- name: "workspace.toast",
991
- key: "unpinFailed",
992
- locale: "workspace",
993
- inject: plainToast((_notice, t) => ({
994
- text: t("toast.unpinFailed"),
995
- tone: "warning"
996
- }))
997
- }, toastEntry);
998
- yield ctx.slots.register({
999
- name: "workspace.toast",
1000
- key: "archivedNotOpenable",
1001
- locale: "workspace",
1002
- inject: plainToast((_notice, t) => ({
1003
- text: t("toast.archivedNotOpenable"),
1004
- tone: "warning"
1005
- }))
1006
- }, toastEntry);
1007
- yield ctx.slots.register({
1008
- name: "workspace.toast",
1009
- key: "defaultWorkspaceFailed",
1010
- locale: "workspace",
1011
- inject: plainToast((_notice, t) => ({
1012
- text: t("defaultWorkspace.failed"),
1013
- tone: "warning"
1014
- }))
1015
- }, toastEntry);
1016
- yield ctx.slots.register({
1017
- name: "workspace.toast",
1018
- key: "shortcutForkFailed",
1019
- locale: "workspace",
1020
- inject: plainToast((notice, t) => ({
1021
- text: t(notice.reason === "unavailable" ? "shortcut.noCompletedTurn" : "shortcut.forkFailed"),
1022
- tone: "plain"
1023
- }))
1024
- }, toastEntry);
1025
- yield ctx.slots.register({
1026
- name: "workspace.toast",
1027
- key: "createFailed",
1028
- locale: "workspace",
1029
- inject: plainToast((notice, t) => ({
1030
- text: t("toast.createFailed", { message: notice.message }),
1031
- tone: "warning",
1032
- holdMs: 6e3
1033
- }))
1034
- }, toastEntry);
1035
- yield ctx.slots.register({
1088
+ for (const { key, inject } of plainToasts) yield ctx.slots.register({
1036
1089
  name: "workspace.toast",
1037
- key: "forkRequiresOpen",
1090
+ key,
1038
1091
  locale: "workspace",
1039
- inject: plainToast((notice, t, dismiss) => ({
1040
- text: t("toast.forkRequiresOpen"),
1041
- tone: "warning",
1042
- holdMs: 6e3,
1043
- actions: [{
1044
- label: t("toast.openForkSource"),
1045
- onClick: () => {
1046
- dismiss();
1047
- actions.openForkSource(notice.sessionId);
1048
- }
1049
- }]
1050
- }))
1092
+ inject
1051
1093
  }, toastEntry);
1052
- yield ctx.slots.register({
1053
- name: "workspace.toast",
1054
- key: "archived",
1055
- locale: "workspace",
1056
- store: viewStore,
1057
- inject: () => ({
1058
- display: (notice, t, dismiss) => ({
1059
- text: t("toast.archived"),
1060
- tone: "success",
1061
- holdMs: 6e3,
1062
- actions: [{
1063
- label: t("toast.archivedUndo"),
1064
- onClick: () => {
1065
- dismiss();
1066
- actions.undoArchive(notice.sessionId);
1067
- }
1068
- }]
1069
- }),
1070
- filterAction: archiveFilterAction
1071
- })
1072
- }, archiveToastEntry);
1073
- yield ctx.slots.register({
1094
+ for (const { key, text } of ARCHIVE_TOASTS) yield ctx.slots.register({
1074
1095
  name: "workspace.toast",
1075
- key: "stoppedAndArchived",
1096
+ key,
1076
1097
  locale: "workspace",
1077
1098
  store: viewStore,
1078
1099
  inject: () => ({
1079
1100
  display: (notice, t, dismiss) => ({
1080
- text: t("toast.stoppedAndArchived"),
1101
+ text: t(text),
1081
1102
  tone: "success",
1082
1103
  holdMs: 6e3,
1083
1104
  actions: [{
@@ -1773,6 +1794,12 @@ window.__ModuleLoader__.load({
1773
1794
  function owningGroupKey(workspaces, sessionId) {
1774
1795
  return workspaces.find((workspace) => workspace.sessionIds.includes(sessionId))?.workspaceId ?? "";
1775
1796
  }
1797
+ /** 宿主认的 kind 集合(查表用)。 */
1798
+ const PENDING_INTERACTION_KIND_SET = /* @__PURE__ */ new Set([
1799
+ "approval",
1800
+ "plan-review",
1801
+ "question"
1802
+ ]);
1776
1803
  function mainSessionId(list) {
1777
1804
  return Object.values(list.byId).find((session) => (session.retainedBy.mainView ?? 0) > 0)?.id;
1778
1805
  }
@@ -1956,12 +1983,7 @@ window.__ModuleLoader__.load({
1956
1983
  }
1957
1984
  /** Keep navigation presentation independent from domain-owned interaction objects. */
1958
1985
  function visiblePendingKind(kind) {
1959
- switch (kind) {
1960
- case "approval":
1961
- case "plan-review":
1962
- case "question": return kind;
1963
- default: return;
1964
- }
1986
+ return kind !== void 0 && PENDING_INTERACTION_KIND_SET.has(kind) ? kind : void 0;
1965
1987
  }
1966
1988
  function runningChildCount(list, parentId, statuses) {
1967
1989
  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;
@@ -2643,1293 +2665,888 @@ window.__ModuleLoader__.load({
2643
2665
  function workspaceDisplayTitle(title, localizedDefault) {
2644
2666
  return title === "default-workspace" ? localizedDefault : title;
2645
2667
  }
2646
- /**
2647
- * Zero-valued sentinel for {@link GroupLiveStatus}.
2648
- *
2649
- * Used when a group has nothing to report. Kept as a frozen constant so the
2650
- * reference is stable across renders — `Object.is` comparisons in React selectors
2651
- * never trigger spurious re-renders on it.
2652
- */
2653
- const EMPTY_LIVE_STATUS = Object.freeze({
2654
- running: 0,
2655
- approval: 0,
2656
- planReview: 0,
2657
- question: 0,
2658
- done: 0,
2659
- subagents: 0,
2660
- facets: /* @__PURE__ */ new Map()
2661
- });
2662
- /**
2663
- * 合并两份分组折果:本插件自己的计数相加,外部状态来源**按 facet 各自合并**。
2664
- *
2665
- * 用于多层聚合:折叠的父文件夹要覆盖整棵子树,而子树里每一层都已各自折过一遍。
2666
- * `FacetFold.data` 怎么合归该 facet 自己决定,本模块从不解释。
2667
- */
2668
- function mergeLiveStatus(a, b) {
2669
- return {
2670
- running: a.running + b.running,
2671
- approval: a.approval + b.approval,
2672
- planReview: a.planReview + b.planReview,
2673
- question: a.question + b.question,
2674
- done: a.done + b.done,
2675
- subagents: a.subagents + b.subagents,
2676
- facets: mergeFacets(a.facets, b.facets)
2677
- };
2678
- }
2679
- /** 按 facet id 合并两侧折果;只有一侧有的来源原样带过。 */
2680
- function mergeFacets(a, b) {
2681
- if (a.size === 0) return b;
2682
- if (b.size === 0) return a;
2683
- const merged = new Map(a);
2684
- for (const [id, entry] of b) {
2685
- const own = merged.get(id);
2686
- merged.set(id, own === void 0 ? entry : {
2687
- facet: own.facet,
2688
- fold: own.facet.merge?.(own.fold, entry.fold) ?? defaultMergeFold(own.fold, entry.fold)
2689
- });
2690
- }
2691
- return merged;
2668
+ //#endregion
2669
+ //#region \0dsh-css:src/client/WorkspacePicker.module.css.mjs
2670
+ const css$3 = ".S_JYgq_modalAction{min-width:72px}.S_JYgq_modalError,.S_JYgq_menuStatus{margin-top:8px;font-size:12px;line-height:18px}.S_JYgq_modalError{color:var(--dsw-alias-state-error-primary)}.S_JYgq_menuStatus{color:var(--dsw-alias-label-secondary)}";
2671
+ const tagId$3 = "dsh-group-status/WorkspacePicker.module.css";
2672
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$3) + "]") === null) {
2673
+ const tag = document.createElement("style");
2674
+ tag.dataset.plugin = "dsh-group-status";
2675
+ tag.dataset.pluginCss = tagId$3;
2676
+ tag.textContent = css$3;
2677
+ document.head.appendChild(tag);
2692
2678
  }
2679
+ var WorkspacePicker_module_css_default = {
2680
+ "menuStatus": "S_JYgq_menuStatus",
2681
+ "modalAction": "S_JYgq_modalAction",
2682
+ "modalError": "S_JYgq_modalError"
2683
+ };
2684
+ //#endregion
2685
+ //#region src/client/WorkspacePicker.tsx
2686
+ const ADD_WORKSPACE = "::add-workspace";
2693
2687
  /**
2694
- * Fold every member row of a group into counts so a collapsed folder can hint at
2695
- * hidden live sessions. Mirrors the session-row status priority (pending
2696
- * outranks running outranks completed) at the counting level; labels resolve with
2697
- * locale at render time.
2698
- *
2699
- * @param nodes - member rows of the group.
2700
- * @param sessionIds - the same members' Session ids (facets address their own data by id).
2701
- * @param facets - the registered status sources to fold over this group.
2702
- * @param now - one clock reading for the whole pass, so sibling facets cannot disagree
2703
- * about which deadlines are still ahead.
2688
+ * Render the pick menu plus the adoption error dialog.
2689
+ * @param props - owner-controlled flow props.
2690
+ * @returns menu + dialog elements.
2704
2691
  */
2705
- function summarizeGroupStatus(nodes, sessionIds, facets = [], now = Date.now()) {
2706
- let running = 0;
2707
- let approval = 0;
2708
- let planReview = 0;
2709
- let question = 0;
2710
- let done = 0;
2711
- let subagents = 0;
2712
- for (const n of nodes) {
2713
- if (n.blank || n.archived) continue;
2714
- const hasSubagents = n.runningSubagentCount > 0;
2715
- if (n.pendingInteraction === "approval") {
2716
- approval += 1;
2717
- subagents += n.runningSubagentCount;
2718
- continue;
2719
- }
2720
- if (n.pendingInteraction === "plan-review") {
2721
- planReview += 1;
2722
- subagents += n.runningSubagentCount;
2723
- continue;
2724
- }
2725
- if (n.pendingInteraction === "question") {
2726
- question += 1;
2727
- subagents += n.runningSubagentCount;
2728
- continue;
2729
- }
2730
- if (n.running) {
2731
- running += 1;
2732
- subagents += n.runningSubagentCount;
2733
- continue;
2734
- }
2735
- if (hasSubagents) {
2736
- subagents += n.runningSubagentCount;
2737
- continue;
2738
- }
2739
- if (n.completed) done += 1;
2740
- }
2741
- const folded = /* @__PURE__ */ new Map();
2742
- for (const { facet } of facets) {
2743
- let snapshot;
2744
- try {
2745
- snapshot = facet.getSnapshot();
2746
- } catch {
2747
- snapshot = void 0;
2748
- }
2749
- try {
2750
- const result = facet.fold(snapshot, sessionIds, now);
2751
- if (result !== void 0 && result.count > 0) folded.set(facet.id, {
2752
- facet,
2753
- fold: result
2754
- });
2755
- } catch {}
2756
- }
2757
- return {
2758
- running,
2759
- approval,
2760
- planReview,
2761
- question,
2762
- done,
2763
- subagents,
2764
- facets: folded
2765
- };
2766
- }
2767
- /**
2768
- * Measure each group's own members, as the input to {@link aggregateGroupStatuses}.
2769
- *
2770
- * Every group is measured, expanded ones included. A folder row shows no badge while
2771
- * it is expanded, but its sessions still need attention and a collapsed ancestor must
2772
- * keep counting them — so expansion is a rendering fact, never an input to the
2773
- * aggregation. Short-circuiting expanded groups to `EMPTY_LIVE_STATUS` (the shape this
2774
- * had before the tree-wide aggregator existed, harmless while nothing aggregated) makes
2775
- * every collapsed parent undercount by the size of its expanded descendants.
2776
- *
2777
- * @param groups - derived groups, each with its member rows already populated.
2778
- * @param facets - registered sources, already filtered by the visibility settings.
2779
- * @param now - one clock reading shared by every group in the pass.
2780
- */
2781
- function directStatusesForGroups(groups, facets = [], now = Date.now()) {
2782
- const map = /* @__PURE__ */ new Map();
2783
- for (const group of groups) map.set(group.key, summarizeGroupStatus(group.sessions, group.sessions.map((s) => s.id), facets, now));
2784
- return map;
2785
- }
2786
- /**
2787
- * Fold every group's DIRECT status up the workspace tree, so a collapsed folder
2788
- * counts every descendant session. A group's own row shows no badge while it is
2789
- * expanded, but its sessions still need attention and its collapsed ancestor must
2790
- * keep counting them — so the walk never depends on a group's expanded state.
2791
- *
2792
- * @param direct - each group's own-member status, keyed by group key.
2793
- * @param childrenByParent - the rendered tree: parent key (`undefined` = roots) → children.
2794
- * @returns aggregated status per group key: own members plus the whole subtree.
2795
- */
2796
- function aggregateGroupStatuses(direct, childrenByParent) {
2797
- const aggregated = /* @__PURE__ */ new Map();
2798
- const visiting = /* @__PURE__ */ new Set();
2799
- const fold = (key) => {
2800
- const cached = aggregated.get(key);
2801
- if (cached !== void 0) return cached;
2802
- if (visiting.has(key)) return direct.get(key) ?? EMPTY_LIVE_STATUS;
2803
- visiting.add(key);
2804
- let total = direct.get(key) ?? EMPTY_LIVE_STATUS;
2805
- for (const child of childrenByParent.get(key) ?? []) total = mergeLiveStatus(total, fold(child.key));
2806
- visiting.delete(key);
2807
- aggregated.set(key, total);
2808
- return total;
2692
+ function WorkspacePickFlow({ t, open, anchorRef, useWorkspaces, createWorkspace, useDirectoryFlow, renderDirectoryFlow, onPick, onClose, addOnly = false, onBusyChange, side = "bottom", selectedId }) {
2693
+ const workspaceSnapshot = useWorkspaces((state) => state);
2694
+ const workspaces = workspaceSnapshot.items;
2695
+ const getAnchorRect = (0, react.useCallback)(() => anchorRef?.current?.getBoundingClientRect() ?? null, [anchorRef]);
2696
+ const [errorOpen, setErrorOpen] = (0, react.useState)(false);
2697
+ const [modalError, setModalError] = (0, react.useState)(null);
2698
+ const [flowOpen, setFlowOpen] = (0, react.useState)(false);
2699
+ const [pickingFolder, setPickingFolder] = (0, react.useState)(false);
2700
+ const flowBusy = flowOpen || pickingFolder;
2701
+ (0, react.useEffect)(() => {
2702
+ onBusyChange?.(flowBusy);
2703
+ }, [flowBusy, onBusyChange]);
2704
+ const flowAvailable = useDirectoryFlow((occupied) => occupied);
2705
+ (0, react.useEffect)(() => {
2706
+ if (flowOpen && !flowAvailable) setFlowOpen(false);
2707
+ }, [flowOpen, flowAvailable]);
2708
+ const addEntries = flowAvailable ? [{
2709
+ id: ADD_WORKSPACE,
2710
+ label: t("menu.addWorkspace"),
2711
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPlusOutlineRegular, { size: 16 }),
2712
+ disabled: flowBusy
2713
+ }] : [];
2714
+ const pinAdd = !addOnly && workspaces.length > 0;
2715
+ const items = pinAdd ? workspaces.map((workspace) => ({
2716
+ id: workspace.workspaceId,
2717
+ label: workspaceDisplayTitle(workspace.title, t("workspace.defaultName")),
2718
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderCloseRegular, { size: 16 }),
2719
+ disabled: flowBusy
2720
+ })) : addEntries;
2721
+ const menuIsEmpty = items.length === 0;
2722
+ const closeModal = () => {
2723
+ setErrorOpen(false);
2724
+ setModalError(null);
2809
2725
  };
2810
- for (const children of childrenByParent.values()) for (const child of children) fold(child.key);
2811
- return aggregated;
2812
- }
2813
- /**
2814
- * Attach each group's subtree-wide status, in derivation order. This is the producer
2815
- * the sidebar renders from.
2816
- *
2817
- * Expanded groups carry their status too: their own row draws no badge (a decision
2818
- * inside `groupLiveBadge`), but a collapsed ancestor still has to count them, so
2819
- * expansion never suppresses the measurement.
2820
- *
2821
- * @param groups - derived rows, in the order they render.
2822
- * @param direct - each group's own-member status, from {@link directStatusesForGroups}.
2823
- * @param childrenByParent - the rendered tree: parent key (`undefined` = roots) → children.
2824
- */
2825
- function withAggregatedStatus(groups, direct, childrenByParent) {
2826
- const aggregated = aggregateGroupStatuses(direct, childrenByParent);
2827
- return groups.map((group) => ({
2828
- ...group,
2829
- liveStatus: aggregated.get(group.key) ?? EMPTY_LIVE_STATUS
2830
- }));
2831
- }
2832
- /** The blank row plus the first `watermark` non-blank rows, in list order. */
2833
- function watermarkRows(sessions, watermark) {
2834
- let count = 0;
2835
- const rows = sessions.filter((session) => {
2836
- if (session.blank) return true;
2837
- count += 1;
2838
- return count <= watermark;
2726
+ /** Adopt a picked directory; failures land in the folder-error dialog (Choose again reopens the flow). */
2727
+ const adoptDirectory = (path) => createWorkspace({ path }).then((workspace) => {
2728
+ setFlowOpen(false);
2729
+ onPick(workspace.workspaceId);
2730
+ }).catch((reason) => {
2731
+ setModalError(reason instanceof Error ? reason.message : String(reason));
2732
+ setFlowOpen(false);
2733
+ setErrorOpen(true);
2839
2734
  });
2840
- return {
2841
- rows,
2842
- hiddenCount: sessions.length - rows.length
2735
+ const openDirectoryFlow = (0, react.useCallback)(() => {
2736
+ onClose();
2737
+ setErrorOpen(false);
2738
+ setModalError(null);
2739
+ setFlowOpen(true);
2740
+ }, [onClose]);
2741
+ const listSettled = addOnly || workspaceSnapshot.phase === "ready";
2742
+ const addIsTheOnlyEntry = !pinAdd && listSettled && addEntries.length === 1;
2743
+ (0, react.useEffect)(() => {
2744
+ if (open && addIsTheOnlyEntry && !flowBusy) openDirectoryFlow();
2745
+ }, [
2746
+ open,
2747
+ addIsTheOnlyEntry,
2748
+ flowBusy,
2749
+ openDirectoryFlow
2750
+ ]);
2751
+ /** Owner side of the flow conversation: adopt keeps the flow open (busy) until the Host answers. */
2752
+ const flowOwner = {
2753
+ open: flowOpen,
2754
+ busy: pickingFolder,
2755
+ onPicked: (path) => {
2756
+ setPickingFolder(true);
2757
+ adoptDirectory(path).finally(() => {
2758
+ setPickingFolder(false);
2759
+ });
2760
+ },
2761
+ onCancel: () => {
2762
+ setFlowOpen(false);
2763
+ },
2764
+ onError: (message) => {
2765
+ setFlowOpen(false);
2766
+ setModalError(message);
2767
+ setErrorOpen(true);
2768
+ }
2843
2769
  };
2844
- }
2845
- /**
2846
- * Smallest watermark that covers every non-blank row within the automatic maximum that is running, has a running
2847
- * child, awaits the user, carries an unread completion, or is current, and the reveal target at any position.
2848
- * @param sessions - one group's ordered Session rows.
2849
- * @param current - Session retained by the main view, if the sidebar is the active surface.
2850
- * @param revealSessionId - Session selected from search and required to be visible.
2851
- * @returns the group's baseline non-blank row count.
2852
- */
2853
- function requiredWatermark(sessions, current, revealSessionId) {
2854
- let index = 0;
2855
- let required = 0;
2856
- for (const session of sessions) {
2857
- if (session.blank) continue;
2858
- index += 1;
2859
- if (index <= 50 && (session.running || session.runningSubagentCount > 0 || session.pendingInteraction !== void 0 || session.completed || session.id === current) || session.id === revealSessionId) required = index;
2860
- }
2861
- return required;
2862
- }
2863
- /**
2864
- * Raise each listed group's watermark to its requirement; returns `marks` itself when nothing rises.
2865
- * @param marks - watermarks stored by the current browser tree.
2866
- * @param required - required baselines, in group order.
2867
- * @returns either the unchanged input or a new object containing only raised groups.
2868
- */
2869
- function raiseWatermarks(marks, required) {
2870
- let raised = marks;
2871
- for (const [key, watermark] of required) if (watermark > (raised[key] ?? 5)) raised = {
2872
- ...raised,
2873
- [key]: watermark
2770
+ const handleSelect = (id) => {
2771
+ if (id === ADD_WORKSPACE) {
2772
+ openDirectoryFlow();
2773
+ return;
2774
+ }
2775
+ onPick(id);
2874
2776
  };
2875
- return raised;
2777
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2778
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
2779
+ open: open && !addIsTheOnlyEntry && !menuIsEmpty,
2780
+ anchor: null,
2781
+ items,
2782
+ ...pinAdd ? { footer: addEntries } : {},
2783
+ selectedId,
2784
+ onSelect: handleSelect,
2785
+ onClose,
2786
+ side,
2787
+ portal: true,
2788
+ getAnchorRect
2789
+ }),
2790
+ open && !addIsTheOnlyEntry && !menuIsEmpty && workspaceSnapshot.phase === "pending" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2791
+ className: WorkspacePicker_module_css_default.menuStatus,
2792
+ role: "status",
2793
+ children: t("picker.loading")
2794
+ }),
2795
+ renderDirectoryFlow(flowOwner),
2796
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
2797
+ open: errorOpen,
2798
+ onClose: closeModal,
2799
+ closeLabel: t("close"),
2800
+ title: t("folderError.title"),
2801
+ footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2802
+ variant: "outline",
2803
+ className: WorkspacePicker_module_css_default.modalAction,
2804
+ onClick: closeModal,
2805
+ children: t("cancel")
2806
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2807
+ variant: "primary",
2808
+ className: WorkspacePicker_module_css_default.modalAction,
2809
+ disabled: !flowAvailable,
2810
+ onClick: openDirectoryFlow,
2811
+ children: t("folderError.retry")
2812
+ })] }),
2813
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2814
+ className: WorkspacePicker_module_css_default.modalError,
2815
+ role: "alert",
2816
+ children: modalError
2817
+ })
2818
+ })
2819
+ ] });
2876
2820
  }
2877
- //#endregion
2878
- //#region src/client/facet-visibility.ts
2879
2821
  /**
2880
- * 按用户设置筛掉不参与折叠的状态来源。
2881
- *
2882
- * 单独成文是因为它属于**徽标自身**的可见性规则,而不是某个来源的适配:来源只在
2883
- * `facet.settingKey` 上声明"我受哪个开关管",开关的语义与过滤时机归本插件。
2884
- *
2885
- * 过滤发生在**折叠之前**而不是渲染阶段:徽标数字、图标、`aria-label`、悬停明细与
2886
- * "要不要画徽标"因此天然同源。放到渲染阶段会出现"label 里有、计数里没有",而那正是
2887
- * 徽标最容易被怀疑撒谎的地方。
2888
- *
2889
- * @module dsh-group-status/client/facet-visibility
2822
+ * The conversation empty-state registration: adapts the owner share to the
2823
+ * core flow (all state and semantics live in the flow / the owner).
2824
+ * @param props - empty-state slot props (owner share + injected creation callback).
2825
+ * @returns the flow element.
2890
2826
  */
2891
- /** 筛出设置允许显示的登记项;顺序保持登记面的顺序(已按 `order` 排好)。 */
2892
- function visibleFacets(entries, settings) {
2893
- return entries.filter((entry) => facetVisibleBySettings(settings, entry.facet.settingKey));
2827
+ function WorkspacePicker({ open, anchorRef, useWorkspaces, selectedId, onPick, onClose, createWorkspace, useDirectoryFlow, renderSlot, t }) {
2828
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspacePickFlow, {
2829
+ t,
2830
+ open,
2831
+ anchorRef,
2832
+ useWorkspaces,
2833
+ createWorkspace,
2834
+ useDirectoryFlow,
2835
+ renderDirectoryFlow: (owner) => renderSlot("conversation.hero.workspace.directoryFlow", owner),
2836
+ selectedId,
2837
+ onPick,
2838
+ onClose
2839
+ });
2894
2840
  }
2895
2841
  //#endregion
2896
- //#region src/client/badge-labels.ts
2897
- /**
2898
- * 把侧栏行的翻译位交给 facet。
2899
- *
2900
- * 这里有一次**故意的放宽**:协议的 `t` 收任意字符串键,而行位只收本插件 `workspace`
2901
- * 命名空间的键。放宽是安全的——宿主 locale 服务对未注册的键一律**原样回显键名**(不是
2902
- * 抛错),所以外来 facet 传了自己命名空间的键,最坏读出的是那串键名而不是崩渲染。
2903
- * 要正确本地化,provider 应当闭包自己的翻译函数、忽略这里传入的 `t`。
2904
- */
2905
- function asFacetTranslate(t) {
2906
- return (key, options) => t(key, options);
2907
- }
2908
- /** 该 facet 贡献进徽标数字的条数。 */
2909
- function facetCountTotal(s) {
2910
- let total = 0;
2911
- for (const entry of s.facets.values()) total += entry.fold.count;
2912
- return total;
2842
+ //#region \0dsh-css:src/client/rows/WorkspaceBrowser.module.css.mjs
2843
+ const css$2 = ".QAYkWG_root{--dsh-session-list-edge-inset:var(--dsh-sidebar-inline-padding);--dsh-session-list-scrollbar-width:5px;--dsh-session-list-scrollbar-offset:2px;box-sizing:border-box;min-height:0;padding-right:var(--dsh-session-list-edge-inset);flex-direction:column;flex:1;display:flex}.QAYkWG_root.QAYkWG_rail{padding-right:0}.QAYkWG_iconButton{border-radius:var(--dsw-radius-sm);cursor:pointer;width:28px;height:28px;color:var(--dsw-alias-label-secondary);background:0 0;border:none;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex}.QAYkWG_iconButton:focus-visible,.QAYkWG_searchButton:focus-visible,.QAYkWG_clearButton:focus-visible{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:-2px}.QAYkWG_iconButton:hover{background:var(--dsw-alias-interactive-bg-hover)}.QAYkWG_viewOptionsMenu{min-width:200px}.QAYkWG_sectionHeader{box-sizing:border-box;border-radius:var(--dsw-radius-md);height:36px;color:var(--dsw-alias-label-tertiary);flex:none;justify-content:flex-end;align-items:center;gap:4px;margin-bottom:4px;padding-left:4px;display:flex;overflow:hidden}.QAYkWG_root:not(.QAYkWG_rail) .QAYkWG_sectionHeader{margin-top:2px;margin-right:-4px}.QAYkWG_sectionLabel{white-space:nowrap;opacity:1;visibility:visible;min-width:0;max-width:45%;transition:max-width .18s var(--ds-ease-in-out), margin-right .18s var(--ds-ease-in-out), opacity .12s var(--ds-ease-in-out), transform .18s var(--ds-ease-in-out), visibility 0s linear;flex:none;line-height:20px;overflow:hidden}.QAYkWG_sectionLabelHidden{opacity:0;visibility:hidden;max-width:0;margin-right:-4px;transition-delay:0s,0s,0s,0s,.18s;transform:translate(-4px)}.QAYkWG_searchSlot{box-sizing:border-box;min-width:0;max-width:28px;transition:max-width .18s var(--ds-ease-in-out), padding-left .18s var(--ds-ease-in-out);flex:1;align-items:center;margin-left:auto;padding-left:0;display:flex}.QAYkWG_searchSlotExpanded{max-width:100%;padding-left:0}.QAYkWG_headerActions{opacity:1;visibility:visible;max-width:60px;transition:max-width .18s var(--ds-ease-in-out), opacity .12s var(--ds-ease-in-out), transform .18s var(--ds-ease-in-out), visibility 0s linear;flex:none;align-items:center;gap:4px;display:flex;overflow:hidden}.QAYkWG_headerActionsHidden{opacity:0;visibility:hidden;pointer-events:none;max-width:0;transition-delay:0s,0s,0s,.18s;transform:translate(4px)}.QAYkWG_search{box-sizing:border-box;border-radius:var(--dsw-radius-sm);cursor:text;width:100%;height:28px;color:var(--dsw-alias-label-secondary);transition:width .18s var(--ds-ease-in-out), padding .18s var(--ds-ease-in-out), border-color .18s var(--ds-ease-in-out), background-color .18s var(--ds-ease-in-out);background:0 0;border:none;flex:none;align-items:center;gap:0;margin:0;padding:0;display:flex;overflow:hidden}.QAYkWG_searchExpanded{border:.5px solid var(--dsw-alias-border-l4);border-radius:var(--dsw-radius-md);width:calc(100% + 4px);height:30px;color:var(--dsw-alias-label-caption);background:0 0;margin-inline:-2px;padding:0 4px 0 0}.QAYkWG_searchButton{border-radius:var(--dsw-radius-sm);cursor:pointer;width:28px;height:28px;color:inherit;background:0 0;border:none;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex}.QAYkWG_searchExpanded .QAYkWG_searchButton{width:28px;height:30px}.QAYkWG_searchButton:hover{background:var(--dsw-alias-interactive-bg-hover)}.QAYkWG_searchExpanded .QAYkWG_searchButton:hover{background:0 0}.QAYkWG_searchInput{opacity:0;pointer-events:none;width:0;min-width:0;color:var(--dsw-alias-label-primary);transition:opacity .12s var(--ds-ease-in-out);background:0 0;border:none;outline:none;flex:1;font-size:13px;line-height:18px}.QAYkWG_searchExpanded .QAYkWG_searchInput{opacity:1;pointer-events:auto;margin-left:-2px}.QAYkWG_searchInput::placeholder{color:var(--dsw-alias-label-tertiary)}.QAYkWG_clearButton{border-radius:var(--dsw-radius-sm);cursor:pointer;width:24px;height:24px;color:var(--dsw-alias-label-secondary);background:0 0;border:none;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex}.QAYkWG_clearButton:hover{background:var(--dsw-alias-interactive-bg-hover)}.QAYkWG_rail .QAYkWG_sectionHeader{justify-content:flex-start;gap:0;margin-bottom:12px;padding-left:0}.QAYkWG_rail .QAYkWG_headerActions{max-width:none}.QAYkWG_rail .QAYkWG_iconButton{border-radius:var(--dsw-radius-md);width:36px;height:36px;color:var(--dsw-alias-label-primary)}.QAYkWG_rail .QAYkWG_search{border-radius:var(--dsw-radius-md);background:0 0;border-color:#0000;gap:0;width:36px;height:36px;margin:0 0 12px;padding:0}.QAYkWG_rail .QAYkWG_searchButton{border-radius:var(--dsw-radius-md);width:36px;height:36px;color:var(--dsw-alias-label-primary)}.QAYkWG_rail .QAYkWG_searchButton:hover{background:var(--dsw-alias-interactive-bg-hover)}.QAYkWG_listArea{min-height:0;margin-left:-4px;margin-right:calc(-1 * var(--dsh-session-list-edge-inset));flex-direction:column;flex:1;padding-left:4px;display:flex;overflow:visible}.QAYkWG_rail .QAYkWG_listArea{margin-left:0;margin-right:0;padding-left:0}.QAYkWG_treeBody{flex-direction:column;flex:1;min-height:0;display:flex;position:relative}.QAYkWG_fade{left:0;right:var(--dsh-session-list-edge-inset);background:linear-gradient(to bottom, transparent, var(--dsw-specific-sidebar-fill));pointer-events:none;height:24px;position:absolute;bottom:0}[data-platform=darwin] .QAYkWG_fade{display:none}.QAYkWG_wide{animation:QAYkWG_wide-in .2s var(--ds-ease-in-out)}@keyframes QAYkWG_wide-in{0%{opacity:0}}.QAYkWG_list{min-height:0;margin-left:-4px;margin-right:var(--dsh-session-list-scrollbar-offset);padding-left:4px;padding-right:calc(var(--dsh-session-list-edge-inset) - var(--dsh-session-list-scrollbar-width) - var(--dsh-session-list-scrollbar-offset));scrollbar-gutter:stable;flex:1;padding-bottom:16px;overflow-y:auto}.QAYkWG_flatList>*+*,.QAYkWG_searchTree>[role=treeitem]+[role=treeitem],.QAYkWG_groupSection>*+*{margin-top:2px}.QAYkWG_searchStatus{color:var(--dsw-alias-label-tertiary);padding:10px 12px;font-size:12px;line-height:18px}.QAYkWG_skeletonRow{box-sizing:border-box;align-items:flex-start;gap:8px;min-height:48px;padding:6px 8px 7px;display:flex}.QAYkWG_skeletonDot{corner-shape:round;border-radius:50%;flex:none;width:16px;height:16px}.QAYkWG_skeletonBars{flex-direction:column;flex:1;gap:6px;min-width:0;display:flex}.QAYkWG_skeletonDot,.QAYkWG_skeletonBar{background:var(--dsw-alias-bg-skeleton);animation:2s cubic-bezier(.36,0,.64,1) infinite QAYkWG_search-skeleton}.QAYkWG_skeletonBar{border-radius:var(--dsw-radius-xs);width:65%;height:16px}.QAYkWG_skeletonBarWide{width:90%;height:13px}@keyframes QAYkWG_search-skeleton{0%{opacity:1}40%{opacity:.6}80%,to{opacity:1}}.QAYkWG_groupSection{position:relative}.QAYkWG_groupSection+.QAYkWG_groupSection{margin-top:4px}.QAYkWG_listTopDropIndicator,.QAYkWG_workspaceDropBefore:before,.QAYkWG_workspaceDropAfter:after{content:\"\";z-index:1;background:linear-gradient(55deg, transparent calc(50% - 1px), var(--dsw-alias-state-business-primary) calc(50% - 1px) calc(50% + 1px), transparent calc(50% + 1px)) 0 0 / 5px 7px no-repeat, linear-gradient(125deg, transparent calc(50% - 1px), var(--dsw-alias-state-business-primary) calc(50% - 1px) calc(50% + 1px), transparent calc(50% + 1px)) 0 5px / 5px 7px no-repeat, linear-gradient(var(--dsw-alias-state-business-primary) 0 0) 4px 5px / calc(100% - 4px) 2px no-repeat;pointer-events:none;height:12px;position:absolute;left:0;right:0}.QAYkWG_listTopDropIndicator{top:-8px;left:0;right:var(--dsh-session-list-edge-inset)}.QAYkWG_listTopDropActive>.QAYkWG_workspaceDropBefore:first-child:before{display:none}.QAYkWG_workspaceDropBefore:before{top:-8px}.QAYkWG_workspaceDropAfter:after{bottom:-8px}.QAYkWG_sessionOverflowButton{border-radius:var(--dsw-radius-sm);width:100%;height:28px;padding:0 12px 0 calc(28px + var(--dsh-workspace-indent,0px));cursor:pointer;text-align:left;color:var(--dsw-alias-label-tertiary);background:0 0;border:none;font-size:12px}.QAYkWG_groupSection>.QAYkWG_sessionOverflowButton{margin-top:0}.QAYkWG_sessionOverflowButton:hover{color:var(--dsw-alias-label-secondary);background:0 0}.QAYkWG_empty{color:var(--dsw-alias-label-tertiary);padding:16px 12px;font-size:13px}.QAYkWG_emptyState{color:var(--dsw-alias-label-tertiary);flex-direction:column;align-items:center;gap:8px;margin-top:80px;padding:0 12px;font-size:13px;line-height:20px;display:flex}.QAYkWG_emptyState>svg{color:var(--dsw-alias-label-caption);margin-bottom:4px}.QAYkWG_emptyAction{cursor:pointer;color:var(--dsw-alias-link);background:0 0;border:none;padding:0;font-size:13px;line-height:20px}.QAYkWG_renameInput{box-sizing:border-box;border:.5px solid var(--dsw-alias-border-l4);border-radius:var(--dsw-radius-lg);width:100%;height:44px;color:var(--dsw-alias-label-primary);background:0 0;outline:none;padding:7px 14px;font-size:14px;font-weight:400;line-height:22px}.QAYkWG_renameInput:disabled{color:var(--dsw-alias-label-dimmed)}.QAYkWG_renameError{color:var(--dsw-alias-state-error-primary);margin-top:8px;font-size:12px;line-height:18px}.QAYkWG_deleteAction:not(:disabled){color:var(--dsw-alias-state-error-primary)}.QAYkWG_deleteStatus{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}.QAYkWG_archiveActivity{color:var(--dsw-alias-label-primary);margin:0 0 8px;padding-left:18px;font-size:13px;line-height:20px}.QAYkWG_archiveActivity li{overflow-wrap:anywhere}@media (prefers-reduced-motion:reduce){.QAYkWG_wide,.QAYkWG_skeletonDot,.QAYkWG_skeletonBar{animation:none}.QAYkWG_search,.QAYkWG_sectionLabel,.QAYkWG_searchSlot,.QAYkWG_searchInput,.QAYkWG_headerActions{transition:none}}";
2844
+ const tagId$2 = "dsh-group-status/WorkspaceBrowser.module.css";
2845
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]") === null) {
2846
+ const tag = document.createElement("style");
2847
+ tag.dataset.plugin = "dsh-group-status";
2848
+ tag.dataset.pluginCss = tagId$2;
2849
+ tag.textContent = css$2;
2850
+ document.head.appendChild(tag);
2913
2851
  }
2914
- /**
2915
- * 每个外部状态来源的那句话,按登记顺序。
2916
- *
2917
- * facet 没实现 `label` 时**不能**退化成 `2 dsh-later.schedule` 这种读法,所以给一句
2918
- * 通用措辞兜底:只说有几个来源在响,具体语义等该来源自己补文案。
2919
- */
2920
- function facetLines(s, t, now) {
2921
- const translate = asFacetTranslate(t);
2922
- const lines = [];
2923
- for (const entry of s.facets.values()) {
2924
- const text = entry.facet.label?.(entry.fold, {
2925
- now,
2926
- t: translate
2927
- });
2928
- lines.push(text === void 0 || text === "" ? `${entry.fold.count} ${t("status.needsAttention")}` : text);
2929
- }
2930
- return lines;
2852
+ var WorkspaceBrowser_module_css_default = {
2853
+ "archiveActivity": "QAYkWG_archiveActivity",
2854
+ "clearButton": "QAYkWG_clearButton",
2855
+ "deleteAction": "QAYkWG_deleteAction",
2856
+ "deleteStatus": "QAYkWG_deleteStatus",
2857
+ "empty": "QAYkWG_empty",
2858
+ "emptyAction": "QAYkWG_emptyAction",
2859
+ "emptyState": "QAYkWG_emptyState",
2860
+ "fade": "QAYkWG_fade",
2861
+ "flatList": "QAYkWG_flatList",
2862
+ "groupSection": "QAYkWG_groupSection",
2863
+ "headerActions": "QAYkWG_headerActions",
2864
+ "headerActionsHidden": "QAYkWG_headerActionsHidden",
2865
+ "iconButton": "QAYkWG_iconButton",
2866
+ "list": "QAYkWG_list",
2867
+ "listArea": "QAYkWG_listArea",
2868
+ "listTopDropActive": "QAYkWG_listTopDropActive",
2869
+ "listTopDropIndicator": "QAYkWG_listTopDropIndicator",
2870
+ "rail": "QAYkWG_rail",
2871
+ "renameError": "QAYkWG_renameError",
2872
+ "renameInput": "QAYkWG_renameInput",
2873
+ "root": "QAYkWG_root",
2874
+ "search": "QAYkWG_search",
2875
+ "search-skeleton": "QAYkWG_search-skeleton",
2876
+ "searchButton": "QAYkWG_searchButton",
2877
+ "searchExpanded": "QAYkWG_searchExpanded",
2878
+ "searchInput": "QAYkWG_searchInput",
2879
+ "searchSlot": "QAYkWG_searchSlot",
2880
+ "searchSlotExpanded": "QAYkWG_searchSlotExpanded",
2881
+ "searchStatus": "QAYkWG_searchStatus",
2882
+ "searchTree": "QAYkWG_searchTree",
2883
+ "sectionHeader": "QAYkWG_sectionHeader",
2884
+ "sectionLabel": "QAYkWG_sectionLabel",
2885
+ "sectionLabelHidden": "QAYkWG_sectionLabelHidden",
2886
+ "sessionOverflowButton": "QAYkWG_sessionOverflowButton",
2887
+ "skeletonBar": "QAYkWG_skeletonBar",
2888
+ "skeletonBarWide": "QAYkWG_skeletonBarWide",
2889
+ "skeletonBars": "QAYkWG_skeletonBars",
2890
+ "skeletonDot": "QAYkWG_skeletonDot",
2891
+ "skeletonRow": "QAYkWG_skeletonRow",
2892
+ "treeBody": "QAYkWG_treeBody",
2893
+ "viewOptionsMenu": "QAYkWG_viewOptionsMenu",
2894
+ "wide": "QAYkWG_wide",
2895
+ "wide-in": "QAYkWG_wide-in",
2896
+ "workspaceDropAfter": "QAYkWG_workspaceDropAfter",
2897
+ "workspaceDropBefore": "QAYkWG_workspaceDropBefore"
2898
+ };
2899
+ /** Keep controlled input and RPC payload inside the session.search wire contract. */
2900
+ function sanitizeSearchQuery(value) {
2901
+ const withoutNul = value.replaceAll("\0", "");
2902
+ if (withoutNul.length <= 500) return withoutNul;
2903
+ let end = 500;
2904
+ const last = withoutNul.charCodeAt(end - 1);
2905
+ const next = withoutNul.charCodeAt(end);
2906
+ if (last >= 55296 && last <= 56319 && next >= 56320 && next <= 57343) end--;
2907
+ return withoutNul.slice(0, end);
2931
2908
  }
2932
2909
  /**
2933
- * Accessible label for a folded group's aggregated status, reusing the
2934
- * session-row locale keys so both surfaces read identically.
2935
- *
2936
- * 按设置过滤:**关掉的外部来源根本不在折果里**(`WorkspaceBrowser` 折叠前就按
2937
- * `facet.settingKey` 过滤了登记面),所以这里不必再判设置,`aria-label` 与徽标视觉
2938
- * 天然一致;宿主的行级状态仍按设置逐项过滤。
2910
+ * Accept the native drag at document level while a row drag is active: row
2911
+ * hover still owns the insertion marker, and releasing outside the list must
2912
+ * not be rendered as a rejected drop before dragend commits that last marker.
2939
2913
  */
2940
- function groupLiveLabel(s, t, settings, now = Date.now()) {
2941
- const parts = [];
2942
- if (settings.showPendingInBadge) {
2943
- if (s.approval > 0) parts.push(`${s.approval} ${t("status.waitingApproval")}`);
2944
- if (s.planReview > 0) parts.push(`${s.planReview} ${t("status.planReview")}`);
2945
- if (s.question > 0) parts.push(`${s.question} ${t("status.waitingAnswer")}`);
2946
- }
2947
- if (settings.showRunningInBadge) {
2948
- if (s.running > 0) parts.push(`${s.running} ${t("status.running")}`);
2949
- if (s.subagents > 0) parts.push(t(s.subagents === 1 ? "status.subagentsRunning.one" : "status.subagentsRunning.other", { n: s.subagents }));
2950
- }
2951
- parts.push(...facetLines(s, t, now));
2952
- if (settings.showCompletedInBadge && s.done > 0) parts.push(`${s.done} ${t("status.completed")}`);
2953
- return parts.join(" · ");
2914
+ function useNativeDragAcceptance(active) {
2915
+ (0, react.useEffect)(() => {
2916
+ if (!active) return;
2917
+ const acceptDrag = (event) => {
2918
+ event.preventDefault();
2919
+ if (event.dataTransfer !== null) event.dataTransfer.dropEffect = "move";
2920
+ };
2921
+ const acceptDrop = (event) => {
2922
+ event.preventDefault();
2923
+ };
2924
+ document.addEventListener("dragover", acceptDrag);
2925
+ document.addEventListener("drop", acceptDrop);
2926
+ return () => {
2927
+ document.removeEventListener("dragover", acceptDrag);
2928
+ document.removeEventListener("drop", acceptDrop);
2929
+ };
2930
+ }, [active]);
2954
2931
  }
2955
- /**
2956
- * 每个外部状态来源的明细行,**带它自己的图标**(#24)。
2957
- *
2958
- * 徽标侧画的是 provider 交出的、已含色档的元素(`rows/GroupBadge.tsx` 的 `facetGlyph`),
2959
- * 明细侧此前只画一个统一的 `StateDot`,于是同一条状态、同一次悬停里,徽标是红色沙漏、
2960
- * 明细是黄点——同一行的两个视图各说一套。宿主 `StateDot` 只有 warning / ongoing / done
2961
- * 三档、没有 error 档,所以色档**只能**由来源自己的图标表达,这也是本函数存在的理由。
2962
- *
2963
- * provider 没交图标时退回通用点(与徽标侧同一套兜底),陌生来源仍然可见。
2964
- *
2965
- * @returns 明细行;`glyph` 为 `undefined` 表示该行用 `StateDot` 渲染。
2966
- */
2967
- function facetDetailLines(s, t, now) {
2968
- const translate = asFacetTranslate(t);
2969
- const lines = [];
2970
- for (const entry of s.facets.values()) {
2971
- const raw = entry.facet.label?.(entry.fold, {
2972
- now,
2973
- t: translate
2974
- });
2975
- const text = raw === void 0 || raw === "" ? `${entry.fold.count} ${t("status.needsAttention")}` : raw;
2976
- lines.push({
2977
- state: "warning",
2978
- text,
2979
- glyph: facetGlyph(entry, t, now)
2980
- });
2981
- }
2982
- return lines;
2932
+ /** Apply a visible drop to the complete account without removing hidden members. */
2933
+ function sessionDragOrder(order, rows, drag, over) {
2934
+ const source = rows.find((row) => row.id === drag.sessionId);
2935
+ const target = rows.find((row) => row.id === over.id);
2936
+ if (source === void 0 || target === void 0 || source.blank || source.pinned !== drag.pinned || target.pinned !== drag.pinned || source.id === target.id || !order.includes(source.id)) return;
2937
+ const section = rows.filter((row) => row.pinned === drag.pinned);
2938
+ const sourceIndex = section.findIndex((row) => row.id === source.id);
2939
+ if (section.filter((row) => row.id !== source.id).findIndex((row) => row.id === target.id) + (over.half === "after" ? 1 : 0) === sourceIndex) return;
2940
+ const next = order.filter((id) => id !== source.id);
2941
+ const targetIndex = next.indexOf(target.id);
2942
+ if (targetIndex === -1) return;
2943
+ next.splice(targetIndex + (over.half === "after" ? 1 : 0), 0, source.id);
2944
+ return pinCurrentBlank(next, rows.find((row) => row.blank)?.id);
2983
2945
  }
2984
- /**
2985
- * Build the status detail lines for the workspace hover card.
2986
- *
2987
- * Extracted so `WorkspaceHoverContent` in `Rows.tsx` only needs to map the returned
2988
- * array — the fork-specific status→line logic lives here.
2989
- *
2990
- * 按设置过滤:关掉的来源不出现在明细行里;`showHoverDetails=false` 时整段返回空。
2991
- * 明细行的顺序与徽标**完全一致**(外部来源排在会话状态之后、已完成之前):两处顺序
2992
- * 不一样会让人怀疑它们是两套事实。
2993
- */
2994
- function hoverStatusLines(status, t, settings, now = Date.now()) {
2995
- const lines = [];
2996
- if (status === void 0 || !settings.showHoverDetails) return lines;
2997
- if (settings.showPendingInBadge) {
2998
- if (status.approval > 0) lines.push({
2999
- state: "warning",
3000
- text: `${status.approval} ${t("status.waitingApproval")}`
3001
- });
3002
- if (status.planReview > 0) lines.push({
3003
- state: "warning",
3004
- text: `${status.planReview} ${t("status.planReview")}`
3005
- });
3006
- if (status.question > 0) lines.push({
3007
- state: "warning",
3008
- text: `${status.question} ${t("status.waitingAnswer")}`
3009
- });
3010
- }
3011
- if (settings.showRunningInBadge) {
3012
- if (status.running > 0) lines.push({
3013
- state: "ongoing",
3014
- text: `${status.running} ${t("status.running")}`
3015
- });
3016
- if (status.subagents > 0) lines.push({
3017
- state: "ongoing",
3018
- text: t(status.subagents === 1 ? "status.subagentsRunning.one" : "status.subagentsRunning.other", { n: status.subagents })
3019
- });
3020
- }
3021
- lines.push(...facetDetailLines(status, t, now));
3022
- if (settings.showCompletedInBadge && status.done > 0) lines.push({
3023
- state: "done",
3024
- text: `${status.done} ${t("status.completed")}`
3025
- });
3026
- return lines;
2946
+ /** Resolve an insertion side across the Workspace header, descendants, and Sessions. */
2947
+ function workspaceGroupHalf(e) {
2948
+ const rect = e.currentTarget.getBoundingClientRect();
2949
+ return e.clientY < rect.top + rect.height / 2 ? "before" : "after";
3027
2950
  }
3028
2951
  /**
3029
- * 某一条外部来源的元素该带哪只 class(#22)。
3030
- *
3031
- * 脉冲判据仍由各 facet 自己声明(`FacetFold.pulse`),但**作用对象必须下沉到声明它的那个
3032
- * 元素**。此前 `groupBadgeIsUrgent()` 只返回一个整徽标的布尔值、class 加在容器上,于是同组
3033
- * 里任何一家声明 `pulse: true` 都会让别家本该静止的图标跟着呼吸:`pulse: false` 是否生效,
3034
- * 取决于同组里恰好有没有别人开了闪。判断交出去了,作用对象没交出去——这是修过一次的回归
3035
- * (见下)。
2952
+ * Session the main view currently retains, if any.
3036
2953
  *
3037
- * 保留这条历史的原因写在所有者那边:dsh-gitea-dispatch 把 `overdue` 画成**静止**(其
3038
- * `lib/waiting-glyph.js`:「超时后停动画:静止即不再期待」),且它只代表 CI 窗口到期、
3039
- * 不代表评审失败。同一条等待在行内静止、在父级闪动,就是同一事实的两种说法。
2954
+ * All three list bodies and the shell need this same scan, and `Object.values`
2955
+ * allocates a fresh array each time; the call sites memoise it against the list
2956
+ * identity rather than re-scanning on every render.
2957
+ * @param list - always-mounted Session list snapshot.
2958
+ * @returns the retained Session id, or undefined when the main view holds none.
3040
2959
  */
3041
- function facetPulseClass(entry, urgentClass) {
3042
- return entry.fold.pulse ? urgentClass : void 0;
2960
+ function mainViewSessionId(list) {
2961
+ return Object.values(list.byId).find((session) => (session.retainedBy.mainView ?? 0) > 0)?.id;
3043
2962
  }
2963
+ //#endregion
2964
+ //#region src/client/rows/ViewOptionsMenu.tsx
3044
2965
  /**
3045
- * 一个来源的图标。
2966
+ * Grouping, ordering, and archived-filter menu for the section header.
3046
2967
  *
3047
- * 归 provider 画(`facet.renderIcon`):「这个状态长什么样」是它的知识。没实现时本插件
3048
- * 按色档退回一个通用点(见 `rows/GroupBadge.tsx`),保证陌生来源仍然可见、只是朴素。
2968
+ * Own open state so it resets with the wide chrome; the trigger button is the
2969
+ * menu's own anchor, so the header does not thread a callback down for it.
3049
2970
  */
3050
- function facetGlyph(entry, t, now) {
3051
- return entry.facet.renderIcon?.(entry.fold, {
3052
- now,
3053
- t: asFacetTranslate(t)
3054
- });
3055
- }
3056
- //#endregion
3057
- //#region \0dsh-css:src/client/rows/Rows.module.css.mjs
3058
- const css$3 = "._1Jb0BW_projectRow,._1Jb0BW_sessionRow{border-radius:var(--dsw-radius-md);padding:0 8px;cursor:pointer;user-select:none;color:var(--dsw-alias-label-primary);align-items:center;gap:6px;padding-inline-start:calc(8px + var(--dsh-workspace-indent,0px));display:flex}._1Jb0BW_projectRow:hover,._1Jb0BW_sessionRow:hover,._1Jb0BW_sessionRow._1Jb0BW_selected{background:var(--dsw-alias-interactive-bg-hover)}._1Jb0BW_searchResultRow{box-sizing:border-box;border-radius:var(--dsw-radius-lg);cursor:pointer;text-align:left;width:100%;min-height:48px;color:var(--dsw-alias-label-primary);background:0 0;border:none;flex-direction:column;align-items:stretch;padding:4px 8px;display:flex}._1Jb0BW_searchResultRow:hover,._1Jb0BW_searchResultRow._1Jb0BW_selected{background:var(--dsw-alias-interactive-bg-hover)}._1Jb0BW_searchResultHeading{align-items:center;min-width:0;display:flex}._1Jb0BW_searchResultTitle{text-overflow:ellipsis;white-space:nowrap;flex:0 auto;min-width:0;margin-left:4px;font-size:14px;line-height:20px;overflow:hidden}._1Jb0BW_searchResultMeta{align-items:center;gap:6px;min-width:0;margin-left:20px;display:flex}._1Jb0BW_searchResultWorkspace,._1Jb0BW_searchResultSnippet{text-overflow:ellipsis;white-space:nowrap;font-size:12px;line-height:17px;overflow:hidden}._1Jb0BW_searchResultWorkspace{max-width:40%;color:var(--dsw-alias-label-tertiary);flex:none}._1Jb0BW_searchResultWorkspace:only-child{max-width:100%}._1Jb0BW_searchResultSnippet{min-width:0;color:var(--dsw-alias-label-secondary);flex:1}._1Jb0BW_projectRow{box-sizing:border-box;align-items:center;height:34px}._1Jb0BW_projectRow ._1Jb0BW_rowActions{height:20px}._1Jb0BW_sessionRow{gap:0;height:32px}._1Jb0BW_sessionRow ._1Jb0BW_title{margin:0 6px 0 4px}._1Jb0BW_slot{width:16px;height:20px;color:var(--dsw-alias-label-tertiary);flex:none;justify-content:center;align-items:center;display:inline-flex}._1Jb0BW_visuallyHidden{clip:rect(0 0 0 0);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}._1Jb0BW_folderActive{color:var(--dsw-alias-state-business-primary)}._1Jb0BW_projectRow ._1Jb0BW_chevron{display:none}._1Jb0BW_projectRow:hover ._1Jb0BW_chevron{display:inline-flex}._1Jb0BW_projectRow:hover ._1Jb0BW_folder{display:none}._1Jb0BW_arrow{transition:transform .15s var(--ds-ease-in-out)}._1Jb0BW_arrowOpen{transform:rotate(90deg)}._1Jb0BW_projectText{flex-direction:column;flex:1;gap:2px;min-width:0;display:flex}._1Jb0BW_title{text-overflow:ellipsis;white-space:nowrap;min-width:0;font-size:14px;line-height:20px;overflow:hidden}._1Jb0BW_renameInput{border:.5px solid var(--dsw-alias-border-l4);border-radius:var(--dsw-radius-sm);background:var(--dsw-alias-button-elevated-fill);min-width:0;color:inherit;outline:none;padding:0 2px;font-size:14px;line-height:20px}._1Jb0BW_sessionRow ._1Jb0BW_title{flex:1}._1Jb0BW_sessionRow ._1Jb0BW_title[data-scrolled]{mask-image:linear-gradient(90deg,#0000,#000 12px)}._1Jb0BW_sessionRow ._1Jb0BW_title[data-clipped]{mask-image:linear-gradient(270deg,#0000,#000 12px)}._1Jb0BW_sessionRow ._1Jb0BW_title[data-scrolled][data-clipped]{mask-image:linear-gradient(90deg,#0000,#000 12px calc(100% - 12px),#0000)}._1Jb0BW_meta{text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:20px;overflow:hidden}._1Jb0BW_time{color:var(--dsw-alias-label-tertiary);flex:none;font-size:10px;line-height:16px}._1Jb0BW_pinIndicator{width:16px;height:20px;color:var(--dsw-alias-label-caption);flex:none;justify-content:center;align-items:center;margin-left:6px;display:inline-flex}._1Jb0BW_sessionRow._1Jb0BW_archived ._1Jb0BW_title,._1Jb0BW_searchResultRow._1Jb0BW_archived ._1Jb0BW_searchResultTitle,._1Jb0BW_searchResultRow._1Jb0BW_archived ._1Jb0BW_searchResultWorkspace,._1Jb0BW_searchResultRow._1Jb0BW_archived ._1Jb0BW_searchResultSnippet{color:var(--dsw-alias-label-caption)}._1Jb0BW_dot{flex:none}._1Jb0BW_rowActions{flex:none;align-items:center;gap:10px;display:none}._1Jb0BW_projectRow:hover ._1Jb0BW_rowActions,._1Jb0BW_sessionRow:hover ._1Jb0BW_rowActions,._1Jb0BW_searchResultRow:hover ._1Jb0BW_rowActions,._1Jb0BW_projectRow._1Jb0BW_menuOpen ._1Jb0BW_rowActions,._1Jb0BW_sessionRow._1Jb0BW_menuOpen ._1Jb0BW_rowActions{display:inline-flex}._1Jb0BW_searchResultHeading ._1Jb0BW_rowActions{margin-left:auto}._1Jb0BW_sessionRow:hover ._1Jb0BW_time,._1Jb0BW_sessionRow._1Jb0BW_menuOpen ._1Jb0BW_time,._1Jb0BW_sessionRow:hover ._1Jb0BW_pinIndicator,._1Jb0BW_sessionRow._1Jb0BW_menuOpen ._1Jb0BW_pinIndicator{display:none}@media (hover:hover){._1Jb0BW_sessionRow:hover ._1Jb0BW_title,._1Jb0BW_sessionRow._1Jb0BW_menuOpen ._1Jb0BW_title{text-overflow:clip}}._1Jb0BW_projectRow._1Jb0BW_menuOpen,._1Jb0BW_sessionRow._1Jb0BW_menuOpen{background:var(--dsw-alias-interactive-bg-hover)}._1Jb0BW_sessionRow._1Jb0BW_dropBefore,._1Jb0BW_sessionRow._1Jb0BW_dropAfter{position:relative}._1Jb0BW_sessionRow._1Jb0BW_dropBefore:before,._1Jb0BW_sessionRow._1Jb0BW_dropAfter:after{content:\"\";z-index:1;background:linear-gradient(55deg, transparent calc(50% - 1px), var(--dsw-alias-state-business-primary) calc(50% - 1px) calc(50% + 1px), transparent calc(50% + 1px)) 0 0 / 5px 7px no-repeat, linear-gradient(125deg, transparent calc(50% - 1px), var(--dsw-alias-state-business-primary) calc(50% - 1px) calc(50% + 1px), transparent calc(50% + 1px)) 0 5px / 5px 7px no-repeat, linear-gradient(var(--dsw-alias-state-business-primary) 0 0) 4px 5px / calc(100% - 4px) 2px no-repeat;pointer-events:none;height:12px;position:absolute;left:0;right:4px}._1Jb0BW_sessionRow._1Jb0BW_dropBefore:before{top:-7px}._1Jb0BW_sessionRow._1Jb0BW_dropAfter:after{bottom:-7px}._1Jb0BW_hoverContent{flex-direction:column;gap:8px;display:flex}._1Jb0BW_hoverTitle{color:var(--dsw-alias-label-primary);overflow-wrap:break-word;font-size:14px;line-height:20px}._1Jb0BW_hoverPath{color:var(--dsw-alias-label-secondary);word-break:break-all;font-size:12px;line-height:16px}._1Jb0BW_hoverTime{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:16px}._1Jb0BW_hoverStatus{color:var(--dsw-alias-label-tertiary);align-items:center;gap:8px;font-size:12px;line-height:20px;display:flex}._1Jb0BW_groupBadge{flex:none;align-items:center;gap:3px;margin-inline-start:4px;display:inline-flex}._1Jb0BW_groupBadgeCount{color:var(--dsw-alias-label-tertiary);border:1px solid;border-radius:6px;padding:0 3px;font-size:9px;line-height:12px}@keyframes _1Jb0BW_groupBadgePulse{0%,to{opacity:1}50%{opacity:.6}}._1Jb0BW_groupBadgeUrgent{animation:2s ease-in-out infinite _1Jb0BW_groupBadgePulse}._1Jb0BW_hoverArchived svg{flex-shrink:0;margin:0 -4px}._1Jb0BW_iconButton{border-radius:var(--dsw-radius-xs);cursor:pointer;width:16px;height:16px;color:var(--dsw-alias-label-tertiary);background:0 0;border:none;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex}._1Jb0BW_iconButton:hover{color:var(--dsw-alias-label-primary)}._1Jb0BW_chevron{color:var(--dsw-alias-label-caption)}@media (prefers-reduced-motion:reduce){._1Jb0BW_arrow{transition:none;animation:none}}";
3059
- const tagId$3 = "dsh-group-status/Rows.module.css";
3060
- if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$3) + "]") === null) {
3061
- const tag = document.createElement("style");
3062
- tag.dataset.plugin = "dsh-group-status";
3063
- tag.dataset.pluginCss = tagId$3;
3064
- tag.textContent = css$3;
3065
- document.head.appendChild(tag);
3066
- }
3067
- var Rows_module_css_default = {
3068
- "archived": "_1Jb0BW_archived",
3069
- "arrow": "_1Jb0BW_arrow",
3070
- "arrowOpen": "_1Jb0BW_arrowOpen",
3071
- "chevron": "_1Jb0BW_chevron",
3072
- "dot": "_1Jb0BW_dot",
3073
- "dropAfter": "_1Jb0BW_dropAfter",
3074
- "dropBefore": "_1Jb0BW_dropBefore",
3075
- "folder": "_1Jb0BW_folder",
3076
- "folderActive": "_1Jb0BW_folderActive",
3077
- "groupBadge": "_1Jb0BW_groupBadge",
3078
- "groupBadgeCount": "_1Jb0BW_groupBadgeCount",
3079
- "groupBadgePulse": "_1Jb0BW_groupBadgePulse",
3080
- "groupBadgeUrgent": "_1Jb0BW_groupBadgeUrgent",
3081
- "hoverArchived": "_1Jb0BW_hoverArchived",
3082
- "hoverContent": "_1Jb0BW_hoverContent",
3083
- "hoverPath": "_1Jb0BW_hoverPath",
3084
- "hoverStatus": "_1Jb0BW_hoverStatus",
3085
- "hoverTime": "_1Jb0BW_hoverTime",
3086
- "hoverTitle": "_1Jb0BW_hoverTitle",
3087
- "iconButton": "_1Jb0BW_iconButton",
3088
- "menuOpen": "_1Jb0BW_menuOpen",
3089
- "meta": "_1Jb0BW_meta",
3090
- "pinIndicator": "_1Jb0BW_pinIndicator",
3091
- "projectRow": "_1Jb0BW_projectRow",
3092
- "projectText": "_1Jb0BW_projectText",
3093
- "renameInput": "_1Jb0BW_renameInput",
3094
- "rowActions": "_1Jb0BW_rowActions",
3095
- "searchResultHeading": "_1Jb0BW_searchResultHeading",
3096
- "searchResultMeta": "_1Jb0BW_searchResultMeta",
3097
- "searchResultRow": "_1Jb0BW_searchResultRow",
3098
- "searchResultSnippet": "_1Jb0BW_searchResultSnippet",
3099
- "searchResultTitle": "_1Jb0BW_searchResultTitle",
3100
- "searchResultWorkspace": "_1Jb0BW_searchResultWorkspace",
3101
- "selected": "_1Jb0BW_selected",
3102
- "sessionRow": "_1Jb0BW_sessionRow",
3103
- "slot": "_1Jb0BW_slot",
3104
- "time": "_1Jb0BW_time",
3105
- "title": "_1Jb0BW_title",
3106
- "visuallyHidden": "_1Jb0BW_visuallyHidden"
3107
- };
3108
- //#endregion
3109
- //#region src/client/rows/GroupBadge.tsx
3110
- /**
3111
- * Aggregated live-status badge for a folded folder row. Dots reuse StateDot so
3112
- * colors match session rows (warning = pending, ongoing = running, done = completed).
3113
- * Renders nothing while expanded or when nothing is pending/active/completed or any
3114
- * status source reports a contribution.
3115
- *
3116
- * Count matches the dots: host-side sessions plus every registered facet's own count.
3117
- *
3118
- * `settings` 按来源过滤:关掉的来源不画点、不计数、不进 aria-label、不参与「是否显示
3119
- * 徽标」的判断。会话行自身的状态点不受影响:那是行级状态,不是分组提示。**外部**来源的
3120
- * 过滤发生在折叠之前(`WorkspaceBrowser` 按 `facet.settingKey` 筛登记面),所以到这里
3121
- * 的折果已经是"该显示的",label 与视觉不可能各说一套。
3122
- *
3123
- * `now` 缺省取渲染时刻(与 `WorkspaceBrowser` 为相对时间读的时钟同一姿态):文案里的
3124
- * 剩余时长以此计算,只在提供方 store 通知时随渲染更新。
3125
- */
3126
- function groupLiveBadge(group, t, settings, now = Date.now()) {
3127
- const s = group.liveStatus;
3128
- if (group.expanded || s === void 0) return null;
3129
- const pending = settings.showPendingInBadge ? s.approval + s.planReview + s.question : 0;
3130
- const active = settings.showRunningInBadge ? s.running + s.subagents : 0;
3131
- const completed = settings.showCompletedInBadge ? s.done : 0;
3132
- const facetTotal = facetCountTotal(s);
3133
- if (pending === 0 && active === 0 && completed === 0 && facetTotal === 0) return null;
3134
- const label = groupLiveLabel(s, t, settings, now);
3135
- const dots = [];
3136
- if (pending > 0) dots.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: "warning" }, "warn"));
3137
- if (active > 0) dots.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: "ongoing" }, "run"));
3138
- const urgentClass = Rows_module_css_default.groupBadgeUrgent ?? "";
3139
- for (const entry of s.facets.values()) {
3140
- const glyph = facetGlyph(entry, t, now);
3141
- const pulseClass = facetPulseClass(entry, urgentClass) || void 0;
3142
- dots.push(glyph === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: entry.fold.tone === "info" ? "ongoing" : "warning" }, entry.facet.id) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3143
- className: pulseClass,
3144
- children: glyph
3145
- }, entry.facet.id));
3146
- }
3147
- if (completed > 0) dots.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: "done" }, "done"));
3148
- const count = pending + active + completed + facetTotal;
3149
- const countLabel = count > 99 ? "99+" : count >= settings.countThreshold ? String(count) : "";
3150
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3151
- className: Rows_module_css_default.groupBadge,
3152
- role: "img",
3153
- "aria-label": label,
3154
- title: label,
3155
- children: [dots, countLabel && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3156
- className: Rows_module_css_default.groupBadgeCount,
3157
- children: countLabel
3158
- })]
2971
+ /** Grouping, ordering, and archived-filter menu; own open state so it resets with the wide chrome. */
2972
+ function ViewOptionsMenu({ groupBy, orderBy, archivedFilter, onGroupPick, onOrderPick, onArchivedFilterPick, t }) {
2973
+ const [open, setOpen] = (0, react.useState)(false);
2974
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
2975
+ open,
2976
+ onClose: () => {
2977
+ setOpen(false);
2978
+ },
2979
+ items: [
2980
+ {
2981
+ type: "label",
2982
+ id: "group-by",
2983
+ text: t("groupBy.label")
2984
+ },
2985
+ {
2986
+ id: "workspace",
2987
+ label: t("groupBy.workspace"),
2988
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderCloseRegular, {})
2989
+ },
2990
+ {
2991
+ id: "workspace-tree",
2992
+ label: t("groupBy.workspaceTree"),
2993
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconWorkspaceTreeOutlineRegular, {})
2994
+ },
2995
+ {
2996
+ id: "flat",
2997
+ label: t("groupBy.flat"),
2998
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFlatListOutlineRegular, {})
2999
+ },
3000
+ {
3001
+ type: "separator",
3002
+ id: "order-by-separator"
3003
+ },
3004
+ {
3005
+ type: "label",
3006
+ id: "order-by",
3007
+ text: t("orderBy.label")
3008
+ },
3009
+ {
3010
+ id: "manual",
3011
+ label: t("orderBy.manual"),
3012
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronsUpDownOutlineRegular, {})
3013
+ },
3014
+ {
3015
+ id: "updated",
3016
+ label: t("orderBy.updated"),
3017
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconClockOutlineRegular, {})
3018
+ },
3019
+ {
3020
+ type: "separator",
3021
+ id: "archived-filter-separator"
3022
+ },
3023
+ {
3024
+ type: "label",
3025
+ id: "filter-by",
3026
+ text: t("filterBy.label")
3027
+ },
3028
+ {
3029
+ id: "hide-archived",
3030
+ label: t("viewOptions.hideArchived"),
3031
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconArchiveOffOutlineRegular, {})
3032
+ },
3033
+ {
3034
+ id: "show-archived",
3035
+ label: t("viewOptions.showArchived"),
3036
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconQueueOutlineRegular, {})
3037
+ },
3038
+ {
3039
+ id: "only-archived",
3040
+ label: t("viewOptions.onlyArchived"),
3041
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconArchiveCheckOutlineRegular, {})
3042
+ }
3043
+ ],
3044
+ selectedIds: [
3045
+ groupBy,
3046
+ orderBy,
3047
+ {
3048
+ default: "hide-archived",
3049
+ show: "show-archived",
3050
+ only: "only-archived"
3051
+ }[archivedFilter]
3052
+ ],
3053
+ onSelect: (id) => {
3054
+ if (id === "workspace" || id === "workspace-tree" || id === "flat") onGroupPick(id);
3055
+ else if (id === "manual" || id === "updated") onOrderPick(id);
3056
+ else if (id === "hide-archived") onArchivedFilterPick("default");
3057
+ else if (id === "show-archived") onArchivedFilterPick("show");
3058
+ else if (id === "only-archived") onArchivedFilterPick("only");
3059
+ setOpen(false);
3060
+ },
3061
+ align: "end",
3062
+ dense: true,
3063
+ listClassName: WorkspaceBrowser_module_css_default.viewOptionsMenu,
3064
+ portal: true,
3065
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
3066
+ label: t("viewOptions.label"),
3067
+ side: "bottom",
3068
+ delayMs: 500,
3069
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3070
+ type: "button",
3071
+ className: clsx(WorkspaceBrowser_module_css_default.iconButton, WorkspaceBrowser_module_css_default.wide),
3072
+ "aria-label": t("viewOptions.label"),
3073
+ onClick: () => {
3074
+ setOpen((v) => !v);
3075
+ },
3076
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSlidersTwoOutlineRegular, {})
3077
+ })
3078
+ })
3159
3079
  });
3160
3080
  }
3161
- //#endregion
3162
- //#region src/client/rows/Rows.tsx
3163
3081
  /**
3164
- * Workspace browser tree row components (figma Cell set 14:3080): pure presentational —
3165
- * all data and callbacks arrive via props. Hover swaps (folder->chevron,
3166
- * time->ellipsis, action buttons) are CSS-only, and a session row's clipped
3167
- * title marquees programmatically while the row is hovered. Workspace row
3168
- * menus are visual-only except Rename/Delete. A Session row's "..." menu and
3169
- * its hover buttons are the `sidebar.workspaces.session.menu.item` and
3170
- * `sidebar.workspaces.session.row.action` lists, rendered through the
3171
- * browser's `renderSlot` with the menu's open state as the occurrence's hook
3172
- * context; this package's own actions are entries like any plugin's. The
3173
- * session and workspace hover cards are suppressed while a menu is open.
3082
+ * Zero-valued sentinel for {@link GroupLiveStatus}.
3083
+ *
3084
+ * Used when a group has nothing to report. Kept as a frozen constant so the
3085
+ * reference is stable across renders — `Object.is` comparisons in React selectors
3086
+ * never trigger spurious re-renders on it.
3174
3087
  */
3175
- /** Row display title: blank rows show the localized New Session label. */
3176
- function displayTitle(node, t) {
3177
- return node.blank ? t("session.new") : node.title || t("session.untitled");
3178
- }
3179
- const MIN_TITLE_REVEAL_PX = 8;
3180
- const TITLE_MARQUEE_PX_PER_MS = .03;
3088
+ const EMPTY_LIVE_STATUS = Object.freeze({
3089
+ running: 0,
3090
+ approval: 0,
3091
+ planReview: 0,
3092
+ question: 0,
3093
+ done: 0,
3094
+ subagents: 0,
3095
+ facets: /* @__PURE__ */ new Map()
3096
+ });
3181
3097
  /**
3182
- * Place the title's scroll position and publish the stylesheet's fade-mask
3183
- * hooks: `data-scrolled` while the title has left its start (left fade) and
3184
- * `data-clipped` while text remains beyond the right edge (right fade).
3185
- * @param title - the row's clipping title element.
3186
- * @param left - scroll offset in CSS pixels.
3187
- * @param range - the title's maximum scroll offset in CSS pixels.
3098
+ * 一份**没有任何贡献**的折果:六个计数全 0 且没有外部来源。
3099
+ *
3100
+ * 它在合并里是加法单位元,所以「加它」不该产生新对象——见 {@link mergeLiveStatus}。
3188
3101
  */
3189
- function placeTitle(title, left, range) {
3190
- if (typeof title.scrollTo === "function") title.scrollTo({
3191
- left,
3192
- behavior: "instant"
3193
- });
3194
- else title.scrollLeft = left;
3195
- if (left > 0) title.dataset.scrolled = "";
3196
- else delete title.dataset.scrolled;
3197
- if (left < range) title.dataset.clipped = "";
3198
- else delete title.dataset.clipped;
3102
+ function isIdleStatus(status) {
3103
+ return status.running === 0 && status.approval === 0 && status.planReview === 0 && status.question === 0 && status.done === 0 && status.subagents === 0 && status.facets.size === 0;
3199
3104
  }
3200
3105
  /**
3201
- * Return the title to its resting state: scrolled to the start with both fade
3202
- * masks off, so the resting ellipsis renders at full strength.
3203
- * @param title - the row's clipping title element.
3106
+ * 合并两份分组折果:本插件自己的计数相加,外部状态来源**按 facet 各自合并**。
3107
+ *
3108
+ * 用于多层聚合:折叠的父文件夹要覆盖整棵子树,而子树里每一层都已各自折过一遍。
3109
+ * `FacetFold.data` 怎么合归该 facet 自己决定,本模块从不解释。
3110
+ *
3111
+ * 加法单位元走**引用原样返回**:值不变时换一个新对象,会让"同一棵空闲子树"在两轮
3112
+ * 计算之间有不同的身份。`EMPTY_LIVE_STATUS` 留在这个模块里的理由就是身份稳定(见其
3113
+ * 注释),合并这一层不该每加一次零就把那个理由作废。返回的是入参本身(不改写、不
3114
+ * 拷贝),因此与逐字段相加的结果等值。
3204
3115
  */
3205
- function restTitle(title) {
3206
- if (typeof title.scrollTo === "function") title.scrollTo({
3207
- left: 0,
3208
- behavior: "instant"
3209
- });
3210
- else title.scrollLeft = 0;
3211
- delete title.dataset.scrolled;
3212
- delete title.dataset.clipped;
3116
+ function mergeLiveStatus(a, b) {
3117
+ if (isIdleStatus(b)) return a;
3118
+ if (isIdleStatus(a)) return b;
3119
+ return {
3120
+ running: a.running + b.running,
3121
+ approval: a.approval + b.approval,
3122
+ planReview: a.planReview + b.planReview,
3123
+ question: a.question + b.question,
3124
+ done: a.done + b.done,
3125
+ subagents: a.subagents + b.subagents,
3126
+ facets: mergeFacets(a.facets, b.facets)
3127
+ };
3213
3128
  }
3214
- /**
3215
- * Marquee a title wider than its one-line cell while its row is hovered: the
3216
- * title clips its own text, so entering crawls it at a constant speed until the
3217
- * far edge (a fork's incremented title, for example) is in view, then rests
3218
- * there under the pointer. Overflow of at most {@link MIN_TITLE_REVEAL_PX}
3219
- * stays put — a barely-clipped title moving a few pixels reads as jitter, not a
3220
- * reveal. Leaving returns the title to the start in one step, because the
3221
- * resting ellipsis and the narrowed cell would otherwise meet the text while it
3222
- * travelled back. Reduced motion jumps to the far edge instead of crawling.
3223
- * @param title - ref to the row's clipping title element.
3224
- * @returns stable pointer enter/leave handlers for the row.
3129
+ /** 按 facet id 合并两侧折果;只有一侧有的来源原样带过。 */
3130
+ function mergeFacets(a, b) {
3131
+ if (a.size === 0) return b;
3132
+ if (b.size === 0) return a;
3133
+ const merged = new Map(a);
3134
+ for (const [id, entry] of b) {
3135
+ const own = merged.get(id);
3136
+ merged.set(id, own === void 0 ? entry : {
3137
+ facet: own.facet,
3138
+ fold: own.facet.merge?.(own.fold, entry.fold) ?? defaultMergeFold(own.fold, entry.fold)
3139
+ });
3140
+ }
3141
+ return merged;
3142
+ }
3143
+ /**
3144
+ * Fold every member row of a group into counts so a collapsed folder can hint at
3145
+ * hidden live sessions. Mirrors the session-row status priority (pending
3146
+ * outranks running outranks completed) at the counting level; labels resolve with
3147
+ * locale at render time.
3148
+ *
3149
+ * @param nodes - member rows of the group.
3150
+ * @param sessionIds - the same members' Session ids (facets address their own data by id).
3151
+ * @param facets - the registered status sources to fold over this group.
3152
+ * @param now - one clock reading for the whole pass, so sibling facets cannot disagree
3153
+ * about which deadlines are still ahead.
3225
3154
  */
3226
- function useTitleMarquee(title) {
3227
- const frame = (0, react.useRef)(0);
3228
- (0, react.useEffect)(() => () => {
3229
- cancelAnimationFrame(frame.current);
3230
- }, []);
3231
- return (0, react.useMemo)(() => ({
3232
- enter: () => {
3233
- /* v8 ignore next -- defensive: the title span renders unconditionally. */
3234
- if (title.current === null) return;
3235
- const element = title.current;
3236
- const range = element.scrollWidth - element.clientWidth;
3237
- if (range <= MIN_TITLE_REVEAL_PX) return;
3238
- if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
3239
- placeTitle(element, range, range);
3240
- return;
3241
- }
3242
- cancelAnimationFrame(frame.current);
3243
- let previous;
3244
- let position = 0;
3245
- const step = (now) => {
3246
- position += previous === void 0 ? 0 : (now - previous) * TITLE_MARQUEE_PX_PER_MS;
3247
- previous = now;
3248
- placeTitle(element, Math.min(position, range), range);
3249
- if (position < range) frame.current = requestAnimationFrame(step);
3250
- };
3251
- frame.current = requestAnimationFrame(step);
3252
- },
3253
- leave: () => {
3254
- cancelAnimationFrame(frame.current);
3255
- /* v8 ignore next -- defensive: the title span renders unconditionally. */
3256
- if (title.current === null) return;
3257
- restTitle(title.current);
3155
+ function summarizeGroupStatus(nodes, sessionIds, facets = [], now = Date.now()) {
3156
+ let running = 0;
3157
+ let approval = 0;
3158
+ let planReview = 0;
3159
+ let question = 0;
3160
+ let done = 0;
3161
+ let subagents = 0;
3162
+ for (const n of nodes) {
3163
+ if (n.blank || n.archived) continue;
3164
+ const hasSubagents = n.runningSubagentCount > 0;
3165
+ if (n.pendingInteraction === "approval") {
3166
+ approval += 1;
3167
+ subagents += n.runningSubagentCount;
3168
+ continue;
3258
3169
  }
3259
- }), [title]);
3260
- }
3261
- /** Localized compact relative time ("刚刚"/"5分钟" in zh, "now"/"5min" in en). */
3262
- function timeLabel(updatedAt, now, t) {
3263
- const { unit, n } = (0, _deepseek_ai_dsh_client_ui_primitives.relativeTime)(updatedAt, now);
3264
- return unit === "now" ? t("time.now") : t(`time.${unit}`, { n });
3265
- }
3266
- /** Hover-card variant: distances wrap in the ago template; the now bucket stays bare (no "now ago"). */
3267
- function hoverTimeLabel(updatedAt, now, t) {
3268
- const { unit, n } = (0, _deepseek_ai_dsh_client_ui_primitives.relativeTime)(updatedAt, now);
3269
- return unit === "now" ? t("time.now") : t("time.ago", { t: t(`time.${unit}`, { n }) });
3170
+ if (n.pendingInteraction === "plan-review") {
3171
+ planReview += 1;
3172
+ subagents += n.runningSubagentCount;
3173
+ continue;
3174
+ }
3175
+ if (n.pendingInteraction === "question") {
3176
+ question += 1;
3177
+ subagents += n.runningSubagentCount;
3178
+ continue;
3179
+ }
3180
+ if (n.running) {
3181
+ running += 1;
3182
+ subagents += n.runningSubagentCount;
3183
+ continue;
3184
+ }
3185
+ if (hasSubagents) {
3186
+ subagents += n.runningSubagentCount;
3187
+ continue;
3188
+ }
3189
+ if (n.completed) done += 1;
3190
+ }
3191
+ const folded = /* @__PURE__ */ new Map();
3192
+ for (const { facet } of facets) {
3193
+ let snapshot;
3194
+ try {
3195
+ snapshot = facet.getSnapshot();
3196
+ } catch {
3197
+ snapshot = void 0;
3198
+ }
3199
+ try {
3200
+ const result = facet.fold(snapshot, sessionIds, now);
3201
+ if (result !== void 0 && result.count > 0) folded.set(facet.id, {
3202
+ facet,
3203
+ fold: result
3204
+ });
3205
+ } catch {}
3206
+ }
3207
+ if (running === 0 && approval === 0 && planReview === 0 && question === 0 && done === 0 && subagents === 0 && folded.size === 0) return EMPTY_LIVE_STATUS;
3208
+ return {
3209
+ running,
3210
+ approval,
3211
+ planReview,
3212
+ question,
3213
+ done,
3214
+ subagents,
3215
+ facets: folded
3216
+ };
3270
3217
  }
3271
3218
  /**
3272
- * Absolute creation time through the dictionary's date template (the message
3273
- * clock pattern): `toLocaleString` would follow the browser language, not the
3274
- * app locale, and produce mixed-language text after a switch.
3219
+ * Measure each group's own members, as the input to {@link aggregateGroupStatuses}.
3220
+ *
3221
+ * Every group is measured, expanded ones included. A folder row shows no badge while
3222
+ * it is expanded, but its sessions still need attention and a collapsed ancestor must
3223
+ * keep counting them — so expansion is a rendering fact, never an input to the
3224
+ * aggregation. Short-circuiting expanded groups to `EMPTY_LIVE_STATUS` (the shape this
3225
+ * had before the tree-wide aggregator existed, harmless while nothing aggregated) makes
3226
+ * every collapsed parent undercount by the size of its expanded descendants.
3227
+ *
3228
+ * @param groups - derived groups, each with its member rows already populated.
3229
+ * @param facets - registered sources, already filtered by the visibility settings.
3230
+ * @param now - one clock reading shared by every group in the pass.
3275
3231
  */
3276
- function createdLabel(createdAt, t) {
3277
- const d = new Date(createdAt);
3278
- const pad2 = (v) => String(v).padStart(2, "0");
3279
- return t("hover.created", { time: `${t("date.ymd", {
3280
- y: d.getFullYear(),
3281
- m: d.getMonth() + 1,
3282
- d: d.getDate()
3283
- })} ${pad2(d.getHours())}:${pad2(d.getMinutes())}` });
3232
+ function directStatusesForGroups(groups, facets = [], now = Date.now()) {
3233
+ const map = /* @__PURE__ */ new Map();
3234
+ for (const group of groups) map.set(group.key, summarizeGroupStatus(group.sessions, group.sessions.map((s) => s.id), facets, now));
3235
+ return map;
3284
3236
  }
3285
- /** Hover-card body: workspace title, display directory path, absolute creation time. */
3286
- function WorkspaceHoverContent({ label, cwd, createdAt, status, settings, now, t }) {
3287
- const lines = hoverStatusLines(status, t, settings, now);
3288
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3289
- className: Rows_module_css_default.hoverContent,
3290
- children: [
3291
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3292
- className: Rows_module_css_default.hoverTitle,
3293
- children: label
3294
- }),
3295
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3296
- className: Rows_module_css_default.hoverPath,
3297
- children: cwd
3298
- }),
3299
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3300
- className: Rows_module_css_default.hoverTime,
3301
- children: createdLabel(createdAt, t)
3302
- }),
3303
- lines.map((line) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3304
- className: Rows_module_css_default.hoverStatus,
3305
- children: [line.glyph ?? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: line.state }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: line.text })]
3306
- }, line.text))
3307
- ]
3308
- });
3237
+ /**
3238
+ * Fold every group's DIRECT status up the workspace tree, so a collapsed folder
3239
+ * counts every descendant session. A group's own row shows no badge while it is
3240
+ * expanded, but its sessions still need attention and its collapsed ancestor must
3241
+ * keep counting them — so the walk never depends on a group's expanded state.
3242
+ *
3243
+ * @param direct - each group's own-member status, keyed by group key.
3244
+ * @param childrenByParent - the rendered tree: parent key (`undefined` = roots) → children.
3245
+ * @returns aggregated status per group key: own members plus the whole subtree.
3246
+ */
3247
+ function aggregateGroupStatuses(direct, childrenByParent) {
3248
+ const aggregated = /* @__PURE__ */ new Map();
3249
+ const visiting = /* @__PURE__ */ new Set();
3250
+ const fold = (key) => {
3251
+ const cached = aggregated.get(key);
3252
+ if (cached !== void 0) return cached;
3253
+ if (visiting.has(key)) return direct.get(key) ?? EMPTY_LIVE_STATUS;
3254
+ visiting.add(key);
3255
+ let total = direct.get(key) ?? EMPTY_LIVE_STATUS;
3256
+ for (const child of childrenByParent.get(key) ?? []) total = mergeLiveStatus(total, fold(child.key));
3257
+ visiting.delete(key);
3258
+ aggregated.set(key, total);
3259
+ return total;
3260
+ };
3261
+ for (const children of childrenByParent.values()) for (const child of children) fold(child.key);
3262
+ return aggregated;
3309
3263
  }
3310
- /** Pointer-position half of a row (insert line above or below). */
3311
- function rowHalf(e) {
3312
- const rect = e.currentTarget.getBoundingClientRect();
3313
- return e.clientY < rect.top + rect.height / 2 ? "before" : "after";
3264
+ /**
3265
+ * Attach each group's subtree-wide status, in derivation order. This is the producer
3266
+ * the sidebar renders from.
3267
+ *
3268
+ * Expanded groups carry their status too: their own row draws no badge (a decision
3269
+ * inside `groupLiveBadge`), but a collapsed ancestor still has to count them, so
3270
+ * expansion never suppresses the measurement.
3271
+ *
3272
+ * @param groups - derived rows, in the order they render.
3273
+ * @param direct - each group's own-member status, from {@link directStatusesForGroups}.
3274
+ * @param childrenByParent - the rendered tree: parent key (`undefined` = roots) → children.
3275
+ */
3276
+ function withAggregatedStatus(groups, direct, childrenByParent) {
3277
+ const aggregated = aggregateGroupStatuses(direct, childrenByParent);
3278
+ return groups.map((group) => ({
3279
+ ...group,
3280
+ liveStatus: aggregated.get(group.key) ?? EMPTY_LIVE_STATUS
3281
+ }));
3314
3282
  }
3315
3283
  /**
3316
- * Project (workspace) header row: folder + title;
3317
- * hover reveals the chevron and create button, and dwelling on a real
3318
- * Workspace shows its hover card (the ungrouped bucket has none).
3319
- * `containsCurrent` arrives on the node (derivation fact, no renderer scan).
3320
- * @param props.group - derived group node.
3321
- * @param props.containsCurrentDescendant - highlight an ancestor even when its subtree is collapsed.
3322
- * @param props.onToggle - expand/collapse the group.
3323
- * @param props.onCreate - start a frontend Session inside this Workspace.
3324
- * @param props.drag - optional workspace-row drag wiring.
3325
- * @param props.home - host account home for POSIX hover-path abbreviation.
3326
- * @param props.t - the browser root's locale seat.
3327
- * @returns the row element.
3284
+ * The blank row plus the first `watermark` non-blank rows, in list order.
3285
+ * @param sessions - one group's rows, whose blank placeholder(s) lead the list
3286
+ * (`sectionMembers` puts every blank session ahead of the pinned/ordinary partitions).
3287
+ * @param watermark - highest non-blank row index that stays visible; `0` keeps only blank rows.
3328
3288
  */
3329
- function ProjectRowItem({ group, containsCurrentDescendant = false, onToggle, onCreate, actions, drag, home, newShortcut, settings, now, t }) {
3330
- const row = group;
3331
- const label = row.workspaceId === void 0 ? t("group.ungrouped") : row.label;
3332
- const active = containsCurrentDescendant || group.expanded && group.containsCurrent;
3333
- const [menuOpen, setMenuOpen] = (0, react.useState)(false);
3334
- const workspaceMenuItems = [{
3335
- id: "rename",
3336
- label: t("rename"),
3337
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconEditOutlineRegular, {})
3338
- }, {
3339
- id: "delete",
3340
- label: t("delete.workspace"),
3341
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutlineRegular, {}),
3342
- danger: true
3343
- }];
3344
- const ownRow = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3345
- className: clsx(Rows_module_css_default.projectRow, menuOpen && Rows_module_css_default.menuOpen),
3346
- "data-row-key": `workspace:${group.key}`,
3347
- role: "treeitem",
3348
- "aria-expanded": row.expanded,
3349
- onClick: onToggle,
3350
- draggable: drag !== void 0,
3351
- onDragStart: drag === void 0 ? void 0 : (e) => {
3352
- e.dataTransfer.effectAllowed = "move";
3353
- e.dataTransfer.setData("text/plain", row.key);
3354
- drag.start();
3355
- },
3356
- onDragEnd: drag?.end,
3357
- children: [
3358
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3359
- className: clsx(Rows_module_css_default.slot, Rows_module_css_default.folder, active && Rows_module_css_default.folderActive),
3360
- children: row.expanded ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderOpenRegular, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderCloseRegular, {})
3361
- }),
3362
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3363
- className: clsx(Rows_module_css_default.slot, Rows_module_css_default.chevron),
3364
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTriangleRightFillRegular, { className: clsx(Rows_module_css_default.arrow, row.expanded && Rows_module_css_default.arrowOpen) })
3365
- }),
3366
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3367
- className: Rows_module_css_default.projectText,
3368
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3369
- className: Rows_module_css_default.title,
3370
- children: label
3371
- })
3372
- }),
3373
- groupLiveBadge(group, t, settings, now),
3374
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3375
- className: Rows_module_css_default.rowActions,
3376
- children: [actions !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
3377
- open: menuOpen,
3378
- onClose: () => {
3379
- setMenuOpen(false);
3380
- },
3381
- items: workspaceMenuItems,
3382
- onSelect: (id) => {
3383
- setMenuOpen(false);
3384
- /* v8 ignore next -- Menu can emit only the rename and delete rows supplied above. */
3385
- if (id !== "rename" && id !== "delete") return;
3386
- if (id === "rename") actions.rename();
3387
- else actions.delete();
3388
- },
3389
- portal: true,
3390
- closeOnPointerLeave: true,
3391
- anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3392
- type: "button",
3393
- className: Rows_module_css_default.iconButton,
3394
- "aria-label": t("actions.workspace.aria", { name: label }),
3395
- onClick: (e) => {
3396
- e.stopPropagation();
3397
- setMenuOpen((v) => !v);
3398
- },
3399
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconEllipsisOutlineRegular, {})
3400
- })
3401
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
3402
- label: t("actions.newSession"),
3403
- shortcutKeys: newShortcut?.keys,
3404
- side: "bottom",
3405
- align: "end",
3406
- delayMs: 500,
3407
- portal: true,
3408
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3409
- type: "button",
3410
- className: Rows_module_css_default.iconButton,
3411
- "aria-keyshortcuts": newShortcut?.aria,
3412
- "aria-label": t("actions.newSession.aria", { name: label }),
3413
- onClick: (e) => {
3414
- e.stopPropagation();
3415
- onCreate();
3416
- },
3417
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconNewChatOutlineRegular, {})
3418
- })
3419
- })]
3420
- })
3421
- ]
3422
- });
3423
- if (row.createdAt === void 0) return ownRow;
3424
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.HoverCard, {
3425
- anchor: ownRow,
3426
- content: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceHoverContent, {
3427
- label: row.label,
3428
- cwd: row.cwd === void 0 ? void 0 : abbreviateHomePath(row.cwd, home),
3429
- createdAt: row.createdAt,
3430
- status: row.expanded ? void 0 : row.liveStatus,
3431
- settings,
3432
- now: now ?? Date.now(),
3433
- t
3434
- }),
3435
- openDelayMs: 800,
3436
- grouped: true,
3437
- disabled: menuOpen,
3438
- copyText: row.cwd,
3439
- copyLabel: t("copy"),
3440
- copiedLabel: t("hover.copied")
3441
- });
3442
- }
3443
- /* v8 ignore next 3 -- closed-union backstop; only reached if the status is forged */
3444
- function assertNever(value) {
3445
- throw new Error(`unknown pending interaction: ${String(value)}`);
3289
+ function watermarkRows(sessions, watermark) {
3290
+ const rows = [];
3291
+ let count = 0;
3292
+ for (const session of sessions) {
3293
+ if (session.blank) {
3294
+ rows.push(session);
3295
+ continue;
3296
+ }
3297
+ if (count >= watermark) break;
3298
+ count += 1;
3299
+ rows.push(session);
3300
+ }
3301
+ return {
3302
+ rows,
3303
+ hiddenCount: sessions.length - rows.length
3304
+ };
3446
3305
  }
3447
3306
  /**
3448
- * Session status presentation; pending interaction is primary and live activity
3449
- * outranks completion reminders.
3307
+ * Smallest watermark that covers every non-blank row within the automatic maximum that is running, has a running
3308
+ * child, awaits the user, carries an unread completion, or is current, and the reveal target at any position.
3309
+ * @param sessions - one group's ordered Session rows.
3310
+ * @param current - Session retained by the main view, if the sidebar is the active surface.
3311
+ * @param revealSessionId - Session selected from search and required to be visible.
3312
+ * @returns the group's baseline non-blank row count.
3450
3313
  */
3451
- function sessionStatuses(node, t) {
3452
- const subagents = node.runningSubagentCount === 0 ? void 0 : {
3453
- state: "ongoing",
3454
- label: t(node.runningSubagentCount === 1 ? "status.subagentsRunning.one" : "status.subagentsRunning.other", { n: node.runningSubagentCount })
3455
- };
3456
- let pending;
3457
- switch (node.pendingInteraction) {
3458
- case "approval":
3459
- pending = {
3460
- state: "warning",
3461
- label: t("status.waitingApproval"),
3462
- trailingLabel: t("status.compact.approval")
3463
- };
3464
- break;
3465
- case "plan-review":
3466
- pending = {
3467
- state: "warning",
3468
- label: t("status.planReview"),
3469
- trailingLabel: t("status.compact.planReview")
3470
- };
3471
- break;
3472
- case "question":
3473
- pending = {
3474
- state: "warning",
3475
- label: t("status.waitingAnswer"),
3476
- trailingLabel: t("status.compact.answer")
3477
- };
3478
- break;
3479
- case void 0: break;
3480
- /* v8 ignore next -- closed PendingInteractionStatus union */
3481
- default: return assertNever(node.pendingInteraction);
3482
- }
3483
- if (pending !== void 0) return subagents === void 0 ? [pending] : [pending, subagents];
3484
- if (node.running) {
3485
- const primary = {
3486
- state: "ongoing",
3487
- label: t("status.running")
3488
- };
3489
- return subagents === void 0 ? [primary] : [primary, subagents];
3314
+ function requiredWatermark(sessions, current, revealSessionId) {
3315
+ let index = 0;
3316
+ let required = 0;
3317
+ for (const session of sessions) {
3318
+ if (session.blank) continue;
3319
+ index += 1;
3320
+ if (index <= 50 && (session.running || session.runningSubagentCount > 0 || session.pendingInteraction !== void 0 || session.completed || session.id === current) || session.id === revealSessionId) required = index;
3490
3321
  }
3491
- if (subagents !== void 0) return [subagents];
3492
- if (node.completed) return [{
3493
- state: "done",
3494
- label: t("status.completed")
3495
- }];
3496
- return [{
3497
- state: "idle",
3498
- label: t("status.idle")
3499
- }];
3500
- }
3501
- /** Primary status dot plus every status's screen-reader label, shared by the search and session rows. */
3502
- function SessionStatusDots({ statuses }) {
3503
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: statuses[0].state }), statuses.map((status) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3504
- className: Rows_module_css_default.visuallyHidden,
3505
- children: status.label
3506
- }, status.label))] });
3507
- }
3508
- /** Non-interactive pinned-row marker; the enclosing row remains the only action. */
3509
- function PinnedIndicator({ t }) {
3510
- const label = t("row.pinned");
3511
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3512
- className: Rows_module_css_default.pinIndicator,
3513
- role: "img",
3514
- "aria-label": label,
3515
- title: label,
3516
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPinFillRegular, { size: 14 })
3517
- });
3322
+ return required;
3518
3323
  }
3519
3324
  /**
3520
- * Hover-card body: full title, relative time, the Session's own scheduled-task
3521
- * section, and every relevant live status. The task section sits above the
3522
- * status lines so they stay the card's trailing status line.
3325
+ * Raise each listed group's watermark to its requirement; returns `marks` itself when nothing rises.
3326
+ * @param marks - watermarks stored by the current browser tree.
3327
+ * @param required - required baselines, in group order.
3328
+ * @returns either the unchanged input or a new object containing only raised groups.
3523
3329
  */
3524
- function SessionHoverContent({ node, now, renderSlot, t }) {
3525
- const statuses = sessionStatuses(node, t).filter((status) => !(node.archived && (status.state === "done" || status.state === "idle")));
3526
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3527
- className: Rows_module_css_default.hoverContent,
3528
- children: [
3529
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3530
- className: Rows_module_css_default.hoverTitle,
3531
- children: displayTitle(node, t)
3532
- }),
3533
- !node.blank && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3534
- className: Rows_module_css_default.hoverTime,
3535
- children: hoverTimeLabel(node.updatedAt, now, t)
3536
- }),
3537
- renderSlot("sidebar.session.row.hover", { sessionId: node.id }),
3538
- statuses.map((status) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3539
- className: Rows_module_css_default.hoverStatus,
3540
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: status.state }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: status.label })]
3541
- }, status.label)),
3542
- node.archived && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3543
- className: clsx(Rows_module_css_default.hoverStatus, Rows_module_css_default.hoverArchived),
3544
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconArchiveOutlineRegular, { size: 14 }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("row.archived") })]
3545
- })
3546
- ]
3547
- });
3330
+ function raiseWatermarks(marks, required) {
3331
+ let raised;
3332
+ for (const [key, watermark] of required) {
3333
+ if (watermark <= ((raised ?? marks)[key] ?? 5)) continue;
3334
+ raised ??= { ...marks };
3335
+ raised[key] = watermark;
3336
+ }
3337
+ return raised ?? marks;
3548
3338
  }
3339
+ //#endregion
3340
+ //#region src/client/use-registered-facets.ts
3549
3341
  /**
3550
- * One flat search result: title, Workspace context, and optional content
3551
- * excerpt. Search navigation opens the session only; it does not address an
3552
- * event inside the conversation. Archived rows carry a hover unarchive
3553
- * button, because search is where the filter surfaces them for recovery.
3554
- * @param props.result - merged local/content search row.
3555
- * @param props.currentId - selected session id.
3556
- * @param props.onOpen - open the selected session.
3557
- * @param props.onUnarchive - unarchive an archived result row.
3558
- * @param props.t - Workspace-browser translation seat.
3559
- * @returns the result row.
3342
+ * 把登记面读成「这一轮渲染该折哪些来源」的**唯一**一处接线。
3343
+ *
3344
+ * ## 为什么单独成文
3345
+ *
3346
+ * 这一步此前直接写在 `rows/WorkspaceBrowser.tsx` 里,两行:
3347
+ *
3348
+ * ```ts
3349
+ * useSyncExternalStore(facetsStore.subscribe, facetsStore.getVersion, …) // 返回值丢掉
3350
+ * const facets = useMemo(() => visibleFacets(facetsStore.list(), settings), [settings])
3351
+ * ```
3352
+ *
3353
+ * 看着没问题——订阅了、也 memo 了。实际是**订阅通知只重渲染、不重算**:
3354
+ * `useSyncExternalStore` 的返回值被丢掉,`useMemo` 的依赖里就没有任何东西随登记面变化,
3355
+ * 于是那次重渲染原样拿回**上一次**的登记面。提供方在自己的 `apply` 里注册,而宿主不保证
3356
+ * `apply` 与本组件首渲染的先后:注册**晚于**首渲染时,facet 明明登记上了,却永远进不了
3357
+ * 徽标——不报错、不红测试(能渲染这条路径的那份上游 spec 在本仓库是跳过的,见
3358
+ * `vitest.config.ts` 的 `MONOREPO_ONLY_SPECS`),只能靠刷新页面碰运气。
3359
+ *
3360
+ * 版本号必须**同时**当两件事用:① 触发重渲染的信号,② memo 的依赖。这里把它收进一个
3361
+ * hook,让「只用①、丢掉②」在结构上写不出来,并且让这条接缝能被 `@testing-library/react`
3362
+ * 直接覆盖(本文件零宿主 import,宿主依赖闭包进不来)。
3363
+ *
3364
+ * ## 为什么现在不再按设置筛
3365
+ *
3366
+ * 旧版本这里会过一道 `visibleFacets(list, settings)`,按 `facet.settingKey` 查本插件的
3367
+ * 布尔开关。那意味着**本插件的配置面里写着别人家的语义**(`showScheduledInBadge` /
3368
+ * `showWaitingReviewInBadge`,连图标形状都替对方写死),与"本插件不认识任何来源"直接
3369
+ * 矛盾。现在外部来源的显隐归**来源自己**:它想被关掉就不注册 facet,登记面即所见。
3370
+ * 本插件的开关只管自己那三档会话状态(见 `badge-settings.ts`)。
3371
+ *
3372
+ * @module dsh-group-status/client/use-registered-facets
3560
3373
  */
3561
- function SearchResultItem({ result, currentId, onOpen, onUnarchive, t }) {
3562
- const selected = result.id === currentId;
3563
- const statuses = sessionStatuses(result, t);
3564
- const primaryStatus = statuses[0];
3565
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3566
- className: clsx(Rows_module_css_default.searchResultRow, selected && Rows_module_css_default.selected, result.archived && Rows_module_css_default.archived),
3567
- role: "treeitem",
3568
- "aria-selected": selected,
3569
- "aria-description": result.archived ? t("toast.archivedNotOpenable") : void 0,
3570
- onClick: () => {
3571
- onOpen(result.id);
3572
- },
3573
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3574
- className: Rows_module_css_default.searchResultHeading,
3575
- children: [
3576
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3577
- className: Rows_module_css_default.slot,
3578
- children: !result.archived && primaryStatus.state !== "idle" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SessionStatusDots, { statuses })
3579
- }),
3580
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3581
- className: Rows_module_css_default.searchResultTitle,
3582
- children: result.title || t("session.untitled")
3583
- }),
3584
- result.archived && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3585
- className: Rows_module_css_default.rowActions,
3586
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
3587
- label: t("actions.unarchive"),
3588
- side: "bottom",
3589
- align: "end",
3590
- delayMs: 500,
3591
- portal: true,
3592
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3593
- type: "button",
3594
- className: Rows_module_css_default.iconButton,
3595
- "aria-label": t("menu.unarchiveSession"),
3596
- onClick: (e) => {
3597
- e.stopPropagation();
3598
- onUnarchive(result.id);
3599
- },
3600
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconUnarchiveOutlineRegular, { size: 14 })
3601
- })
3602
- })
3603
- })
3604
- ]
3605
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3606
- className: Rows_module_css_default.searchResultMeta,
3607
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3608
- className: Rows_module_css_default.searchResultWorkspace,
3609
- children: result.workspace || t("group.ungrouped")
3610
- }), result.snippet !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3611
- className: Rows_module_css_default.searchResultSnippet,
3612
- children: result.snippet
3613
- })]
3614
- })]
3615
- });
3616
- }
3617
3374
  /**
3618
- * One top-level 32px session row: leading 16px cell (status dot, or the
3619
- * leading seat while the row's primary state is idle), title, relative time or
3620
- * compact pending label, and the row actions menu. A row that owns a state dot
3621
- * keeps that cell and renders no seat, so an ambient automation mark never
3622
- * appears beside the row's own state dot. An archived row keeps the cell blank:
3623
- * neither marker renders there, and its live status stays on the hover card.
3624
- * @param props.node - derived session node.
3625
- * @param props.currentId - selected session id (row highlight).
3626
- * @param props.now - epoch ms for relative-time formatting.
3627
- * @param props.onOpen - open a session by id.
3628
- * @param props.onRenameRequest - open the rename dialog from a title double-click (id + current title).
3629
- * @param props.renderSlot - child-seat renderer for the row's action lists
3630
- * (`sidebar.workspaces.session.menu.item` / `sidebar.workspaces.session.row.action`),
3631
- * its leading decoration, and its hover-card section.
3632
- * @param props.onReveal - scroll this row into view after search navigation, then acknowledge it.
3633
- * @param props.drag - optional row-drag target wiring; blank rows cannot start a drag.
3634
- * @param props.t - the browser root's locale seat.
3635
- * @returns the session row.
3375
+ * 当前登记的来源,按登记面顺序(已按 `order` 排好)。
3376
+ *
3377
+ * 引用在版本号不变时保持稳定:登记面是低频事件,重算成本 = O(facet × 组内会话)。
3378
+ *
3379
+ * @param store - facet 消费侧存储(`facetStore()` 单例)。
3636
3380
  */
3637
- function SessionNodeItem({ node, currentId, now, onOpen, onRenameRequest, renderSlot, onReveal, drag, t }) {
3638
- const row = node;
3639
- const title = displayTitle(node, t);
3640
- const selected = node.id === currentId;
3641
- const statuses = sessionStatuses(node, t);
3642
- const primaryStatus = statuses[0];
3643
- const showStatus = primaryStatus.state !== "idle";
3644
- const draggable = drag !== void 0 && !row.blank && !row.archived;
3645
- const [menuOpen, setMenuOpen] = (0, react.useState)(false);
3646
- const menuOpenState = (0, react.useMemo)(() => [menuOpen, setMenuOpen], [menuOpen]);
3647
- const rowRef = (0, react.useRef)(null);
3648
- const titleRef = (0, react.useRef)(null);
3649
- const marquee = useTitleMarquee(titleRef);
3650
- (0, react.useEffect)(() => {
3651
- if (onReveal === void 0) return;
3652
- rowRef.current?.scrollIntoView({ block: "nearest" });
3653
- onReveal();
3654
- }, [onReveal]);
3655
- const ownRow = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3656
- ref: rowRef,
3657
- "data-row-key": `session:${node.id}`,
3658
- className: clsx(Rows_module_css_default.sessionRow, selected && Rows_module_css_default.selected, menuOpen && Rows_module_css_default.menuOpen, row.archived && Rows_module_css_default.archived, drag?.marker === "before" && Rows_module_css_default.dropBefore, drag?.marker === "after" && Rows_module_css_default.dropAfter),
3659
- role: "treeitem",
3660
- "aria-selected": selected,
3661
- "aria-description": row.archived ? t("toast.archivedNotOpenable") : void 0,
3662
- onClick: () => {
3663
- onOpen(node.id);
3664
- },
3665
- onPointerEnter: marquee.enter,
3666
- onPointerLeave: marquee.leave,
3667
- draggable,
3668
- onDragStart: !draggable ? void 0 : (e) => {
3669
- e.dataTransfer.effectAllowed = "move";
3670
- e.dataTransfer.setData("text/plain", node.id);
3671
- drag.start();
3672
- },
3673
- onDragEnd: !draggable ? void 0 : drag.end,
3674
- onDragOver: drag === void 0 ? void 0 : (e) => {
3675
- if (!drag.active) return;
3676
- e.preventDefault();
3677
- e.dataTransfer.dropEffect = "move";
3678
- drag.hover(rowHalf(e));
3679
- },
3680
- onDrop: drag === void 0 ? void 0 : (e) => {
3681
- if (!drag.active) return;
3682
- e.preventDefault();
3683
- drag.drop(rowHalf(e));
3684
- },
3685
- children: [
3686
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3687
- className: Rows_module_css_default.slot,
3688
- children: !row.archived && !row.blank && (showStatus ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SessionStatusDots, { statuses }) : renderSlot("sidebar.session.row.leading", { sessionId: node.id }))
3689
- }),
3690
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3691
- ref: titleRef,
3692
- className: Rows_module_css_default.title,
3693
- onDoubleClick: row.blank ? void 0 : (e) => {
3694
- e.stopPropagation();
3695
- onRenameRequest(node.id, row.title);
3696
- },
3697
- children: title
3698
- }),
3699
- !row.blank && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3700
- className: Rows_module_css_default.time,
3701
- "aria-hidden": primaryStatus.trailingLabel === void 0 ? void 0 : true,
3702
- children: primaryStatus.trailingLabel ?? timeLabel(row.updatedAt, now, t)
3703
- }),
3704
- row.pinned && !row.archived && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PinnedIndicator, { t }),
3705
- !row.blank && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3706
- className: Rows_module_css_default.rowActions,
3707
- onClick: (e) => {
3708
- e.stopPropagation();
3709
- },
3710
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
3711
- open: menuOpen,
3712
- onClose: () => {
3713
- setMenuOpen(false);
3714
- },
3715
- portal: true,
3716
- closeOnPointerLeave: true,
3717
- anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3718
- type: "button",
3719
- className: Rows_module_css_default.iconButton,
3720
- "aria-label": t("actions.session.aria", { name: title }),
3721
- onClick: () => {
3722
- setMenuOpen((v) => !v);
3723
- },
3724
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconEllipsisOutlineRegular, {})
3725
- }),
3726
- children: renderSlot("sidebar.workspaces.session.menu.item", {
3727
- sessionId: node.id,
3728
- displayTitle: row.title
3729
- }, { hookContext: menuOpenState })
3730
- }), renderSlot("sidebar.workspaces.session.row.action", {
3731
- sessionId: node.id,
3732
- displayTitle: row.title
3733
- })]
3734
- })
3735
- ]
3736
- });
3737
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.HoverCard, {
3738
- anchor: ownRow,
3739
- content: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SessionHoverContent, {
3740
- node,
3741
- now,
3742
- renderSlot,
3743
- t
3744
- }),
3745
- openDelayMs: 800,
3746
- grouped: true,
3747
- disabled: menuOpen || drag?.active === true,
3748
- copyText: row.blank || row.title === "" ? void 0 : row.title,
3749
- copyLabel: t("copy"),
3750
- copiedLabel: t("hover.copied")
3751
- });
3381
+ function useRegisteredFacets(store) {
3382
+ const version = (0, react.useSyncExternalStore)(store.subscribe, store.getVersion, store.getServerVersion);
3383
+ return (0, react.useMemo)(() => [...store.list()], [version]);
3752
3384
  }
3753
3385
  //#endregion
3754
- //#region \0dsh-css:src/client/rows/AnimatedRows.module.css.mjs
3755
- const css$2 = "._8TnAsq_exits{contain:strict;pointer-events:none;position:absolute;inset:0;overflow:clip}";
3756
- const tagId$2 = "dsh-group-status/AnimatedRows.module.css";
3757
- if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]") === null) {
3758
- const tag = document.createElement("style");
3759
- tag.dataset.plugin = "dsh-group-status";
3760
- tag.dataset.pluginCss = tagId$2;
3761
- tag.textContent = css$2;
3762
- document.head.appendChild(tag);
3386
+ //#region src/client/badge-labels.ts
3387
+ /**
3388
+ * 把侧栏行的翻译位交给 facet。
3389
+ *
3390
+ * 这里有一次**故意的放宽**:协议的 `t` 收任意字符串键,而行位只收本插件 `workspace`
3391
+ * 命名空间的键。放宽是安全的——宿主 locale 服务对未注册的键一律**原样回显键名**(不是
3392
+ * 抛错),所以外来 facet 传了自己命名空间的键,最坏读出的是那串键名而不是崩渲染。
3393
+ * 要正确本地化,provider 应当闭包自己的翻译函数、忽略这里传入的 `t`。
3394
+ */
3395
+ function asFacetTranslate(t) {
3396
+ return (key, options) => t(key, options);
3763
3397
  }
3764
- var AnimatedRows_module_css_default = { "exits": "_8TnAsq_exits" };
3765
- //#endregion
3766
- //#region src/client/rows/AnimatedRows.tsx
3767
- /** React-commit-driven movement and entry/exit fades for the sidebar's keyed rows. */
3768
- const ROW_FADE_MS = 100;
3769
- const ROW_GLIDE_MS = 200;
3770
- function sameRows(previous, next) {
3771
- return previous.rowKeys.length === next.rowKeys.length && previous.rowKeys.every((key, index) => key === next.rowKeys[index]);
3398
+ /** 该 facet 贡献进徽标数字的条数。 */
3399
+ function facetCountTotal(s) {
3400
+ let total = 0;
3401
+ for (const entry of s.facets.values()) total += entry.fold.count;
3402
+ return total;
3772
3403
  }
3773
- function intersects(row, viewport) {
3774
- return row.bottom > viewport.top && row.top < viewport.bottom && row.right > viewport.left && row.left < viewport.right;
3404
+ /**
3405
+ * 每个外部状态来源的那句话,按登记顺序。
3406
+ *
3407
+ * facet 没实现 `label` 时**不能**退化成 `2 dsh-later.schedule` 这种读法,所以给一句
3408
+ * 通用措辞兜底:只说有几个来源在响,具体语义等该来源自己补文案。
3409
+ */
3410
+ function facetLines(s, t, now) {
3411
+ const translate = asFacetTranslate(t);
3412
+ const lines = [];
3413
+ for (const entry of s.facets.values()) {
3414
+ const text = entry.facet.label?.(entry.fold, {
3415
+ now,
3416
+ t: translate
3417
+ });
3418
+ lines.push(text === void 0 || text === "" ? `${entry.fold.count} ${t("status.needsAttention")}` : text);
3419
+ }
3420
+ return lines;
3775
3421
  }
3776
3422
  /**
3777
- * Animates keyed sidebar rows only when their rendered membership or order changes.
3778
- * Motion starts after the first pointer or keyboard input inside the mounted list.
3779
- * The parent supplies a positioned container for the inert exit overlay.
3423
+ * Accessible label for a folded group's aggregated status, reusing the
3424
+ * session-row locale keys so both surfaces read identically.
3425
+ *
3426
+ * 按设置过滤:本插件那三档会话状态按设置逐项过滤;**外部来源不参与过滤** —— 登记了就
3427
+ * 一定出现在这里,没登记就不在折果里,所以 `aria-label` 与徽标视觉天然一致,不必在这里
3428
+ * 再判一次设置(本插件也没有替别人家存的开关)。
3780
3429
  */
3781
- var AnimatedRows = class extends react.Component {
3782
- armed = false;
3783
- list = (0, react.createRef)();
3784
- overlay = (0, react.createRef)();
3785
- movements = /* @__PURE__ */ new Map();
3786
- exits = /* @__PURE__ */ new Map();
3787
- getSnapshotBeforeUpdate(previous) {
3788
- const list = this.list.current;
3789
- if (!this.armed || sameRows(previous, this.props) || previous.resetKey !== this.props.resetKey || !previous.ready || !this.props.ready || list === null || typeof list.animate !== "function" || window.matchMedia("(prefers-reduced-motion: reduce)").matches) return null;
3790
- const viewport = list.getBoundingClientRect();
3791
- const positions = this.readPositions();
3792
- const nextKeys = new Set(this.props.rowKeys);
3793
- const removed = /* @__PURE__ */ new Map();
3794
- for (const [key, row] of positions) {
3795
- if (nextKeys.has(key) || !intersects(row.rect, viewport)) continue;
3796
- const clone = row.element.cloneNode(true);
3797
- clone.removeAttribute("data-row-key");
3798
- clone.inert = true;
3799
- clone.style.setProperty("--dsh-workspace-indent", getComputedStyle(row.element).getPropertyValue("--dsh-workspace-indent"));
3800
- removed.set(key, {
3801
- ...row,
3802
- element: clone
3803
- });
3804
- }
3805
- return {
3806
- positions,
3807
- removed
3808
- };
3809
- }
3810
- componentDidUpdate(previous, _state, snapshot) {
3811
- if (snapshot === null) {
3812
- if (!sameRows(previous, this.props) || previous.resetKey !== this.props.resetKey || previous.ready !== this.props.ready) this.clear();
3813
- return;
3814
- }
3815
- this.cancelMovements();
3816
- const list = this.list.current;
3817
- const overlay = this.overlay.current;
3818
- const viewport = list.getBoundingClientRect();
3819
- const origin = overlay.getBoundingClientRect();
3820
- const positions = this.readPositions();
3821
- for (const [key, row] of positions) {
3822
- this.removeExit(key);
3823
- const previousRow = snapshot.positions.get(key);
3824
- if (!intersects(row.rect, viewport) && (previousRow === void 0 || !intersects(previousRow.rect, viewport))) continue;
3825
- if (previousRow === void 0) {
3826
- this.move(row.element, [{ opacity: 0 }, { opacity: 1 }], ROW_FADE_MS);
3827
- continue;
3828
- }
3829
- const dx = previousRow.rect.left - row.rect.left;
3830
- const dy = previousRow.rect.top - row.rect.top;
3831
- if (dx === 0 && dy === 0 && previousRow.opacity === 1) continue;
3832
- this.move(row.element, [{
3833
- transform: `translate(${String(dx)}px, ${String(dy)}px)`,
3834
- opacity: previousRow.opacity
3835
- }, {
3836
- transform: "translate(0, 0)",
3837
- opacity: 1
3838
- }], ROW_GLIDE_MS);
3839
- }
3840
- for (const [key, row] of snapshot.removed) {
3841
- const { element } = row;
3842
- this.removeExit(key);
3843
- Object.assign(element.style, {
3844
- position: "absolute",
3845
- margin: "0",
3846
- transform: "none",
3847
- boxSizing: "border-box",
3848
- left: `${String(row.rect.left - origin.left)}px`,
3849
- top: `${String(row.rect.top - origin.top)}px`,
3850
- width: `${String(row.rect.width)}px`,
3851
- height: `${String(row.rect.height)}px`
3852
- });
3853
- overlay.append(element);
3854
- const animation = element.animate([{ opacity: row.opacity }, { opacity: 0 }], {
3855
- duration: ROW_FADE_MS,
3856
- easing: "ease-out",
3857
- fill: "forwards"
3858
- });
3859
- this.exits.set(key, {
3860
- element,
3861
- animation
3862
- });
3863
- animation.onfinish = () => {
3864
- this.removeExit(key);
3865
- };
3866
- }
3867
- }
3868
- componentWillUnmount() {
3869
- this.clear();
3430
+ function groupLiveLabel(s, t, settings, now = Date.now()) {
3431
+ const parts = [];
3432
+ if (settings.showPendingInBadge) {
3433
+ if (s.approval > 0) parts.push(`${s.approval} ${t("status.waitingApproval")}`);
3434
+ if (s.planReview > 0) parts.push(`${s.planReview} ${t("status.planReview")}`);
3435
+ if (s.question > 0) parts.push(`${s.question} ${t("status.waitingAnswer")}`);
3870
3436
  }
3871
- readPositions() {
3872
- const rows = this.list.current.querySelectorAll("[data-row-key]");
3873
- return new Map(Array.from(rows, (element) => [element.dataset.rowKey, {
3874
- element,
3875
- rect: element.getBoundingClientRect(),
3876
- opacity: this.movements.has(element) ? Number(getComputedStyle(element).opacity) : 1
3877
- }]));
3437
+ if (settings.showRunningInBadge) {
3438
+ if (s.running > 0) parts.push(`${s.running} ${t("status.running")}`);
3439
+ if (s.subagents > 0) parts.push(t(s.subagents === 1 ? "status.subagentsRunning.one" : "status.subagentsRunning.other", { n: s.subagents }));
3878
3440
  }
3879
- move(element, keyframes, duration) {
3880
- const animation = element.animate(keyframes, {
3881
- duration,
3882
- easing: "ease-out"
3441
+ parts.push(...facetLines(s, t, now));
3442
+ if (settings.showCompletedInBadge && s.done > 0) parts.push(`${s.done} ${t("status.completed")}`);
3443
+ return parts.join(" · ");
3444
+ }
3445
+ /**
3446
+ * 每个外部状态来源的明细行,**带它自己的图标**(#24)。
3447
+ *
3448
+ * 徽标侧画的是 provider 交出的、已含色档的元素(`rows/GroupBadge.tsx` 的 `facetGlyph`),
3449
+ * 明细侧此前只画一个统一的 `StateDot`,于是同一条状态、同一次悬停里,徽标是红色沙漏、
3450
+ * 明细是黄点——同一行的两个视图各说一套。宿主 `StateDot` 只有 warning / ongoing / done
3451
+ * 三档、没有 error 档,所以色档**只能**由来源自己的图标表达,这也是本函数存在的理由。
3452
+ *
3453
+ * provider 没交图标时退回通用点(与徽标侧同一套兜底),陌生来源仍然可见。
3454
+ *
3455
+ * @returns 明细行;`glyph` 为 `undefined` 表示该行用 `StateDot` 渲染。
3456
+ */
3457
+ function facetDetailLines(s, t, now) {
3458
+ const translate = asFacetTranslate(t);
3459
+ const lines = [];
3460
+ for (const entry of s.facets.values()) {
3461
+ const raw = entry.facet.label?.(entry.fold, {
3462
+ now,
3463
+ t: translate
3464
+ });
3465
+ const text = raw === void 0 || raw === "" ? `${entry.fold.count} ${t("status.needsAttention")}` : raw;
3466
+ lines.push({
3467
+ state: "warning",
3468
+ text,
3469
+ glyph: facetGlyph(entry, t, now)
3883
3470
  });
3884
- this.movements.set(element, animation);
3885
- animation.onfinish = () => {
3886
- this.movements.delete(element);
3887
- animation.cancel();
3888
- };
3889
- }
3890
- cancelMovements() {
3891
- for (const animation of this.movements.values()) {
3892
- animation.onfinish = null;
3893
- animation.cancel();
3894
- }
3895
- this.movements.clear();
3896
- }
3897
- removeExit(key) {
3898
- const exit = this.exits.get(key);
3899
- if (exit === void 0) return;
3900
- exit.animation.onfinish = null;
3901
- exit.animation.cancel();
3902
- exit.element.remove();
3903
- this.exits.delete(key);
3904
3471
  }
3905
- clear() {
3906
- this.cancelMovements();
3907
- for (const key of this.exits.keys()) this.removeExit(key);
3472
+ return lines;
3473
+ }
3474
+ /**
3475
+ * Build the status detail lines for the workspace hover card.
3476
+ *
3477
+ * Extracted so `WorkspaceHoverContent` in `Rows.tsx` only needs to map the returned
3478
+ * array — the fork-specific status→line logic lives here.
3479
+ *
3480
+ * 按设置过滤:关掉的来源不出现在明细行里;`showHoverDetails=false` 时整段返回空。
3481
+ * 明细行的顺序与徽标**完全一致**(外部来源排在会话状态之后、已完成之前):两处顺序
3482
+ * 不一样会让人怀疑它们是两套事实。
3483
+ */
3484
+ function hoverStatusLines(status, t, settings, now = Date.now()) {
3485
+ const lines = [];
3486
+ if (status === void 0 || !settings.showHoverDetails) return lines;
3487
+ if (settings.showPendingInBadge) {
3488
+ if (status.approval > 0) lines.push({
3489
+ state: "warning",
3490
+ text: `${status.approval} ${t("status.waitingApproval")}`
3491
+ });
3492
+ if (status.planReview > 0) lines.push({
3493
+ state: "warning",
3494
+ text: `${status.planReview} ${t("status.planReview")}`
3495
+ });
3496
+ if (status.question > 0) lines.push({
3497
+ state: "warning",
3498
+ text: `${status.question} ${t("status.waitingAnswer")}`
3499
+ });
3908
3500
  }
3909
- render() {
3910
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3911
- ref: this.list,
3912
- className: this.props.className,
3913
- role: "tree",
3914
- "aria-label": this.props.label,
3915
- onPointerDownCapture: () => {
3916
- this.armed = true;
3917
- },
3918
- onKeyDownCapture: () => {
3919
- this.armed = true;
3920
- },
3921
- children: this.props.children
3922
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3923
- ref: this.overlay,
3924
- className: AnimatedRows_module_css_default.exits,
3925
- "aria-hidden": "true"
3926
- })] });
3501
+ if (settings.showRunningInBadge) {
3502
+ if (status.running > 0) lines.push({
3503
+ state: "ongoing",
3504
+ text: `${status.running} ${t("status.running")}`
3505
+ });
3506
+ if (status.subagents > 0) lines.push({
3507
+ state: "ongoing",
3508
+ text: t(status.subagents === 1 ? "status.subagentsRunning.one" : "status.subagentsRunning.other", { n: status.subagents })
3509
+ });
3927
3510
  }
3928
- };
3511
+ lines.push(...facetDetailLines(status, t, now));
3512
+ if (settings.showCompletedInBadge && status.done > 0) lines.push({
3513
+ state: "done",
3514
+ text: `${status.done} ${t("status.completed")}`
3515
+ });
3516
+ return lines;
3517
+ }
3518
+ /**
3519
+ * 某一条外部来源的元素该带哪只 class(#22)。
3520
+ *
3521
+ * 脉冲判据仍由各 facet 自己声明(`FacetFold.pulse`),但**作用对象必须下沉到声明它的那个
3522
+ * 元素**。此前 `groupBadgeIsUrgent()` 只返回一个整徽标的布尔值、class 加在容器上,于是同组
3523
+ * 里任何一家声明 `pulse: true` 都会让别家本该静止的图标跟着呼吸:`pulse: false` 是否生效,
3524
+ * 取决于同组里恰好有没有别人开了闪。判断交出去了,作用对象没交出去——这是修过一次的回归
3525
+ * (见下)。
3526
+ *
3527
+ * 保留这条历史的原因写在所有者那边:dsh-gitea-dispatch 把 `overdue` 画成**静止**(其
3528
+ * `lib/waiting-glyph.js`:「超时后停动画:静止即不再期待」),且它只代表 CI 窗口到期、
3529
+ * 不代表评审失败。同一条等待在行内静止、在父级闪动,就是同一事实的两种说法。
3530
+ */
3531
+ function facetPulseClass(entry, urgentClass) {
3532
+ return entry.fold.pulse ? urgentClass : void 0;
3533
+ }
3534
+ /**
3535
+ * 一个来源的图标。
3536
+ *
3537
+ * 归 provider 画(`facet.renderIcon`):「这个状态长什么样」是它的知识。没实现时本插件
3538
+ * 按色档退回一个通用点(见 `rows/GroupBadge.tsx`),保证陌生来源仍然可见、只是朴素。
3539
+ */
3540
+ function facetGlyph(entry, t, now) {
3541
+ return entry.facet.renderIcon?.(entry.fold, {
3542
+ now,
3543
+ t: asFacetTranslate(t)
3544
+ });
3545
+ }
3929
3546
  //#endregion
3930
- //#region \0dsh-css:src/client/WorkspacePicker.module.css.mjs
3931
- const css$1 = ".S_JYgq_modalAction{min-width:72px}.S_JYgq_modalError,.S_JYgq_menuStatus{margin-top:8px;font-size:12px;line-height:18px}.S_JYgq_modalError{color:var(--dsw-alias-state-error-primary)}.S_JYgq_menuStatus{color:var(--dsw-alias-label-secondary)}";
3932
- const tagId$1 = "dsh-group-status/WorkspacePicker.module.css";
3547
+ //#region \0dsh-css:src/client/rows/Rows.module.css.mjs
3548
+ const css$1 = "._1Jb0BW_projectRow,._1Jb0BW_sessionRow{border-radius:var(--dsw-radius-md);padding:0 8px;cursor:pointer;user-select:none;color:var(--dsw-alias-label-primary);align-items:center;gap:6px;padding-inline-start:calc(8px + var(--dsh-workspace-indent,0px));display:flex}._1Jb0BW_projectRow:hover,._1Jb0BW_sessionRow:hover,._1Jb0BW_sessionRow._1Jb0BW_selected{background:var(--dsw-alias-interactive-bg-hover)}._1Jb0BW_searchResultRow{box-sizing:border-box;border-radius:var(--dsw-radius-lg);cursor:pointer;text-align:left;width:100%;min-height:48px;color:var(--dsw-alias-label-primary);background:0 0;border:none;flex-direction:column;align-items:stretch;padding:4px 8px;display:flex}._1Jb0BW_searchResultRow:hover,._1Jb0BW_searchResultRow._1Jb0BW_selected{background:var(--dsw-alias-interactive-bg-hover)}._1Jb0BW_searchResultHeading{align-items:center;min-width:0;display:flex}._1Jb0BW_searchResultTitle{text-overflow:ellipsis;white-space:nowrap;flex:0 auto;min-width:0;margin-left:4px;font-size:14px;line-height:20px;overflow:hidden}._1Jb0BW_searchResultMeta{align-items:center;gap:6px;min-width:0;margin-left:20px;display:flex}._1Jb0BW_searchResultWorkspace,._1Jb0BW_searchResultSnippet{text-overflow:ellipsis;white-space:nowrap;font-size:12px;line-height:17px;overflow:hidden}._1Jb0BW_searchResultWorkspace{max-width:40%;color:var(--dsw-alias-label-tertiary);flex:none}._1Jb0BW_searchResultWorkspace:only-child{max-width:100%}._1Jb0BW_searchResultSnippet{min-width:0;color:var(--dsw-alias-label-secondary);flex:1}._1Jb0BW_projectRow{box-sizing:border-box;align-items:center;height:34px}._1Jb0BW_projectRow ._1Jb0BW_rowActions{height:20px}._1Jb0BW_sessionRow{gap:0;height:32px}._1Jb0BW_sessionRow ._1Jb0BW_title{margin:0 6px 0 4px}._1Jb0BW_slot{width:16px;height:20px;color:var(--dsw-alias-label-tertiary);flex:none;justify-content:center;align-items:center;display:inline-flex}._1Jb0BW_visuallyHidden{clip:rect(0 0 0 0);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}._1Jb0BW_folderActive{color:var(--dsw-alias-state-business-primary)}._1Jb0BW_projectRow ._1Jb0BW_chevron{display:none}._1Jb0BW_projectRow:hover ._1Jb0BW_chevron{display:inline-flex}._1Jb0BW_projectRow:hover ._1Jb0BW_folder{display:none}._1Jb0BW_arrow{transition:transform .15s var(--ds-ease-in-out)}._1Jb0BW_arrowOpen{transform:rotate(90deg)}._1Jb0BW_projectText{flex-direction:column;flex:1;gap:2px;min-width:0;display:flex}._1Jb0BW_title{text-overflow:ellipsis;white-space:nowrap;min-width:0;font-size:14px;line-height:20px;overflow:hidden}._1Jb0BW_renameInput{border:.5px solid var(--dsw-alias-border-l4);border-radius:var(--dsw-radius-sm);background:var(--dsw-alias-button-elevated-fill);min-width:0;color:inherit;outline:none;padding:0 2px;font-size:14px;line-height:20px}._1Jb0BW_sessionRow ._1Jb0BW_title{flex:1}._1Jb0BW_sessionRow ._1Jb0BW_title[data-scrolled]{mask-image:linear-gradient(90deg,#0000,#000 12px)}._1Jb0BW_sessionRow ._1Jb0BW_title[data-clipped]{mask-image:linear-gradient(270deg,#0000,#000 12px)}._1Jb0BW_sessionRow ._1Jb0BW_title[data-scrolled][data-clipped]{mask-image:linear-gradient(90deg,#0000,#000 12px calc(100% - 12px),#0000)}._1Jb0BW_meta{text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:20px;overflow:hidden}._1Jb0BW_time{color:var(--dsw-alias-label-tertiary);flex:none;font-size:10px;line-height:16px}._1Jb0BW_pinIndicator{width:16px;height:20px;color:var(--dsw-alias-label-caption);flex:none;justify-content:center;align-items:center;margin-left:6px;display:inline-flex}._1Jb0BW_sessionRow._1Jb0BW_archived ._1Jb0BW_title,._1Jb0BW_searchResultRow._1Jb0BW_archived ._1Jb0BW_searchResultTitle,._1Jb0BW_searchResultRow._1Jb0BW_archived ._1Jb0BW_searchResultWorkspace,._1Jb0BW_searchResultRow._1Jb0BW_archived ._1Jb0BW_searchResultSnippet{color:var(--dsw-alias-label-caption)}._1Jb0BW_dot{flex:none}._1Jb0BW_rowActions{flex:none;align-items:center;gap:10px;display:none}._1Jb0BW_projectRow:hover ._1Jb0BW_rowActions,._1Jb0BW_sessionRow:hover ._1Jb0BW_rowActions,._1Jb0BW_searchResultRow:hover ._1Jb0BW_rowActions,._1Jb0BW_projectRow._1Jb0BW_menuOpen ._1Jb0BW_rowActions,._1Jb0BW_sessionRow._1Jb0BW_menuOpen ._1Jb0BW_rowActions{display:inline-flex}._1Jb0BW_searchResultHeading ._1Jb0BW_rowActions{margin-left:auto}._1Jb0BW_sessionRow:hover ._1Jb0BW_time,._1Jb0BW_sessionRow._1Jb0BW_menuOpen ._1Jb0BW_time,._1Jb0BW_sessionRow:hover ._1Jb0BW_pinIndicator,._1Jb0BW_sessionRow._1Jb0BW_menuOpen ._1Jb0BW_pinIndicator{display:none}@media (hover:hover){._1Jb0BW_sessionRow:hover ._1Jb0BW_title,._1Jb0BW_sessionRow._1Jb0BW_menuOpen ._1Jb0BW_title{text-overflow:clip}}._1Jb0BW_projectRow._1Jb0BW_menuOpen,._1Jb0BW_sessionRow._1Jb0BW_menuOpen{background:var(--dsw-alias-interactive-bg-hover)}._1Jb0BW_sessionRow._1Jb0BW_dropBefore,._1Jb0BW_sessionRow._1Jb0BW_dropAfter{position:relative}._1Jb0BW_sessionRow._1Jb0BW_dropBefore:before,._1Jb0BW_sessionRow._1Jb0BW_dropAfter:after{content:\"\";z-index:1;background:linear-gradient(55deg, transparent calc(50% - 1px), var(--dsw-alias-state-business-primary) calc(50% - 1px) calc(50% + 1px), transparent calc(50% + 1px)) 0 0 / 5px 7px no-repeat, linear-gradient(125deg, transparent calc(50% - 1px), var(--dsw-alias-state-business-primary) calc(50% - 1px) calc(50% + 1px), transparent calc(50% + 1px)) 0 5px / 5px 7px no-repeat, linear-gradient(var(--dsw-alias-state-business-primary) 0 0) 4px 5px / calc(100% - 4px) 2px no-repeat;pointer-events:none;height:12px;position:absolute;left:0;right:4px}._1Jb0BW_sessionRow._1Jb0BW_dropBefore:before{top:-7px}._1Jb0BW_sessionRow._1Jb0BW_dropAfter:after{bottom:-7px}._1Jb0BW_hoverContent{flex-direction:column;gap:8px;display:flex}._1Jb0BW_hoverTitle{color:var(--dsw-alias-label-primary);overflow-wrap:break-word;font-size:14px;line-height:20px}._1Jb0BW_hoverPath{color:var(--dsw-alias-label-secondary);word-break:break-all;font-size:12px;line-height:16px}._1Jb0BW_hoverTime{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:16px}._1Jb0BW_hoverStatus{color:var(--dsw-alias-label-tertiary);align-items:center;gap:8px;font-size:12px;line-height:20px;display:flex}._1Jb0BW_groupBadge{flex:none;align-items:center;gap:3px;margin-inline-start:4px;display:inline-flex}._1Jb0BW_groupBadgeCount{color:var(--dsw-alias-label-tertiary);border:1px solid;border-radius:6px;padding:0 3px;font-size:9px;line-height:12px}@keyframes _1Jb0BW_groupBadgePulse{0%,to{opacity:1}50%{opacity:.6}}._1Jb0BW_groupBadgeUrgent{animation:2s ease-in-out infinite _1Jb0BW_groupBadgePulse}._1Jb0BW_hoverArchived svg{flex-shrink:0;margin:0 -4px}._1Jb0BW_iconButton{border-radius:var(--dsw-radius-xs);cursor:pointer;width:16px;height:16px;color:var(--dsw-alias-label-tertiary);background:0 0;border:none;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex}._1Jb0BW_iconButton:hover{color:var(--dsw-alias-label-primary)}._1Jb0BW_chevron{color:var(--dsw-alias-label-caption)}@media (prefers-reduced-motion:reduce){._1Jb0BW_arrow{transition:none;animation:none}}";
3549
+ const tagId$1 = "dsh-group-status/Rows.module.css";
3933
3550
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
3934
3551
  const tag = document.createElement("style");
3935
3552
  tag.dataset.plugin = "dsh-group-status";
@@ -3937,416 +3554,871 @@ window.__ModuleLoader__.load({
3937
3554
  tag.textContent = css$1;
3938
3555
  document.head.appendChild(tag);
3939
3556
  }
3940
- var WorkspacePicker_module_css_default = {
3941
- "menuStatus": "S_JYgq_menuStatus",
3942
- "modalAction": "S_JYgq_modalAction",
3943
- "modalError": "S_JYgq_modalError"
3944
- };
3945
- //#endregion
3946
- //#region src/client/WorkspacePicker.tsx
3947
- const ADD_WORKSPACE = "::add-workspace";
3948
- /**
3949
- * Render the pick menu plus the adoption error dialog.
3950
- * @param props - owner-controlled flow props.
3951
- * @returns menu + dialog elements.
3952
- */
3953
- function WorkspacePickFlow({ t, open, anchorRef, useWorkspaces, createWorkspace, useDirectoryFlow, renderDirectoryFlow, onPick, onClose, addOnly = false, onBusyChange, side = "bottom", selectedId }) {
3954
- const workspaceSnapshot = useWorkspaces((state) => state);
3955
- const workspaces = workspaceSnapshot.items;
3956
- const getAnchorRect = (0, react.useCallback)(() => anchorRef?.current?.getBoundingClientRect() ?? null, [anchorRef]);
3957
- const [errorOpen, setErrorOpen] = (0, react.useState)(false);
3958
- const [modalError, setModalError] = (0, react.useState)(null);
3959
- const [flowOpen, setFlowOpen] = (0, react.useState)(false);
3960
- const [pickingFolder, setPickingFolder] = (0, react.useState)(false);
3961
- const flowBusy = flowOpen || pickingFolder;
3962
- (0, react.useEffect)(() => {
3963
- onBusyChange?.(flowBusy);
3964
- }, [flowBusy, onBusyChange]);
3965
- const flowAvailable = useDirectoryFlow((occupied) => occupied);
3966
- (0, react.useEffect)(() => {
3967
- if (flowOpen && !flowAvailable) setFlowOpen(false);
3968
- }, [flowOpen, flowAvailable]);
3969
- const addEntries = flowAvailable ? [{
3970
- id: ADD_WORKSPACE,
3971
- label: t("menu.addWorkspace"),
3972
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPlusOutlineRegular, { size: 16 }),
3973
- disabled: flowBusy
3974
- }] : [];
3975
- const pinAdd = !addOnly && workspaces.length > 0;
3976
- const items = pinAdd ? workspaces.map((workspace) => ({
3977
- id: workspace.workspaceId,
3978
- label: workspaceDisplayTitle(workspace.title, t("workspace.defaultName")),
3979
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderCloseRegular, { size: 16 }),
3980
- disabled: flowBusy
3981
- })) : addEntries;
3982
- const menuIsEmpty = items.length === 0;
3983
- const closeModal = () => {
3984
- setErrorOpen(false);
3985
- setModalError(null);
3986
- };
3987
- /** Adopt a picked directory; failures land in the folder-error dialog (Choose again reopens the flow). */
3988
- const adoptDirectory = (path) => createWorkspace({ path }).then((workspace) => {
3989
- setFlowOpen(false);
3990
- onPick(workspace.workspaceId);
3991
- }).catch((reason) => {
3992
- setModalError(reason instanceof Error ? reason.message : String(reason));
3993
- setFlowOpen(false);
3994
- setErrorOpen(true);
3557
+ var Rows_module_css_default = {
3558
+ "archived": "_1Jb0BW_archived",
3559
+ "arrow": "_1Jb0BW_arrow",
3560
+ "arrowOpen": "_1Jb0BW_arrowOpen",
3561
+ "chevron": "_1Jb0BW_chevron",
3562
+ "dot": "_1Jb0BW_dot",
3563
+ "dropAfter": "_1Jb0BW_dropAfter",
3564
+ "dropBefore": "_1Jb0BW_dropBefore",
3565
+ "folder": "_1Jb0BW_folder",
3566
+ "folderActive": "_1Jb0BW_folderActive",
3567
+ "groupBadge": "_1Jb0BW_groupBadge",
3568
+ "groupBadgeCount": "_1Jb0BW_groupBadgeCount",
3569
+ "groupBadgePulse": "_1Jb0BW_groupBadgePulse",
3570
+ "groupBadgeUrgent": "_1Jb0BW_groupBadgeUrgent",
3571
+ "hoverArchived": "_1Jb0BW_hoverArchived",
3572
+ "hoverContent": "_1Jb0BW_hoverContent",
3573
+ "hoverPath": "_1Jb0BW_hoverPath",
3574
+ "hoverStatus": "_1Jb0BW_hoverStatus",
3575
+ "hoverTime": "_1Jb0BW_hoverTime",
3576
+ "hoverTitle": "_1Jb0BW_hoverTitle",
3577
+ "iconButton": "_1Jb0BW_iconButton",
3578
+ "menuOpen": "_1Jb0BW_menuOpen",
3579
+ "meta": "_1Jb0BW_meta",
3580
+ "pinIndicator": "_1Jb0BW_pinIndicator",
3581
+ "projectRow": "_1Jb0BW_projectRow",
3582
+ "projectText": "_1Jb0BW_projectText",
3583
+ "renameInput": "_1Jb0BW_renameInput",
3584
+ "rowActions": "_1Jb0BW_rowActions",
3585
+ "searchResultHeading": "_1Jb0BW_searchResultHeading",
3586
+ "searchResultMeta": "_1Jb0BW_searchResultMeta",
3587
+ "searchResultRow": "_1Jb0BW_searchResultRow",
3588
+ "searchResultSnippet": "_1Jb0BW_searchResultSnippet",
3589
+ "searchResultTitle": "_1Jb0BW_searchResultTitle",
3590
+ "searchResultWorkspace": "_1Jb0BW_searchResultWorkspace",
3591
+ "selected": "_1Jb0BW_selected",
3592
+ "sessionRow": "_1Jb0BW_sessionRow",
3593
+ "slot": "_1Jb0BW_slot",
3594
+ "time": "_1Jb0BW_time",
3595
+ "title": "_1Jb0BW_title",
3596
+ "visuallyHidden": "_1Jb0BW_visuallyHidden"
3597
+ };
3598
+ //#endregion
3599
+ //#region src/client/rows/GroupBadge.tsx
3600
+ /**
3601
+ * Aggregated live-status badge for a folded folder row. Dots reuse StateDot so
3602
+ * colors match session rows (warning = pending, ongoing = running, done = completed).
3603
+ * Renders nothing while expanded or when nothing is pending/active/completed or any
3604
+ * status source reports a contribution.
3605
+ *
3606
+ * Count matches the dots: host-side sessions plus every registered facet's own count.
3607
+ *
3608
+ * `settings` 只过滤**本插件自己的**三档会话状态:关掉的档位不画点、不计数、不进
3609
+ * aria-label、不参与「是否显示徽标」的判断。会话行自身的状态点不受影响:那是行级状态,
3610
+ * 不是分组提示。**外部来源在这里没有开关可关** —— 它们要么注册了(一定显示),要么没
3611
+ * 注册(根本不在登记面上)。要关某个来源,去那个插件自己的设置卡片:显隐是来源的决定。
3612
+ *
3613
+ * `now` 缺省取渲染时刻(与 `WorkspaceBrowser` 为相对时间读的时钟同一姿态):文案里的
3614
+ * 剩余时长以此计算,只在提供方 store 通知时随渲染更新。
3615
+ */
3616
+ function groupLiveBadge(group, t, settings, now = Date.now()) {
3617
+ const s = group.liveStatus;
3618
+ if (group.expanded || s === void 0) return null;
3619
+ const pending = settings.showPendingInBadge ? s.approval + s.planReview + s.question : 0;
3620
+ const active = settings.showRunningInBadge ? s.running + s.subagents : 0;
3621
+ const completed = settings.showCompletedInBadge ? s.done : 0;
3622
+ const facetTotal = facetCountTotal(s);
3623
+ if (pending === 0 && active === 0 && completed === 0 && facetTotal === 0) return null;
3624
+ const label = groupLiveLabel(s, t, settings, now);
3625
+ const dots = [];
3626
+ 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"));
3627
+ 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"));
3628
+ const urgentClass = Rows_module_css_default.groupBadgeUrgent ?? "";
3629
+ for (const entry of s.facets.values()) {
3630
+ const glyph = facetGlyph(entry, t, now);
3631
+ const pulseClass = facetPulseClass(entry, urgentClass) || void 0;
3632
+ dots.push(glyph === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: entry.fold.tone === "info" ? "ongoing" : "warning" }, entry.facet.id) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3633
+ className: pulseClass,
3634
+ children: glyph
3635
+ }, entry.facet.id));
3636
+ }
3637
+ 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"));
3638
+ const count = pending + active + completed + facetTotal;
3639
+ const countLabel = count > 99 ? "99+" : count >= settings.countThreshold ? String(count) : "";
3640
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3641
+ className: Rows_module_css_default.groupBadge,
3642
+ role: "img",
3643
+ "aria-label": label,
3644
+ title: label,
3645
+ children: [dots, countLabel && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3646
+ className: Rows_module_css_default.groupBadgeCount,
3647
+ children: countLabel
3648
+ })]
3995
3649
  });
3996
- const openDirectoryFlow = (0, react.useCallback)(() => {
3997
- onClose();
3998
- setErrorOpen(false);
3999
- setModalError(null);
4000
- setFlowOpen(true);
4001
- }, [onClose]);
4002
- const listSettled = addOnly || workspaceSnapshot.phase === "ready";
4003
- const addIsTheOnlyEntry = !pinAdd && listSettled && addEntries.length === 1;
4004
- (0, react.useEffect)(() => {
4005
- if (open && addIsTheOnlyEntry && !flowBusy) openDirectoryFlow();
4006
- }, [
4007
- open,
4008
- addIsTheOnlyEntry,
4009
- flowBusy,
4010
- openDirectoryFlow
4011
- ]);
4012
- /** Owner side of the flow conversation: adopt keeps the flow open (busy) until the Host answers. */
4013
- const flowOwner = {
4014
- open: flowOpen,
4015
- busy: pickingFolder,
4016
- onPicked: (path) => {
4017
- setPickingFolder(true);
4018
- adoptDirectory(path).finally(() => {
4019
- setPickingFolder(false);
4020
- });
4021
- },
4022
- onCancel: () => {
4023
- setFlowOpen(false);
3650
+ }
3651
+ //#endregion
3652
+ //#region src/client/rows/Rows.tsx
3653
+ /**
3654
+ * Workspace browser tree row components (figma Cell set 14:3080): pure presentational —
3655
+ * all data and callbacks arrive via props. Hover swaps (folder->chevron,
3656
+ * time->ellipsis, action buttons) are CSS-only, and a session row's clipped
3657
+ * title marquees programmatically while the row is hovered. Workspace row
3658
+ * menus are visual-only except Rename/Delete. A Session row's "..." menu and
3659
+ * its hover buttons are the `sidebar.workspaces.session.menu.item` and
3660
+ * `sidebar.workspaces.session.row.action` lists, rendered through the
3661
+ * browser's `renderSlot` with the menu's open state as the occurrence's hook
3662
+ * context; this package's own actions are entries like any plugin's. The
3663
+ * session and workspace hover cards are suppressed while a menu is open.
3664
+ */
3665
+ /** Row display title: blank rows show the localized New Session label. */
3666
+ function displayTitle(node, t) {
3667
+ return node.blank ? t("session.new") : node.title || t("session.untitled");
3668
+ }
3669
+ const MIN_TITLE_REVEAL_PX = 8;
3670
+ const TITLE_MARQUEE_PX_PER_MS = .03;
3671
+ /**
3672
+ * Place the title's scroll position and publish the stylesheet's fade-mask
3673
+ * hooks: `data-scrolled` while the title has left its start (left fade) and
3674
+ * `data-clipped` while text remains beyond the right edge (right fade).
3675
+ * @param title - the row's clipping title element.
3676
+ * @param left - scroll offset in CSS pixels.
3677
+ * @param range - the title's maximum scroll offset in CSS pixels.
3678
+ */
3679
+ function placeTitle(title, left, range) {
3680
+ if (typeof title.scrollTo === "function") title.scrollTo({
3681
+ left,
3682
+ behavior: "instant"
3683
+ });
3684
+ else title.scrollLeft = left;
3685
+ if (left > 0) title.dataset.scrolled = "";
3686
+ else delete title.dataset.scrolled;
3687
+ if (left < range) title.dataset.clipped = "";
3688
+ else delete title.dataset.clipped;
3689
+ }
3690
+ /**
3691
+ * Return the title to its resting state: scrolled to the start with both fade
3692
+ * masks off, so the resting ellipsis renders at full strength.
3693
+ * @param title - the row's clipping title element.
3694
+ */
3695
+ function restTitle(title) {
3696
+ if (typeof title.scrollTo === "function") title.scrollTo({
3697
+ left: 0,
3698
+ behavior: "instant"
3699
+ });
3700
+ else title.scrollLeft = 0;
3701
+ delete title.dataset.scrolled;
3702
+ delete title.dataset.clipped;
3703
+ }
3704
+ /**
3705
+ * Marquee a title wider than its one-line cell while its row is hovered: the
3706
+ * title clips its own text, so entering crawls it at a constant speed until the
3707
+ * far edge (a fork's incremented title, for example) is in view, then rests
3708
+ * there under the pointer. Overflow of at most {@link MIN_TITLE_REVEAL_PX}
3709
+ * stays put — a barely-clipped title moving a few pixels reads as jitter, not a
3710
+ * reveal. Leaving returns the title to the start in one step, because the
3711
+ * resting ellipsis and the narrowed cell would otherwise meet the text while it
3712
+ * travelled back. Reduced motion jumps to the far edge instead of crawling.
3713
+ * @param title - ref to the row's clipping title element.
3714
+ * @returns stable pointer enter/leave handlers for the row.
3715
+ */
3716
+ function useTitleMarquee(title) {
3717
+ const frame = (0, react.useRef)(0);
3718
+ (0, react.useEffect)(() => () => {
3719
+ cancelAnimationFrame(frame.current);
3720
+ }, []);
3721
+ return (0, react.useMemo)(() => ({
3722
+ enter: () => {
3723
+ /* v8 ignore next -- defensive: the title span renders unconditionally. */
3724
+ if (title.current === null) return;
3725
+ const element = title.current;
3726
+ const range = element.scrollWidth - element.clientWidth;
3727
+ if (range <= MIN_TITLE_REVEAL_PX) return;
3728
+ if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
3729
+ placeTitle(element, range, range);
3730
+ return;
3731
+ }
3732
+ cancelAnimationFrame(frame.current);
3733
+ let previous;
3734
+ let position = 0;
3735
+ const step = (now) => {
3736
+ position += previous === void 0 ? 0 : (now - previous) * TITLE_MARQUEE_PX_PER_MS;
3737
+ previous = now;
3738
+ placeTitle(element, Math.min(position, range), range);
3739
+ if (position < range) frame.current = requestAnimationFrame(step);
3740
+ };
3741
+ frame.current = requestAnimationFrame(step);
4024
3742
  },
4025
- onError: (message) => {
4026
- setFlowOpen(false);
4027
- setModalError(message);
4028
- setErrorOpen(true);
4029
- }
4030
- };
4031
- const handleSelect = (id) => {
4032
- if (id === ADD_WORKSPACE) {
4033
- openDirectoryFlow();
4034
- return;
3743
+ leave: () => {
3744
+ cancelAnimationFrame(frame.current);
3745
+ /* v8 ignore next -- defensive: the title span renders unconditionally. */
3746
+ if (title.current === null) return;
3747
+ restTitle(title.current);
4035
3748
  }
4036
- onPick(id);
4037
- };
4038
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
4039
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
4040
- open: open && !addIsTheOnlyEntry && !menuIsEmpty,
4041
- anchor: null,
4042
- items,
4043
- ...pinAdd ? { footer: addEntries } : {},
4044
- selectedId,
4045
- onSelect: handleSelect,
4046
- onClose,
4047
- side,
4048
- portal: true,
4049
- getAnchorRect
4050
- }),
4051
- open && !addIsTheOnlyEntry && !menuIsEmpty && workspaceSnapshot.phase === "pending" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4052
- className: WorkspacePicker_module_css_default.menuStatus,
4053
- role: "status",
4054
- children: t("picker.loading")
4055
- }),
4056
- renderDirectoryFlow(flowOwner),
4057
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
4058
- open: errorOpen,
4059
- onClose: closeModal,
4060
- closeLabel: t("close"),
4061
- title: t("folderError.title"),
4062
- footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
4063
- variant: "outline",
4064
- className: WorkspacePicker_module_css_default.modalAction,
4065
- onClick: closeModal,
4066
- children: t("cancel")
4067
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
4068
- variant: "primary",
4069
- className: WorkspacePicker_module_css_default.modalAction,
4070
- disabled: !flowAvailable,
4071
- onClick: openDirectoryFlow,
4072
- children: t("folderError.retry")
4073
- })] }),
4074
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4075
- className: WorkspacePicker_module_css_default.modalError,
4076
- role: "alert",
4077
- children: modalError
3749
+ }), [title]);
3750
+ }
3751
+ /** Localized compact relative time ("刚刚"/"5分钟" in zh, "now"/"5min" in en). */
3752
+ function timeLabel(updatedAt, now, t) {
3753
+ const { unit, n } = (0, _deepseek_ai_dsh_client_ui_primitives.relativeTime)(updatedAt, now);
3754
+ return unit === "now" ? t("time.now") : t(`time.${unit}`, { n });
3755
+ }
3756
+ /** Hover-card variant: distances wrap in the ago template; the now bucket stays bare (no "now ago"). */
3757
+ function hoverTimeLabel(updatedAt, now, t) {
3758
+ const { unit, n } = (0, _deepseek_ai_dsh_client_ui_primitives.relativeTime)(updatedAt, now);
3759
+ return unit === "now" ? t("time.now") : t("time.ago", { t: t(`time.${unit}`, { n }) });
3760
+ }
3761
+ /**
3762
+ * Absolute creation time through the dictionary's date template (the message
3763
+ * clock pattern): `toLocaleString` would follow the browser language, not the
3764
+ * app locale, and produce mixed-language text after a switch.
3765
+ */
3766
+ function createdLabel(createdAt, t) {
3767
+ const d = new Date(createdAt);
3768
+ const pad2 = (v) => String(v).padStart(2, "0");
3769
+ return t("hover.created", { time: `${t("date.ymd", {
3770
+ y: d.getFullYear(),
3771
+ m: d.getMonth() + 1,
3772
+ d: d.getDate()
3773
+ })} ${pad2(d.getHours())}:${pad2(d.getMinutes())}` });
3774
+ }
3775
+ /** Hover-card body: workspace title, display directory path, absolute creation time. */
3776
+ function WorkspaceHoverContent({ label, cwd, createdAt, status, settings, now, t }) {
3777
+ const lines = hoverStatusLines(status, t, settings, now);
3778
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3779
+ className: Rows_module_css_default.hoverContent,
3780
+ children: [
3781
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3782
+ className: Rows_module_css_default.hoverTitle,
3783
+ children: label
3784
+ }),
3785
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3786
+ className: Rows_module_css_default.hoverPath,
3787
+ children: cwd
3788
+ }),
3789
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3790
+ className: Rows_module_css_default.hoverTime,
3791
+ children: createdLabel(createdAt, t)
3792
+ }),
3793
+ lines.map((line) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3794
+ className: Rows_module_css_default.hoverStatus,
3795
+ children: [line.glyph ?? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: line.state }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: line.text })]
3796
+ }, line.text))
3797
+ ]
3798
+ });
3799
+ }
3800
+ /** Pointer-position half of a row (insert line above or below). */
3801
+ function rowHalf(e) {
3802
+ const rect = e.currentTarget.getBoundingClientRect();
3803
+ return e.clientY < rect.top + rect.height / 2 ? "before" : "after";
3804
+ }
3805
+ /**
3806
+ * Project (workspace) header row: folder + title;
3807
+ * hover reveals the chevron and create button, and dwelling on a real
3808
+ * Workspace shows its hover card (the ungrouped bucket has none).
3809
+ * `containsCurrent` arrives on the node (derivation fact, no renderer scan).
3810
+ * @param props.group - derived group node.
3811
+ * @param props.containsCurrentDescendant - highlight an ancestor even when its subtree is collapsed.
3812
+ * @param props.onToggle - expand/collapse the group.
3813
+ * @param props.onCreate - start a frontend Session inside this Workspace.
3814
+ * @param props.drag - optional workspace-row drag wiring.
3815
+ * @param props.home - host account home for POSIX hover-path abbreviation.
3816
+ * @param props.t - the browser root's locale seat.
3817
+ * @returns the row element.
3818
+ */
3819
+ function ProjectRowItem({ group, containsCurrentDescendant = false, onToggle, onCreate, actions, drag, home, newShortcut, settings, now, t }) {
3820
+ const row = group;
3821
+ const label = row.workspaceId === void 0 ? t("group.ungrouped") : row.label;
3822
+ const active = containsCurrentDescendant || group.expanded && group.containsCurrent;
3823
+ const [menuOpen, setMenuOpen] = (0, react.useState)(false);
3824
+ const workspaceMenuItems = [{
3825
+ id: "rename",
3826
+ label: t("rename"),
3827
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconEditOutlineRegular, {})
3828
+ }, {
3829
+ id: "delete",
3830
+ label: t("delete.workspace"),
3831
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutlineRegular, {}),
3832
+ danger: true
3833
+ }];
3834
+ const ownRow = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3835
+ className: clsx(Rows_module_css_default.projectRow, menuOpen && Rows_module_css_default.menuOpen),
3836
+ "data-row-key": `workspace:${group.key}`,
3837
+ role: "treeitem",
3838
+ "aria-expanded": row.expanded,
3839
+ onClick: onToggle,
3840
+ draggable: drag !== void 0,
3841
+ onDragStart: drag === void 0 ? void 0 : (e) => {
3842
+ e.dataTransfer.effectAllowed = "move";
3843
+ e.dataTransfer.setData("text/plain", row.key);
3844
+ drag.start();
3845
+ },
3846
+ onDragEnd: drag?.end,
3847
+ children: [
3848
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3849
+ className: clsx(Rows_module_css_default.slot, Rows_module_css_default.folder, active && Rows_module_css_default.folderActive),
3850
+ children: row.expanded ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderOpenRegular, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderCloseRegular, {})
3851
+ }),
3852
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3853
+ className: clsx(Rows_module_css_default.slot, Rows_module_css_default.chevron),
3854
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTriangleRightFillRegular, { className: clsx(Rows_module_css_default.arrow, row.expanded && Rows_module_css_default.arrowOpen) })
3855
+ }),
3856
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3857
+ className: Rows_module_css_default.projectText,
3858
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3859
+ className: Rows_module_css_default.title,
3860
+ children: label
3861
+ })
3862
+ }),
3863
+ groupLiveBadge(group, t, settings, now),
3864
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3865
+ className: Rows_module_css_default.rowActions,
3866
+ children: [actions !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
3867
+ open: menuOpen,
3868
+ onClose: () => {
3869
+ setMenuOpen(false);
3870
+ },
3871
+ items: workspaceMenuItems,
3872
+ onSelect: (id) => {
3873
+ setMenuOpen(false);
3874
+ /* v8 ignore next -- Menu can emit only the rename and delete rows supplied above. */
3875
+ if (id !== "rename" && id !== "delete") return;
3876
+ if (id === "rename") actions.rename();
3877
+ else actions.delete();
3878
+ },
3879
+ portal: true,
3880
+ closeOnPointerLeave: true,
3881
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3882
+ type: "button",
3883
+ className: Rows_module_css_default.iconButton,
3884
+ "aria-label": t("actions.workspace.aria", { name: label }),
3885
+ onClick: (e) => {
3886
+ e.stopPropagation();
3887
+ setMenuOpen((v) => !v);
3888
+ },
3889
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconEllipsisOutlineRegular, {})
3890
+ })
3891
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
3892
+ label: t("actions.newSession"),
3893
+ shortcutKeys: newShortcut?.keys,
3894
+ side: "bottom",
3895
+ align: "end",
3896
+ delayMs: 500,
3897
+ portal: true,
3898
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3899
+ type: "button",
3900
+ className: Rows_module_css_default.iconButton,
3901
+ "aria-keyshortcuts": newShortcut?.aria,
3902
+ "aria-label": t("actions.newSession.aria", { name: label }),
3903
+ onClick: (e) => {
3904
+ e.stopPropagation();
3905
+ onCreate();
3906
+ },
3907
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconNewChatOutlineRegular, {})
3908
+ })
3909
+ })]
4078
3910
  })
4079
- })
4080
- ] });
3911
+ ]
3912
+ });
3913
+ if (row.createdAt === void 0) return ownRow;
3914
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.HoverCard, {
3915
+ anchor: ownRow,
3916
+ content: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceHoverContent, {
3917
+ label: row.label,
3918
+ cwd: row.cwd === void 0 ? void 0 : abbreviateHomePath(row.cwd, home),
3919
+ createdAt: row.createdAt,
3920
+ status: row.expanded ? void 0 : row.liveStatus,
3921
+ settings,
3922
+ now: now ?? Date.now(),
3923
+ t
3924
+ }),
3925
+ openDelayMs: 800,
3926
+ grouped: true,
3927
+ disabled: menuOpen,
3928
+ copyText: row.cwd,
3929
+ copyLabel: t("copy"),
3930
+ copiedLabel: t("hover.copied")
3931
+ });
3932
+ }
3933
+ /* v8 ignore next 3 -- closed-union backstop; only reached if the status is forged */
3934
+ function assertNever(value) {
3935
+ throw new Error(`unknown pending interaction: ${String(value)}`);
4081
3936
  }
4082
3937
  /**
4083
- * The conversation empty-state registration: adapts the owner share to the
4084
- * core flow (all state and semantics live in the flow / the owner).
4085
- * @param props - empty-state slot props (owner share + injected creation callback).
4086
- * @returns the flow element.
3938
+ * Session status presentation; pending interaction is primary and live activity
3939
+ * outranks completion reminders.
4087
3940
  */
4088
- function WorkspacePicker({ open, anchorRef, useWorkspaces, selectedId, onPick, onClose, createWorkspace, useDirectoryFlow, renderSlot, t }) {
4089
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspacePickFlow, {
4090
- t,
4091
- open,
4092
- anchorRef,
4093
- useWorkspaces,
4094
- createWorkspace,
4095
- useDirectoryFlow,
4096
- renderDirectoryFlow: (owner) => renderSlot("conversation.hero.workspace.directoryFlow", owner),
4097
- selectedId,
4098
- onPick,
4099
- onClose
4100
- });
3941
+ function sessionStatuses(node, t) {
3942
+ const subagents = node.runningSubagentCount === 0 ? void 0 : {
3943
+ state: "ongoing",
3944
+ label: t(node.runningSubagentCount === 1 ? "status.subagentsRunning.one" : "status.subagentsRunning.other", { n: node.runningSubagentCount })
3945
+ };
3946
+ let pending;
3947
+ switch (node.pendingInteraction) {
3948
+ case "approval":
3949
+ pending = {
3950
+ state: "warning",
3951
+ label: t("status.waitingApproval"),
3952
+ trailingLabel: t("status.compact.approval")
3953
+ };
3954
+ break;
3955
+ case "plan-review":
3956
+ pending = {
3957
+ state: "warning",
3958
+ label: t("status.planReview"),
3959
+ trailingLabel: t("status.compact.planReview")
3960
+ };
3961
+ break;
3962
+ case "question":
3963
+ pending = {
3964
+ state: "warning",
3965
+ label: t("status.waitingAnswer"),
3966
+ trailingLabel: t("status.compact.answer")
3967
+ };
3968
+ break;
3969
+ case void 0: break;
3970
+ /* v8 ignore next -- closed PendingInteractionStatus union */
3971
+ default: return assertNever(node.pendingInteraction);
3972
+ }
3973
+ if (pending !== void 0) return subagents === void 0 ? [pending] : [pending, subagents];
3974
+ if (node.running) {
3975
+ const primary = {
3976
+ state: "ongoing",
3977
+ label: t("status.running")
3978
+ };
3979
+ return subagents === void 0 ? [primary] : [primary, subagents];
3980
+ }
3981
+ if (subagents !== void 0) return [subagents];
3982
+ if (node.completed) return [{
3983
+ state: "done",
3984
+ label: t("status.completed")
3985
+ }];
3986
+ return [{
3987
+ state: "idle",
3988
+ label: t("status.idle")
3989
+ }];
4101
3990
  }
4102
- //#endregion
4103
- //#region \0dsh-css:src/client/rows/WorkspaceBrowser.module.css.mjs
4104
- const css = ".QAYkWG_root{--dsh-session-list-edge-inset:var(--dsh-sidebar-inline-padding);--dsh-session-list-scrollbar-width:5px;--dsh-session-list-scrollbar-offset:2px;box-sizing:border-box;min-height:0;padding-right:var(--dsh-session-list-edge-inset);flex-direction:column;flex:1;display:flex}.QAYkWG_root.QAYkWG_rail{padding-right:0}.QAYkWG_iconButton{border-radius:var(--dsw-radius-sm);cursor:pointer;width:28px;height:28px;color:var(--dsw-alias-label-secondary);background:0 0;border:none;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex}.QAYkWG_iconButton:focus-visible,.QAYkWG_searchButton:focus-visible,.QAYkWG_clearButton:focus-visible{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:-2px}.QAYkWG_iconButton:hover{background:var(--dsw-alias-interactive-bg-hover)}.QAYkWG_viewOptionsMenu{min-width:200px}.QAYkWG_sectionHeader{box-sizing:border-box;border-radius:var(--dsw-radius-md);height:36px;color:var(--dsw-alias-label-tertiary);flex:none;justify-content:flex-end;align-items:center;gap:4px;margin-bottom:4px;padding-left:4px;display:flex;overflow:hidden}.QAYkWG_root:not(.QAYkWG_rail) .QAYkWG_sectionHeader{margin-top:2px;margin-right:-4px}.QAYkWG_sectionLabel{white-space:nowrap;opacity:1;visibility:visible;min-width:0;max-width:45%;transition:max-width .18s var(--ds-ease-in-out), margin-right .18s var(--ds-ease-in-out), opacity .12s var(--ds-ease-in-out), transform .18s var(--ds-ease-in-out), visibility 0s linear;flex:none;line-height:20px;overflow:hidden}.QAYkWG_sectionLabelHidden{opacity:0;visibility:hidden;max-width:0;margin-right:-4px;transition-delay:0s,0s,0s,0s,.18s;transform:translate(-4px)}.QAYkWG_searchSlot{box-sizing:border-box;min-width:0;max-width:28px;transition:max-width .18s var(--ds-ease-in-out), padding-left .18s var(--ds-ease-in-out);flex:1;align-items:center;margin-left:auto;padding-left:0;display:flex}.QAYkWG_searchSlotExpanded{max-width:100%;padding-left:0}.QAYkWG_headerActions{opacity:1;visibility:visible;max-width:60px;transition:max-width .18s var(--ds-ease-in-out), opacity .12s var(--ds-ease-in-out), transform .18s var(--ds-ease-in-out), visibility 0s linear;flex:none;align-items:center;gap:4px;display:flex;overflow:hidden}.QAYkWG_headerActionsHidden{opacity:0;visibility:hidden;pointer-events:none;max-width:0;transition-delay:0s,0s,0s,.18s;transform:translate(4px)}.QAYkWG_search{box-sizing:border-box;border-radius:var(--dsw-radius-sm);cursor:text;width:100%;height:28px;color:var(--dsw-alias-label-secondary);transition:width .18s var(--ds-ease-in-out), padding .18s var(--ds-ease-in-out), border-color .18s var(--ds-ease-in-out), background-color .18s var(--ds-ease-in-out);background:0 0;border:none;flex:none;align-items:center;gap:0;margin:0;padding:0;display:flex;overflow:hidden}.QAYkWG_searchExpanded{border:.5px solid var(--dsw-alias-border-l4);border-radius:var(--dsw-radius-md);width:calc(100% + 4px);height:30px;color:var(--dsw-alias-label-caption);background:0 0;margin-inline:-2px;padding:0 4px 0 0}.QAYkWG_searchButton{border-radius:var(--dsw-radius-sm);cursor:pointer;width:28px;height:28px;color:inherit;background:0 0;border:none;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex}.QAYkWG_searchExpanded .QAYkWG_searchButton{width:28px;height:30px}.QAYkWG_searchButton:hover{background:var(--dsw-alias-interactive-bg-hover)}.QAYkWG_searchExpanded .QAYkWG_searchButton:hover{background:0 0}.QAYkWG_searchInput{opacity:0;pointer-events:none;width:0;min-width:0;color:var(--dsw-alias-label-primary);transition:opacity .12s var(--ds-ease-in-out);background:0 0;border:none;outline:none;flex:1;font-size:13px;line-height:18px}.QAYkWG_searchExpanded .QAYkWG_searchInput{opacity:1;pointer-events:auto;margin-left:-2px}.QAYkWG_searchInput::placeholder{color:var(--dsw-alias-label-tertiary)}.QAYkWG_clearButton{border-radius:var(--dsw-radius-sm);cursor:pointer;width:24px;height:24px;color:var(--dsw-alias-label-secondary);background:0 0;border:none;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex}.QAYkWG_clearButton:hover{background:var(--dsw-alias-interactive-bg-hover)}.QAYkWG_rail .QAYkWG_sectionHeader{justify-content:flex-start;gap:0;margin-bottom:12px;padding-left:0}.QAYkWG_rail .QAYkWG_headerActions{max-width:none}.QAYkWG_rail .QAYkWG_iconButton{border-radius:var(--dsw-radius-md);width:36px;height:36px;color:var(--dsw-alias-label-primary)}.QAYkWG_rail .QAYkWG_search{border-radius:var(--dsw-radius-md);background:0 0;border-color:#0000;gap:0;width:36px;height:36px;margin:0 0 12px;padding:0}.QAYkWG_rail .QAYkWG_searchButton{border-radius:var(--dsw-radius-md);width:36px;height:36px;color:var(--dsw-alias-label-primary)}.QAYkWG_rail .QAYkWG_searchButton:hover{background:var(--dsw-alias-interactive-bg-hover)}.QAYkWG_listArea{min-height:0;margin-left:-4px;margin-right:calc(-1 * var(--dsh-session-list-edge-inset));flex-direction:column;flex:1;padding-left:4px;display:flex;overflow:visible}.QAYkWG_rail .QAYkWG_listArea{margin-left:0;margin-right:0;padding-left:0}.QAYkWG_treeBody{flex-direction:column;flex:1;min-height:0;display:flex;position:relative}.QAYkWG_fade{left:0;right:var(--dsh-session-list-edge-inset);background:linear-gradient(to bottom, transparent, var(--dsw-specific-sidebar-fill));pointer-events:none;height:24px;position:absolute;bottom:0}[data-platform=darwin] .QAYkWG_fade{display:none}.QAYkWG_wide{animation:QAYkWG_wide-in .2s var(--ds-ease-in-out)}@keyframes QAYkWG_wide-in{0%{opacity:0}}.QAYkWG_list{min-height:0;margin-left:-4px;margin-right:var(--dsh-session-list-scrollbar-offset);padding-left:4px;padding-right:calc(var(--dsh-session-list-edge-inset) - var(--dsh-session-list-scrollbar-width) - var(--dsh-session-list-scrollbar-offset));scrollbar-gutter:stable;flex:1;padding-bottom:16px;overflow-y:auto}.QAYkWG_flatList>*+*,.QAYkWG_searchTree>[role=treeitem]+[role=treeitem],.QAYkWG_groupSection>*+*{margin-top:2px}.QAYkWG_searchStatus{color:var(--dsw-alias-label-tertiary);padding:10px 12px;font-size:12px;line-height:18px}.QAYkWG_skeletonRow{box-sizing:border-box;align-items:flex-start;gap:8px;min-height:48px;padding:6px 8px 7px;display:flex}.QAYkWG_skeletonDot{corner-shape:round;border-radius:50%;flex:none;width:16px;height:16px}.QAYkWG_skeletonBars{flex-direction:column;flex:1;gap:6px;min-width:0;display:flex}.QAYkWG_skeletonDot,.QAYkWG_skeletonBar{background:var(--dsw-alias-bg-skeleton);animation:2s cubic-bezier(.36,0,.64,1) infinite QAYkWG_search-skeleton}.QAYkWG_skeletonBar{border-radius:var(--dsw-radius-xs);width:65%;height:16px}.QAYkWG_skeletonBarWide{width:90%;height:13px}@keyframes QAYkWG_search-skeleton{0%{opacity:1}40%{opacity:.6}80%,to{opacity:1}}.QAYkWG_groupSection{position:relative}.QAYkWG_groupSection+.QAYkWG_groupSection{margin-top:4px}.QAYkWG_listTopDropIndicator,.QAYkWG_workspaceDropBefore:before,.QAYkWG_workspaceDropAfter:after{content:\"\";z-index:1;background:linear-gradient(55deg, transparent calc(50% - 1px), var(--dsw-alias-state-business-primary) calc(50% - 1px) calc(50% + 1px), transparent calc(50% + 1px)) 0 0 / 5px 7px no-repeat, linear-gradient(125deg, transparent calc(50% - 1px), var(--dsw-alias-state-business-primary) calc(50% - 1px) calc(50% + 1px), transparent calc(50% + 1px)) 0 5px / 5px 7px no-repeat, linear-gradient(var(--dsw-alias-state-business-primary) 0 0) 4px 5px / calc(100% - 4px) 2px no-repeat;pointer-events:none;height:12px;position:absolute;left:0;right:0}.QAYkWG_listTopDropIndicator{top:-8px;left:0;right:var(--dsh-session-list-edge-inset)}.QAYkWG_listTopDropActive>.QAYkWG_workspaceDropBefore:first-child:before{display:none}.QAYkWG_workspaceDropBefore:before{top:-8px}.QAYkWG_workspaceDropAfter:after{bottom:-8px}.QAYkWG_sessionOverflowButton{border-radius:var(--dsw-radius-sm);width:100%;height:28px;padding:0 12px 0 calc(28px + var(--dsh-workspace-indent,0px));cursor:pointer;text-align:left;color:var(--dsw-alias-label-tertiary);background:0 0;border:none;font-size:12px}.QAYkWG_groupSection>.QAYkWG_sessionOverflowButton{margin-top:0}.QAYkWG_sessionOverflowButton:hover{color:var(--dsw-alias-label-secondary);background:0 0}.QAYkWG_empty{color:var(--dsw-alias-label-tertiary);padding:16px 12px;font-size:13px}.QAYkWG_emptyState{color:var(--dsw-alias-label-tertiary);flex-direction:column;align-items:center;gap:8px;margin-top:80px;padding:0 12px;font-size:13px;line-height:20px;display:flex}.QAYkWG_emptyState>svg{color:var(--dsw-alias-label-caption);margin-bottom:4px}.QAYkWG_emptyAction{cursor:pointer;color:var(--dsw-alias-link);background:0 0;border:none;padding:0;font-size:13px;line-height:20px}.QAYkWG_renameInput{box-sizing:border-box;border:.5px solid var(--dsw-alias-border-l4);border-radius:var(--dsw-radius-lg);width:100%;height:44px;color:var(--dsw-alias-label-primary);background:0 0;outline:none;padding:7px 14px;font-size:14px;font-weight:400;line-height:22px}.QAYkWG_renameInput:disabled{color:var(--dsw-alias-label-dimmed)}.QAYkWG_renameError{color:var(--dsw-alias-state-error-primary);margin-top:8px;font-size:12px;line-height:18px}.QAYkWG_deleteAction:not(:disabled){color:var(--dsw-alias-state-error-primary)}.QAYkWG_deleteStatus{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}.QAYkWG_archiveActivity{color:var(--dsw-alias-label-primary);margin:0 0 8px;padding-left:18px;font-size:13px;line-height:20px}.QAYkWG_archiveActivity li{overflow-wrap:anywhere}@media (prefers-reduced-motion:reduce){.QAYkWG_wide,.QAYkWG_skeletonDot,.QAYkWG_skeletonBar{animation:none}.QAYkWG_search,.QAYkWG_sectionLabel,.QAYkWG_searchSlot,.QAYkWG_searchInput,.QAYkWG_headerActions{transition:none}}";
4105
- const tagId = "dsh-group-status/WorkspaceBrowser.module.css";
4106
- if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
4107
- const tag = document.createElement("style");
4108
- tag.dataset.plugin = "dsh-group-status";
4109
- tag.dataset.pluginCss = tagId;
4110
- tag.textContent = css;
4111
- document.head.appendChild(tag);
3991
+ /** Primary status dot plus every status's screen-reader label, shared by the search and session rows. */
3992
+ function SessionStatusDots({ statuses }) {
3993
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: statuses[0].state }), statuses.map((status) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3994
+ className: Rows_module_css_default.visuallyHidden,
3995
+ children: status.label
3996
+ }, status.label))] });
3997
+ }
3998
+ /** Non-interactive pinned-row marker; the enclosing row remains the only action. */
3999
+ function PinnedIndicator({ t }) {
4000
+ const label = t("row.pinned");
4001
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4002
+ className: Rows_module_css_default.pinIndicator,
4003
+ role: "img",
4004
+ "aria-label": label,
4005
+ title: label,
4006
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPinFillRegular, { size: 14 })
4007
+ });
4112
4008
  }
4113
- var WorkspaceBrowser_module_css_default = {
4114
- "archiveActivity": "QAYkWG_archiveActivity",
4115
- "clearButton": "QAYkWG_clearButton",
4116
- "deleteAction": "QAYkWG_deleteAction",
4117
- "deleteStatus": "QAYkWG_deleteStatus",
4118
- "empty": "QAYkWG_empty",
4119
- "emptyAction": "QAYkWG_emptyAction",
4120
- "emptyState": "QAYkWG_emptyState",
4121
- "fade": "QAYkWG_fade",
4122
- "flatList": "QAYkWG_flatList",
4123
- "groupSection": "QAYkWG_groupSection",
4124
- "headerActions": "QAYkWG_headerActions",
4125
- "headerActionsHidden": "QAYkWG_headerActionsHidden",
4126
- "iconButton": "QAYkWG_iconButton",
4127
- "list": "QAYkWG_list",
4128
- "listArea": "QAYkWG_listArea",
4129
- "listTopDropActive": "QAYkWG_listTopDropActive",
4130
- "listTopDropIndicator": "QAYkWG_listTopDropIndicator",
4131
- "rail": "QAYkWG_rail",
4132
- "renameError": "QAYkWG_renameError",
4133
- "renameInput": "QAYkWG_renameInput",
4134
- "root": "QAYkWG_root",
4135
- "search": "QAYkWG_search",
4136
- "search-skeleton": "QAYkWG_search-skeleton",
4137
- "searchButton": "QAYkWG_searchButton",
4138
- "searchExpanded": "QAYkWG_searchExpanded",
4139
- "searchInput": "QAYkWG_searchInput",
4140
- "searchSlot": "QAYkWG_searchSlot",
4141
- "searchSlotExpanded": "QAYkWG_searchSlotExpanded",
4142
- "searchStatus": "QAYkWG_searchStatus",
4143
- "searchTree": "QAYkWG_searchTree",
4144
- "sectionHeader": "QAYkWG_sectionHeader",
4145
- "sectionLabel": "QAYkWG_sectionLabel",
4146
- "sectionLabelHidden": "QAYkWG_sectionLabelHidden",
4147
- "sessionOverflowButton": "QAYkWG_sessionOverflowButton",
4148
- "skeletonBar": "QAYkWG_skeletonBar",
4149
- "skeletonBarWide": "QAYkWG_skeletonBarWide",
4150
- "skeletonBars": "QAYkWG_skeletonBars",
4151
- "skeletonDot": "QAYkWG_skeletonDot",
4152
- "skeletonRow": "QAYkWG_skeletonRow",
4153
- "treeBody": "QAYkWG_treeBody",
4154
- "viewOptionsMenu": "QAYkWG_viewOptionsMenu",
4155
- "wide": "QAYkWG_wide",
4156
- "wide-in": "QAYkWG_wide-in",
4157
- "workspaceDropAfter": "QAYkWG_workspaceDropAfter",
4158
- "workspaceDropBefore": "QAYkWG_workspaceDropBefore"
4159
- };
4160
- //#endregion
4161
- //#region src/client/rows/WorkspaceBrowser.tsx
4162
4009
  /**
4163
- * The workspace/session browsing region filling the sidebar shell's
4164
- * `sidebar.workspaces` hole: section header (title + view options + add
4165
- * workspace), search, the grouped tree or flat list, and the workspace
4166
- * dialogs. Wide state renders the full browser; rail state renders the two
4167
- * region icons (search / add workspace) as 36px controls on the shell's shared
4168
- * rail entry path, each requesting expansion through the owner share. Adding
4169
- * is the header button's one action, so it raises the directory flow with no
4170
- * menu in between; the flow and its error dialog live in WorkspacePicker
4171
- * (same package — direct composition, no slot between them). A Session row's
4172
- * "..." menu and hover buttons are the `sidebar.workspaces.session.menu.item`
4173
- * and `sidebar.workspaces.session.row.action` lists rendered through this
4174
- * entry's `renderSlot`; the actions in them, this package's own included,
4175
- * are slot entries with their own behavior, so this component threads no
4176
- * action callbacks and hosts no action surface.
4010
+ * Hover-card body: full title, relative time, the Session's own scheduled-task
4011
+ * section, and every relevant live status. The task section sits above the
4012
+ * status lines so they stay the card's trailing status line.
4177
4013
  */
4178
- const facetsStore = facetStore();
4014
+ function SessionHoverContent({ node, now, renderSlot, t }) {
4015
+ const statuses = sessionStatuses(node, t).filter((status) => !(node.archived && (status.state === "done" || status.state === "idle")));
4016
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4017
+ className: Rows_module_css_default.hoverContent,
4018
+ children: [
4019
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4020
+ className: Rows_module_css_default.hoverTitle,
4021
+ children: displayTitle(node, t)
4022
+ }),
4023
+ !node.blank && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4024
+ className: Rows_module_css_default.hoverTime,
4025
+ children: hoverTimeLabel(node.updatedAt, now, t)
4026
+ }),
4027
+ renderSlot("sidebar.session.row.hover", { sessionId: node.id }),
4028
+ statuses.map((status) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4029
+ className: Rows_module_css_default.hoverStatus,
4030
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: status.state }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: status.label })]
4031
+ }, status.label)),
4032
+ node.archived && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4033
+ className: clsx(Rows_module_css_default.hoverStatus, Rows_module_css_default.hoverArchived),
4034
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconArchiveOutlineRegular, { size: 14 }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("row.archived") })]
4035
+ })
4036
+ ]
4037
+ });
4038
+ }
4179
4039
  /**
4180
- * Column slide length (--ds-transition-duration-slow): rail-search focus waits it out —
4181
- * focus() forces a synchronous layout and would jank the slide.
4040
+ * One flat search result: title, Workspace context, and optional content
4041
+ * excerpt. Search navigation opens the session only; it does not address an
4042
+ * event inside the conversation. Archived rows carry a hover unarchive
4043
+ * button, because search is where the filter surfaces them for recovery.
4044
+ * @param props.result - merged local/content search row.
4045
+ * @param props.currentId - selected session id.
4046
+ * @param props.onOpen - open the selected session.
4047
+ * @param props.onUnarchive - unarchive an archived result row.
4048
+ * @param props.t - Workspace-browser translation seat.
4049
+ * @returns the result row.
4182
4050
  */
4183
- const EXPAND_SLIDE_MS = 300;
4184
- /** Pause between the latest keystroke and a Host content-search request. */
4185
- const SEARCH_DEBOUNCE_MS = 250;
4186
- /** `session.search` wire bound, measured in JavaScript UTF-16 code units. */
4187
- const SEARCH_QUERY_MAX_CODE_UNITS = 500;
4188
- /** Keep controlled input and RPC payload inside the session.search wire contract. */
4189
- function sanitizeSearchQuery(value) {
4190
- const withoutNul = value.replaceAll("\0", "");
4191
- if (withoutNul.length <= SEARCH_QUERY_MAX_CODE_UNITS) return withoutNul;
4192
- let end = SEARCH_QUERY_MAX_CODE_UNITS;
4193
- const last = withoutNul.charCodeAt(end - 1);
4194
- const next = withoutNul.charCodeAt(end);
4195
- if (last >= 55296 && last <= 56319 && next >= 56320 && next <= 57343) end--;
4196
- return withoutNul.slice(0, end);
4051
+ function SearchResultItem({ result, currentId, onOpen, onUnarchive, t }) {
4052
+ const selected = result.id === currentId;
4053
+ const statuses = sessionStatuses(result, t);
4054
+ const primaryStatus = statuses[0];
4055
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4056
+ className: clsx(Rows_module_css_default.searchResultRow, selected && Rows_module_css_default.selected, result.archived && Rows_module_css_default.archived),
4057
+ role: "treeitem",
4058
+ "aria-selected": selected,
4059
+ "aria-description": result.archived ? t("toast.archivedNotOpenable") : void 0,
4060
+ onClick: () => {
4061
+ onOpen(result.id);
4062
+ },
4063
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4064
+ className: Rows_module_css_default.searchResultHeading,
4065
+ children: [
4066
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4067
+ className: Rows_module_css_default.slot,
4068
+ children: !result.archived && primaryStatus.state !== "idle" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SessionStatusDots, { statuses })
4069
+ }),
4070
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4071
+ className: Rows_module_css_default.searchResultTitle,
4072
+ children: result.title || t("session.untitled")
4073
+ }),
4074
+ result.archived && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4075
+ className: Rows_module_css_default.rowActions,
4076
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
4077
+ label: t("actions.unarchive"),
4078
+ side: "bottom",
4079
+ align: "end",
4080
+ delayMs: 500,
4081
+ portal: true,
4082
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4083
+ type: "button",
4084
+ className: Rows_module_css_default.iconButton,
4085
+ "aria-label": t("menu.unarchiveSession"),
4086
+ onClick: (e) => {
4087
+ e.stopPropagation();
4088
+ onUnarchive(result.id);
4089
+ },
4090
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconUnarchiveOutlineRegular, { size: 14 })
4091
+ })
4092
+ })
4093
+ })
4094
+ ]
4095
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4096
+ className: Rows_module_css_default.searchResultMeta,
4097
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4098
+ className: Rows_module_css_default.searchResultWorkspace,
4099
+ children: result.workspace || t("group.ungrouped")
4100
+ }), result.snippet !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4101
+ className: Rows_module_css_default.searchResultSnippet,
4102
+ children: result.snippet
4103
+ })]
4104
+ })]
4105
+ });
4197
4106
  }
4198
4107
  /**
4199
- * Accept the native drag at document level while a row drag is active: row
4200
- * hover still owns the insertion marker, and releasing outside the list must
4201
- * not be rendered as a rejected drop before dragend commits that last marker.
4108
+ * One top-level 32px session row: leading 16px cell (status dot, or the
4109
+ * leading seat while the row's primary state is idle), title, relative time or
4110
+ * compact pending label, and the row actions menu. A row that owns a state dot
4111
+ * keeps that cell and renders no seat, so an ambient automation mark never
4112
+ * appears beside the row's own state dot. An archived row keeps the cell blank:
4113
+ * neither marker renders there, and its live status stays on the hover card.
4114
+ * @param props.node - derived session node.
4115
+ * @param props.currentId - selected session id (row highlight).
4116
+ * @param props.now - epoch ms for relative-time formatting.
4117
+ * @param props.onOpen - open a session by id.
4118
+ * @param props.onRenameRequest - open the rename dialog from a title double-click (id + current title).
4119
+ * @param props.renderSlot - child-seat renderer for the row's action lists
4120
+ * (`sidebar.workspaces.session.menu.item` / `sidebar.workspaces.session.row.action`),
4121
+ * its leading decoration, and its hover-card section.
4122
+ * @param props.onReveal - scroll this row into view after search navigation, then acknowledge it.
4123
+ * @param props.drag - optional row-drag target wiring; blank rows cannot start a drag.
4124
+ * @param props.t - the browser root's locale seat.
4125
+ * @returns the session row.
4202
4126
  */
4203
- function useNativeDragAcceptance(active) {
4127
+ function SessionNodeItem({ node, currentId, now, onOpen, onRenameRequest, renderSlot, onReveal, drag, t }) {
4128
+ const row = node;
4129
+ const title = displayTitle(node, t);
4130
+ const selected = node.id === currentId;
4131
+ const statuses = sessionStatuses(node, t);
4132
+ const primaryStatus = statuses[0];
4133
+ const showStatus = primaryStatus.state !== "idle";
4134
+ const draggable = drag !== void 0 && !row.blank && !row.archived;
4135
+ const [menuOpen, setMenuOpen] = (0, react.useState)(false);
4136
+ const menuOpenState = (0, react.useMemo)(() => [menuOpen, setMenuOpen], [menuOpen]);
4137
+ const rowRef = (0, react.useRef)(null);
4138
+ const titleRef = (0, react.useRef)(null);
4139
+ const marquee = useTitleMarquee(titleRef);
4204
4140
  (0, react.useEffect)(() => {
4205
- if (!active) return;
4206
- const acceptDrag = (event) => {
4207
- event.preventDefault();
4208
- if (event.dataTransfer !== null) event.dataTransfer.dropEffect = "move";
4209
- };
4210
- const acceptDrop = (event) => {
4211
- event.preventDefault();
4212
- };
4213
- document.addEventListener("dragover", acceptDrag);
4214
- document.addEventListener("drop", acceptDrop);
4215
- return () => {
4216
- document.removeEventListener("dragover", acceptDrag);
4217
- document.removeEventListener("drop", acceptDrop);
4218
- };
4219
- }, [active]);
4220
- }
4221
- /** Grouping, ordering, and archived-filter menu; own open state so it resets with the wide chrome. */
4222
- function ViewOptionsMenu({ groupBy, orderBy, archivedFilter, onGroupPick, onOrderPick, onArchivedFilterPick, t }) {
4223
- const [open, setOpen] = (0, react.useState)(false);
4224
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
4225
- open,
4226
- onClose: () => {
4227
- setOpen(false);
4141
+ if (onReveal === void 0) return;
4142
+ rowRef.current?.scrollIntoView({ block: "nearest" });
4143
+ onReveal();
4144
+ }, [onReveal]);
4145
+ const ownRow = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4146
+ ref: rowRef,
4147
+ "data-row-key": `session:${node.id}`,
4148
+ className: clsx(Rows_module_css_default.sessionRow, selected && Rows_module_css_default.selected, menuOpen && Rows_module_css_default.menuOpen, row.archived && Rows_module_css_default.archived, drag?.marker === "before" && Rows_module_css_default.dropBefore, drag?.marker === "after" && Rows_module_css_default.dropAfter),
4149
+ role: "treeitem",
4150
+ "aria-selected": selected,
4151
+ "aria-description": row.archived ? t("toast.archivedNotOpenable") : void 0,
4152
+ onClick: () => {
4153
+ onOpen(node.id);
4228
4154
  },
4229
- items: [
4230
- {
4231
- type: "label",
4232
- id: "group-by",
4233
- text: t("groupBy.label")
4234
- },
4235
- {
4236
- id: "workspace",
4237
- label: t("groupBy.workspace"),
4238
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderCloseRegular, {})
4239
- },
4240
- {
4241
- id: "workspace-tree",
4242
- label: t("groupBy.workspaceTree"),
4243
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconWorkspaceTreeOutlineRegular, {})
4244
- },
4245
- {
4246
- id: "flat",
4247
- label: t("groupBy.flat"),
4248
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFlatListOutlineRegular, {})
4249
- },
4250
- {
4251
- type: "separator",
4252
- id: "order-by-separator"
4253
- },
4254
- {
4255
- type: "label",
4256
- id: "order-by",
4257
- text: t("orderBy.label")
4258
- },
4259
- {
4260
- id: "manual",
4261
- label: t("orderBy.manual"),
4262
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronsUpDownOutlineRegular, {})
4263
- },
4264
- {
4265
- id: "updated",
4266
- label: t("orderBy.updated"),
4267
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconClockOutlineRegular, {})
4268
- },
4269
- {
4270
- type: "separator",
4271
- id: "archived-filter-separator"
4272
- },
4273
- {
4274
- type: "label",
4275
- id: "filter-by",
4276
- text: t("filterBy.label")
4277
- },
4278
- {
4279
- id: "hide-archived",
4280
- label: t("viewOptions.hideArchived"),
4281
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconArchiveOffOutlineRegular, {})
4282
- },
4283
- {
4284
- id: "show-archived",
4285
- label: t("viewOptions.showArchived"),
4286
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconQueueOutlineRegular, {})
4287
- },
4288
- {
4289
- id: "only-archived",
4290
- label: t("viewOptions.onlyArchived"),
4291
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconArchiveCheckOutlineRegular, {})
4292
- }
4293
- ],
4294
- selectedIds: [
4295
- groupBy,
4296
- orderBy,
4297
- {
4298
- default: "hide-archived",
4299
- show: "show-archived",
4300
- only: "only-archived"
4301
- }[archivedFilter]
4302
- ],
4303
- onSelect: (id) => {
4304
- if (id === "workspace" || id === "workspace-tree" || id === "flat") onGroupPick(id);
4305
- else if (id === "manual" || id === "updated") onOrderPick(id);
4306
- else if (id === "hide-archived") onArchivedFilterPick("default");
4307
- else if (id === "show-archived") onArchivedFilterPick("show");
4308
- else if (id === "only-archived") onArchivedFilterPick("only");
4309
- setOpen(false);
4155
+ onPointerEnter: marquee.enter,
4156
+ onPointerLeave: marquee.leave,
4157
+ draggable,
4158
+ onDragStart: !draggable ? void 0 : (e) => {
4159
+ e.dataTransfer.effectAllowed = "move";
4160
+ e.dataTransfer.setData("text/plain", node.id);
4161
+ drag.start();
4310
4162
  },
4311
- align: "end",
4312
- dense: true,
4313
- listClassName: WorkspaceBrowser_module_css_default.viewOptionsMenu,
4314
- portal: true,
4315
- anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
4316
- label: t("viewOptions.label"),
4317
- side: "bottom",
4318
- delayMs: 500,
4319
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4320
- type: "button",
4321
- className: clsx(WorkspaceBrowser_module_css_default.iconButton, WorkspaceBrowser_module_css_default.wide),
4322
- "aria-label": t("viewOptions.label"),
4323
- onClick: () => {
4324
- setOpen((v) => !v);
4163
+ onDragEnd: !draggable ? void 0 : drag.end,
4164
+ onDragOver: drag === void 0 ? void 0 : (e) => {
4165
+ if (!drag.active) return;
4166
+ e.preventDefault();
4167
+ e.dataTransfer.dropEffect = "move";
4168
+ drag.hover(rowHalf(e));
4169
+ },
4170
+ onDrop: drag === void 0 ? void 0 : (e) => {
4171
+ if (!drag.active) return;
4172
+ e.preventDefault();
4173
+ drag.drop(rowHalf(e));
4174
+ },
4175
+ children: [
4176
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4177
+ className: Rows_module_css_default.slot,
4178
+ children: !row.archived && !row.blank && (showStatus ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SessionStatusDots, { statuses }) : renderSlot("sidebar.session.row.leading", { sessionId: node.id }))
4179
+ }),
4180
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4181
+ ref: titleRef,
4182
+ className: Rows_module_css_default.title,
4183
+ onDoubleClick: row.blank ? void 0 : (e) => {
4184
+ e.stopPropagation();
4185
+ onRenameRequest(node.id, row.title);
4325
4186
  },
4326
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSlidersTwoOutlineRegular, {})
4187
+ children: title
4188
+ }),
4189
+ !row.blank && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4190
+ className: Rows_module_css_default.time,
4191
+ "aria-hidden": primaryStatus.trailingLabel === void 0 ? void 0 : true,
4192
+ children: primaryStatus.trailingLabel ?? timeLabel(row.updatedAt, now, t)
4193
+ }),
4194
+ row.pinned && !row.archived && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PinnedIndicator, { t }),
4195
+ !row.blank && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4196
+ className: Rows_module_css_default.rowActions,
4197
+ onClick: (e) => {
4198
+ e.stopPropagation();
4199
+ },
4200
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
4201
+ open: menuOpen,
4202
+ onClose: () => {
4203
+ setMenuOpen(false);
4204
+ },
4205
+ portal: true,
4206
+ closeOnPointerLeave: true,
4207
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4208
+ type: "button",
4209
+ className: Rows_module_css_default.iconButton,
4210
+ "aria-label": t("actions.session.aria", { name: title }),
4211
+ onClick: () => {
4212
+ setMenuOpen((v) => !v);
4213
+ },
4214
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconEllipsisOutlineRegular, {})
4215
+ }),
4216
+ children: renderSlot("sidebar.workspaces.session.menu.item", {
4217
+ sessionId: node.id,
4218
+ displayTitle: row.title
4219
+ }, { hookContext: menuOpenState })
4220
+ }), renderSlot("sidebar.workspaces.session.row.action", {
4221
+ sessionId: node.id,
4222
+ displayTitle: row.title
4223
+ })]
4327
4224
  })
4328
- })
4225
+ ]
4226
+ });
4227
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.HoverCard, {
4228
+ anchor: ownRow,
4229
+ content: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SessionHoverContent, {
4230
+ node,
4231
+ now,
4232
+ renderSlot,
4233
+ t
4234
+ }),
4235
+ openDelayMs: 800,
4236
+ grouped: true,
4237
+ disabled: menuOpen || drag?.active === true,
4238
+ copyText: row.blank || row.title === "" ? void 0 : row.title,
4239
+ copyLabel: t("copy"),
4240
+ copiedLabel: t("hover.copied")
4329
4241
  });
4330
4242
  }
4331
- /** Apply a visible drop to the complete account without removing hidden members. */
4332
- function sessionDragOrder(order, rows, drag, over) {
4333
- const source = rows.find((row) => row.id === drag.sessionId);
4334
- const target = rows.find((row) => row.id === over.id);
4335
- if (source === void 0 || target === void 0 || source.blank || source.pinned !== drag.pinned || target.pinned !== drag.pinned || source.id === target.id || !order.includes(source.id)) return;
4336
- const section = rows.filter((row) => row.pinned === drag.pinned);
4337
- const sourceIndex = section.findIndex((row) => row.id === source.id);
4338
- if (section.filter((row) => row.id !== source.id).findIndex((row) => row.id === target.id) + (over.half === "after" ? 1 : 0) === sourceIndex) return;
4339
- const next = order.filter((id) => id !== source.id);
4340
- const targetIndex = next.indexOf(target.id);
4341
- if (targetIndex === -1) return;
4342
- next.splice(targetIndex + (over.half === "after" ? 1 : 0), 0, source.id);
4343
- return pinCurrentBlank(next, rows.find((row) => row.blank)?.id);
4243
+ //#endregion
4244
+ //#region \0dsh-css:src/client/rows/AnimatedRows.module.css.mjs
4245
+ const css = "._8TnAsq_exits{contain:strict;pointer-events:none;position:absolute;inset:0;overflow:clip}";
4246
+ const tagId = "dsh-group-status/AnimatedRows.module.css";
4247
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
4248
+ const tag = document.createElement("style");
4249
+ tag.dataset.plugin = "dsh-group-status";
4250
+ tag.dataset.pluginCss = tagId;
4251
+ tag.textContent = css;
4252
+ document.head.appendChild(tag);
4344
4253
  }
4345
- /** Resolve an insertion side across the Workspace header, descendants, and Sessions. */
4346
- function workspaceGroupHalf(e) {
4347
- const rect = e.currentTarget.getBoundingClientRect();
4348
- return e.clientY < rect.top + rect.height / 2 ? "before" : "after";
4254
+ var AnimatedRows_module_css_default = { "exits": "_8TnAsq_exits" };
4255
+ //#endregion
4256
+ //#region src/client/rows/AnimatedRows.tsx
4257
+ /** React-commit-driven movement and entry/exit fades for the sidebar's keyed rows. */
4258
+ const ROW_FADE_MS = 100;
4259
+ const ROW_GLIDE_MS = 200;
4260
+ function sameRows(previous, next) {
4261
+ return previous.rowKeys.length === next.rowKeys.length && previous.rowKeys.every((key, index) => key === next.rowKeys[index]);
4349
4262
  }
4263
+ function intersects(row, viewport) {
4264
+ return row.bottom > viewport.top && row.top < viewport.bottom && row.right > viewport.left && row.left < viewport.right;
4265
+ }
4266
+ /**
4267
+ * Animates keyed sidebar rows only when their rendered membership or order changes.
4268
+ * Motion starts after the first pointer or keyboard input inside the mounted list.
4269
+ * The parent supplies a positioned container for the inert exit overlay.
4270
+ */
4271
+ var AnimatedRows = class extends react.Component {
4272
+ armed = false;
4273
+ list = (0, react.createRef)();
4274
+ overlay = (0, react.createRef)();
4275
+ movements = /* @__PURE__ */ new Map();
4276
+ exits = /* @__PURE__ */ new Map();
4277
+ getSnapshotBeforeUpdate(previous) {
4278
+ const list = this.list.current;
4279
+ if (!this.armed || sameRows(previous, this.props) || previous.resetKey !== this.props.resetKey || !previous.ready || !this.props.ready || list === null || typeof list.animate !== "function" || window.matchMedia("(prefers-reduced-motion: reduce)").matches) return null;
4280
+ const viewport = list.getBoundingClientRect();
4281
+ const positions = this.readPositions();
4282
+ const nextKeys = new Set(this.props.rowKeys);
4283
+ const removed = /* @__PURE__ */ new Map();
4284
+ for (const [key, row] of positions) {
4285
+ if (nextKeys.has(key) || !intersects(row.rect, viewport)) continue;
4286
+ const clone = row.element.cloneNode(true);
4287
+ clone.removeAttribute("data-row-key");
4288
+ clone.inert = true;
4289
+ clone.style.setProperty("--dsh-workspace-indent", getComputedStyle(row.element).getPropertyValue("--dsh-workspace-indent"));
4290
+ removed.set(key, {
4291
+ ...row,
4292
+ element: clone
4293
+ });
4294
+ }
4295
+ return {
4296
+ positions,
4297
+ removed
4298
+ };
4299
+ }
4300
+ componentDidUpdate(previous, _state, snapshot) {
4301
+ if (snapshot === null) {
4302
+ if (!sameRows(previous, this.props) || previous.resetKey !== this.props.resetKey || previous.ready !== this.props.ready) this.clear();
4303
+ return;
4304
+ }
4305
+ this.cancelMovements();
4306
+ const list = this.list.current;
4307
+ const overlay = this.overlay.current;
4308
+ const viewport = list.getBoundingClientRect();
4309
+ const origin = overlay.getBoundingClientRect();
4310
+ const positions = this.readPositions();
4311
+ for (const [key, row] of positions) {
4312
+ this.removeExit(key);
4313
+ const previousRow = snapshot.positions.get(key);
4314
+ if (!intersects(row.rect, viewport) && (previousRow === void 0 || !intersects(previousRow.rect, viewport))) continue;
4315
+ if (previousRow === void 0) {
4316
+ this.move(row.element, [{ opacity: 0 }, { opacity: 1 }], ROW_FADE_MS);
4317
+ continue;
4318
+ }
4319
+ const dx = previousRow.rect.left - row.rect.left;
4320
+ const dy = previousRow.rect.top - row.rect.top;
4321
+ if (dx === 0 && dy === 0 && previousRow.opacity === 1) continue;
4322
+ this.move(row.element, [{
4323
+ transform: `translate(${String(dx)}px, ${String(dy)}px)`,
4324
+ opacity: previousRow.opacity
4325
+ }, {
4326
+ transform: "translate(0, 0)",
4327
+ opacity: 1
4328
+ }], ROW_GLIDE_MS);
4329
+ }
4330
+ for (const [key, row] of snapshot.removed) {
4331
+ const { element } = row;
4332
+ this.removeExit(key);
4333
+ Object.assign(element.style, {
4334
+ position: "absolute",
4335
+ margin: "0",
4336
+ transform: "none",
4337
+ boxSizing: "border-box",
4338
+ left: `${String(row.rect.left - origin.left)}px`,
4339
+ top: `${String(row.rect.top - origin.top)}px`,
4340
+ width: `${String(row.rect.width)}px`,
4341
+ height: `${String(row.rect.height)}px`
4342
+ });
4343
+ overlay.append(element);
4344
+ const animation = element.animate([{ opacity: row.opacity }, { opacity: 0 }], {
4345
+ duration: ROW_FADE_MS,
4346
+ easing: "ease-out",
4347
+ fill: "forwards"
4348
+ });
4349
+ this.exits.set(key, {
4350
+ element,
4351
+ animation
4352
+ });
4353
+ animation.onfinish = () => {
4354
+ this.removeExit(key);
4355
+ };
4356
+ }
4357
+ }
4358
+ componentWillUnmount() {
4359
+ this.clear();
4360
+ }
4361
+ readPositions() {
4362
+ const rows = this.list.current.querySelectorAll("[data-row-key]");
4363
+ return new Map(Array.from(rows, (element) => [element.dataset.rowKey, {
4364
+ element,
4365
+ rect: element.getBoundingClientRect(),
4366
+ opacity: this.movements.has(element) ? Number(getComputedStyle(element).opacity) : 1
4367
+ }]));
4368
+ }
4369
+ move(element, keyframes, duration) {
4370
+ const animation = element.animate(keyframes, {
4371
+ duration,
4372
+ easing: "ease-out"
4373
+ });
4374
+ this.movements.set(element, animation);
4375
+ animation.onfinish = () => {
4376
+ this.movements.delete(element);
4377
+ animation.cancel();
4378
+ };
4379
+ }
4380
+ cancelMovements() {
4381
+ for (const animation of this.movements.values()) {
4382
+ animation.onfinish = null;
4383
+ animation.cancel();
4384
+ }
4385
+ this.movements.clear();
4386
+ }
4387
+ removeExit(key) {
4388
+ const exit = this.exits.get(key);
4389
+ if (exit === void 0) return;
4390
+ exit.animation.onfinish = null;
4391
+ exit.animation.cancel();
4392
+ exit.element.remove();
4393
+ this.exits.delete(key);
4394
+ }
4395
+ clear() {
4396
+ this.cancelMovements();
4397
+ for (const key of this.exits.keys()) this.removeExit(key);
4398
+ }
4399
+ render() {
4400
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4401
+ ref: this.list,
4402
+ className: this.props.className,
4403
+ role: "tree",
4404
+ "aria-label": this.props.label,
4405
+ onPointerDownCapture: () => {
4406
+ this.armed = true;
4407
+ },
4408
+ onKeyDownCapture: () => {
4409
+ this.armed = true;
4410
+ },
4411
+ children: this.props.children
4412
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4413
+ ref: this.overlay,
4414
+ className: AnimatedRows_module_css_default.exits,
4415
+ "aria-hidden": "true"
4416
+ })] });
4417
+ }
4418
+ };
4419
+ //#endregion
4420
+ //#region src/client/rows/EmptySessions.tsx
4421
+ /** The list-empty placeholder shared by the tree, flat, and search list bodies. */
4350
4422
  /** The list-empty placeholder — a glyph over the text; the archived-only view names its filter and offers the way back. */
4351
4423
  function EmptySessions({ rowState, onLeaveArchivedOnly, t }) {
4352
4424
  const archivedOnly = rowState.archivedFilter === "only";
@@ -4365,14 +4437,25 @@ window.__ModuleLoader__.load({
4365
4437
  ]
4366
4438
  });
4367
4439
  }
4440
+ //#endregion
4441
+ //#region src/client/rows/SessionTree.tsx
4442
+ /**
4443
+ * The scrolling session tree: Workspace groups, aggregated live-status badges,
4444
+ * watermarks, and row drag ordering.
4445
+ *
4446
+ * Split out of `WorkspaceBrowser.tsx` so the shell keeps only the header, search
4447
+ * box, and dialogs; this file owns the tree body and its local row limits, and
4448
+ * unmounting it drops both the sessions subscription and those limits.
4449
+ */
4450
+ const facetsStore = facetStore();
4368
4451
  /** The scrolling session tree; unmounting drops the sessions subscription and local row limits. */
4369
4452
  function SessionTree({ list, useSessionStatus, startSession, open, workspaces, ungroupedSessionIds, rowState, onLeaveArchivedOnly, workspaceReady, animationResetKey, usePanelInfo, onRenameRequest, onDeleteRequest, onSessionRenameRequest, renderSlot, insertWorkspaceBefore, nestWorkspaces, groupExpansion, setGroupExpanded, setSessionOrder, home, t, revealSessionId, onSessionRevealed, shortcuts, useBadgeSettings }) {
4370
4453
  const panelActive = usePanelInfo((info) => info.activePanelId !== null);
4371
4454
  const badgeSettings = useBadgeSettings((settings) => settings);
4372
4455
  const statuses = useSessionStatus((s) => s);
4373
- (0, react.useSyncExternalStore)(facetsStore.subscribe, facetsStore.getVersion, facetsStore.getServerVersion);
4374
- const facets = (0, react.useMemo)(() => visibleFacets(facetsStore.list(), badgeSettings), [badgeSettings]);
4375
- const current = panelActive ? void 0 : Object.values(list.byId).find((session) => (session.retainedBy.mainView ?? 0) > 0)?.id;
4456
+ const facets = useRegisteredFacets(facetsStore);
4457
+ const mainViewId = (0, react.useMemo)(() => mainViewSessionId(list), [list]);
4458
+ const current = panelActive ? void 0 : mainViewId;
4376
4459
  const revealGroup = revealSessionId === void 0 || !workspaceReady ? void 0 : owningGroupKey(workspaces, revealSessionId);
4377
4460
  const [storedWatermarks, setStoredWatermarks] = (0, react.useState)({});
4378
4461
  const [watermarkMotionKey, setWatermarkMotionKey] = (0, react.useState)(0);
@@ -4719,6 +4802,9 @@ window.__ModuleLoader__.load({
4719
4802
  ]
4720
4803
  });
4721
4804
  }
4805
+ //#endregion
4806
+ //#region src/client/rows/ListView.tsx
4807
+ /** The two wide-only list bodies: the flat single-list view and the search results view. */
4722
4808
  /** The flat "In one list" body: every session is one draggable top-level row. */
4723
4809
  function FlatList({ list, sessionIds, rowState, onLeaveArchivedOnly, useSessionStatus, open, onSessionRenameRequest, usePanelInfo, setSessionOrder, workspaceReady, animationResetKey, revealSessionId, onSessionRevealed, renderSlot, t }) {
4724
4810
  const panelActive = usePanelInfo((info) => info.activePanelId !== null);
@@ -4732,7 +4818,8 @@ window.__ModuleLoader__.load({
4732
4818
  const [drag, setDrag] = (0, react.useState)(null);
4733
4819
  const dropCommitted = (0, react.useRef)(false);
4734
4820
  useNativeDragAcceptance(drag !== null);
4735
- const currentId = panelActive ? void 0 : Object.values(list.byId).find((session) => (session.retainedBy.mainView ?? 0) > 0)?.id;
4821
+ const mainViewId = (0, react.useMemo)(() => mainViewSessionId(list), [list]);
4822
+ const currentId = panelActive ? void 0 : mainViewId;
4736
4823
  const commitDrag = (activeDrag, over) => {
4737
4824
  if (dropCommitted.current) return;
4738
4825
  dropCommitted.current = true;
@@ -4827,7 +4914,8 @@ window.__ModuleLoader__.load({
4827
4914
  resultLimit
4828
4915
  ]);
4829
4916
  const pending = currentRemote.status === "loading";
4830
- const currentId = panelActive ? void 0 : Object.values(list.byId).find((session) => (session.retainedBy.mainView ?? 0) > 0)?.id;
4917
+ const mainViewId = (0, react.useMemo)(() => mainViewSessionId(list), [list]);
4918
+ const currentId = panelActive ? void 0 : mainViewId;
4831
4919
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4832
4920
  className: clsx(WorkspaceBrowser_module_css_default.treeBody, WorkspaceBrowser_module_css_default.wide),
4833
4921
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -4837,6 +4925,7 @@ window.__ModuleLoader__.load({
4837
4925
  className: WorkspaceBrowser_module_css_default.searchTree,
4838
4926
  role: "tree",
4839
4927
  "aria-label": t("search.results.aria"),
4928
+ "aria-busy": pending,
4840
4929
  children: results.items.map((result) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SearchResultItem, {
4841
4930
  result,
4842
4931
  currentId,
@@ -4859,20 +4948,35 @@ window.__ModuleLoader__.load({
4859
4948
  }),
4860
4949
  !pending && results.items.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4861
4950
  className: WorkspaceBrowser_module_css_default.empty,
4951
+ role: "status",
4862
4952
  children: t("search.noMatches")
4863
4953
  }),
4864
4954
  results.hasMore && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4865
4955
  className: WorkspaceBrowser_module_css_default.searchStatus,
4956
+ role: "status",
4866
4957
  children: t("search.hasMore", { n: resultLimit })
4867
4958
  })
4868
4959
  ]
4869
4960
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: WorkspaceBrowser_module_css_default.fade })]
4870
4961
  });
4871
4962
  }
4963
+ //#endregion
4964
+ //#region src/client/rows/WorkspaceBrowser.tsx
4872
4965
  /**
4873
- * Render the browsing region.
4874
- * @param props - composed slot props (shell owner share + store + injected actions).
4875
- * @returns the region element tree.
4966
+ * The workspace/session browsing region filling the sidebar shell's
4967
+ * `sidebar.workspaces` hole: section header (title + view options + add
4968
+ * workspace), search, the grouped tree or flat list, and the workspace
4969
+ * dialogs. Wide state renders the full browser; rail state renders the two
4970
+ * region icons (search / add workspace) as 36px controls on the shell's shared
4971
+ * rail entry path, each requesting expansion through the owner share. Adding
4972
+ * is the header button's one action, so it raises the directory flow with no
4973
+ * menu in between; the flow and its error dialog live in WorkspacePicker
4974
+ * (same package — direct composition, no slot between them). A Session row's
4975
+ * "..." menu and hover buttons are the `sidebar.workspaces.session.menu.item`
4976
+ * and `sidebar.workspaces.session.row.action` lists rendered through this
4977
+ * entry's `renderSlot`; the actions in them, this package's own included,
4978
+ * are slot entries with their own behavior, so this component threads no
4979
+ * action callbacks and hosts no action surface.
4876
4980
  */
4877
4981
  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 }) {
4878
4982
  const home = useHostInfo((info) => info.home);
@@ -4908,7 +5012,7 @@ window.__ModuleLoader__.load({
4908
5012
  actions.setArchivedFilter("default");
4909
5013
  };
4910
5014
  const workspaceReady = workspacePhase === "ready" && workspaceStreamState !== "loading";
4911
- const mainSessionId = Object.values(list.byId).find((session) => (session.retainedBy.mainView ?? 0) > 0)?.id;
5015
+ const mainSessionId = (0, react.useMemo)(() => mainViewSessionId(list), [list]);
4912
5016
  const currentBlank = mainSessionId !== void 0 && list.byId[mainSessionId]?.blank === true ? mainSessionId : void 0;
4913
5017
  const ungroupedMemberIds = (0, react.useMemo)(() => {
4914
5018
  const accounted = new Set(workspaces.flatMap((workspace) => workspace.sessionIds));
@@ -5051,7 +5155,7 @@ window.__ModuleLoader__.load({
5051
5155
  const timer = window.setTimeout(() => {
5052
5156
  searchInput.current?.focus({ preventScroll: true });
5053
5157
  setSearchOnExpand(false);
5054
- }, EXPAND_SLIDE_MS);
5158
+ }, 300);
5055
5159
  return () => {
5056
5160
  window.clearTimeout(timer);
5057
5161
  };
@@ -5127,7 +5231,7 @@ window.__ModuleLoader__.load({
5127
5231
  hasMore: false
5128
5232
  });
5129
5233
  });
5130
- }, SEARCH_DEBOUNCE_MS);
5234
+ }, 250);
5131
5235
  return () => {
5132
5236
  window.clearTimeout(timer);
5133
5237
  controller.abort();
@@ -5234,7 +5338,7 @@ window.__ModuleLoader__.load({
5234
5338
  className: WorkspaceBrowser_module_css_default.searchInput,
5235
5339
  type: "text",
5236
5340
  placeholder: t("search.placeholder"),
5237
- maxLength: SEARCH_QUERY_MAX_CODE_UNITS,
5341
+ maxLength: 500,
5238
5342
  value: query,
5239
5343
  tabIndex: searchExpanded ? 0 : -1,
5240
5344
  onChange: (e) => {
@@ -5647,6 +5751,15 @@ window.__ModuleLoader__.load({
5647
5751
  //#endregion
5648
5752
  //#region src/client/session-actions/derived.ts
5649
5753
  /**
5754
+ * "还没投影过"的标记。
5755
+ *
5756
+ * 不能用 `undefined` 兼任:投影**合法地**可能返回 `undefined`("当前没有待处理的
5757
+ * 请求"就是一次正常的空结果),把它当成"未计算"会让每次读都重跑一次 `project`。
5758
+ * 投影若新建 `Set` / 数组,消费者每次读都拿到新引用,选择器于是永远不收敛——这正是
5759
+ * 本函数要避免的那件事。Symbol 不与任何投影结果相等,且只在本模块内存在。
5760
+ */
5761
+ const NOT_PROJECTED = Symbol("derive.notProjected");
5762
+ /**
5650
5763
  * Project one observable into another, recomputing only when the source
5651
5764
  * snapshot changes identity, so consumers that select from the projection
5652
5765
  * (a Set lookup per row) never rebuild it per read.
@@ -5656,11 +5769,11 @@ window.__ModuleLoader__.load({
5656
5769
  */
5657
5770
  function derive(source, project) {
5658
5771
  let seen;
5659
- let value;
5772
+ let value = NOT_PROJECTED;
5660
5773
  return {
5661
5774
  getSnapshot: () => {
5662
5775
  const snapshot = source.getSnapshot();
5663
- if (value === void 0 || snapshot !== seen) {
5776
+ if (value === NOT_PROJECTED || snapshot !== seen) {
5664
5777
  seen = snapshot;
5665
5778
  value = project(snapshot);
5666
5779
  }
@@ -6042,16 +6155,14 @@ window.__ModuleLoader__.load({
6042
6155
  "settings.badge.showPendingHint": "关闭后,等待审批 / 计划评审 / 等待回答的会话不再画黄点,也不再计入折叠徽标的数字与悬停明细。",
6043
6156
  "settings.badge.showCompleted": "显示「已完成」绿点",
6044
6157
  "settings.badge.showCompletedHint": "关闭后,「已完成」的会话不再画绿点,也不再计入折叠徽标的数字与悬停明细。会话行自身的绿点不受影响。",
6045
- "settings.badge.showScheduled": "显示「定时任务」时钟",
6046
- "settings.badge.showScheduledHint": "关闭后,带定时提醒的会话(来自 dsh-later)不再画时钟图标,也不再计入折叠徽标的数字与悬停明细。",
6047
- "settings.badge.showWaitingReview": "显示「等待评审」沙漏",
6048
- "settings.badge.showWaitingReviewHint": "关闭后,等待代码评审的会话(来自 dsh-gitea-dispatch)不再画沙漏图标,也不再计入折叠徽标的数字与悬停明细。",
6049
6158
  "settings.hover.section": "悬停明细",
6050
6159
  "settings.hover.showDetails": "显示状态明细行",
6051
6160
  "settings.hover.showDetailsHint": "关闭后,悬停卡只显示工作区标题、路径和创建时间——不再显示聚合状态明细行。",
6052
6161
  "settings.count.section": "数字显示",
6053
6162
  "settings.count.threshold": "数字显示阈值",
6054
6163
  "settings.count.thresholdHint": "数量 >= 此值时才在徽标上显示数字。默认 2(即 count=1 时省略,因为状态点已表达\"有东西\")。设为 1 则始终显示。",
6164
+ "settings.count.decrease": "减少",
6165
+ "settings.count.increase": "增加",
6055
6166
  "settings.save": "保存",
6056
6167
  "settings.saving": "保存中…",
6057
6168
  "settings.saveFailed": "本部署没有接受这些值,已保留供你修改。",
@@ -6187,16 +6298,14 @@ window.__ModuleLoader__.load({
6187
6298
  "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.",
6188
6299
  "settings.badge.showCompleted": "Show the completed dot",
6189
6300
  "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.",
6190
- "settings.badge.showScheduled": "Show the scheduled icon",
6191
- "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.",
6192
- "settings.badge.showWaitingReview": "Show the waiting-for-review icon",
6193
- "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.",
6194
6301
  "settings.hover.section": "Hover details",
6195
6302
  "settings.hover.showDetails": "Show status detail lines",
6196
6303
  "settings.hover.showDetailsHint": "When off, the hover card shows only the workspace title, path, and creation time — no aggregated status lines.",
6197
6304
  "settings.count.section": "Count display",
6198
6305
  "settings.count.threshold": "Count display threshold",
6199
6306
  "settings.count.thresholdHint": "The count is shown on the badge only when the total is >= this value. Default 2 (omits the count when it equals 1 — the dots already convey \"something is happening\"). Set to 1 to always show.",
6307
+ "settings.count.decrease": "Decrease",
6308
+ "settings.count.increase": "Increase",
6200
6309
  "settings.save": "Save",
6201
6310
  "settings.saving": "Saving…",
6202
6311
  "settings.saveFailed": "The deployment did not accept these values; they were left for you to correct.",