dsh-group-status 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/CHANGELOG.md +117 -0
  2. package/README.en.md +138 -133
  3. package/README.md +140 -133
  4. package/docs/facet-protocol.md +274 -0
  5. package/docs/media/glyph-completed.svg +3 -0
  6. package/docs/media/glyph-pending.svg +3 -0
  7. package/docs/media/glyph-running.svg +6 -0
  8. package/docs/media/screenshot-folders.png +0 -0
  9. package/docs/media/screenshot-hover.png +0 -0
  10. package/docs/upgrading.md +241 -0
  11. package/lib/client.js +1197 -1159
  12. package/lib/client.js.map +1 -1
  13. package/lib/index.js +31 -4
  14. package/lib/types/build-stamp.d.ts +4 -0
  15. package/lib/types/build-stamp.d.ts.map +1 -0
  16. package/lib/types/build-stamp.js +23 -0
  17. package/lib/types/build-stamp.js.map +1 -0
  18. package/lib/types/client/BadgeSettingsCard.js +3 -3
  19. package/lib/types/client/BadgeSettingsCard.js.map +1 -1
  20. package/lib/types/client/group-status.d.ts +5 -0
  21. package/lib/types/client/group-status.d.ts.map +1 -1
  22. package/lib/types/client/group-status.js +25 -0
  23. package/lib/types/client/group-status.js.map +1 -1
  24. package/lib/types/client/locales.d.ts +4 -0
  25. package/lib/types/client/locales.d.ts.map +1 -1
  26. package/lib/types/client/locales.js +4 -0
  27. package/lib/types/client/locales.js.map +1 -1
  28. package/lib/types/client/navigation.d.ts +23 -8
  29. package/lib/types/client/navigation.d.ts.map +1 -1
  30. package/lib/types/client/navigation.js +20 -3
  31. package/lib/types/client/navigation.js.map +1 -1
  32. package/lib/types/client/rows/EmptySessions.d.ts +4 -0
  33. package/lib/types/client/rows/EmptySessions.d.ts.map +1 -0
  34. package/lib/types/client/rows/EmptySessions.js +10 -0
  35. package/lib/types/client/rows/EmptySessions.js.map +1 -0
  36. package/lib/types/client/rows/ListView.d.ts +24 -0
  37. package/lib/types/client/rows/ListView.d.ts.map +1 -0
  38. package/lib/types/client/rows/ListView.js +81 -0
  39. package/lib/types/client/rows/ListView.js.map +1 -0
  40. package/lib/types/client/rows/SessionTree.d.ts +4 -0
  41. package/lib/types/client/rows/SessionTree.d.ts.map +1 -0
  42. package/lib/types/client/rows/SessionTree.js +381 -0
  43. package/lib/types/client/rows/SessionTree.js.map +1 -0
  44. package/lib/types/client/rows/ViewOptionsMenu.d.ts +14 -0
  45. package/lib/types/client/rows/ViewOptionsMenu.d.ts.map +1 -0
  46. package/lib/types/client/rows/ViewOptionsMenu.js +50 -0
  47. package/lib/types/client/rows/ViewOptionsMenu.js.map +1 -0
  48. package/lib/types/client/rows/WorkspaceBrowser.d.ts +0 -5
  49. package/lib/types/client/rows/WorkspaceBrowser.d.ts.map +1 -1
  50. package/lib/types/client/rows/WorkspaceBrowser.js +7 -557
  51. package/lib/types/client/rows/WorkspaceBrowser.js.map +1 -1
  52. package/lib/types/client/rows/browser-shared.d.ts +105 -0
  53. package/lib/types/client/rows/browser-shared.d.ts.map +1 -0
  54. package/lib/types/client/rows/browser-shared.js +93 -0
  55. package/lib/types/client/rows/browser-shared.js.map +1 -0
  56. package/lib/types/client/session-actions/derived.d.ts.map +1 -1
  57. package/lib/types/client/session-actions/derived.js +13 -2
  58. package/lib/types/client/session-actions/derived.js.map +1 -1
  59. package/lib/types/client/session-watermarks.d.ts +6 -1
  60. package/lib/types/client/session-watermarks.d.ts.map +1 -1
  61. package/lib/types/client/session-watermarks.js +31 -11
  62. package/lib/types/client/session-watermarks.js.map +1 -1
  63. package/lib/types/client/toast/register.d.ts.map +1 -1
  64. package/lib/types/client/toast/register.js +102 -132
  65. package/lib/types/client/toast/register.js.map +1 -1
  66. package/lib/types/host-contract.d.ts +15 -1
  67. package/lib/types/host-contract.d.ts.map +1 -1
  68. package/lib/types/host-contract.js +47 -4
  69. package/lib/types/host-contract.js.map +1 -1
  70. package/lib/types/index.d.ts +6 -3
  71. package/lib/types/index.d.ts.map +1 -1
  72. package/lib/types/index.js +10 -4
  73. package/lib/types/index.js.map +1 -1
  74. package/package.json +17 -3
package/lib/client.js CHANGED
@@ -857,6 +857,8 @@ window.__ModuleLoader__.load({
857
857
  text: fieldText(BADGE_FIELDS.countThreshold),
858
858
  label: t("settings.count.threshold"),
859
859
  hint: t("settings.count.thresholdHint"),
860
+ decreaseLabel: t("settings.count.decrease"),
861
+ increaseLabel: t("settings.count.increase"),
860
862
  state,
861
863
  actions
862
864
  })]
@@ -891,7 +893,7 @@ window.__ModuleLoader__.load({
891
893
  });
892
894
  }
893
895
  /** 单行数字输入:label + 带加减按钮的数字输入 + hint。 */
894
- function NumberRow({ field, text, label, hint, state, actions }) {
896
+ function NumberRow({ field, text, label, hint, decreaseLabel, increaseLabel, state, actions }) {
895
897
  const disabled = !state.writable || state.saving;
896
898
  const currentValue = Number(text) || 0;
897
899
  const decrement = () => {
@@ -915,7 +917,7 @@ window.__ModuleLoader__.load({
915
917
  className: BadgeSettingsCard_module_css_default.badgeNumberButton,
916
918
  disabled: disabled || currentValue <= 0,
917
919
  onClick: decrement,
918
- "aria-label": "减少",
920
+ "aria-label": decreaseLabel,
919
921
  children: "−"
920
922
  }),
921
923
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -927,7 +929,7 @@ window.__ModuleLoader__.load({
927
929
  className: BadgeSettingsCard_module_css_default.badgeNumberButton,
928
930
  disabled,
929
931
  onClick: increment,
930
- "aria-label": "增加",
932
+ "aria-label": increaseLabel,
931
933
  children: "+"
932
934
  })
933
935
  ]
@@ -987,11 +989,34 @@ window.__ModuleLoader__.load({
987
989
  }
988
990
  //#endregion
989
991
  //#region src/client/toast/register.tsx
990
- /** 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
+ */
991
996
  function plainToast(display) {
992
997
  return () => ({ display });
993
998
  }
994
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
+ /**
995
1020
  * Register notice presentations with their business-owned display fields and actions.
996
1021
  * @param ctx - workspace plugin context.
997
1022
  * @param viewStore - viewing store, read only by archive notices.
@@ -1008,130 +1033,72 @@ window.__ModuleLoader__.load({
1008
1033
  actions.showArchived();
1009
1034
  }
1010
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
+ ];
1011
1083
  ctx.slots.registerFactory({
1012
1084
  name: "workspace.toast.content",
1013
1085
  scope: "root"
1014
1086
  }, WorkspaceToast);
1015
1087
  ctx.slots.inject("workspace.toast", function* () {
1016
- yield ctx.slots.register({
1017
- name: "workspace.toast",
1018
- key: "sessionIdCopied",
1019
- locale: "workspace",
1020
- inject: plainToast((_notice, t) => ({
1021
- text: t("toast.sessionIdCopied"),
1022
- tone: "success"
1023
- }))
1024
- }, toastEntry);
1025
- yield ctx.slots.register({
1026
- name: "workspace.toast",
1027
- key: "sessionIdCopyFailed",
1028
- locale: "workspace",
1029
- inject: plainToast((_notice, t) => ({
1030
- text: t("toast.sessionIdCopyFailed"),
1031
- tone: "warning"
1032
- }))
1033
- }, toastEntry);
1034
- yield ctx.slots.register({
1035
- name: "workspace.toast",
1036
- key: "pinFailed",
1037
- locale: "workspace",
1038
- inject: plainToast((_notice, t) => ({
1039
- text: t("toast.pinFailed"),
1040
- tone: "warning"
1041
- }))
1042
- }, toastEntry);
1043
- yield ctx.slots.register({
1044
- name: "workspace.toast",
1045
- key: "unpinFailed",
1046
- locale: "workspace",
1047
- inject: plainToast((_notice, t) => ({
1048
- text: t("toast.unpinFailed"),
1049
- tone: "warning"
1050
- }))
1051
- }, toastEntry);
1052
- yield ctx.slots.register({
1053
- name: "workspace.toast",
1054
- key: "archivedNotOpenable",
1055
- locale: "workspace",
1056
- inject: plainToast((_notice, t) => ({
1057
- text: t("toast.archivedNotOpenable"),
1058
- tone: "warning"
1059
- }))
1060
- }, toastEntry);
1061
- yield ctx.slots.register({
1062
- name: "workspace.toast",
1063
- key: "defaultWorkspaceFailed",
1064
- locale: "workspace",
1065
- inject: plainToast((_notice, t) => ({
1066
- text: t("defaultWorkspace.failed"),
1067
- tone: "warning"
1068
- }))
1069
- }, toastEntry);
1070
- yield ctx.slots.register({
1071
- name: "workspace.toast",
1072
- key: "shortcutForkFailed",
1073
- locale: "workspace",
1074
- inject: plainToast((notice, t) => ({
1075
- text: t(notice.reason === "unavailable" ? "shortcut.noCompletedTurn" : "shortcut.forkFailed"),
1076
- tone: "plain"
1077
- }))
1078
- }, toastEntry);
1079
- yield ctx.slots.register({
1080
- name: "workspace.toast",
1081
- key: "createFailed",
1082
- locale: "workspace",
1083
- inject: plainToast((notice, t) => ({
1084
- text: t("toast.createFailed", { message: notice.message }),
1085
- tone: "warning",
1086
- holdMs: 6e3
1087
- }))
1088
- }, toastEntry);
1089
- yield ctx.slots.register({
1088
+ for (const { key, inject } of plainToasts) yield ctx.slots.register({
1090
1089
  name: "workspace.toast",
1091
- key: "forkRequiresOpen",
1090
+ key,
1092
1091
  locale: "workspace",
1093
- inject: plainToast((notice, t, dismiss) => ({
1094
- text: t("toast.forkRequiresOpen"),
1095
- tone: "warning",
1096
- holdMs: 6e3,
1097
- actions: [{
1098
- label: t("toast.openForkSource"),
1099
- onClick: () => {
1100
- dismiss();
1101
- actions.openForkSource(notice.sessionId);
1102
- }
1103
- }]
1104
- }))
1092
+ inject
1105
1093
  }, toastEntry);
1106
- yield ctx.slots.register({
1107
- name: "workspace.toast",
1108
- key: "archived",
1109
- locale: "workspace",
1110
- store: viewStore,
1111
- inject: () => ({
1112
- display: (notice, t, dismiss) => ({
1113
- text: t("toast.archived"),
1114
- tone: "success",
1115
- holdMs: 6e3,
1116
- actions: [{
1117
- label: t("toast.archivedUndo"),
1118
- onClick: () => {
1119
- dismiss();
1120
- actions.undoArchive(notice.sessionId);
1121
- }
1122
- }]
1123
- }),
1124
- filterAction: archiveFilterAction
1125
- })
1126
- }, archiveToastEntry);
1127
- yield ctx.slots.register({
1094
+ for (const { key, text } of ARCHIVE_TOASTS) yield ctx.slots.register({
1128
1095
  name: "workspace.toast",
1129
- key: "stoppedAndArchived",
1096
+ key,
1130
1097
  locale: "workspace",
1131
1098
  store: viewStore,
1132
1099
  inject: () => ({
1133
1100
  display: (notice, t, dismiss) => ({
1134
- text: t("toast.stoppedAndArchived"),
1101
+ text: t(text),
1135
1102
  tone: "success",
1136
1103
  holdMs: 6e3,
1137
1104
  actions: [{
@@ -2385,7 +2352,10 @@ window.__ModuleLoader__.load({
2385
2352
  const sessions = this.sessions.list.getSnapshot();
2386
2353
  const blank = sessions.ids.find((id) => {
2387
2354
  const summary = sessions.byId[id];
2388
- return summary !== void 0 && summary.blank && summary.cwd === workspace.path && workspace.sessionIds.includes(id) && !archived.includes(id);
2355
+ if (summary === void 0 || !summary.blank || summary.cwd !== workspace.path) return false;
2356
+ if (!workspace.sessionIds.includes(id) || archived.includes(id)) return false;
2357
+ const recorded = summary.projectionValues?.agentPreset;
2358
+ return typeof recorded !== "string" || recorded === options.agentPreset;
2389
2359
  });
2390
2360
  return this.createNamed({
2391
2361
  workspaceId: workspace.workspaceId,
@@ -2464,7 +2434,7 @@ window.__ModuleLoader__.load({
2464
2434
  startSession(workspaceId, options) {
2465
2435
  const { agentPreset, ...draft } = options ?? {};
2466
2436
  const draftOptions = options === void 0 ? void 0 : draft;
2467
- const initializeDraft = draftOptions !== void 0 && (draftOptions.prompt !== void 0 || draftOptions.clearPreviousDraft === true);
2437
+ const initializeDraft = draftOptions !== void 0 && (draft.prompt !== void 0 || draft.clearPreviousDraft === true || draft.whenDraftExists !== void 0);
2468
2438
  const workspace = this.workspaces.list.getSnapshot();
2469
2439
  const sessions = this.sessions.list.getSnapshot();
2470
2440
  const current = this.mainReference?.sessionId;
@@ -2698,462 +2668,902 @@ window.__ModuleLoader__.load({
2698
2668
  function workspaceDisplayTitle(title, localizedDefault) {
2699
2669
  return title === "default-workspace" ? localizedDefault : title;
2700
2670
  }
2701
- /**
2702
- * Zero-valued sentinel for {@link GroupLiveStatus}.
2703
- *
2704
- * Used when a group has nothing to report. Kept as a frozen constant so the
2705
- * reference is stable across renders — `Object.is` comparisons in React selectors
2706
- * never trigger spurious re-renders on it.
2707
- */
2708
- const EMPTY_LIVE_STATUS = Object.freeze({
2709
- running: 0,
2710
- approval: 0,
2711
- planReview: 0,
2712
- question: 0,
2713
- done: 0,
2714
- subagents: 0,
2715
- facets: /* @__PURE__ */ new Map()
2716
- });
2717
- /**
2718
- * 合并两份分组折果:本插件自己的计数相加,外部状态来源**按 facet 各自合并**。
2719
- *
2720
- * 用于多层聚合:折叠的父文件夹要覆盖整棵子树,而子树里每一层都已各自折过一遍。
2721
- * `FacetFold.data` 怎么合归该 facet 自己决定,本模块从不解释。
2722
- */
2723
- function mergeLiveStatus(a, b) {
2724
- return {
2725
- running: a.running + b.running,
2726
- approval: a.approval + b.approval,
2727
- planReview: a.planReview + b.planReview,
2728
- question: a.question + b.question,
2729
- done: a.done + b.done,
2730
- subagents: a.subagents + b.subagents,
2731
- facets: mergeFacets(a.facets, b.facets)
2732
- };
2733
- }
2734
- /** 按 facet id 合并两侧折果;只有一侧有的来源原样带过。 */
2735
- function mergeFacets(a, b) {
2736
- if (a.size === 0) return b;
2737
- if (b.size === 0) return a;
2738
- const merged = new Map(a);
2739
- for (const [id, entry] of b) {
2740
- const own = merged.get(id);
2741
- merged.set(id, own === void 0 ? entry : {
2742
- facet: own.facet,
2743
- fold: own.facet.merge?.(own.fold, entry.fold) ?? defaultMergeFold(own.fold, entry.fold)
2744
- });
2745
- }
2746
- return merged;
2671
+ //#endregion
2672
+ //#region \0dsh-css:src/client/WorkspacePicker.module.css.mjs
2673
+ 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)}";
2674
+ const tagId$3 = "dsh-group-status/WorkspacePicker.module.css";
2675
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$3) + "]") === null) {
2676
+ const tag = document.createElement("style");
2677
+ tag.dataset.plugin = "dsh-group-status";
2678
+ tag.dataset.pluginCss = tagId$3;
2679
+ tag.textContent = css$3;
2680
+ document.head.appendChild(tag);
2747
2681
  }
2682
+ var WorkspacePicker_module_css_default = {
2683
+ "menuStatus": "S_JYgq_menuStatus",
2684
+ "modalAction": "S_JYgq_modalAction",
2685
+ "modalError": "S_JYgq_modalError"
2686
+ };
2687
+ //#endregion
2688
+ //#region src/client/WorkspacePicker.tsx
2689
+ const ADD_WORKSPACE = "::add-workspace";
2748
2690
  /**
2749
- * Fold every member row of a group into counts so a collapsed folder can hint at
2750
- * hidden live sessions. Mirrors the session-row status priority (pending
2751
- * outranks running outranks completed) at the counting level; labels resolve with
2752
- * locale at render time.
2753
- *
2754
- * @param nodes - member rows of the group.
2755
- * @param sessionIds - the same members' Session ids (facets address their own data by id).
2756
- * @param facets - the registered status sources to fold over this group.
2757
- * @param now - one clock reading for the whole pass, so sibling facets cannot disagree
2758
- * about which deadlines are still ahead.
2691
+ * Render the pick menu plus the adoption error dialog.
2692
+ * @param props - owner-controlled flow props.
2693
+ * @returns menu + dialog elements.
2759
2694
  */
2760
- function summarizeGroupStatus(nodes, sessionIds, facets = [], now = Date.now()) {
2761
- let running = 0;
2762
- let approval = 0;
2763
- let planReview = 0;
2764
- let question = 0;
2765
- let done = 0;
2766
- let subagents = 0;
2767
- for (const n of nodes) {
2768
- if (n.blank || n.archived) continue;
2769
- const hasSubagents = n.runningSubagentCount > 0;
2770
- if (n.pendingInteraction === "approval") {
2771
- approval += 1;
2772
- subagents += n.runningSubagentCount;
2773
- continue;
2774
- }
2775
- if (n.pendingInteraction === "plan-review") {
2776
- planReview += 1;
2777
- subagents += n.runningSubagentCount;
2778
- continue;
2779
- }
2780
- if (n.pendingInteraction === "question") {
2781
- question += 1;
2782
- subagents += n.runningSubagentCount;
2783
- continue;
2784
- }
2785
- if (n.running) {
2786
- running += 1;
2787
- subagents += n.runningSubagentCount;
2788
- continue;
2695
+ function WorkspacePickFlow({ t, open, anchorRef, useWorkspaces, createWorkspace, useDirectoryFlow, renderDirectoryFlow, onPick, onClose, addOnly = false, onBusyChange, side = "bottom", selectedId }) {
2696
+ const workspaceSnapshot = useWorkspaces((state) => state);
2697
+ const workspaces = workspaceSnapshot.items;
2698
+ const getAnchorRect = (0, react.useCallback)(() => anchorRef?.current?.getBoundingClientRect() ?? null, [anchorRef]);
2699
+ const [errorOpen, setErrorOpen] = (0, react.useState)(false);
2700
+ const [modalError, setModalError] = (0, react.useState)(null);
2701
+ const [flowOpen, setFlowOpen] = (0, react.useState)(false);
2702
+ const [pickingFolder, setPickingFolder] = (0, react.useState)(false);
2703
+ const flowBusy = flowOpen || pickingFolder;
2704
+ (0, react.useEffect)(() => {
2705
+ onBusyChange?.(flowBusy);
2706
+ }, [flowBusy, onBusyChange]);
2707
+ const flowAvailable = useDirectoryFlow((occupied) => occupied);
2708
+ (0, react.useEffect)(() => {
2709
+ if (flowOpen && !flowAvailable) setFlowOpen(false);
2710
+ }, [flowOpen, flowAvailable]);
2711
+ const addEntries = flowAvailable ? [{
2712
+ id: ADD_WORKSPACE,
2713
+ label: t("menu.addWorkspace"),
2714
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPlusOutlineRegular, { size: 16 }),
2715
+ disabled: flowBusy
2716
+ }] : [];
2717
+ const pinAdd = !addOnly && workspaces.length > 0;
2718
+ const items = pinAdd ? workspaces.map((workspace) => ({
2719
+ id: workspace.workspaceId,
2720
+ label: workspaceDisplayTitle(workspace.title, t("workspace.defaultName")),
2721
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderCloseRegular, { size: 16 }),
2722
+ disabled: flowBusy
2723
+ })) : addEntries;
2724
+ const menuIsEmpty = items.length === 0;
2725
+ const closeModal = () => {
2726
+ setErrorOpen(false);
2727
+ setModalError(null);
2728
+ };
2729
+ /** Adopt a picked directory; failures land in the folder-error dialog (Choose again reopens the flow). */
2730
+ const adoptDirectory = (path) => createWorkspace({ path }).then((workspace) => {
2731
+ setFlowOpen(false);
2732
+ onPick(workspace.workspaceId);
2733
+ }).catch((reason) => {
2734
+ setModalError(reason instanceof Error ? reason.message : String(reason));
2735
+ setFlowOpen(false);
2736
+ setErrorOpen(true);
2737
+ });
2738
+ const openDirectoryFlow = (0, react.useCallback)(() => {
2739
+ onClose();
2740
+ setErrorOpen(false);
2741
+ setModalError(null);
2742
+ setFlowOpen(true);
2743
+ }, [onClose]);
2744
+ const listSettled = addOnly || workspaceSnapshot.phase === "ready";
2745
+ const addIsTheOnlyEntry = !pinAdd && listSettled && addEntries.length === 1;
2746
+ (0, react.useEffect)(() => {
2747
+ if (open && addIsTheOnlyEntry && !flowBusy) openDirectoryFlow();
2748
+ }, [
2749
+ open,
2750
+ addIsTheOnlyEntry,
2751
+ flowBusy,
2752
+ openDirectoryFlow
2753
+ ]);
2754
+ /** Owner side of the flow conversation: adopt keeps the flow open (busy) until the Host answers. */
2755
+ const flowOwner = {
2756
+ open: flowOpen,
2757
+ busy: pickingFolder,
2758
+ onPicked: (path) => {
2759
+ setPickingFolder(true);
2760
+ adoptDirectory(path).finally(() => {
2761
+ setPickingFolder(false);
2762
+ });
2763
+ },
2764
+ onCancel: () => {
2765
+ setFlowOpen(false);
2766
+ },
2767
+ onError: (message) => {
2768
+ setFlowOpen(false);
2769
+ setModalError(message);
2770
+ setErrorOpen(true);
2789
2771
  }
2790
- if (hasSubagents) {
2791
- subagents += n.runningSubagentCount;
2792
- continue;
2772
+ };
2773
+ const handleSelect = (id) => {
2774
+ if (id === ADD_WORKSPACE) {
2775
+ openDirectoryFlow();
2776
+ return;
2793
2777
  }
2794
- if (n.completed) done += 1;
2795
- }
2796
- const folded = /* @__PURE__ */ new Map();
2797
- for (const { facet } of facets) {
2798
- let snapshot;
2799
- try {
2800
- snapshot = facet.getSnapshot();
2801
- } catch {
2802
- snapshot = void 0;
2803
- }
2804
- try {
2805
- const result = facet.fold(snapshot, sessionIds, now);
2806
- if (result !== void 0 && result.count > 0) folded.set(facet.id, {
2807
- facet,
2808
- fold: result
2809
- });
2810
- } catch {}
2811
- }
2812
- return {
2813
- running,
2814
- approval,
2815
- planReview,
2816
- question,
2817
- done,
2818
- subagents,
2819
- facets: folded
2778
+ onPick(id);
2820
2779
  };
2780
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2781
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
2782
+ open: open && !addIsTheOnlyEntry && !menuIsEmpty,
2783
+ anchor: null,
2784
+ items,
2785
+ ...pinAdd ? { footer: addEntries } : {},
2786
+ selectedId,
2787
+ onSelect: handleSelect,
2788
+ onClose,
2789
+ side,
2790
+ portal: true,
2791
+ getAnchorRect
2792
+ }),
2793
+ open && !addIsTheOnlyEntry && !menuIsEmpty && workspaceSnapshot.phase === "pending" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2794
+ className: WorkspacePicker_module_css_default.menuStatus,
2795
+ role: "status",
2796
+ children: t("picker.loading")
2797
+ }),
2798
+ renderDirectoryFlow(flowOwner),
2799
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
2800
+ open: errorOpen,
2801
+ onClose: closeModal,
2802
+ closeLabel: t("close"),
2803
+ title: t("folderError.title"),
2804
+ 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, {
2805
+ variant: "outline",
2806
+ className: WorkspacePicker_module_css_default.modalAction,
2807
+ onClick: closeModal,
2808
+ children: t("cancel")
2809
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2810
+ variant: "primary",
2811
+ className: WorkspacePicker_module_css_default.modalAction,
2812
+ disabled: !flowAvailable,
2813
+ onClick: openDirectoryFlow,
2814
+ children: t("folderError.retry")
2815
+ })] }),
2816
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2817
+ className: WorkspacePicker_module_css_default.modalError,
2818
+ role: "alert",
2819
+ children: modalError
2820
+ })
2821
+ })
2822
+ ] });
2821
2823
  }
2822
2824
  /**
2823
- * Measure each group's own members, as the input to {@link aggregateGroupStatuses}.
2824
- *
2825
- * Every group is measured, expanded ones included. A folder row shows no badge while
2826
- * it is expanded, but its sessions still need attention and a collapsed ancestor must
2827
- * keep counting them — so expansion is a rendering fact, never an input to the
2828
- * aggregation. Short-circuiting expanded groups to `EMPTY_LIVE_STATUS` (the shape this
2829
- * had before the tree-wide aggregator existed, harmless while nothing aggregated) makes
2830
- * every collapsed parent undercount by the size of its expanded descendants.
2831
- *
2832
- * @param groups - derived groups, each with its member rows already populated.
2833
- * @param facets - registered sources, already filtered by the visibility settings.
2834
- * @param now - one clock reading shared by every group in the pass.
2825
+ * The conversation empty-state registration: adapts the owner share to the
2826
+ * core flow (all state and semantics live in the flow / the owner).
2827
+ * @param props - empty-state slot props (owner share + injected creation callback).
2828
+ * @returns the flow element.
2835
2829
  */
2836
- function directStatusesForGroups(groups, facets = [], now = Date.now()) {
2837
- const map = /* @__PURE__ */ new Map();
2838
- for (const group of groups) map.set(group.key, summarizeGroupStatus(group.sessions, group.sessions.map((s) => s.id), facets, now));
2839
- return map;
2830
+ function WorkspacePicker({ open, anchorRef, useWorkspaces, selectedId, onPick, onClose, createWorkspace, useDirectoryFlow, renderSlot, t }) {
2831
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspacePickFlow, {
2832
+ t,
2833
+ open,
2834
+ anchorRef,
2835
+ useWorkspaces,
2836
+ createWorkspace,
2837
+ useDirectoryFlow,
2838
+ renderDirectoryFlow: (owner) => renderSlot("conversation.hero.workspace.directoryFlow", owner),
2839
+ selectedId,
2840
+ onPick,
2841
+ onClose
2842
+ });
2843
+ }
2844
+ //#endregion
2845
+ //#region \0dsh-css:src/client/rows/WorkspaceBrowser.module.css.mjs
2846
+ 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}}";
2847
+ const tagId$2 = "dsh-group-status/WorkspaceBrowser.module.css";
2848
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]") === null) {
2849
+ const tag = document.createElement("style");
2850
+ tag.dataset.plugin = "dsh-group-status";
2851
+ tag.dataset.pluginCss = tagId$2;
2852
+ tag.textContent = css$2;
2853
+ document.head.appendChild(tag);
2854
+ }
2855
+ var WorkspaceBrowser_module_css_default = {
2856
+ "archiveActivity": "QAYkWG_archiveActivity",
2857
+ "clearButton": "QAYkWG_clearButton",
2858
+ "deleteAction": "QAYkWG_deleteAction",
2859
+ "deleteStatus": "QAYkWG_deleteStatus",
2860
+ "empty": "QAYkWG_empty",
2861
+ "emptyAction": "QAYkWG_emptyAction",
2862
+ "emptyState": "QAYkWG_emptyState",
2863
+ "fade": "QAYkWG_fade",
2864
+ "flatList": "QAYkWG_flatList",
2865
+ "groupSection": "QAYkWG_groupSection",
2866
+ "headerActions": "QAYkWG_headerActions",
2867
+ "headerActionsHidden": "QAYkWG_headerActionsHidden",
2868
+ "iconButton": "QAYkWG_iconButton",
2869
+ "list": "QAYkWG_list",
2870
+ "listArea": "QAYkWG_listArea",
2871
+ "listTopDropActive": "QAYkWG_listTopDropActive",
2872
+ "listTopDropIndicator": "QAYkWG_listTopDropIndicator",
2873
+ "rail": "QAYkWG_rail",
2874
+ "renameError": "QAYkWG_renameError",
2875
+ "renameInput": "QAYkWG_renameInput",
2876
+ "root": "QAYkWG_root",
2877
+ "search": "QAYkWG_search",
2878
+ "search-skeleton": "QAYkWG_search-skeleton",
2879
+ "searchButton": "QAYkWG_searchButton",
2880
+ "searchExpanded": "QAYkWG_searchExpanded",
2881
+ "searchInput": "QAYkWG_searchInput",
2882
+ "searchSlot": "QAYkWG_searchSlot",
2883
+ "searchSlotExpanded": "QAYkWG_searchSlotExpanded",
2884
+ "searchStatus": "QAYkWG_searchStatus",
2885
+ "searchTree": "QAYkWG_searchTree",
2886
+ "sectionHeader": "QAYkWG_sectionHeader",
2887
+ "sectionLabel": "QAYkWG_sectionLabel",
2888
+ "sectionLabelHidden": "QAYkWG_sectionLabelHidden",
2889
+ "sessionOverflowButton": "QAYkWG_sessionOverflowButton",
2890
+ "skeletonBar": "QAYkWG_skeletonBar",
2891
+ "skeletonBarWide": "QAYkWG_skeletonBarWide",
2892
+ "skeletonBars": "QAYkWG_skeletonBars",
2893
+ "skeletonDot": "QAYkWG_skeletonDot",
2894
+ "skeletonRow": "QAYkWG_skeletonRow",
2895
+ "treeBody": "QAYkWG_treeBody",
2896
+ "viewOptionsMenu": "QAYkWG_viewOptionsMenu",
2897
+ "wide": "QAYkWG_wide",
2898
+ "wide-in": "QAYkWG_wide-in",
2899
+ "workspaceDropAfter": "QAYkWG_workspaceDropAfter",
2900
+ "workspaceDropBefore": "QAYkWG_workspaceDropBefore"
2901
+ };
2902
+ /** Keep controlled input and RPC payload inside the session.search wire contract. */
2903
+ function sanitizeSearchQuery(value) {
2904
+ const withoutNul = value.replaceAll("\0", "");
2905
+ if (withoutNul.length <= 500) return withoutNul;
2906
+ let end = 500;
2907
+ const last = withoutNul.charCodeAt(end - 1);
2908
+ const next = withoutNul.charCodeAt(end);
2909
+ if (last >= 55296 && last <= 56319 && next >= 56320 && next <= 57343) end--;
2910
+ return withoutNul.slice(0, end);
2840
2911
  }
2841
2912
  /**
2842
- * Fold every group's DIRECT status up the workspace tree, so a collapsed folder
2843
- * counts every descendant session. A group's own row shows no badge while it is
2844
- * expanded, but its sessions still need attention and its collapsed ancestor must
2845
- * keep counting them — so the walk never depends on a group's expanded state.
2846
- *
2847
- * @param direct - each group's own-member status, keyed by group key.
2848
- * @param childrenByParent - the rendered tree: parent key (`undefined` = roots) → children.
2849
- * @returns aggregated status per group key: own members plus the whole subtree.
2913
+ * Accept the native drag at document level while a row drag is active: row
2914
+ * hover still owns the insertion marker, and releasing outside the list must
2915
+ * not be rendered as a rejected drop before dragend commits that last marker.
2850
2916
  */
2851
- function aggregateGroupStatuses(direct, childrenByParent) {
2852
- const aggregated = /* @__PURE__ */ new Map();
2853
- const visiting = /* @__PURE__ */ new Set();
2854
- const fold = (key) => {
2855
- const cached = aggregated.get(key);
2856
- if (cached !== void 0) return cached;
2857
- if (visiting.has(key)) return direct.get(key) ?? EMPTY_LIVE_STATUS;
2858
- visiting.add(key);
2859
- let total = direct.get(key) ?? EMPTY_LIVE_STATUS;
2860
- for (const child of childrenByParent.get(key) ?? []) total = mergeLiveStatus(total, fold(child.key));
2861
- visiting.delete(key);
2862
- aggregated.set(key, total);
2863
- return total;
2864
- };
2865
- for (const children of childrenByParent.values()) for (const child of children) fold(child.key);
2866
- return aggregated;
2917
+ function useNativeDragAcceptance(active) {
2918
+ (0, react.useEffect)(() => {
2919
+ if (!active) return;
2920
+ const acceptDrag = (event) => {
2921
+ event.preventDefault();
2922
+ if (event.dataTransfer !== null) event.dataTransfer.dropEffect = "move";
2923
+ };
2924
+ const acceptDrop = (event) => {
2925
+ event.preventDefault();
2926
+ };
2927
+ document.addEventListener("dragover", acceptDrag);
2928
+ document.addEventListener("drop", acceptDrop);
2929
+ return () => {
2930
+ document.removeEventListener("dragover", acceptDrag);
2931
+ document.removeEventListener("drop", acceptDrop);
2932
+ };
2933
+ }, [active]);
2934
+ }
2935
+ /** Apply a visible drop to the complete account without removing hidden members. */
2936
+ function sessionDragOrder(order, rows, drag, over) {
2937
+ const source = rows.find((row) => row.id === drag.sessionId);
2938
+ const target = rows.find((row) => row.id === over.id);
2939
+ 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;
2940
+ const section = rows.filter((row) => row.pinned === drag.pinned);
2941
+ const sourceIndex = section.findIndex((row) => row.id === source.id);
2942
+ if (section.filter((row) => row.id !== source.id).findIndex((row) => row.id === target.id) + (over.half === "after" ? 1 : 0) === sourceIndex) return;
2943
+ const next = order.filter((id) => id !== source.id);
2944
+ const targetIndex = next.indexOf(target.id);
2945
+ if (targetIndex === -1) return;
2946
+ next.splice(targetIndex + (over.half === "after" ? 1 : 0), 0, source.id);
2947
+ return pinCurrentBlank(next, rows.find((row) => row.blank)?.id);
2948
+ }
2949
+ /** Resolve an insertion side across the Workspace header, descendants, and Sessions. */
2950
+ function workspaceGroupHalf(e) {
2951
+ const rect = e.currentTarget.getBoundingClientRect();
2952
+ return e.clientY < rect.top + rect.height / 2 ? "before" : "after";
2867
2953
  }
2868
2954
  /**
2869
- * Attach each group's subtree-wide status, in derivation order. This is the producer
2870
- * the sidebar renders from.
2955
+ * Session the main view currently retains, if any.
2871
2956
  *
2872
- * Expanded groups carry their status too: their own row draws no badge (a decision
2873
- * inside `groupLiveBadge`), but a collapsed ancestor still has to count them, so
2874
- * expansion never suppresses the measurement.
2875
- *
2876
- * @param groups - derived rows, in the order they render.
2877
- * @param direct - each group's own-member status, from {@link directStatusesForGroups}.
2878
- * @param childrenByParent - the rendered tree: parent key (`undefined` = roots) → children.
2957
+ * All three list bodies and the shell need this same scan, and `Object.values`
2958
+ * allocates a fresh array each time; the call sites memoise it against the list
2959
+ * identity rather than re-scanning on every render.
2960
+ * @param list - always-mounted Session list snapshot.
2961
+ * @returns the retained Session id, or undefined when the main view holds none.
2879
2962
  */
2880
- function withAggregatedStatus(groups, direct, childrenByParent) {
2881
- const aggregated = aggregateGroupStatuses(direct, childrenByParent);
2882
- return groups.map((group) => ({
2883
- ...group,
2884
- liveStatus: aggregated.get(group.key) ?? EMPTY_LIVE_STATUS
2885
- }));
2886
- }
2887
- /** The blank row plus the first `watermark` non-blank rows, in list order. */
2888
- function watermarkRows(sessions, watermark) {
2889
- let count = 0;
2890
- const rows = sessions.filter((session) => {
2891
- if (session.blank) return true;
2892
- count += 1;
2893
- return count <= watermark;
2894
- });
2895
- return {
2896
- rows,
2897
- hiddenCount: sessions.length - rows.length
2898
- };
2963
+ function mainViewSessionId(list) {
2964
+ return Object.values(list.byId).find((session) => (session.retainedBy.mainView ?? 0) > 0)?.id;
2899
2965
  }
2966
+ //#endregion
2967
+ //#region src/client/rows/ViewOptionsMenu.tsx
2900
2968
  /**
2901
- * Smallest watermark that covers every non-blank row within the automatic maximum that is running, has a running
2902
- * child, awaits the user, carries an unread completion, or is current, and the reveal target at any position.
2903
- * @param sessions - one group's ordered Session rows.
2904
- * @param current - Session retained by the main view, if the sidebar is the active surface.
2905
- * @param revealSessionId - Session selected from search and required to be visible.
2906
- * @returns the group's baseline non-blank row count.
2907
- */
2908
- function requiredWatermark(sessions, current, revealSessionId) {
2909
- let index = 0;
2910
- let required = 0;
2911
- for (const session of sessions) {
2912
- if (session.blank) continue;
2913
- index += 1;
2914
- if (index <= 50 && (session.running || session.runningSubagentCount > 0 || session.pendingInteraction !== void 0 || session.completed || session.id === current) || session.id === revealSessionId) required = index;
2915
- }
2916
- return required;
2917
- }
2918
- /**
2919
- * Raise each listed group's watermark to its requirement; returns `marks` itself when nothing rises.
2920
- * @param marks - watermarks stored by the current browser tree.
2921
- * @param required - required baselines, in group order.
2922
- * @returns either the unchanged input or a new object containing only raised groups.
2969
+ * Grouping, ordering, and archived-filter menu for the section header.
2970
+ *
2971
+ * Own open state so it resets with the wide chrome; the trigger button is the
2972
+ * menu's own anchor, so the header does not thread a callback down for it.
2923
2973
  */
2924
- function raiseWatermarks(marks, required) {
2925
- let raised = marks;
2926
- for (const [key, watermark] of required) if (watermark > (raised[key] ?? 5)) raised = {
2927
- ...raised,
2928
- [key]: watermark
2929
- };
2930
- return raised;
2974
+ /** Grouping, ordering, and archived-filter menu; own open state so it resets with the wide chrome. */
2975
+ function ViewOptionsMenu({ groupBy, orderBy, archivedFilter, onGroupPick, onOrderPick, onArchivedFilterPick, t }) {
2976
+ const [open, setOpen] = (0, react.useState)(false);
2977
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
2978
+ open,
2979
+ onClose: () => {
2980
+ setOpen(false);
2981
+ },
2982
+ items: [
2983
+ {
2984
+ type: "label",
2985
+ id: "group-by",
2986
+ text: t("groupBy.label")
2987
+ },
2988
+ {
2989
+ id: "workspace",
2990
+ label: t("groupBy.workspace"),
2991
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderCloseRegular, {})
2992
+ },
2993
+ {
2994
+ id: "workspace-tree",
2995
+ label: t("groupBy.workspaceTree"),
2996
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconWorkspaceTreeOutlineRegular, {})
2997
+ },
2998
+ {
2999
+ id: "flat",
3000
+ label: t("groupBy.flat"),
3001
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFlatListOutlineRegular, {})
3002
+ },
3003
+ {
3004
+ type: "separator",
3005
+ id: "order-by-separator"
3006
+ },
3007
+ {
3008
+ type: "label",
3009
+ id: "order-by",
3010
+ text: t("orderBy.label")
3011
+ },
3012
+ {
3013
+ id: "manual",
3014
+ label: t("orderBy.manual"),
3015
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronsUpDownOutlineRegular, {})
3016
+ },
3017
+ {
3018
+ id: "updated",
3019
+ label: t("orderBy.updated"),
3020
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconClockOutlineRegular, {})
3021
+ },
3022
+ {
3023
+ type: "separator",
3024
+ id: "archived-filter-separator"
3025
+ },
3026
+ {
3027
+ type: "label",
3028
+ id: "filter-by",
3029
+ text: t("filterBy.label")
3030
+ },
3031
+ {
3032
+ id: "hide-archived",
3033
+ label: t("viewOptions.hideArchived"),
3034
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconArchiveOffOutlineRegular, {})
3035
+ },
3036
+ {
3037
+ id: "show-archived",
3038
+ label: t("viewOptions.showArchived"),
3039
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconQueueOutlineRegular, {})
3040
+ },
3041
+ {
3042
+ id: "only-archived",
3043
+ label: t("viewOptions.onlyArchived"),
3044
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconArchiveCheckOutlineRegular, {})
3045
+ }
3046
+ ],
3047
+ selectedIds: [
3048
+ groupBy,
3049
+ orderBy,
3050
+ {
3051
+ default: "hide-archived",
3052
+ show: "show-archived",
3053
+ only: "only-archived"
3054
+ }[archivedFilter]
3055
+ ],
3056
+ onSelect: (id) => {
3057
+ if (id === "workspace" || id === "workspace-tree" || id === "flat") onGroupPick(id);
3058
+ else if (id === "manual" || id === "updated") onOrderPick(id);
3059
+ else if (id === "hide-archived") onArchivedFilterPick("default");
3060
+ else if (id === "show-archived") onArchivedFilterPick("show");
3061
+ else if (id === "only-archived") onArchivedFilterPick("only");
3062
+ setOpen(false);
3063
+ },
3064
+ align: "end",
3065
+ dense: true,
3066
+ listClassName: WorkspaceBrowser_module_css_default.viewOptionsMenu,
3067
+ portal: true,
3068
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
3069
+ label: t("viewOptions.label"),
3070
+ side: "bottom",
3071
+ delayMs: 500,
3072
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3073
+ type: "button",
3074
+ className: clsx(WorkspaceBrowser_module_css_default.iconButton, WorkspaceBrowser_module_css_default.wide),
3075
+ "aria-label": t("viewOptions.label"),
3076
+ onClick: () => {
3077
+ setOpen((v) => !v);
3078
+ },
3079
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSlidersTwoOutlineRegular, {})
3080
+ })
3081
+ })
3082
+ });
2931
3083
  }
2932
- //#endregion
2933
- //#region src/client/use-registered-facets.ts
2934
3084
  /**
2935
- * 把登记面读成「这一轮渲染该折哪些来源」的**唯一**一处接线。
2936
- *
2937
- * ## 为什么单独成文
2938
- *
2939
- * 这一步此前直接写在 `rows/WorkspaceBrowser.tsx` 里,两行:
2940
- *
2941
- * ```ts
2942
- * useSyncExternalStore(facetsStore.subscribe, facetsStore.getVersion, …) // 返回值丢掉
2943
- * const facets = useMemo(() => visibleFacets(facetsStore.list(), settings), [settings])
2944
- * ```
2945
- *
2946
- * 看着没问题——订阅了、也 memo 了。实际是**订阅通知只重渲染、不重算**:
2947
- * `useSyncExternalStore` 的返回值被丢掉,`useMemo` 的依赖里就没有任何东西随登记面变化,
2948
- * 于是那次重渲染原样拿回**上一次**的登记面。提供方在自己的 `apply` 里注册,而宿主不保证
2949
- * `apply` 与本组件首渲染的先后:注册**晚于**首渲染时,facet 明明登记上了,却永远进不了
2950
- * 徽标——不报错、不红测试(能渲染这条路径的那份上游 spec 在本仓库是跳过的,见
2951
- * `vitest.config.ts` 的 `MONOREPO_ONLY_SPECS`),只能靠刷新页面碰运气。
2952
- *
2953
- * 版本号必须**同时**当两件事用:① 触发重渲染的信号,② memo 的依赖。这里把它收进一个
2954
- * hook,让「只用①、丢掉②」在结构上写不出来,并且让这条接缝能被 `@testing-library/react`
2955
- * 直接覆盖(本文件零宿主 import,宿主依赖闭包进不来)。
2956
- *
2957
- * ## 为什么现在不再按设置筛
2958
- *
2959
- * 旧版本这里会过一道 `visibleFacets(list, settings)`,按 `facet.settingKey` 查本插件的
2960
- * 布尔开关。那意味着**本插件的配置面里写着别人家的语义**(`showScheduledInBadge` /
2961
- * `showWaitingReviewInBadge`,连图标形状都替对方写死),与"本插件不认识任何来源"直接
2962
- * 矛盾。现在外部来源的显隐归**来源自己**:它想被关掉就不注册 facet,登记面即所见。
2963
- * 本插件的开关只管自己那三档会话状态(见 `badge-settings.ts`)。
3085
+ * Zero-valued sentinel for {@link GroupLiveStatus}.
2964
3086
  *
2965
- * @module dsh-group-status/client/use-registered-facets
3087
+ * Used when a group has nothing to report. Kept as a frozen constant so the
3088
+ * reference is stable across renders — `Object.is` comparisons in React selectors
3089
+ * never trigger spurious re-renders on it.
2966
3090
  */
3091
+ const EMPTY_LIVE_STATUS = Object.freeze({
3092
+ running: 0,
3093
+ approval: 0,
3094
+ planReview: 0,
3095
+ question: 0,
3096
+ done: 0,
3097
+ subagents: 0,
3098
+ facets: /* @__PURE__ */ new Map()
3099
+ });
2967
3100
  /**
2968
- * 当前登记的来源,按登记面顺序(已按 `order` 排好)。
3101
+ * 一份**没有任何贡献**的折果:六个计数全 0 且没有外部来源。
2969
3102
  *
2970
- * 引用在版本号不变时保持稳定:登记面是低频事件,重算成本 = O(facet × 组内会话)。
2971
- *
2972
- * @param store - facet 消费侧存储(`facetStore()` 单例)。
3103
+ * 它在合并里是加法单位元,所以「加它」不该产生新对象——见 {@link mergeLiveStatus}。
2973
3104
  */
2974
- function useRegisteredFacets(store) {
2975
- const version = (0, react.useSyncExternalStore)(store.subscribe, store.getVersion, store.getServerVersion);
2976
- return (0, react.useMemo)(() => [...store.list()], [version]);
3105
+ function isIdleStatus(status) {
3106
+ return status.running === 0 && status.approval === 0 && status.planReview === 0 && status.question === 0 && status.done === 0 && status.subagents === 0 && status.facets.size === 0;
2977
3107
  }
2978
- //#endregion
2979
- //#region src/client/badge-labels.ts
2980
3108
  /**
2981
- * 把侧栏行的翻译位交给 facet。
3109
+ * 合并两份分组折果:本插件自己的计数相加,外部状态来源**按 facet 各自合并**。
2982
3110
  *
2983
- * 这里有一次**故意的放宽**:协议的 `t` 收任意字符串键,而行位只收本插件 `workspace`
2984
- * 命名空间的键。放宽是安全的——宿主 locale 服务对未注册的键一律**原样回显键名**(不是
2985
- * 抛错),所以外来 facet 传了自己命名空间的键,最坏读出的是那串键名而不是崩渲染。
2986
- * 要正确本地化,provider 应当闭包自己的翻译函数、忽略这里传入的 `t`。
2987
- */
2988
- function asFacetTranslate(t) {
2989
- return (key, options) => t(key, options);
2990
- }
2991
- /** 该 facet 贡献进徽标数字的条数。 */
2992
- function facetCountTotal(s) {
2993
- let total = 0;
2994
- for (const entry of s.facets.values()) total += entry.fold.count;
2995
- return total;
2996
- }
2997
- /**
2998
- * 每个外部状态来源的那句话,按登记顺序。
3111
+ * 用于多层聚合:折叠的父文件夹要覆盖整棵子树,而子树里每一层都已各自折过一遍。
3112
+ * `FacetFold.data` 怎么合归该 facet 自己决定,本模块从不解释。
2999
3113
  *
3000
- * facet 没实现 `label` 时**不能**退化成 `2 dsh-later.schedule` 这种读法,所以给一句
3001
- * 通用措辞兜底:只说有几个来源在响,具体语义等该来源自己补文案。
3114
+ * 加法单位元走**引用原样返回**:值不变时换一个新对象,会让"同一棵空闲子树"在两轮
3115
+ * 计算之间有不同的身份。`EMPTY_LIVE_STATUS` 留在这个模块里的理由就是身份稳定(见其
3116
+ * 注释),合并这一层不该每加一次零就把那个理由作废。返回的是入参本身(不改写、不
3117
+ * 拷贝),因此与逐字段相加的结果等值。
3002
3118
  */
3003
- function facetLines(s, t, now) {
3004
- const translate = asFacetTranslate(t);
3005
- const lines = [];
3006
- for (const entry of s.facets.values()) {
3007
- const text = entry.facet.label?.(entry.fold, {
3008
- now,
3009
- t: translate
3119
+ function mergeLiveStatus(a, b) {
3120
+ if (isIdleStatus(b)) return a;
3121
+ if (isIdleStatus(a)) return b;
3122
+ return {
3123
+ running: a.running + b.running,
3124
+ approval: a.approval + b.approval,
3125
+ planReview: a.planReview + b.planReview,
3126
+ question: a.question + b.question,
3127
+ done: a.done + b.done,
3128
+ subagents: a.subagents + b.subagents,
3129
+ facets: mergeFacets(a.facets, b.facets)
3130
+ };
3131
+ }
3132
+ /** 按 facet id 合并两侧折果;只有一侧有的来源原样带过。 */
3133
+ function mergeFacets(a, b) {
3134
+ if (a.size === 0) return b;
3135
+ if (b.size === 0) return a;
3136
+ const merged = new Map(a);
3137
+ for (const [id, entry] of b) {
3138
+ const own = merged.get(id);
3139
+ merged.set(id, own === void 0 ? entry : {
3140
+ facet: own.facet,
3141
+ fold: own.facet.merge?.(own.fold, entry.fold) ?? defaultMergeFold(own.fold, entry.fold)
3010
3142
  });
3011
- lines.push(text === void 0 || text === "" ? `${entry.fold.count} ${t("status.needsAttention")}` : text);
3012
3143
  }
3013
- return lines;
3144
+ return merged;
3014
3145
  }
3015
3146
  /**
3016
- * Accessible label for a folded group's aggregated status, reusing the
3017
- * session-row locale keys so both surfaces read identically.
3147
+ * Fold every member row of a group into counts so a collapsed folder can hint at
3148
+ * hidden live sessions. Mirrors the session-row status priority (pending
3149
+ * outranks running outranks completed) at the counting level; labels resolve with
3150
+ * locale at render time.
3018
3151
  *
3019
- * 按设置过滤:本插件那三档会话状态按设置逐项过滤;**外部来源不参与过滤** —— 登记了就
3020
- * 一定出现在这里,没登记就不在折果里,所以 `aria-label` 与徽标视觉天然一致,不必在这里
3021
- * 再判一次设置(本插件也没有替别人家存的开关)。
3152
+ * @param nodes - member rows of the group.
3153
+ * @param sessionIds - the same members' Session ids (facets address their own data by id).
3154
+ * @param facets - the registered status sources to fold over this group.
3155
+ * @param now - one clock reading for the whole pass, so sibling facets cannot disagree
3156
+ * about which deadlines are still ahead.
3022
3157
  */
3023
- function groupLiveLabel(s, t, settings, now = Date.now()) {
3024
- const parts = [];
3025
- if (settings.showPendingInBadge) {
3026
- if (s.approval > 0) parts.push(`${s.approval} ${t("status.waitingApproval")}`);
3027
- if (s.planReview > 0) parts.push(`${s.planReview} ${t("status.planReview")}`);
3028
- if (s.question > 0) parts.push(`${s.question} ${t("status.waitingAnswer")}`);
3158
+ function summarizeGroupStatus(nodes, sessionIds, facets = [], now = Date.now()) {
3159
+ let running = 0;
3160
+ let approval = 0;
3161
+ let planReview = 0;
3162
+ let question = 0;
3163
+ let done = 0;
3164
+ let subagents = 0;
3165
+ for (const n of nodes) {
3166
+ if (n.blank || n.archived) continue;
3167
+ const hasSubagents = n.runningSubagentCount > 0;
3168
+ if (n.pendingInteraction === "approval") {
3169
+ approval += 1;
3170
+ subagents += n.runningSubagentCount;
3171
+ continue;
3172
+ }
3173
+ if (n.pendingInteraction === "plan-review") {
3174
+ planReview += 1;
3175
+ subagents += n.runningSubagentCount;
3176
+ continue;
3177
+ }
3178
+ if (n.pendingInteraction === "question") {
3179
+ question += 1;
3180
+ subagents += n.runningSubagentCount;
3181
+ continue;
3182
+ }
3183
+ if (n.running) {
3184
+ running += 1;
3185
+ subagents += n.runningSubagentCount;
3186
+ continue;
3187
+ }
3188
+ if (hasSubagents) {
3189
+ subagents += n.runningSubagentCount;
3190
+ continue;
3191
+ }
3192
+ if (n.completed) done += 1;
3029
3193
  }
3030
- if (settings.showRunningInBadge) {
3031
- if (s.running > 0) parts.push(`${s.running} ${t("status.running")}`);
3032
- if (s.subagents > 0) parts.push(t(s.subagents === 1 ? "status.subagentsRunning.one" : "status.subagentsRunning.other", { n: s.subagents }));
3194
+ const folded = /* @__PURE__ */ new Map();
3195
+ for (const { facet } of facets) {
3196
+ let snapshot;
3197
+ try {
3198
+ snapshot = facet.getSnapshot();
3199
+ } catch {
3200
+ snapshot = void 0;
3201
+ }
3202
+ try {
3203
+ const result = facet.fold(snapshot, sessionIds, now);
3204
+ if (result !== void 0 && result.count > 0) folded.set(facet.id, {
3205
+ facet,
3206
+ fold: result
3207
+ });
3208
+ } catch {}
3033
3209
  }
3034
- parts.push(...facetLines(s, t, now));
3035
- if (settings.showCompletedInBadge && s.done > 0) parts.push(`${s.done} ${t("status.completed")}`);
3036
- return parts.join(" · ");
3210
+ if (running === 0 && approval === 0 && planReview === 0 && question === 0 && done === 0 && subagents === 0 && folded.size === 0) return EMPTY_LIVE_STATUS;
3211
+ return {
3212
+ running,
3213
+ approval,
3214
+ planReview,
3215
+ question,
3216
+ done,
3217
+ subagents,
3218
+ facets: folded
3219
+ };
3037
3220
  }
3038
3221
  /**
3039
- * 每个外部状态来源的明细行,**带它自己的图标**(#24)。
3222
+ * Measure each group's own members, as the input to {@link aggregateGroupStatuses}.
3040
3223
  *
3041
- * 徽标侧画的是 provider 交出的、已含色档的元素(`rows/GroupBadge.tsx` 的 `facetGlyph`),
3042
- * 明细侧此前只画一个统一的 `StateDot`,于是同一条状态、同一次悬停里,徽标是红色沙漏、
3043
- * 明细是黄点——同一行的两个视图各说一套。宿主 `StateDot` 只有 warning / ongoing / done
3044
- * 三档、没有 error 档,所以色档**只能**由来源自己的图标表达,这也是本函数存在的理由。
3224
+ * Every group is measured, expanded ones included. A folder row shows no badge while
3225
+ * it is expanded, but its sessions still need attention and a collapsed ancestor must
3226
+ * keep counting them — so expansion is a rendering fact, never an input to the
3227
+ * aggregation. Short-circuiting expanded groups to `EMPTY_LIVE_STATUS` (the shape this
3228
+ * had before the tree-wide aggregator existed, harmless while nothing aggregated) makes
3229
+ * every collapsed parent undercount by the size of its expanded descendants.
3045
3230
  *
3046
- * provider 没交图标时退回通用点(与徽标侧同一套兜底),陌生来源仍然可见。
3231
+ * @param groups - derived groups, each with its member rows already populated.
3232
+ * @param facets - registered sources, already filtered by the visibility settings.
3233
+ * @param now - one clock reading shared by every group in the pass.
3234
+ */
3235
+ function directStatusesForGroups(groups, facets = [], now = Date.now()) {
3236
+ const map = /* @__PURE__ */ new Map();
3237
+ for (const group of groups) map.set(group.key, summarizeGroupStatus(group.sessions, group.sessions.map((s) => s.id), facets, now));
3238
+ return map;
3239
+ }
3240
+ /**
3241
+ * Fold every group's DIRECT status up the workspace tree, so a collapsed folder
3242
+ * counts every descendant session. A group's own row shows no badge while it is
3243
+ * expanded, but its sessions still need attention and its collapsed ancestor must
3244
+ * keep counting them — so the walk never depends on a group's expanded state.
3047
3245
  *
3048
- * @returns 明细行;`glyph` 为 `undefined` 表示该行用 `StateDot` 渲染。
3246
+ * @param direct - each group's own-member status, keyed by group key.
3247
+ * @param childrenByParent - the rendered tree: parent key (`undefined` = roots) → children.
3248
+ * @returns aggregated status per group key: own members plus the whole subtree.
3049
3249
  */
3050
- function facetDetailLines(s, t, now) {
3051
- const translate = asFacetTranslate(t);
3052
- const lines = [];
3053
- for (const entry of s.facets.values()) {
3054
- const raw = entry.facet.label?.(entry.fold, {
3055
- now,
3056
- t: translate
3057
- });
3058
- const text = raw === void 0 || raw === "" ? `${entry.fold.count} ${t("status.needsAttention")}` : raw;
3059
- lines.push({
3060
- state: "warning",
3061
- text,
3062
- glyph: facetGlyph(entry, t, now)
3063
- });
3064
- }
3065
- return lines;
3250
+ function aggregateGroupStatuses(direct, childrenByParent) {
3251
+ const aggregated = /* @__PURE__ */ new Map();
3252
+ const visiting = /* @__PURE__ */ new Set();
3253
+ const fold = (key) => {
3254
+ const cached = aggregated.get(key);
3255
+ if (cached !== void 0) return cached;
3256
+ if (visiting.has(key)) return direct.get(key) ?? EMPTY_LIVE_STATUS;
3257
+ visiting.add(key);
3258
+ let total = direct.get(key) ?? EMPTY_LIVE_STATUS;
3259
+ for (const child of childrenByParent.get(key) ?? []) total = mergeLiveStatus(total, fold(child.key));
3260
+ visiting.delete(key);
3261
+ aggregated.set(key, total);
3262
+ return total;
3263
+ };
3264
+ for (const children of childrenByParent.values()) for (const child of children) fold(child.key);
3265
+ return aggregated;
3066
3266
  }
3067
3267
  /**
3068
- * Build the status detail lines for the workspace hover card.
3268
+ * Attach each group's subtree-wide status, in derivation order. This is the producer
3269
+ * the sidebar renders from.
3069
3270
  *
3070
- * Extracted so `WorkspaceHoverContent` in `Rows.tsx` only needs to map the returned
3071
- * array — the fork-specific status→line logic lives here.
3271
+ * Expanded groups carry their status too: their own row draws no badge (a decision
3272
+ * inside `groupLiveBadge`), but a collapsed ancestor still has to count them, so
3273
+ * expansion never suppresses the measurement.
3072
3274
  *
3073
- * 按设置过滤:关掉的来源不出现在明细行里;`showHoverDetails=false` 时整段返回空。
3074
- * 明细行的顺序与徽标**完全一致**(外部来源排在会话状态之后、已完成之前):两处顺序
3075
- * 不一样会让人怀疑它们是两套事实。
3275
+ * @param groups - derived rows, in the order they render.
3276
+ * @param direct - each group's own-member status, from {@link directStatusesForGroups}.
3277
+ * @param childrenByParent - the rendered tree: parent key (`undefined` = roots) → children.
3076
3278
  */
3077
- function hoverStatusLines(status, t, settings, now = Date.now()) {
3078
- const lines = [];
3079
- if (status === void 0 || !settings.showHoverDetails) return lines;
3080
- if (settings.showPendingInBadge) {
3081
- if (status.approval > 0) lines.push({
3082
- state: "warning",
3083
- text: `${status.approval} ${t("status.waitingApproval")}`
3084
- });
3085
- if (status.planReview > 0) lines.push({
3086
- state: "warning",
3087
- text: `${status.planReview} ${t("status.planReview")}`
3088
- });
3089
- if (status.question > 0) lines.push({
3090
- state: "warning",
3091
- text: `${status.question} ${t("status.waitingAnswer")}`
3092
- });
3093
- }
3094
- if (settings.showRunningInBadge) {
3095
- if (status.running > 0) lines.push({
3096
- state: "ongoing",
3097
- text: `${status.running} ${t("status.running")}`
3098
- });
3099
- if (status.subagents > 0) lines.push({
3100
- state: "ongoing",
3101
- text: t(status.subagents === 1 ? "status.subagentsRunning.one" : "status.subagentsRunning.other", { n: status.subagents })
3102
- });
3279
+ function withAggregatedStatus(groups, direct, childrenByParent) {
3280
+ const aggregated = aggregateGroupStatuses(direct, childrenByParent);
3281
+ return groups.map((group) => ({
3282
+ ...group,
3283
+ liveStatus: aggregated.get(group.key) ?? EMPTY_LIVE_STATUS
3284
+ }));
3285
+ }
3286
+ /**
3287
+ * The blank row plus the first `watermark` non-blank rows, in list order.
3288
+ * @param sessions - one group's rows, whose blank placeholder(s) lead the list
3289
+ * (`sectionMembers` puts every blank session ahead of the pinned/ordinary partitions).
3290
+ * @param watermark - highest non-blank row index that stays visible; `0` keeps only blank rows.
3291
+ */
3292
+ function watermarkRows(sessions, watermark) {
3293
+ const rows = [];
3294
+ let count = 0;
3295
+ for (const session of sessions) {
3296
+ if (session.blank) {
3297
+ rows.push(session);
3298
+ continue;
3299
+ }
3300
+ if (count >= watermark) break;
3301
+ count += 1;
3302
+ rows.push(session);
3103
3303
  }
3104
- lines.push(...facetDetailLines(status, t, now));
3105
- if (settings.showCompletedInBadge && status.done > 0) lines.push({
3106
- state: "done",
3107
- text: `${status.done} ${t("status.completed")}`
3108
- });
3109
- return lines;
3304
+ return {
3305
+ rows,
3306
+ hiddenCount: sessions.length - rows.length
3307
+ };
3110
3308
  }
3111
3309
  /**
3112
- * 某一条外部来源的元素该带哪只 class(#22)。
3113
- *
3114
- * 脉冲判据仍由各 facet 自己声明(`FacetFold.pulse`),但**作用对象必须下沉到声明它的那个
3115
- * 元素**。此前 `groupBadgeIsUrgent()` 只返回一个整徽标的布尔值、class 加在容器上,于是同组
3116
- * 里任何一家声明 `pulse: true` 都会让别家本该静止的图标跟着呼吸:`pulse: false` 是否生效,
3117
- * 取决于同组里恰好有没有别人开了闪。判断交出去了,作用对象没交出去——这是修过一次的回归
3118
- * (见下)。
3119
- *
3120
- * 保留这条历史的原因写在所有者那边:dsh-gitea-dispatch 把 `overdue` 画成**静止**(其
3121
- * `lib/waiting-glyph.js`:「超时后停动画:静止即不再期待」),且它只代表 CI 窗口到期、
3122
- * 不代表评审失败。同一条等待在行内静止、在父级闪动,就是同一事实的两种说法。
3310
+ * Smallest watermark that covers every non-blank row within the automatic maximum that is running, has a running
3311
+ * child, awaits the user, carries an unread completion, or is current, and the reveal target at any position.
3312
+ * @param sessions - one group's ordered Session rows.
3313
+ * @param current - Session retained by the main view, if the sidebar is the active surface.
3314
+ * @param revealSessionId - Session selected from search and required to be visible.
3315
+ * @returns the group's baseline non-blank row count.
3123
3316
  */
3124
- function facetPulseClass(entry, urgentClass) {
3125
- return entry.fold.pulse ? urgentClass : void 0;
3317
+ function requiredWatermark(sessions, current, revealSessionId) {
3318
+ let index = 0;
3319
+ let required = 0;
3320
+ for (const session of sessions) {
3321
+ if (session.blank) continue;
3322
+ index += 1;
3323
+ if (index <= 50 && (session.running || session.runningSubagentCount > 0 || session.pendingInteraction !== void 0 || session.completed || session.id === current) || session.id === revealSessionId) required = index;
3324
+ }
3325
+ return required;
3126
3326
  }
3127
3327
  /**
3128
- * 一个来源的图标。
3129
- *
3130
- * 归 provider 画(`facet.renderIcon`):「这个状态长什么样」是它的知识。没实现时本插件
3131
- * 按色档退回一个通用点(见 `rows/GroupBadge.tsx`),保证陌生来源仍然可见、只是朴素。
3328
+ * Raise each listed group's watermark to its requirement; returns `marks` itself when nothing rises.
3329
+ * @param marks - watermarks stored by the current browser tree.
3330
+ * @param required - required baselines, in group order.
3331
+ * @returns either the unchanged input or a new object containing only raised groups.
3132
3332
  */
3133
- function facetGlyph(entry, t, now) {
3134
- return entry.facet.renderIcon?.(entry.fold, {
3135
- now,
3136
- t: asFacetTranslate(t)
3137
- });
3333
+ function raiseWatermarks(marks, required) {
3334
+ let raised;
3335
+ for (const [key, watermark] of required) {
3336
+ if (watermark <= ((raised ?? marks)[key] ?? 5)) continue;
3337
+ raised ??= { ...marks };
3338
+ raised[key] = watermark;
3339
+ }
3340
+ return raised ?? marks;
3138
3341
  }
3139
3342
  //#endregion
3140
- //#region \0dsh-css:src/client/rows/Rows.module.css.mjs
3141
- 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}}";
3142
- const tagId$3 = "dsh-group-status/Rows.module.css";
3143
- if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$3) + "]") === null) {
3144
- const tag = document.createElement("style");
3145
- tag.dataset.plugin = "dsh-group-status";
3146
- tag.dataset.pluginCss = tagId$3;
3147
- tag.textContent = css$3;
3148
- document.head.appendChild(tag);
3149
- }
3150
- var Rows_module_css_default = {
3151
- "archived": "_1Jb0BW_archived",
3152
- "arrow": "_1Jb0BW_arrow",
3153
- "arrowOpen": "_1Jb0BW_arrowOpen",
3154
- "chevron": "_1Jb0BW_chevron",
3155
- "dot": "_1Jb0BW_dot",
3156
- "dropAfter": "_1Jb0BW_dropAfter",
3343
+ //#region src/client/use-registered-facets.ts
3344
+ /**
3345
+ * 把登记面读成「这一轮渲染该折哪些来源」的**唯一**一处接线。
3346
+ *
3347
+ * ## 为什么单独成文
3348
+ *
3349
+ * 这一步此前直接写在 `rows/WorkspaceBrowser.tsx` 里,两行:
3350
+ *
3351
+ * ```ts
3352
+ * useSyncExternalStore(facetsStore.subscribe, facetsStore.getVersion, …) // 返回值丢掉
3353
+ * const facets = useMemo(() => visibleFacets(facetsStore.list(), settings), [settings])
3354
+ * ```
3355
+ *
3356
+ * 看着没问题——订阅了、也 memo 了。实际是**订阅通知只重渲染、不重算**:
3357
+ * `useSyncExternalStore` 的返回值被丢掉,`useMemo` 的依赖里就没有任何东西随登记面变化,
3358
+ * 于是那次重渲染原样拿回**上一次**的登记面。提供方在自己的 `apply` 里注册,而宿主不保证
3359
+ * `apply` 与本组件首渲染的先后:注册**晚于**首渲染时,facet 明明登记上了,却永远进不了
3360
+ * 徽标——不报错、不红测试(能渲染这条路径的那份上游 spec 在本仓库是跳过的,见
3361
+ * `vitest.config.ts` 的 `MONOREPO_ONLY_SPECS`),只能靠刷新页面碰运气。
3362
+ *
3363
+ * 版本号必须**同时**当两件事用:① 触发重渲染的信号,② memo 的依赖。这里把它收进一个
3364
+ * hook,让「只用①、丢掉②」在结构上写不出来,并且让这条接缝能被 `@testing-library/react`
3365
+ * 直接覆盖(本文件零宿主 import,宿主依赖闭包进不来)。
3366
+ *
3367
+ * ## 为什么现在不再按设置筛
3368
+ *
3369
+ * 旧版本这里会过一道 `visibleFacets(list, settings)`,按 `facet.settingKey` 查本插件的
3370
+ * 布尔开关。那意味着**本插件的配置面里写着别人家的语义**(`showScheduledInBadge` /
3371
+ * `showWaitingReviewInBadge`,连图标形状都替对方写死),与"本插件不认识任何来源"直接
3372
+ * 矛盾。现在外部来源的显隐归**来源自己**:它想被关掉就不注册 facet,登记面即所见。
3373
+ * 本插件的开关只管自己那三档会话状态(见 `badge-settings.ts`)。
3374
+ *
3375
+ * @module dsh-group-status/client/use-registered-facets
3376
+ */
3377
+ /**
3378
+ * 当前登记的来源,按登记面顺序(已按 `order` 排好)。
3379
+ *
3380
+ * 引用在版本号不变时保持稳定:登记面是低频事件,重算成本 = O(facet × 组内会话)。
3381
+ *
3382
+ * @param store - facet 消费侧存储(`facetStore()` 单例)。
3383
+ */
3384
+ function useRegisteredFacets(store) {
3385
+ const version = (0, react.useSyncExternalStore)(store.subscribe, store.getVersion, store.getServerVersion);
3386
+ return (0, react.useMemo)(() => [...store.list()], [version]);
3387
+ }
3388
+ //#endregion
3389
+ //#region src/client/badge-labels.ts
3390
+ /**
3391
+ * 把侧栏行的翻译位交给 facet。
3392
+ *
3393
+ * 这里有一次**故意的放宽**:协议的 `t` 收任意字符串键,而行位只收本插件 `workspace`
3394
+ * 命名空间的键。放宽是安全的——宿主 locale 服务对未注册的键一律**原样回显键名**(不是
3395
+ * 抛错),所以外来 facet 传了自己命名空间的键,最坏读出的是那串键名而不是崩渲染。
3396
+ * 要正确本地化,provider 应当闭包自己的翻译函数、忽略这里传入的 `t`。
3397
+ */
3398
+ function asFacetTranslate(t) {
3399
+ return (key, options) => t(key, options);
3400
+ }
3401
+ /** 该 facet 贡献进徽标数字的条数。 */
3402
+ function facetCountTotal(s) {
3403
+ let total = 0;
3404
+ for (const entry of s.facets.values()) total += entry.fold.count;
3405
+ return total;
3406
+ }
3407
+ /**
3408
+ * 每个外部状态来源的那句话,按登记顺序。
3409
+ *
3410
+ * facet 没实现 `label` 时**不能**退化成 `2 dsh-later.schedule` 这种读法,所以给一句
3411
+ * 通用措辞兜底:只说有几个来源在响,具体语义等该来源自己补文案。
3412
+ */
3413
+ function facetLines(s, t, now) {
3414
+ const translate = asFacetTranslate(t);
3415
+ const lines = [];
3416
+ for (const entry of s.facets.values()) {
3417
+ const text = entry.facet.label?.(entry.fold, {
3418
+ now,
3419
+ t: translate
3420
+ });
3421
+ lines.push(text === void 0 || text === "" ? `${entry.fold.count} ${t("status.needsAttention")}` : text);
3422
+ }
3423
+ return lines;
3424
+ }
3425
+ /**
3426
+ * Accessible label for a folded group's aggregated status, reusing the
3427
+ * session-row locale keys so both surfaces read identically.
3428
+ *
3429
+ * 按设置过滤:本插件那三档会话状态按设置逐项过滤;**外部来源不参与过滤** —— 登记了就
3430
+ * 一定出现在这里,没登记就不在折果里,所以 `aria-label` 与徽标视觉天然一致,不必在这里
3431
+ * 再判一次设置(本插件也没有替别人家存的开关)。
3432
+ */
3433
+ function groupLiveLabel(s, t, settings, now = Date.now()) {
3434
+ const parts = [];
3435
+ if (settings.showPendingInBadge) {
3436
+ if (s.approval > 0) parts.push(`${s.approval} ${t("status.waitingApproval")}`);
3437
+ if (s.planReview > 0) parts.push(`${s.planReview} ${t("status.planReview")}`);
3438
+ if (s.question > 0) parts.push(`${s.question} ${t("status.waitingAnswer")}`);
3439
+ }
3440
+ if (settings.showRunningInBadge) {
3441
+ if (s.running > 0) parts.push(`${s.running} ${t("status.running")}`);
3442
+ if (s.subagents > 0) parts.push(t(s.subagents === 1 ? "status.subagentsRunning.one" : "status.subagentsRunning.other", { n: s.subagents }));
3443
+ }
3444
+ parts.push(...facetLines(s, t, now));
3445
+ if (settings.showCompletedInBadge && s.done > 0) parts.push(`${s.done} ${t("status.completed")}`);
3446
+ return parts.join(" · ");
3447
+ }
3448
+ /**
3449
+ * 每个外部状态来源的明细行,**带它自己的图标**(#24)。
3450
+ *
3451
+ * 徽标侧画的是 provider 交出的、已含色档的元素(`rows/GroupBadge.tsx` 的 `facetGlyph`),
3452
+ * 明细侧此前只画一个统一的 `StateDot`,于是同一条状态、同一次悬停里,徽标是红色沙漏、
3453
+ * 明细是黄点——同一行的两个视图各说一套。宿主 `StateDot` 只有 warning / ongoing / done
3454
+ * 三档、没有 error 档,所以色档**只能**由来源自己的图标表达,这也是本函数存在的理由。
3455
+ *
3456
+ * provider 没交图标时退回通用点(与徽标侧同一套兜底),陌生来源仍然可见。
3457
+ *
3458
+ * @returns 明细行;`glyph` 为 `undefined` 表示该行用 `StateDot` 渲染。
3459
+ */
3460
+ function facetDetailLines(s, t, now) {
3461
+ const translate = asFacetTranslate(t);
3462
+ const lines = [];
3463
+ for (const entry of s.facets.values()) {
3464
+ const raw = entry.facet.label?.(entry.fold, {
3465
+ now,
3466
+ t: translate
3467
+ });
3468
+ const text = raw === void 0 || raw === "" ? `${entry.fold.count} ${t("status.needsAttention")}` : raw;
3469
+ lines.push({
3470
+ state: "warning",
3471
+ text,
3472
+ glyph: facetGlyph(entry, t, now)
3473
+ });
3474
+ }
3475
+ return lines;
3476
+ }
3477
+ /**
3478
+ * Build the status detail lines for the workspace hover card.
3479
+ *
3480
+ * Extracted so `WorkspaceHoverContent` in `Rows.tsx` only needs to map the returned
3481
+ * array — the fork-specific status→line logic lives here.
3482
+ *
3483
+ * 按设置过滤:关掉的来源不出现在明细行里;`showHoverDetails=false` 时整段返回空。
3484
+ * 明细行的顺序与徽标**完全一致**(外部来源排在会话状态之后、已完成之前):两处顺序
3485
+ * 不一样会让人怀疑它们是两套事实。
3486
+ */
3487
+ function hoverStatusLines(status, t, settings, now = Date.now()) {
3488
+ const lines = [];
3489
+ if (status === void 0 || !settings.showHoverDetails) return lines;
3490
+ if (settings.showPendingInBadge) {
3491
+ if (status.approval > 0) lines.push({
3492
+ state: "warning",
3493
+ text: `${status.approval} ${t("status.waitingApproval")}`
3494
+ });
3495
+ if (status.planReview > 0) lines.push({
3496
+ state: "warning",
3497
+ text: `${status.planReview} ${t("status.planReview")}`
3498
+ });
3499
+ if (status.question > 0) lines.push({
3500
+ state: "warning",
3501
+ text: `${status.question} ${t("status.waitingAnswer")}`
3502
+ });
3503
+ }
3504
+ if (settings.showRunningInBadge) {
3505
+ if (status.running > 0) lines.push({
3506
+ state: "ongoing",
3507
+ text: `${status.running} ${t("status.running")}`
3508
+ });
3509
+ if (status.subagents > 0) lines.push({
3510
+ state: "ongoing",
3511
+ text: t(status.subagents === 1 ? "status.subagentsRunning.one" : "status.subagentsRunning.other", { n: status.subagents })
3512
+ });
3513
+ }
3514
+ lines.push(...facetDetailLines(status, t, now));
3515
+ if (settings.showCompletedInBadge && status.done > 0) lines.push({
3516
+ state: "done",
3517
+ text: `${status.done} ${t("status.completed")}`
3518
+ });
3519
+ return lines;
3520
+ }
3521
+ /**
3522
+ * 某一条外部来源的元素该带哪只 class(#22)。
3523
+ *
3524
+ * 脉冲判据仍由各 facet 自己声明(`FacetFold.pulse`),但**作用对象必须下沉到声明它的那个
3525
+ * 元素**。此前 `groupBadgeIsUrgent()` 只返回一个整徽标的布尔值、class 加在容器上,于是同组
3526
+ * 里任何一家声明 `pulse: true` 都会让别家本该静止的图标跟着呼吸:`pulse: false` 是否生效,
3527
+ * 取决于同组里恰好有没有别人开了闪。判断交出去了,作用对象没交出去——这是修过一次的回归
3528
+ * (见下)。
3529
+ *
3530
+ * 保留这条历史的原因写在所有者那边:dsh-gitea-dispatch 把 `overdue` 画成**静止**(其
3531
+ * `lib/waiting-glyph.js`:「超时后停动画:静止即不再期待」),且它只代表 CI 窗口到期、
3532
+ * 不代表评审失败。同一条等待在行内静止、在父级闪动,就是同一事实的两种说法。
3533
+ */
3534
+ function facetPulseClass(entry, urgentClass) {
3535
+ return entry.fold.pulse ? urgentClass : void 0;
3536
+ }
3537
+ /**
3538
+ * 一个来源的图标。
3539
+ *
3540
+ * 归 provider 画(`facet.renderIcon`):「这个状态长什么样」是它的知识。没实现时本插件
3541
+ * 按色档退回一个通用点(见 `rows/GroupBadge.tsx`),保证陌生来源仍然可见、只是朴素。
3542
+ */
3543
+ function facetGlyph(entry, t, now) {
3544
+ return entry.facet.renderIcon?.(entry.fold, {
3545
+ now,
3546
+ t: asFacetTranslate(t)
3547
+ });
3548
+ }
3549
+ //#endregion
3550
+ //#region \0dsh-css:src/client/rows/Rows.module.css.mjs
3551
+ 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}}";
3552
+ const tagId$1 = "dsh-group-status/Rows.module.css";
3553
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
3554
+ const tag = document.createElement("style");
3555
+ tag.dataset.plugin = "dsh-group-status";
3556
+ tag.dataset.pluginCss = tagId$1;
3557
+ tag.textContent = css$1;
3558
+ document.head.appendChild(tag);
3559
+ }
3560
+ var Rows_module_css_default = {
3561
+ "archived": "_1Jb0BW_archived",
3562
+ "arrow": "_1Jb0BW_arrow",
3563
+ "arrowOpen": "_1Jb0BW_arrowOpen",
3564
+ "chevron": "_1Jb0BW_chevron",
3565
+ "dot": "_1Jb0BW_dot",
3566
+ "dropAfter": "_1Jb0BW_dropAfter",
3157
3567
  "dropBefore": "_1Jb0BW_dropBefore",
3158
3568
  "folder": "_1Jb0BW_folder",
3159
3569
  "folderActive": "_1Jb0BW_folderActive",
@@ -3792,644 +4202,226 @@ window.__ModuleLoader__.load({
3792
4202
  },
3793
4203
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
3794
4204
  open: menuOpen,
3795
- onClose: () => {
3796
- setMenuOpen(false);
3797
- },
3798
- portal: true,
3799
- closeOnPointerLeave: true,
3800
- anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3801
- type: "button",
3802
- className: Rows_module_css_default.iconButton,
3803
- "aria-label": t("actions.session.aria", { name: title }),
3804
- onClick: () => {
3805
- setMenuOpen((v) => !v);
3806
- },
3807
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconEllipsisOutlineRegular, {})
3808
- }),
3809
- children: renderSlot("sidebar.workspaces.session.menu.item", {
3810
- sessionId: node.id,
3811
- displayTitle: row.title
3812
- }, { hookContext: menuOpenState })
3813
- }), renderSlot("sidebar.workspaces.session.row.action", {
3814
- sessionId: node.id,
3815
- displayTitle: row.title
3816
- })]
3817
- })
3818
- ]
3819
- });
3820
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.HoverCard, {
3821
- anchor: ownRow,
3822
- content: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SessionHoverContent, {
3823
- node,
3824
- now,
3825
- renderSlot,
3826
- t
3827
- }),
3828
- openDelayMs: 800,
3829
- grouped: true,
3830
- disabled: menuOpen || drag?.active === true,
3831
- copyText: row.blank || row.title === "" ? void 0 : row.title,
3832
- copyLabel: t("copy"),
3833
- copiedLabel: t("hover.copied")
3834
- });
3835
- }
3836
- //#endregion
3837
- //#region \0dsh-css:src/client/rows/AnimatedRows.module.css.mjs
3838
- const css$2 = "._8TnAsq_exits{contain:strict;pointer-events:none;position:absolute;inset:0;overflow:clip}";
3839
- const tagId$2 = "dsh-group-status/AnimatedRows.module.css";
3840
- if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]") === null) {
3841
- const tag = document.createElement("style");
3842
- tag.dataset.plugin = "dsh-group-status";
3843
- tag.dataset.pluginCss = tagId$2;
3844
- tag.textContent = css$2;
3845
- document.head.appendChild(tag);
3846
- }
3847
- var AnimatedRows_module_css_default = { "exits": "_8TnAsq_exits" };
3848
- //#endregion
3849
- //#region src/client/rows/AnimatedRows.tsx
3850
- /** React-commit-driven movement and entry/exit fades for the sidebar's keyed rows. */
3851
- const ROW_FADE_MS = 100;
3852
- const ROW_GLIDE_MS = 200;
3853
- function sameRows(previous, next) {
3854
- return previous.rowKeys.length === next.rowKeys.length && previous.rowKeys.every((key, index) => key === next.rowKeys[index]);
3855
- }
3856
- function intersects(row, viewport) {
3857
- return row.bottom > viewport.top && row.top < viewport.bottom && row.right > viewport.left && row.left < viewport.right;
3858
- }
3859
- /**
3860
- * Animates keyed sidebar rows only when their rendered membership or order changes.
3861
- * Motion starts after the first pointer or keyboard input inside the mounted list.
3862
- * The parent supplies a positioned container for the inert exit overlay.
3863
- */
3864
- var AnimatedRows = class extends react.Component {
3865
- armed = false;
3866
- list = (0, react.createRef)();
3867
- overlay = (0, react.createRef)();
3868
- movements = /* @__PURE__ */ new Map();
3869
- exits = /* @__PURE__ */ new Map();
3870
- getSnapshotBeforeUpdate(previous) {
3871
- const list = this.list.current;
3872
- 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;
3873
- const viewport = list.getBoundingClientRect();
3874
- const positions = this.readPositions();
3875
- const nextKeys = new Set(this.props.rowKeys);
3876
- const removed = /* @__PURE__ */ new Map();
3877
- for (const [key, row] of positions) {
3878
- if (nextKeys.has(key) || !intersects(row.rect, viewport)) continue;
3879
- const clone = row.element.cloneNode(true);
3880
- clone.removeAttribute("data-row-key");
3881
- clone.inert = true;
3882
- clone.style.setProperty("--dsh-workspace-indent", getComputedStyle(row.element).getPropertyValue("--dsh-workspace-indent"));
3883
- removed.set(key, {
3884
- ...row,
3885
- element: clone
3886
- });
3887
- }
3888
- return {
3889
- positions,
3890
- removed
3891
- };
3892
- }
3893
- componentDidUpdate(previous, _state, snapshot) {
3894
- if (snapshot === null) {
3895
- if (!sameRows(previous, this.props) || previous.resetKey !== this.props.resetKey || previous.ready !== this.props.ready) this.clear();
3896
- return;
3897
- }
3898
- this.cancelMovements();
3899
- const list = this.list.current;
3900
- const overlay = this.overlay.current;
3901
- const viewport = list.getBoundingClientRect();
3902
- const origin = overlay.getBoundingClientRect();
3903
- const positions = this.readPositions();
3904
- for (const [key, row] of positions) {
3905
- this.removeExit(key);
3906
- const previousRow = snapshot.positions.get(key);
3907
- if (!intersects(row.rect, viewport) && (previousRow === void 0 || !intersects(previousRow.rect, viewport))) continue;
3908
- if (previousRow === void 0) {
3909
- this.move(row.element, [{ opacity: 0 }, { opacity: 1 }], ROW_FADE_MS);
3910
- continue;
3911
- }
3912
- const dx = previousRow.rect.left - row.rect.left;
3913
- const dy = previousRow.rect.top - row.rect.top;
3914
- if (dx === 0 && dy === 0 && previousRow.opacity === 1) continue;
3915
- this.move(row.element, [{
3916
- transform: `translate(${String(dx)}px, ${String(dy)}px)`,
3917
- opacity: previousRow.opacity
3918
- }, {
3919
- transform: "translate(0, 0)",
3920
- opacity: 1
3921
- }], ROW_GLIDE_MS);
3922
- }
3923
- for (const [key, row] of snapshot.removed) {
3924
- const { element } = row;
3925
- this.removeExit(key);
3926
- Object.assign(element.style, {
3927
- position: "absolute",
3928
- margin: "0",
3929
- transform: "none",
3930
- boxSizing: "border-box",
3931
- left: `${String(row.rect.left - origin.left)}px`,
3932
- top: `${String(row.rect.top - origin.top)}px`,
3933
- width: `${String(row.rect.width)}px`,
3934
- height: `${String(row.rect.height)}px`
3935
- });
3936
- overlay.append(element);
3937
- const animation = element.animate([{ opacity: row.opacity }, { opacity: 0 }], {
3938
- duration: ROW_FADE_MS,
3939
- easing: "ease-out",
3940
- fill: "forwards"
3941
- });
3942
- this.exits.set(key, {
3943
- element,
3944
- animation
3945
- });
3946
- animation.onfinish = () => {
3947
- this.removeExit(key);
3948
- };
3949
- }
3950
- }
3951
- componentWillUnmount() {
3952
- this.clear();
3953
- }
3954
- readPositions() {
3955
- const rows = this.list.current.querySelectorAll("[data-row-key]");
3956
- return new Map(Array.from(rows, (element) => [element.dataset.rowKey, {
3957
- element,
3958
- rect: element.getBoundingClientRect(),
3959
- opacity: this.movements.has(element) ? Number(getComputedStyle(element).opacity) : 1
3960
- }]));
3961
- }
3962
- move(element, keyframes, duration) {
3963
- const animation = element.animate(keyframes, {
3964
- duration,
3965
- easing: "ease-out"
3966
- });
3967
- this.movements.set(element, animation);
3968
- animation.onfinish = () => {
3969
- this.movements.delete(element);
3970
- animation.cancel();
3971
- };
3972
- }
3973
- cancelMovements() {
3974
- for (const animation of this.movements.values()) {
3975
- animation.onfinish = null;
3976
- animation.cancel();
3977
- }
3978
- this.movements.clear();
3979
- }
3980
- removeExit(key) {
3981
- const exit = this.exits.get(key);
3982
- if (exit === void 0) return;
3983
- exit.animation.onfinish = null;
3984
- exit.animation.cancel();
3985
- exit.element.remove();
3986
- this.exits.delete(key);
3987
- }
3988
- clear() {
3989
- this.cancelMovements();
3990
- for (const key of this.exits.keys()) this.removeExit(key);
3991
- }
3992
- render() {
3993
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3994
- ref: this.list,
3995
- className: this.props.className,
3996
- role: "tree",
3997
- "aria-label": this.props.label,
3998
- onPointerDownCapture: () => {
3999
- this.armed = true;
4000
- },
4001
- onKeyDownCapture: () => {
4002
- this.armed = true;
4003
- },
4004
- children: this.props.children
4005
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4006
- ref: this.overlay,
4007
- className: AnimatedRows_module_css_default.exits,
4008
- "aria-hidden": "true"
4009
- })] });
4010
- }
4011
- };
4012
- //#endregion
4013
- //#region \0dsh-css:src/client/WorkspacePicker.module.css.mjs
4014
- 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)}";
4015
- const tagId$1 = "dsh-group-status/WorkspacePicker.module.css";
4016
- if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
4017
- const tag = document.createElement("style");
4018
- tag.dataset.plugin = "dsh-group-status";
4019
- tag.dataset.pluginCss = tagId$1;
4020
- tag.textContent = css$1;
4021
- document.head.appendChild(tag);
4022
- }
4023
- var WorkspacePicker_module_css_default = {
4024
- "menuStatus": "S_JYgq_menuStatus",
4025
- "modalAction": "S_JYgq_modalAction",
4026
- "modalError": "S_JYgq_modalError"
4027
- };
4028
- //#endregion
4029
- //#region src/client/WorkspacePicker.tsx
4030
- const ADD_WORKSPACE = "::add-workspace";
4031
- /**
4032
- * Render the pick menu plus the adoption error dialog.
4033
- * @param props - owner-controlled flow props.
4034
- * @returns menu + dialog elements.
4035
- */
4036
- function WorkspacePickFlow({ t, open, anchorRef, useWorkspaces, createWorkspace, useDirectoryFlow, renderDirectoryFlow, onPick, onClose, addOnly = false, onBusyChange, side = "bottom", selectedId }) {
4037
- const workspaceSnapshot = useWorkspaces((state) => state);
4038
- const workspaces = workspaceSnapshot.items;
4039
- const getAnchorRect = (0, react.useCallback)(() => anchorRef?.current?.getBoundingClientRect() ?? null, [anchorRef]);
4040
- const [errorOpen, setErrorOpen] = (0, react.useState)(false);
4041
- const [modalError, setModalError] = (0, react.useState)(null);
4042
- const [flowOpen, setFlowOpen] = (0, react.useState)(false);
4043
- const [pickingFolder, setPickingFolder] = (0, react.useState)(false);
4044
- const flowBusy = flowOpen || pickingFolder;
4045
- (0, react.useEffect)(() => {
4046
- onBusyChange?.(flowBusy);
4047
- }, [flowBusy, onBusyChange]);
4048
- const flowAvailable = useDirectoryFlow((occupied) => occupied);
4049
- (0, react.useEffect)(() => {
4050
- if (flowOpen && !flowAvailable) setFlowOpen(false);
4051
- }, [flowOpen, flowAvailable]);
4052
- const addEntries = flowAvailable ? [{
4053
- id: ADD_WORKSPACE,
4054
- label: t("menu.addWorkspace"),
4055
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPlusOutlineRegular, { size: 16 }),
4056
- disabled: flowBusy
4057
- }] : [];
4058
- const pinAdd = !addOnly && workspaces.length > 0;
4059
- const items = pinAdd ? workspaces.map((workspace) => ({
4060
- id: workspace.workspaceId,
4061
- label: workspaceDisplayTitle(workspace.title, t("workspace.defaultName")),
4062
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderCloseRegular, { size: 16 }),
4063
- disabled: flowBusy
4064
- })) : addEntries;
4065
- const menuIsEmpty = items.length === 0;
4066
- const closeModal = () => {
4067
- setErrorOpen(false);
4068
- setModalError(null);
4069
- };
4070
- /** Adopt a picked directory; failures land in the folder-error dialog (Choose again reopens the flow). */
4071
- const adoptDirectory = (path) => createWorkspace({ path }).then((workspace) => {
4072
- setFlowOpen(false);
4073
- onPick(workspace.workspaceId);
4074
- }).catch((reason) => {
4075
- setModalError(reason instanceof Error ? reason.message : String(reason));
4076
- setFlowOpen(false);
4077
- setErrorOpen(true);
4078
- });
4079
- const openDirectoryFlow = (0, react.useCallback)(() => {
4080
- onClose();
4081
- setErrorOpen(false);
4082
- setModalError(null);
4083
- setFlowOpen(true);
4084
- }, [onClose]);
4085
- const listSettled = addOnly || workspaceSnapshot.phase === "ready";
4086
- const addIsTheOnlyEntry = !pinAdd && listSettled && addEntries.length === 1;
4087
- (0, react.useEffect)(() => {
4088
- if (open && addIsTheOnlyEntry && !flowBusy) openDirectoryFlow();
4089
- }, [
4090
- open,
4091
- addIsTheOnlyEntry,
4092
- flowBusy,
4093
- openDirectoryFlow
4094
- ]);
4095
- /** Owner side of the flow conversation: adopt keeps the flow open (busy) until the Host answers. */
4096
- const flowOwner = {
4097
- open: flowOpen,
4098
- busy: pickingFolder,
4099
- onPicked: (path) => {
4100
- setPickingFolder(true);
4101
- adoptDirectory(path).finally(() => {
4102
- setPickingFolder(false);
4103
- });
4104
- },
4105
- onCancel: () => {
4106
- setFlowOpen(false);
4107
- },
4108
- onError: (message) => {
4109
- setFlowOpen(false);
4110
- setModalError(message);
4111
- setErrorOpen(true);
4112
- }
4113
- };
4114
- const handleSelect = (id) => {
4115
- if (id === ADD_WORKSPACE) {
4116
- openDirectoryFlow();
4117
- return;
4118
- }
4119
- onPick(id);
4120
- };
4121
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
4122
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
4123
- open: open && !addIsTheOnlyEntry && !menuIsEmpty,
4124
- anchor: null,
4125
- items,
4126
- ...pinAdd ? { footer: addEntries } : {},
4127
- selectedId,
4128
- onSelect: handleSelect,
4129
- onClose,
4130
- side,
4131
- portal: true,
4132
- getAnchorRect
4133
- }),
4134
- open && !addIsTheOnlyEntry && !menuIsEmpty && workspaceSnapshot.phase === "pending" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4135
- className: WorkspacePicker_module_css_default.menuStatus,
4136
- role: "status",
4137
- children: t("picker.loading")
4138
- }),
4139
- renderDirectoryFlow(flowOwner),
4140
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
4141
- open: errorOpen,
4142
- onClose: closeModal,
4143
- closeLabel: t("close"),
4144
- title: t("folderError.title"),
4145
- 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, {
4146
- variant: "outline",
4147
- className: WorkspacePicker_module_css_default.modalAction,
4148
- onClick: closeModal,
4149
- children: t("cancel")
4150
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
4151
- variant: "primary",
4152
- className: WorkspacePicker_module_css_default.modalAction,
4153
- disabled: !flowAvailable,
4154
- onClick: openDirectoryFlow,
4155
- children: t("folderError.retry")
4156
- })] }),
4157
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4158
- className: WorkspacePicker_module_css_default.modalError,
4159
- role: "alert",
4160
- children: modalError
4161
- })
4162
- })
4163
- ] });
4164
- }
4165
- /**
4166
- * The conversation empty-state registration: adapts the owner share to the
4167
- * core flow (all state and semantics live in the flow / the owner).
4168
- * @param props - empty-state slot props (owner share + injected creation callback).
4169
- * @returns the flow element.
4170
- */
4171
- function WorkspacePicker({ open, anchorRef, useWorkspaces, selectedId, onPick, onClose, createWorkspace, useDirectoryFlow, renderSlot, t }) {
4172
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspacePickFlow, {
4173
- t,
4174
- open,
4175
- anchorRef,
4176
- useWorkspaces,
4177
- createWorkspace,
4178
- useDirectoryFlow,
4179
- renderDirectoryFlow: (owner) => renderSlot("conversation.hero.workspace.directoryFlow", owner),
4180
- selectedId,
4181
- onPick,
4182
- onClose
4183
- });
4184
- }
4185
- //#endregion
4186
- //#region \0dsh-css:src/client/rows/WorkspaceBrowser.module.css.mjs
4187
- 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}}";
4188
- const tagId = "dsh-group-status/WorkspaceBrowser.module.css";
4189
- if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
4190
- const tag = document.createElement("style");
4191
- tag.dataset.plugin = "dsh-group-status";
4192
- tag.dataset.pluginCss = tagId;
4193
- tag.textContent = css;
4194
- document.head.appendChild(tag);
4195
- }
4196
- var WorkspaceBrowser_module_css_default = {
4197
- "archiveActivity": "QAYkWG_archiveActivity",
4198
- "clearButton": "QAYkWG_clearButton",
4199
- "deleteAction": "QAYkWG_deleteAction",
4200
- "deleteStatus": "QAYkWG_deleteStatus",
4201
- "empty": "QAYkWG_empty",
4202
- "emptyAction": "QAYkWG_emptyAction",
4203
- "emptyState": "QAYkWG_emptyState",
4204
- "fade": "QAYkWG_fade",
4205
- "flatList": "QAYkWG_flatList",
4206
- "groupSection": "QAYkWG_groupSection",
4207
- "headerActions": "QAYkWG_headerActions",
4208
- "headerActionsHidden": "QAYkWG_headerActionsHidden",
4209
- "iconButton": "QAYkWG_iconButton",
4210
- "list": "QAYkWG_list",
4211
- "listArea": "QAYkWG_listArea",
4212
- "listTopDropActive": "QAYkWG_listTopDropActive",
4213
- "listTopDropIndicator": "QAYkWG_listTopDropIndicator",
4214
- "rail": "QAYkWG_rail",
4215
- "renameError": "QAYkWG_renameError",
4216
- "renameInput": "QAYkWG_renameInput",
4217
- "root": "QAYkWG_root",
4218
- "search": "QAYkWG_search",
4219
- "search-skeleton": "QAYkWG_search-skeleton",
4220
- "searchButton": "QAYkWG_searchButton",
4221
- "searchExpanded": "QAYkWG_searchExpanded",
4222
- "searchInput": "QAYkWG_searchInput",
4223
- "searchSlot": "QAYkWG_searchSlot",
4224
- "searchSlotExpanded": "QAYkWG_searchSlotExpanded",
4225
- "searchStatus": "QAYkWG_searchStatus",
4226
- "searchTree": "QAYkWG_searchTree",
4227
- "sectionHeader": "QAYkWG_sectionHeader",
4228
- "sectionLabel": "QAYkWG_sectionLabel",
4229
- "sectionLabelHidden": "QAYkWG_sectionLabelHidden",
4230
- "sessionOverflowButton": "QAYkWG_sessionOverflowButton",
4231
- "skeletonBar": "QAYkWG_skeletonBar",
4232
- "skeletonBarWide": "QAYkWG_skeletonBarWide",
4233
- "skeletonBars": "QAYkWG_skeletonBars",
4234
- "skeletonDot": "QAYkWG_skeletonDot",
4235
- "skeletonRow": "QAYkWG_skeletonRow",
4236
- "treeBody": "QAYkWG_treeBody",
4237
- "viewOptionsMenu": "QAYkWG_viewOptionsMenu",
4238
- "wide": "QAYkWG_wide",
4239
- "wide-in": "QAYkWG_wide-in",
4240
- "workspaceDropAfter": "QAYkWG_workspaceDropAfter",
4241
- "workspaceDropBefore": "QAYkWG_workspaceDropBefore"
4242
- };
4243
- //#endregion
4244
- //#region src/client/rows/WorkspaceBrowser.tsx
4245
- /**
4246
- * The workspace/session browsing region filling the sidebar shell's
4247
- * `sidebar.workspaces` hole: section header (title + view options + add
4248
- * workspace), search, the grouped tree or flat list, and the workspace
4249
- * dialogs. Wide state renders the full browser; rail state renders the two
4250
- * region icons (search / add workspace) as 36px controls on the shell's shared
4251
- * rail entry path, each requesting expansion through the owner share. Adding
4252
- * is the header button's one action, so it raises the directory flow with no
4253
- * menu in between; the flow and its error dialog live in WorkspacePicker
4254
- * (same package — direct composition, no slot between them). A Session row's
4255
- * "..." menu and hover buttons are the `sidebar.workspaces.session.menu.item`
4256
- * and `sidebar.workspaces.session.row.action` lists rendered through this
4257
- * entry's `renderSlot`; the actions in them, this package's own included,
4258
- * are slot entries with their own behavior, so this component threads no
4259
- * action callbacks and hosts no action surface.
4260
- */
4261
- const facetsStore = facetStore();
4262
- /**
4263
- * Column slide length (--ds-transition-duration-slow): rail-search focus waits it out —
4264
- * focus() forces a synchronous layout and would jank the slide.
4265
- */
4266
- const EXPAND_SLIDE_MS = 300;
4267
- /** Pause between the latest keystroke and a Host content-search request. */
4268
- const SEARCH_DEBOUNCE_MS = 250;
4269
- /** `session.search` wire bound, measured in JavaScript UTF-16 code units. */
4270
- const SEARCH_QUERY_MAX_CODE_UNITS = 500;
4271
- /** Keep controlled input and RPC payload inside the session.search wire contract. */
4272
- function sanitizeSearchQuery(value) {
4273
- const withoutNul = value.replaceAll("\0", "");
4274
- if (withoutNul.length <= SEARCH_QUERY_MAX_CODE_UNITS) return withoutNul;
4275
- let end = SEARCH_QUERY_MAX_CODE_UNITS;
4276
- const last = withoutNul.charCodeAt(end - 1);
4277
- const next = withoutNul.charCodeAt(end);
4278
- if (last >= 55296 && last <= 56319 && next >= 56320 && next <= 57343) end--;
4279
- return withoutNul.slice(0, end);
4280
- }
4281
- /**
4282
- * Accept the native drag at document level while a row drag is active: row
4283
- * hover still owns the insertion marker, and releasing outside the list must
4284
- * not be rendered as a rejected drop before dragend commits that last marker.
4285
- */
4286
- function useNativeDragAcceptance(active) {
4287
- (0, react.useEffect)(() => {
4288
- if (!active) return;
4289
- const acceptDrag = (event) => {
4290
- event.preventDefault();
4291
- if (event.dataTransfer !== null) event.dataTransfer.dropEffect = "move";
4292
- };
4293
- const acceptDrop = (event) => {
4294
- event.preventDefault();
4295
- };
4296
- document.addEventListener("dragover", acceptDrag);
4297
- document.addEventListener("drop", acceptDrop);
4298
- return () => {
4299
- document.removeEventListener("dragover", acceptDrag);
4300
- document.removeEventListener("drop", acceptDrop);
4301
- };
4302
- }, [active]);
4303
- }
4304
- /** Grouping, ordering, and archived-filter menu; own open state so it resets with the wide chrome. */
4305
- function ViewOptionsMenu({ groupBy, orderBy, archivedFilter, onGroupPick, onOrderPick, onArchivedFilterPick, t }) {
4306
- const [open, setOpen] = (0, react.useState)(false);
4307
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
4308
- open,
4309
- onClose: () => {
4310
- setOpen(false);
4311
- },
4312
- items: [
4313
- {
4314
- type: "label",
4315
- id: "group-by",
4316
- text: t("groupBy.label")
4317
- },
4318
- {
4319
- id: "workspace",
4320
- label: t("groupBy.workspace"),
4321
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderCloseRegular, {})
4322
- },
4323
- {
4324
- id: "workspace-tree",
4325
- label: t("groupBy.workspaceTree"),
4326
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconWorkspaceTreeOutlineRegular, {})
4327
- },
4328
- {
4329
- id: "flat",
4330
- label: t("groupBy.flat"),
4331
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFlatListOutlineRegular, {})
4332
- },
4333
- {
4334
- type: "separator",
4335
- id: "order-by-separator"
4336
- },
4337
- {
4338
- type: "label",
4339
- id: "order-by",
4340
- text: t("orderBy.label")
4341
- },
4342
- {
4343
- id: "manual",
4344
- label: t("orderBy.manual"),
4345
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronsUpDownOutlineRegular, {})
4346
- },
4347
- {
4348
- id: "updated",
4349
- label: t("orderBy.updated"),
4350
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconClockOutlineRegular, {})
4351
- },
4352
- {
4353
- type: "separator",
4354
- id: "archived-filter-separator"
4355
- },
4356
- {
4357
- type: "label",
4358
- id: "filter-by",
4359
- text: t("filterBy.label")
4360
- },
4361
- {
4362
- id: "hide-archived",
4363
- label: t("viewOptions.hideArchived"),
4364
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconArchiveOffOutlineRegular, {})
4365
- },
4366
- {
4367
- id: "show-archived",
4368
- label: t("viewOptions.showArchived"),
4369
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconQueueOutlineRegular, {})
4370
- },
4371
- {
4372
- id: "only-archived",
4373
- label: t("viewOptions.onlyArchived"),
4374
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconArchiveCheckOutlineRegular, {})
4375
- }
4376
- ],
4377
- selectedIds: [
4378
- groupBy,
4379
- orderBy,
4380
- {
4381
- default: "hide-archived",
4382
- show: "show-archived",
4383
- only: "only-archived"
4384
- }[archivedFilter]
4385
- ],
4386
- onSelect: (id) => {
4387
- if (id === "workspace" || id === "workspace-tree" || id === "flat") onGroupPick(id);
4388
- else if (id === "manual" || id === "updated") onOrderPick(id);
4389
- else if (id === "hide-archived") onArchivedFilterPick("default");
4390
- else if (id === "show-archived") onArchivedFilterPick("show");
4391
- else if (id === "only-archived") onArchivedFilterPick("only");
4392
- setOpen(false);
4393
- },
4394
- align: "end",
4395
- dense: true,
4396
- listClassName: WorkspaceBrowser_module_css_default.viewOptionsMenu,
4397
- portal: true,
4398
- anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
4399
- label: t("viewOptions.label"),
4400
- side: "bottom",
4401
- delayMs: 500,
4402
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4403
- type: "button",
4404
- className: clsx(WorkspaceBrowser_module_css_default.iconButton, WorkspaceBrowser_module_css_default.wide),
4405
- "aria-label": t("viewOptions.label"),
4406
- onClick: () => {
4407
- setOpen((v) => !v);
4408
- },
4409
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSlidersTwoOutlineRegular, {})
4205
+ onClose: () => {
4206
+ setMenuOpen(false);
4207
+ },
4208
+ portal: true,
4209
+ closeOnPointerLeave: true,
4210
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4211
+ type: "button",
4212
+ className: Rows_module_css_default.iconButton,
4213
+ "aria-label": t("actions.session.aria", { name: title }),
4214
+ onClick: () => {
4215
+ setMenuOpen((v) => !v);
4216
+ },
4217
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconEllipsisOutlineRegular, {})
4218
+ }),
4219
+ children: renderSlot("sidebar.workspaces.session.menu.item", {
4220
+ sessionId: node.id,
4221
+ displayTitle: row.title
4222
+ }, { hookContext: menuOpenState })
4223
+ }), renderSlot("sidebar.workspaces.session.row.action", {
4224
+ sessionId: node.id,
4225
+ displayTitle: row.title
4226
+ })]
4410
4227
  })
4411
- })
4228
+ ]
4229
+ });
4230
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.HoverCard, {
4231
+ anchor: ownRow,
4232
+ content: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SessionHoverContent, {
4233
+ node,
4234
+ now,
4235
+ renderSlot,
4236
+ t
4237
+ }),
4238
+ openDelayMs: 800,
4239
+ grouped: true,
4240
+ disabled: menuOpen || drag?.active === true,
4241
+ copyText: row.blank || row.title === "" ? void 0 : row.title,
4242
+ copyLabel: t("copy"),
4243
+ copiedLabel: t("hover.copied")
4412
4244
  });
4413
4245
  }
4414
- /** Apply a visible drop to the complete account without removing hidden members. */
4415
- function sessionDragOrder(order, rows, drag, over) {
4416
- const source = rows.find((row) => row.id === drag.sessionId);
4417
- const target = rows.find((row) => row.id === over.id);
4418
- 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;
4419
- const section = rows.filter((row) => row.pinned === drag.pinned);
4420
- const sourceIndex = section.findIndex((row) => row.id === source.id);
4421
- if (section.filter((row) => row.id !== source.id).findIndex((row) => row.id === target.id) + (over.half === "after" ? 1 : 0) === sourceIndex) return;
4422
- const next = order.filter((id) => id !== source.id);
4423
- const targetIndex = next.indexOf(target.id);
4424
- if (targetIndex === -1) return;
4425
- next.splice(targetIndex + (over.half === "after" ? 1 : 0), 0, source.id);
4426
- return pinCurrentBlank(next, rows.find((row) => row.blank)?.id);
4246
+ //#endregion
4247
+ //#region \0dsh-css:src/client/rows/AnimatedRows.module.css.mjs
4248
+ const css = "._8TnAsq_exits{contain:strict;pointer-events:none;position:absolute;inset:0;overflow:clip}";
4249
+ const tagId = "dsh-group-status/AnimatedRows.module.css";
4250
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
4251
+ const tag = document.createElement("style");
4252
+ tag.dataset.plugin = "dsh-group-status";
4253
+ tag.dataset.pluginCss = tagId;
4254
+ tag.textContent = css;
4255
+ document.head.appendChild(tag);
4427
4256
  }
4428
- /** Resolve an insertion side across the Workspace header, descendants, and Sessions. */
4429
- function workspaceGroupHalf(e) {
4430
- const rect = e.currentTarget.getBoundingClientRect();
4431
- return e.clientY < rect.top + rect.height / 2 ? "before" : "after";
4257
+ var AnimatedRows_module_css_default = { "exits": "_8TnAsq_exits" };
4258
+ //#endregion
4259
+ //#region src/client/rows/AnimatedRows.tsx
4260
+ /** React-commit-driven movement and entry/exit fades for the sidebar's keyed rows. */
4261
+ const ROW_FADE_MS = 100;
4262
+ const ROW_GLIDE_MS = 200;
4263
+ function sameRows(previous, next) {
4264
+ return previous.rowKeys.length === next.rowKeys.length && previous.rowKeys.every((key, index) => key === next.rowKeys[index]);
4432
4265
  }
4266
+ function intersects(row, viewport) {
4267
+ return row.bottom > viewport.top && row.top < viewport.bottom && row.right > viewport.left && row.left < viewport.right;
4268
+ }
4269
+ /**
4270
+ * Animates keyed sidebar rows only when their rendered membership or order changes.
4271
+ * Motion starts after the first pointer or keyboard input inside the mounted list.
4272
+ * The parent supplies a positioned container for the inert exit overlay.
4273
+ */
4274
+ var AnimatedRows = class extends react.Component {
4275
+ armed = false;
4276
+ list = (0, react.createRef)();
4277
+ overlay = (0, react.createRef)();
4278
+ movements = /* @__PURE__ */ new Map();
4279
+ exits = /* @__PURE__ */ new Map();
4280
+ getSnapshotBeforeUpdate(previous) {
4281
+ const list = this.list.current;
4282
+ 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;
4283
+ const viewport = list.getBoundingClientRect();
4284
+ const positions = this.readPositions();
4285
+ const nextKeys = new Set(this.props.rowKeys);
4286
+ const removed = /* @__PURE__ */ new Map();
4287
+ for (const [key, row] of positions) {
4288
+ if (nextKeys.has(key) || !intersects(row.rect, viewport)) continue;
4289
+ const clone = row.element.cloneNode(true);
4290
+ clone.removeAttribute("data-row-key");
4291
+ clone.inert = true;
4292
+ clone.style.setProperty("--dsh-workspace-indent", getComputedStyle(row.element).getPropertyValue("--dsh-workspace-indent"));
4293
+ removed.set(key, {
4294
+ ...row,
4295
+ element: clone
4296
+ });
4297
+ }
4298
+ return {
4299
+ positions,
4300
+ removed
4301
+ };
4302
+ }
4303
+ componentDidUpdate(previous, _state, snapshot) {
4304
+ if (snapshot === null) {
4305
+ if (!sameRows(previous, this.props) || previous.resetKey !== this.props.resetKey || previous.ready !== this.props.ready) this.clear();
4306
+ return;
4307
+ }
4308
+ this.cancelMovements();
4309
+ const list = this.list.current;
4310
+ const overlay = this.overlay.current;
4311
+ const viewport = list.getBoundingClientRect();
4312
+ const origin = overlay.getBoundingClientRect();
4313
+ const positions = this.readPositions();
4314
+ for (const [key, row] of positions) {
4315
+ this.removeExit(key);
4316
+ const previousRow = snapshot.positions.get(key);
4317
+ if (!intersects(row.rect, viewport) && (previousRow === void 0 || !intersects(previousRow.rect, viewport))) continue;
4318
+ if (previousRow === void 0) {
4319
+ this.move(row.element, [{ opacity: 0 }, { opacity: 1 }], ROW_FADE_MS);
4320
+ continue;
4321
+ }
4322
+ const dx = previousRow.rect.left - row.rect.left;
4323
+ const dy = previousRow.rect.top - row.rect.top;
4324
+ if (dx === 0 && dy === 0 && previousRow.opacity === 1) continue;
4325
+ this.move(row.element, [{
4326
+ transform: `translate(${String(dx)}px, ${String(dy)}px)`,
4327
+ opacity: previousRow.opacity
4328
+ }, {
4329
+ transform: "translate(0, 0)",
4330
+ opacity: 1
4331
+ }], ROW_GLIDE_MS);
4332
+ }
4333
+ for (const [key, row] of snapshot.removed) {
4334
+ const { element } = row;
4335
+ this.removeExit(key);
4336
+ Object.assign(element.style, {
4337
+ position: "absolute",
4338
+ margin: "0",
4339
+ transform: "none",
4340
+ boxSizing: "border-box",
4341
+ left: `${String(row.rect.left - origin.left)}px`,
4342
+ top: `${String(row.rect.top - origin.top)}px`,
4343
+ width: `${String(row.rect.width)}px`,
4344
+ height: `${String(row.rect.height)}px`
4345
+ });
4346
+ overlay.append(element);
4347
+ const animation = element.animate([{ opacity: row.opacity }, { opacity: 0 }], {
4348
+ duration: ROW_FADE_MS,
4349
+ easing: "ease-out",
4350
+ fill: "forwards"
4351
+ });
4352
+ this.exits.set(key, {
4353
+ element,
4354
+ animation
4355
+ });
4356
+ animation.onfinish = () => {
4357
+ this.removeExit(key);
4358
+ };
4359
+ }
4360
+ }
4361
+ componentWillUnmount() {
4362
+ this.clear();
4363
+ }
4364
+ readPositions() {
4365
+ const rows = this.list.current.querySelectorAll("[data-row-key]");
4366
+ return new Map(Array.from(rows, (element) => [element.dataset.rowKey, {
4367
+ element,
4368
+ rect: element.getBoundingClientRect(),
4369
+ opacity: this.movements.has(element) ? Number(getComputedStyle(element).opacity) : 1
4370
+ }]));
4371
+ }
4372
+ move(element, keyframes, duration) {
4373
+ const animation = element.animate(keyframes, {
4374
+ duration,
4375
+ easing: "ease-out"
4376
+ });
4377
+ this.movements.set(element, animation);
4378
+ animation.onfinish = () => {
4379
+ this.movements.delete(element);
4380
+ animation.cancel();
4381
+ };
4382
+ }
4383
+ cancelMovements() {
4384
+ for (const animation of this.movements.values()) {
4385
+ animation.onfinish = null;
4386
+ animation.cancel();
4387
+ }
4388
+ this.movements.clear();
4389
+ }
4390
+ removeExit(key) {
4391
+ const exit = this.exits.get(key);
4392
+ if (exit === void 0) return;
4393
+ exit.animation.onfinish = null;
4394
+ exit.animation.cancel();
4395
+ exit.element.remove();
4396
+ this.exits.delete(key);
4397
+ }
4398
+ clear() {
4399
+ this.cancelMovements();
4400
+ for (const key of this.exits.keys()) this.removeExit(key);
4401
+ }
4402
+ render() {
4403
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4404
+ ref: this.list,
4405
+ className: this.props.className,
4406
+ role: "tree",
4407
+ "aria-label": this.props.label,
4408
+ onPointerDownCapture: () => {
4409
+ this.armed = true;
4410
+ },
4411
+ onKeyDownCapture: () => {
4412
+ this.armed = true;
4413
+ },
4414
+ children: this.props.children
4415
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4416
+ ref: this.overlay,
4417
+ className: AnimatedRows_module_css_default.exits,
4418
+ "aria-hidden": "true"
4419
+ })] });
4420
+ }
4421
+ };
4422
+ //#endregion
4423
+ //#region src/client/rows/EmptySessions.tsx
4424
+ /** The list-empty placeholder shared by the tree, flat, and search list bodies. */
4433
4425
  /** The list-empty placeholder — a glyph over the text; the archived-only view names its filter and offers the way back. */
4434
4426
  function EmptySessions({ rowState, onLeaveArchivedOnly, t }) {
4435
4427
  const archivedOnly = rowState.archivedFilter === "only";
@@ -4448,13 +4440,25 @@ window.__ModuleLoader__.load({
4448
4440
  ]
4449
4441
  });
4450
4442
  }
4443
+ //#endregion
4444
+ //#region src/client/rows/SessionTree.tsx
4445
+ /**
4446
+ * The scrolling session tree: Workspace groups, aggregated live-status badges,
4447
+ * watermarks, and row drag ordering.
4448
+ *
4449
+ * Split out of `WorkspaceBrowser.tsx` so the shell keeps only the header, search
4450
+ * box, and dialogs; this file owns the tree body and its local row limits, and
4451
+ * unmounting it drops both the sessions subscription and those limits.
4452
+ */
4453
+ const facetsStore = facetStore();
4451
4454
  /** The scrolling session tree; unmounting drops the sessions subscription and local row limits. */
4452
4455
  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 }) {
4453
4456
  const panelActive = usePanelInfo((info) => info.activePanelId !== null);
4454
4457
  const badgeSettings = useBadgeSettings((settings) => settings);
4455
4458
  const statuses = useSessionStatus((s) => s);
4456
4459
  const facets = useRegisteredFacets(facetsStore);
4457
- const current = panelActive ? void 0 : Object.values(list.byId).find((session) => (session.retainedBy.mainView ?? 0) > 0)?.id;
4460
+ const mainViewId = (0, react.useMemo)(() => mainViewSessionId(list), [list]);
4461
+ const current = panelActive ? void 0 : mainViewId;
4458
4462
  const revealGroup = revealSessionId === void 0 || !workspaceReady ? void 0 : owningGroupKey(workspaces, revealSessionId);
4459
4463
  const [storedWatermarks, setStoredWatermarks] = (0, react.useState)({});
4460
4464
  const [watermarkMotionKey, setWatermarkMotionKey] = (0, react.useState)(0);
@@ -4801,6 +4805,9 @@ window.__ModuleLoader__.load({
4801
4805
  ]
4802
4806
  });
4803
4807
  }
4808
+ //#endregion
4809
+ //#region src/client/rows/ListView.tsx
4810
+ /** The two wide-only list bodies: the flat single-list view and the search results view. */
4804
4811
  /** The flat "In one list" body: every session is one draggable top-level row. */
4805
4812
  function FlatList({ list, sessionIds, rowState, onLeaveArchivedOnly, useSessionStatus, open, onSessionRenameRequest, usePanelInfo, setSessionOrder, workspaceReady, animationResetKey, revealSessionId, onSessionRevealed, renderSlot, t }) {
4806
4813
  const panelActive = usePanelInfo((info) => info.activePanelId !== null);
@@ -4814,7 +4821,8 @@ window.__ModuleLoader__.load({
4814
4821
  const [drag, setDrag] = (0, react.useState)(null);
4815
4822
  const dropCommitted = (0, react.useRef)(false);
4816
4823
  useNativeDragAcceptance(drag !== null);
4817
- const currentId = panelActive ? void 0 : Object.values(list.byId).find((session) => (session.retainedBy.mainView ?? 0) > 0)?.id;
4824
+ const mainViewId = (0, react.useMemo)(() => mainViewSessionId(list), [list]);
4825
+ const currentId = panelActive ? void 0 : mainViewId;
4818
4826
  const commitDrag = (activeDrag, over) => {
4819
4827
  if (dropCommitted.current) return;
4820
4828
  dropCommitted.current = true;
@@ -4909,7 +4917,8 @@ window.__ModuleLoader__.load({
4909
4917
  resultLimit
4910
4918
  ]);
4911
4919
  const pending = currentRemote.status === "loading";
4912
- const currentId = panelActive ? void 0 : Object.values(list.byId).find((session) => (session.retainedBy.mainView ?? 0) > 0)?.id;
4920
+ const mainViewId = (0, react.useMemo)(() => mainViewSessionId(list), [list]);
4921
+ const currentId = panelActive ? void 0 : mainViewId;
4913
4922
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4914
4923
  className: clsx(WorkspaceBrowser_module_css_default.treeBody, WorkspaceBrowser_module_css_default.wide),
4915
4924
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -4919,6 +4928,7 @@ window.__ModuleLoader__.load({
4919
4928
  className: WorkspaceBrowser_module_css_default.searchTree,
4920
4929
  role: "tree",
4921
4930
  "aria-label": t("search.results.aria"),
4931
+ "aria-busy": pending,
4922
4932
  children: results.items.map((result) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SearchResultItem, {
4923
4933
  result,
4924
4934
  currentId,
@@ -4941,20 +4951,35 @@ window.__ModuleLoader__.load({
4941
4951
  }),
4942
4952
  !pending && results.items.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4943
4953
  className: WorkspaceBrowser_module_css_default.empty,
4954
+ role: "status",
4944
4955
  children: t("search.noMatches")
4945
4956
  }),
4946
4957
  results.hasMore && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4947
4958
  className: WorkspaceBrowser_module_css_default.searchStatus,
4959
+ role: "status",
4948
4960
  children: t("search.hasMore", { n: resultLimit })
4949
4961
  })
4950
4962
  ]
4951
4963
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: WorkspaceBrowser_module_css_default.fade })]
4952
4964
  });
4953
4965
  }
4966
+ //#endregion
4967
+ //#region src/client/rows/WorkspaceBrowser.tsx
4954
4968
  /**
4955
- * Render the browsing region.
4956
- * @param props - composed slot props (shell owner share + store + injected actions).
4957
- * @returns the region element tree.
4969
+ * The workspace/session browsing region filling the sidebar shell's
4970
+ * `sidebar.workspaces` hole: section header (title + view options + add
4971
+ * workspace), search, the grouped tree or flat list, and the workspace
4972
+ * dialogs. Wide state renders the full browser; rail state renders the two
4973
+ * region icons (search / add workspace) as 36px controls on the shell's shared
4974
+ * rail entry path, each requesting expansion through the owner share. Adding
4975
+ * is the header button's one action, so it raises the directory flow with no
4976
+ * menu in between; the flow and its error dialog live in WorkspacePicker
4977
+ * (same package — direct composition, no slot between them). A Session row's
4978
+ * "..." menu and hover buttons are the `sidebar.workspaces.session.menu.item`
4979
+ * and `sidebar.workspaces.session.row.action` lists rendered through this
4980
+ * entry's `renderSlot`; the actions in them, this package's own included,
4981
+ * are slot entries with their own behavior, so this component threads no
4982
+ * action callbacks and hosts no action surface.
4958
4983
  */
4959
4984
  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 }) {
4960
4985
  const home = useHostInfo((info) => info.home);
@@ -4990,7 +5015,7 @@ window.__ModuleLoader__.load({
4990
5015
  actions.setArchivedFilter("default");
4991
5016
  };
4992
5017
  const workspaceReady = workspacePhase === "ready" && workspaceStreamState !== "loading";
4993
- const mainSessionId = Object.values(list.byId).find((session) => (session.retainedBy.mainView ?? 0) > 0)?.id;
5018
+ const mainSessionId = (0, react.useMemo)(() => mainViewSessionId(list), [list]);
4994
5019
  const currentBlank = mainSessionId !== void 0 && list.byId[mainSessionId]?.blank === true ? mainSessionId : void 0;
4995
5020
  const ungroupedMemberIds = (0, react.useMemo)(() => {
4996
5021
  const accounted = new Set(workspaces.flatMap((workspace) => workspace.sessionIds));
@@ -5133,7 +5158,7 @@ window.__ModuleLoader__.load({
5133
5158
  const timer = window.setTimeout(() => {
5134
5159
  searchInput.current?.focus({ preventScroll: true });
5135
5160
  setSearchOnExpand(false);
5136
- }, EXPAND_SLIDE_MS);
5161
+ }, 300);
5137
5162
  return () => {
5138
5163
  window.clearTimeout(timer);
5139
5164
  };
@@ -5209,7 +5234,7 @@ window.__ModuleLoader__.load({
5209
5234
  hasMore: false
5210
5235
  });
5211
5236
  });
5212
- }, SEARCH_DEBOUNCE_MS);
5237
+ }, 250);
5213
5238
  return () => {
5214
5239
  window.clearTimeout(timer);
5215
5240
  controller.abort();
@@ -5316,7 +5341,7 @@ window.__ModuleLoader__.load({
5316
5341
  className: WorkspaceBrowser_module_css_default.searchInput,
5317
5342
  type: "text",
5318
5343
  placeholder: t("search.placeholder"),
5319
- maxLength: SEARCH_QUERY_MAX_CODE_UNITS,
5344
+ maxLength: 500,
5320
5345
  value: query,
5321
5346
  tabIndex: searchExpanded ? 0 : -1,
5322
5347
  onChange: (e) => {
@@ -5729,6 +5754,15 @@ window.__ModuleLoader__.load({
5729
5754
  //#endregion
5730
5755
  //#region src/client/session-actions/derived.ts
5731
5756
  /**
5757
+ * "还没投影过"的标记。
5758
+ *
5759
+ * 不能用 `undefined` 兼任:投影**合法地**可能返回 `undefined`("当前没有待处理的
5760
+ * 请求"就是一次正常的空结果),把它当成"未计算"会让每次读都重跑一次 `project`。
5761
+ * 投影若新建 `Set` / 数组,消费者每次读都拿到新引用,选择器于是永远不收敛——这正是
5762
+ * 本函数要避免的那件事。Symbol 不与任何投影结果相等,且只在本模块内存在。
5763
+ */
5764
+ const NOT_PROJECTED = Symbol("derive.notProjected");
5765
+ /**
5732
5766
  * Project one observable into another, recomputing only when the source
5733
5767
  * snapshot changes identity, so consumers that select from the projection
5734
5768
  * (a Set lookup per row) never rebuild it per read.
@@ -5738,11 +5772,11 @@ window.__ModuleLoader__.load({
5738
5772
  */
5739
5773
  function derive(source, project) {
5740
5774
  let seen;
5741
- let value;
5775
+ let value = NOT_PROJECTED;
5742
5776
  return {
5743
5777
  getSnapshot: () => {
5744
5778
  const snapshot = source.getSnapshot();
5745
- if (value === void 0 || snapshot !== seen) {
5779
+ if (value === NOT_PROJECTED || snapshot !== seen) {
5746
5780
  seen = snapshot;
5747
5781
  value = project(snapshot);
5748
5782
  }
@@ -6130,6 +6164,8 @@ window.__ModuleLoader__.load({
6130
6164
  "settings.count.section": "数字显示",
6131
6165
  "settings.count.threshold": "数字显示阈值",
6132
6166
  "settings.count.thresholdHint": "数量 >= 此值时才在徽标上显示数字。默认 2(即 count=1 时省略,因为状态点已表达\"有东西\")。设为 1 则始终显示。",
6167
+ "settings.count.decrease": "减少",
6168
+ "settings.count.increase": "增加",
6133
6169
  "settings.save": "保存",
6134
6170
  "settings.saving": "保存中…",
6135
6171
  "settings.saveFailed": "本部署没有接受这些值,已保留供你修改。",
@@ -6271,6 +6307,8 @@ window.__ModuleLoader__.load({
6271
6307
  "settings.count.section": "Count display",
6272
6308
  "settings.count.threshold": "Count display threshold",
6273
6309
  "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.",
6310
+ "settings.count.decrease": "Decrease",
6311
+ "settings.count.increase": "Increase",
6274
6312
  "settings.save": "Save",
6275
6313
  "settings.saving": "Saving…",
6276
6314
  "settings.saveFailed": "The deployment did not accept these values; they were left for you to correct.",