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
@@ -1 +1 @@
1
- {"version":3,"file":"WorkspaceBrowser.d.ts","sourceRoot":"","sources":["../../../../src/client/rows/WorkspaceBrowser.tsx"],"names":[],"mappings":"AAmCA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAA;AAw2BjE;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,IAAI,EACJ,YAAY,EACZ,aAAa,EACb,WAAW,EACX,gBAAgB,EAChB,aAAa,EACb,QAAQ,EACR,OAAO,EACP,YAAY,EACZ,IAAI,EACJ,oBAAoB,EACpB,yBAAyB,EACzB,eAAe,EACf,eAAe,EACf,qBAAqB,EACrB,gBAAgB,EAChB,eAAe,EACf,cAAc,EACd,iBAAiB,EACjB,gBAAgB,EAChB,WAAW,EACX,YAAY,EACZ,qBAAqB,EACrB,gBAAgB,EAChB,aAAa,EACb,mBAAmB,EACnB,iBAAiB,EACjB,gBAAgB,EAChB,UAAU,EACV,CAAC,GACF,EAAE,qBAAqB,+BA0mBvB"}
1
+ {"version":3,"file":"WorkspaceBrowser.d.ts","sourceRoot":"","sources":["../../../../src/client/rows/WorkspaceBrowser.tsx"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAA;AAgBjE,wBAAgB,gBAAgB,CAAC,EAC/B,IAAI,EACJ,YAAY,EACZ,aAAa,EACb,WAAW,EACX,gBAAgB,EAChB,aAAa,EACb,QAAQ,EACR,OAAO,EACP,YAAY,EACZ,IAAI,EACJ,oBAAoB,EACpB,yBAAyB,EACzB,eAAe,EACf,eAAe,EACf,qBAAqB,EACrB,gBAAgB,EAChB,eAAe,EACf,cAAc,EACd,iBAAiB,EACjB,gBAAgB,EAChB,WAAW,EACX,YAAY,EACZ,qBAAqB,EACrB,gBAAgB,EAChB,aAAa,EACb,mBAAmB,EACnB,iBAAiB,EACjB,gBAAgB,EAChB,UAAU,EACV,CAAC,GACF,EAAE,qBAAqB,+BAymBvB"}
@@ -17,565 +17,16 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
17
17
  */
18
18
  import { useEffect, useMemo, useRef, useState } from 'react';
19
19
  import clsx from 'clsx';
20
- import { Button, HoverCardGroup, IconArchiveCheckOutlineRegular, IconArchiveOffOutlineRegular, IconArchiveOutlineRegular, IconChevronsUpDownOutlineRegular, IconClockOutlineRegular, IconCloseFillRegular, IconFlatListOutlineRegular, IconFolderCloseRegular, IconProjectAddOutlineRegular, IconQueueOutlineRegular, IconSearchOutlineRegular, IconSlidersTwoOutlineRegular, IconWorkspaceTreeOutlineRegular, Menu, Modal, Tooltip, } from '@deepseek-ai/dsh-client-ui-primitives';
20
+ import { Button, IconCloseFillRegular, IconProjectAddOutlineRegular, IconSearchOutlineRegular, Modal, Tooltip, } from '@deepseek-ai/dsh-client-ui-primitives';
21
21
  import { workspaceDisplayTitle } from '@deepseek-ai/dsh-api-workspace-controller/default-workspace';
22
- import { deriveFlat, deriveGroups, deriveSearchResults, orderByRecency, owningGroupKey, owningParentFolder, pinCurrentBlank, reconcileManualOrder, sessionMemberIds, UNGROUPED_KEY, } from "../tree.js";
23
- import { directStatusesForGroups, withAggregatedStatus } from "../group-status.js";
24
- import { raiseWatermarks, requiredWatermark, SESSION_WATERMARK_BASE, watermarkRows, } from "../session-watermarks.js";
25
- import { facetStore } from "../facet-store.js";
26
- import { useRegisteredFacets } from "../use-registered-facets.js";
27
- import { ProjectRowItem, SearchResultItem, SessionNodeItem } from "./Rows.js";
28
- import { AnimatedRows } from "./AnimatedRows.js";
22
+ import { orderByRecency, pinCurrentBlank, reconcileManualOrder, sessionMemberIds, UNGROUPED_KEY, } from "../tree.js";
29
23
  import { FLAT_SESSION_ORDER_KEY } from "../stores.js";
30
24
  import { WorkspacePickFlow } from "../WorkspacePicker.js";
31
25
  import css from './WorkspaceBrowser.module.css';
32
- // 外部状态来源:**本文件不认识任何具体插件**。运行时只有登记面(`../facets.ts`)与
33
- // 它背后的存储订阅;谁想让自己的状态进折叠徽标,就按协议注册一个 facet
34
- // (`docs/facet-protocol.md`)。现有两家已各自注册,本插件不再代管任何来源。
35
- //
36
- // 这里只经 `useRegisteredFacets` 订阅一次外部存储,与 facet 数量无关,所以增删来源不会
37
- // 改变 hook 调用序列(那正是逐个调 provider hook 的写法做不到的);登记面本身**迟到**也
38
- // 照样生效,理由与踩过的坑写在那个文件里。本插件不再筛外部来源:登记面即所见。
39
- const facetsStore = facetStore();
40
- /**
41
- * Column slide length (--ds-transition-duration-slow): rail-search focus waits it out —
42
- * focus() forces a synchronous layout and would jank the slide.
43
- */
44
- const EXPAND_SLIDE_MS = 300;
45
- /** Pause between the latest keystroke and a Host content-search request. */
46
- const SEARCH_DEBOUNCE_MS = 250;
47
- /** `session.search` wire bound, measured in JavaScript UTF-16 code units. */
48
- const SEARCH_QUERY_MAX_CODE_UNITS = 500;
49
- /** Keep controlled input and RPC payload inside the session.search wire contract. */
50
- function sanitizeSearchQuery(value) {
51
- const withoutNul = value.replaceAll('\0', '');
52
- if (withoutNul.length <= SEARCH_QUERY_MAX_CODE_UNITS)
53
- return withoutNul;
54
- let end = SEARCH_QUERY_MAX_CODE_UNITS;
55
- const last = withoutNul.charCodeAt(end - 1);
56
- const next = withoutNul.charCodeAt(end);
57
- if (last >= 0xD800 && last <= 0xDBFF && next >= 0xDC00 && next <= 0xDFFF)
58
- end--;
59
- return withoutNul.slice(0, end);
60
- }
61
- /**
62
- * Accept the native drag at document level while a row drag is active: row
63
- * hover still owns the insertion marker, and releasing outside the list must
64
- * not be rendered as a rejected drop before dragend commits that last marker.
65
- */
66
- function useNativeDragAcceptance(active) {
67
- useEffect(() => {
68
- if (!active)
69
- return;
70
- const acceptDrag = (event) => {
71
- event.preventDefault();
72
- if (event.dataTransfer !== null)
73
- event.dataTransfer.dropEffect = 'move';
74
- };
75
- const acceptDrop = (event) => { event.preventDefault(); };
76
- document.addEventListener('dragover', acceptDrag);
77
- document.addEventListener('drop', acceptDrop);
78
- return () => {
79
- document.removeEventListener('dragover', acceptDrag);
80
- document.removeEventListener('drop', acceptDrop);
81
- };
82
- }, [active]);
83
- }
84
- /** Grouping, ordering, and archived-filter menu; own open state so it resets with the wide chrome. */
85
- function ViewOptionsMenu({ groupBy, orderBy, archivedFilter, onGroupPick, onOrderPick, onArchivedFilterPick, t }) {
86
- const [open, setOpen] = useState(false);
87
- return (_jsx(Menu, { open: open, onClose: () => { setOpen(false); }, items: [
88
- { type: 'label', id: 'group-by', text: t('groupBy.label') },
89
- { id: 'workspace', label: t('groupBy.workspace'), icon: _jsx(IconFolderCloseRegular, {}) },
90
- { id: 'workspace-tree', label: t('groupBy.workspaceTree'), icon: _jsx(IconWorkspaceTreeOutlineRegular, {}) },
91
- { id: 'flat', label: t('groupBy.flat'), icon: _jsx(IconFlatListOutlineRegular, {}) },
92
- { type: 'separator', id: 'order-by-separator' },
93
- { type: 'label', id: 'order-by', text: t('orderBy.label') },
94
- { id: 'manual', label: t('orderBy.manual'), icon: _jsx(IconChevronsUpDownOutlineRegular, {}) },
95
- { id: 'updated', label: t('orderBy.updated'), icon: _jsx(IconClockOutlineRegular, {}) },
96
- { type: 'separator', id: 'archived-filter-separator' },
97
- { type: 'label', id: 'filter-by', text: t('filterBy.label') },
98
- { id: 'hide-archived', label: t('viewOptions.hideArchived'), icon: _jsx(IconArchiveOffOutlineRegular, {}) },
99
- { id: 'show-archived', label: t('viewOptions.showArchived'), icon: _jsx(IconQueueOutlineRegular, {}) },
100
- { id: 'only-archived', label: t('viewOptions.onlyArchived'), icon: _jsx(IconArchiveCheckOutlineRegular, {}) },
101
- ], selectedIds: [
102
- groupBy,
103
- orderBy,
104
- { default: 'hide-archived', show: 'show-archived', only: 'only-archived' }[archivedFilter],
105
- ], onSelect: (id) => {
106
- if (id === 'workspace' || id === 'workspace-tree' || id === 'flat')
107
- onGroupPick(id);
108
- else if (id === 'manual' || id === 'updated')
109
- onOrderPick(id);
110
- else if (id === 'hide-archived')
111
- onArchivedFilterPick('default');
112
- else if (id === 'show-archived')
113
- onArchivedFilterPick('show');
114
- else if (id === 'only-archived')
115
- onArchivedFilterPick('only');
116
- setOpen(false);
117
- }, align: "end", dense: true, listClassName: css.viewOptionsMenu,
118
- // Portal: the section header clips overflow, so an in-place list would
119
- // be cut off at the header's bounds.
120
- portal: true, anchor: (_jsx(Tooltip, { label: t('viewOptions.label'), side: "bottom", delayMs: 500, children: _jsx("button", { type: "button", className: clsx(css.iconButton, css.wide), "aria-label": t('viewOptions.label'), onClick: () => { setOpen(v => !v); }, children: _jsx(IconSlidersTwoOutlineRegular, {}) }) })) }));
121
- }
122
- /** Apply a visible drop to the complete account without removing hidden members. */
123
- function sessionDragOrder(order, rows, drag, over) {
124
- const source = rows.find(row => row.id === drag.sessionId);
125
- const target = rows.find(row => row.id === over.id);
126
- if (source === undefined || target === undefined || source.blank
127
- || source.pinned !== drag.pinned || target.pinned !== drag.pinned
128
- || source.id === target.id || !order.includes(source.id))
129
- return;
130
- const section = rows.filter(row => row.pinned === drag.pinned);
131
- const sourceIndex = section.findIndex(row => row.id === source.id);
132
- const withoutSource = section.filter(row => row.id !== source.id);
133
- const insertAt = withoutSource.findIndex(row => row.id === target.id) + (over.half === 'after' ? 1 : 0);
134
- if (insertAt === sourceIndex)
135
- return;
136
- const next = order.filter(id => id !== source.id);
137
- const targetIndex = next.indexOf(target.id);
138
- if (targetIndex === -1)
139
- return;
140
- next.splice(targetIndex + (over.half === 'after' ? 1 : 0), 0, source.id);
141
- return pinCurrentBlank(next, rows.find(row => row.blank)?.id);
142
- }
143
- /** Resolve an insertion side across the Workspace header, descendants, and Sessions. */
144
- function workspaceGroupHalf(e) {
145
- const rect = e.currentTarget.getBoundingClientRect();
146
- return e.clientY < rect.top + rect.height / 2 ? 'before' : 'after';
147
- }
148
- /** The list-empty placeholder — a glyph over the text; the archived-only view names its filter and offers the way back. */
149
- function EmptySessions({ rowState, onLeaveArchivedOnly, t }) {
150
- const archivedOnly = rowState.archivedFilter === 'only';
151
- return (_jsxs("div", { className: css.emptyState, "data-row-key": "empty", children: [archivedOnly ? _jsx(IconArchiveOutlineRegular, { size: 24 }) : _jsx(IconQueueOutlineRegular, { size: 24 }), _jsx("div", { children: archivedOnly ? t('empty.noneArchived') : t('empty.none') }), archivedOnly && (_jsx("button", { type: "button", className: css.emptyAction, onClick: onLeaveArchivedOnly, children: t('empty.viewOthers') }))] }));
152
- }
153
- /** The scrolling session tree; unmounting drops the sessions subscription and local row limits. */
154
- 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, }) {
155
- const panelActive = usePanelInfo(info => info.activePanelId !== null);
156
- // 徽标设置:**本插件自己**那三档会话状态的显隐、悬停明细、数字样式。
157
- const badgeSettings = useBadgeSettings(settings => settings);
158
- const statuses = useSessionStatus(s => s);
159
- // 外部状态:订阅 + 读登记面,在 `useRegisteredFacets` 里一次做完。这里刻意**不**自己
160
- // 摊开那两行:登记面是运行时增删的(提供方在自己的 apply 里注册,宿主不保证它早于本
161
- // 组件首渲染),而 memo 的依赖里必须有版本号,否则订阅只换来一次重渲染、算的却还是
162
- // 上一次的登记面——「注册成功了却永远不进徽标」。推导过程与回归用例见
163
- // `../use-registered-facets.ts`。外部来源的显隐不归本插件管:来源不注册就是不显示。
164
- const facets = useRegisteredFacets(facetsStore);
165
- const current = panelActive
166
- ? undefined
167
- : Object.values(list.byId).find(session => (session.retainedBy.mainView ?? 0) > 0)?.id;
168
- const revealGroup = revealSessionId === undefined || !workspaceReady
169
- ? undefined
170
- : owningGroupKey(workspaces, revealSessionId);
171
- const [storedWatermarks, setStoredWatermarks] = useState({});
172
- const [watermarkMotionKey, setWatermarkMotionKey] = useState(0);
173
- // Transient drag marker state; the selected mode owns the resulting order.
174
- const [drag, setDrag] = useState(null);
175
- const sessionDropCommitted = useRef(false);
176
- const [workspaceDrag, setWorkspaceDrag] = useState(null);
177
- const workspaceDropCommitted = useRef(false);
178
- const nativeDragActive = drag !== null || workspaceDrag !== null;
179
- useNativeDragAcceptance(nativeDragActive);
180
- const currentGroup = current === undefined || !workspaceReady
181
- ? undefined
182
- : owningGroupKey(workspaces, current);
183
- useEffect(() => {
184
- if (current === undefined || currentGroup === undefined || Object.hasOwn(groupExpansion, currentGroup))
185
- return;
186
- setGroupExpanded(currentGroup, true);
187
- }, [current, currentGroup, setGroupExpanded, groupExpansion]);
188
- const parents = useMemo(() => {
189
- if (!nestWorkspaces)
190
- return new Map();
191
- const keysByPath = new Map(workspaces.map(workspace => [workspace.path, workspace.workspaceId]));
192
- const paths = [...keysByPath.keys()];
193
- return new Map(workspaces.map((workspace) => {
194
- const path = owningParentFolder(workspace.path, paths);
195
- return [workspace.workspaceId, path === undefined ? undefined : keysByPath.get(path)];
196
- }));
197
- }, [nestWorkspaces, workspaces]);
198
- const currentAncestors = useMemo(() => {
199
- const keys = new Set();
200
- for (let key = currentGroup === undefined ? undefined : parents.get(currentGroup); key !== undefined; key = parents.get(key)) {
201
- keys.add(key);
202
- }
203
- return keys;
204
- }, [currentGroup, parents]);
205
- const expandedGroups = useMemo(() => {
206
- const ancestorKeys = new Set(parents.values());
207
- return [...workspaces.map(workspace => workspace.workspaceId), UNGROUPED_KEY]
208
- .filter(key => groupExpansion[key] ?? ancestorKeys.has(key));
209
- }, [groupExpansion, parents, workspaces]);
210
- const groups = useMemo(() => deriveGroups(list, workspaces, rowState, statuses, {
211
- expandedGroups,
212
- ungroupedOrder: ungroupedSessionIds,
213
- }), [list, workspaces, rowState, statuses, expandedGroups, ungroupedSessionIds]);
214
- useEffect(() => {
215
- for (let key = revealGroup; key !== undefined; key = parents.get(key)) {
216
- if (groupExpansion[key] === false || (key === revealGroup && groupExpansion[key] !== true)) {
217
- setGroupExpanded(key, true);
218
- }
219
- }
220
- }, [groupExpansion, parents, revealGroup, setGroupExpanded]);
221
- const baselines = new Map(groups.map(group => [
222
- group.key,
223
- Math.max(SESSION_WATERMARK_BASE, requiredWatermark(group.expanded ? group.sessions : [], current, group.key === revealGroup ? revealSessionId : undefined)),
224
- ]));
225
- const foldedKeys = new Set(groups.filter(group => !group.expanded).map(group => group.key));
226
- const underFoldedAncestor = (key) => {
227
- for (let owner = parents.get(key); owner !== undefined; owner = parents.get(owner)) {
228
- if (foldedKeys.has(owner))
229
- return true;
230
- }
231
- return false;
232
- };
233
- const requiredWatermarks = new Map([...baselines].filter(([key]) => !underFoldedAncestor(key)));
234
- const sessionWatermarks = raiseWatermarks(storedWatermarks, requiredWatermarks);
235
- if (sessionWatermarks !== storedWatermarks) {
236
- setStoredWatermarks(marks => raiseWatermarks(marks, requiredWatermarks));
237
- }
238
- const dropWatermarks = (drop) => {
239
- if (!Object.keys(storedWatermarks).some(drop))
240
- return;
241
- setStoredWatermarks(marks => Object.fromEntries(Object.entries(marks).filter(([key]) => !drop(key))));
242
- setWatermarkMotionKey(key => key + 1);
243
- };
244
- const withinGroup = (key, root) => {
245
- for (let owner = key; owner !== undefined; owner = parents.get(owner)) {
246
- if (owner === root)
247
- return true;
248
- }
249
- return false;
250
- };
251
- const now = Date.now();
252
- const commitSessionDrag = (activeDrag, over) => {
253
- if (sessionDropCommitted.current)
254
- return;
255
- sessionDropCommitted.current = true;
256
- setDrag(null);
257
- const group = groups.find(candidate => candidate.key === activeDrag.accountKey);
258
- if (group === undefined)
259
- return;
260
- if (over.id === activeDrag.sessionId)
261
- return;
262
- const accountSessionIds = activeDrag.accountKey === UNGROUPED_KEY
263
- ? ungroupedSessionIds
264
- : workspaces.find(workspace => workspace.workspaceId === activeDrag.accountKey)?.sessionIds;
265
- if (accountSessionIds === undefined)
266
- return;
267
- const renderedSessions = watermarkRows(group.sessions, sessionWatermarks[group.key] ?? SESSION_WATERMARK_BASE).rows;
268
- const nextOrder = sessionDragOrder(accountSessionIds, renderedSessions, activeDrag, over);
269
- if (nextOrder !== undefined)
270
- setSessionOrder(activeDrag.accountKey, nextOrder);
271
- };
272
- const commitWorkspaceDrag = (activeDrag, over) => {
273
- if (workspaceDropCommitted.current)
274
- return;
275
- workspaceDropCommitted.current = true;
276
- setWorkspaceDrag(null);
277
- const owner = parents.get(activeDrag.workspaceId);
278
- const siblings = workspaces.filter(workspace => parents.get(workspace.workspaceId) === owner);
279
- const rowIndex = siblings.findIndex(workspace => workspace.workspaceId === over.id);
280
- if (rowIndex === -1)
281
- return;
282
- const anchor = over.half === 'before' ? over.id : siblings[rowIndex + 1]?.workspaceId;
283
- if (anchor === activeDrag.workspaceId)
284
- return;
285
- const sourceIndex = siblings.findIndex(workspace => workspace.workspaceId === activeDrag.workspaceId);
286
- const anchorIndex = anchor === undefined
287
- ? siblings.length
288
- : siblings.findIndex(workspace => workspace.workspaceId === anchor);
289
- if (sourceIndex !== -1 && (anchorIndex === sourceIndex || anchorIndex === sourceIndex + 1))
290
- return;
291
- insertWorkspaceBefore(activeDrag.workspaceId, anchor).catch((reason) => {
292
- console.warn('workspace reorder rejected:', reason);
293
- });
294
- };
295
- // Build tree structure from original groups (for aggregation traversal)
296
- const childrenByParentForAggregation = useMemo(() => {
297
- const rendered = new Set(groups.map(group => group.key));
298
- const children = new Map();
299
- for (const group of groups) {
300
- let parent = parents.get(group.key);
301
- while (parent !== undefined && !rendered.has(parent))
302
- parent = parents.get(parent);
303
- const siblings = children.get(parent);
304
- if (siblings === undefined)
305
- children.set(parent, [group]);
306
- else
307
- siblings.push(group);
308
- }
309
- return children;
310
- }, [groups, parents]);
311
- // Fork-owned: each group's DIRECT member status — the input to the tree-wide
312
- // aggregation below. deriveGroups stopped computing liveStatus (PR #10 moved it
313
- // out), so this map is the aggregator's only data source. Expanded groups are
314
- // measured too; see directStatusesForGroups.
315
- const directStatusByGroup = useMemo(() => directStatusesForGroups(groups, facets, now),
316
- // `now` 刻意不进依赖:它是渲染期的时钟读数,唯一影响的是"哪些截止还算将来"。
317
- // 提供方的 store 到点会通知,需要新答案的那次渲染本来就带着新的 `now` 发生。
318
- [groups, facets]);
319
- // Multi-level aggregation: a collapsed folder's badge covers its own members plus
320
- // every descendant group's, at any depth. Every row comes back measured, so the
321
- // render path can never see an unresolved liveStatus.
322
- const groupsWithAggregatedStatus = useMemo(() => withAggregatedStatus(groups, directStatusByGroup, childrenByParentForAggregation), [groups, directStatusByGroup, childrenByParentForAggregation]);
323
- // Rebuild tree structure with aggregated groups (for rendering)
324
- const childrenByParent = useMemo(() => {
325
- const rendered = new Set(groupsWithAggregatedStatus.map(group => group.key));
326
- const children = new Map();
327
- for (const group of groupsWithAggregatedStatus) {
328
- // The archived-only view drops empty groups, so an ancestor may be
329
- // absent; nest under the nearest rendered one.
330
- let parent = parents.get(group.key);
331
- while (parent !== undefined && !rendered.has(parent))
332
- parent = parents.get(parent);
333
- const siblings = children.get(parent);
334
- if (siblings === undefined)
335
- children.set(parent, [group]);
336
- else
337
- siblings.push(group);
338
- }
339
- return children;
340
- }, [groupsWithAggregatedStatus, parents]);
341
- const rootGroups = childrenByParent.get(undefined) ?? [];
342
- const workspaceDropAtListStart = rootGroups[0]?.workspaceId !== undefined
343
- && workspaceDrag?.over?.id === rootGroups[0].workspaceId
344
- && workspaceDrag.over.half === 'before';
345
- const rowKeys = groupsWithAggregatedStatus.length === 0 ? ['empty'] : [];
346
- const renderGroup = (group, depth) => {
347
- const workspaceId = group.workspaceId;
348
- const children = childrenByParent.get(group.key) ?? [];
349
- const compatibleDrag = workspaceDrag !== null && parents.get(workspaceDrag.workspaceId) === parents.get(group.key);
350
- const watermark = sessionWatermarks[group.key] ?? SESSION_WATERMARK_BASE;
351
- const collapsed = watermarkRows(group.sessions, baselines.get(group.key) ?? SESSION_WATERMARK_BASE);
352
- const visible = watermarkRows(group.sessions, watermark);
353
- const sessionsExpanded = visible.hiddenCount === 0;
354
- rowKeys.push(`workspace:${group.key}`);
355
- const childRows = group.expanded ? children.map(child => renderGroup(child, depth + 1)) : [];
356
- const sessions = visible.rows;
357
- for (const node of sessions)
358
- rowKeys.push(`session:${node.id}`);
359
- if (collapsed.hiddenCount > 0)
360
- rowKeys.push(`overflow:${group.key}`);
361
- const workspaceMarker = workspaceId !== undefined && workspaceDrag?.over?.id === workspaceId
362
- ? workspaceDrag.over.half
363
- : null;
364
- const workspaceDragProps = workspaceId === undefined ? undefined : {
365
- start: () => {
366
- workspaceDropCommitted.current = false;
367
- setWorkspaceDrag({ workspaceId, over: null });
368
- },
369
- end: () => {
370
- if (workspaceDrag?.over !== null && workspaceDrag?.over !== undefined) {
371
- commitWorkspaceDrag(workspaceDrag, workspaceDrag.over);
372
- }
373
- else {
374
- setWorkspaceDrag(null);
375
- }
376
- workspaceDropCommitted.current = false;
377
- },
378
- };
379
- const hoverWorkspace = workspaceId === undefined || !compatibleDrag
380
- ? undefined
381
- : (half) => {
382
- setWorkspaceDrag(active => active === null
383
- ? active
384
- : { ...active, over: { id: workspaceId, half } });
385
- };
386
- const dropWorkspace = workspaceId === undefined || !compatibleDrag
387
- ? undefined
388
- : (half) => {
389
- commitWorkspaceDrag(workspaceDrag, { id: workspaceId, half });
390
- };
391
- // group arrives from groupsWithAggregatedStatus: collapsed groups already
392
- // carry the bottom-up aggregated liveStatus (direct + all descendants).
393
- return (
394
- // Group section: header, descendant Workspaces, and own Session rows. The
395
- // inter-group breathing room is the section's own margin
396
- // (WorkspaceBrowser.module.css).
397
- _jsxs("div", { style: { '--dsh-workspace-indent': `${depth * 12}px` }, className: clsx(css.groupSection, workspaceMarker === 'before' && css.workspaceDropBefore, workspaceMarker === 'after' && css.workspaceDropAfter), onDragOver: workspaceDrag === null
398
- ? undefined
399
- : (e) => {
400
- e.preventDefault();
401
- if (hoverWorkspace === undefined && parents.get(group.key) !== undefined)
402
- return;
403
- e.stopPropagation();
404
- if (hoverWorkspace === undefined) {
405
- e.dataTransfer.dropEffect = 'none';
406
- if (workspaceDrag.over !== null)
407
- setWorkspaceDrag({ ...workspaceDrag, over: null });
408
- }
409
- else {
410
- e.dataTransfer.dropEffect = 'move';
411
- hoverWorkspace(workspaceGroupHalf(e));
412
- }
413
- }, onDrop: workspaceDrag === null
414
- ? undefined
415
- : (e) => {
416
- e.preventDefault();
417
- if (dropWorkspace === undefined && parents.get(group.key) !== undefined)
418
- return;
419
- e.stopPropagation();
420
- if (dropWorkspace === undefined) {
421
- workspaceDropCommitted.current = true;
422
- setWorkspaceDrag(null);
423
- }
424
- else {
425
- dropWorkspace(workspaceGroupHalf(e));
426
- }
427
- }, children: [_jsx(ProjectRowItem, { newShortcut: shortcuts.find(row => row.id === 'session.new'), group: group, containsCurrentDescendant: currentAncestors.has(group.key), home: home, settings: badgeSettings, now: now, t: t, onToggle: () => {
428
- if (group.expanded)
429
- dropWatermarks(key => withinGroup(key, group.key));
430
- setGroupExpanded(group.key, !group.expanded);
431
- }, onCreate: () => {
432
- if (group.workspaceId !== undefined) {
433
- setGroupExpanded(group.key, true);
434
- startSession(group.workspaceId);
435
- }
436
- }, drag: workspaceDragProps, actions: group.workspaceId === undefined
437
- ? undefined
438
- : {
439
- rename: () => {
440
- /* v8 ignore next -- narrowing guard: the actions object exists only for real-workspace groups. */
441
- if (group.workspaceId !== undefined)
442
- onRenameRequest(group.workspaceId, group.label);
443
- },
444
- delete: () => {
445
- /* v8 ignore next -- narrowing guard: the actions object exists only for real-workspace groups. */
446
- if (group.workspaceId !== undefined)
447
- onDeleteRequest(group.workspaceId, group.label);
448
- },
449
- } }), childRows.length > 0 && (_jsx("div", { role: "group", children: childRows })), group.expanded && sessions.map((node) => {
450
- // Session drag never leaves its browser-local account, and pinned
451
- // rows reorder only within their leading pinned block.
452
- const sameGroupDrag = drag !== null && drag.accountKey === group.key;
453
- const compatibleTarget = sameGroupDrag && drag.pinned === node.pinned;
454
- const normalizeHalf = (half) => node.blank ? 'after' : half;
455
- const dragProps = {
456
- start: () => {
457
- sessionDropCommitted.current = false;
458
- setDrag({ accountKey: group.key, sessionId: node.id, pinned: node.pinned, over: null });
459
- },
460
- active: compatibleTarget,
461
- marker: sameGroupDrag && drag.over?.id === node.id ? drag.over.half : null,
462
- hover: (half) => {
463
- /* v8 ignore next -- narrowing guard: Rows gates hover on `active`, which is false while the drag state is null. */
464
- setDrag(d => (d === null ? d : {
465
- ...d, over: { id: node.id, half: normalizeHalf(half) },
466
- }));
467
- },
468
- drop: (half) => {
469
- /* v8 ignore next -- narrowing guard: Rows gates drop on `active`, which is false while the drag state is null. */
470
- if (drag === null)
471
- return;
472
- commitSessionDrag(drag, { id: node.id, half: normalizeHalf(half) });
473
- },
474
- end: () => {
475
- if (drag?.over !== null && drag?.over !== undefined)
476
- commitSessionDrag(drag, drag.over);
477
- else
478
- setDrag(null);
479
- sessionDropCommitted.current = false;
480
- },
481
- };
482
- return (_jsx(SessionNodeItem, { node: node, currentId: current, now: now, onOpen: open, onRenameRequest: onSessionRenameRequest, renderSlot: renderSlot, onReveal: node.id === revealSessionId && group.key === revealGroup
483
- ? () => { onSessionRevealed(node.id); }
484
- : undefined, drag: dragProps, t: t }, node.id));
485
- }), group.expanded && collapsed.hiddenCount > 0 && (_jsx("button", { type: "button", className: css.sessionOverflowButton, "data-row-key": `overflow:${group.key}`, "aria-expanded": sessionsExpanded, onClick: () => {
486
- if (sessionsExpanded) {
487
- dropWatermarks(key => key === group.key);
488
- return;
489
- }
490
- setStoredWatermarks(marks => ({
491
- ...marks,
492
- [group.key]: visible.hiddenCount <= SESSION_WATERMARK_BASE
493
- ? Infinity
494
- : watermark + SESSION_WATERMARK_BASE,
495
- }));
496
- setWatermarkMotionKey(key => key + 1);
497
- }, children: sessionsExpanded
498
- ? t('sessions.collapse')
499
- : t('sessions.expand', { n: visible.hiddenCount }) }))] }, group.key));
500
- };
501
- const groupRows = rootGroups.map(group => renderGroup(group, 0));
502
- return (_jsxs(HoverCardGroup, { className: clsx(css.treeBody, css.wide), children: [workspaceDropAtListStart && _jsx("span", { className: css.listTopDropIndicator, "aria-hidden": "true" }), _jsxs(AnimatedRows, { className: clsx(css.list, workspaceDropAtListStart && css.listTopDropActive), label: t('section.sessions'), rowKeys: rowKeys, ready: list.phase === 'ready' && workspaceReady && !nativeDragActive, resetKey: JSON.stringify([animationResetKey, watermarkMotionKey]), children: [groupsWithAggregatedStatus.length === 0 && (_jsx(EmptySessions, { rowState: rowState, onLeaveArchivedOnly: onLeaveArchivedOnly, t: t })), groupRows] }), _jsx("span", { className: css.fade })] }));
503
- }
504
- /** The flat "In one list" body: every session is one draggable top-level row. */
505
- function FlatList({ list, sessionIds, rowState, onLeaveArchivedOnly, useSessionStatus, open, onSessionRenameRequest, usePanelInfo, setSessionOrder, workspaceReady, animationResetKey, revealSessionId, onSessionRevealed, renderSlot, t, }) {
506
- const panelActive = usePanelInfo(info => info.activePanelId !== null);
507
- const statuses = useSessionStatus(s => s);
508
- const rows = useMemo(() => deriveFlat(list, sessionIds, rowState, statuses), [list, sessionIds, rowState, statuses]);
509
- const [drag, setDrag] = useState(null);
510
- const dropCommitted = useRef(false);
511
- useNativeDragAcceptance(drag !== null);
512
- const currentId = panelActive
513
- ? undefined
514
- : Object.values(list.byId).find(session => (session.retainedBy.mainView ?? 0) > 0)?.id;
515
- const commitDrag = (activeDrag, over) => {
516
- if (dropCommitted.current)
517
- return;
518
- dropCommitted.current = true;
519
- setDrag(null);
520
- const nextOrder = sessionDragOrder(sessionIds, rows, activeDrag, over);
521
- if (nextOrder !== undefined)
522
- setSessionOrder(FLAT_SESSION_ORDER_KEY, nextOrder);
523
- };
524
- const now = Date.now();
525
- return (_jsxs(HoverCardGroup, { className: clsx(css.treeBody, css.wide), children: [_jsxs(AnimatedRows, { className: clsx(css.list, css.flatList), label: t('section.sessions'), rowKeys: rows.length === 0 ? ['empty'] : rows.map(row => `session:${row.id}`), ready: list.phase === 'ready' && workspaceReady && drag === null, resetKey: animationResetKey, children: [rows.length === 0 && (_jsx(EmptySessions, { rowState: rowState, onLeaveArchivedOnly: onLeaveArchivedOnly, t: t })), rows.map((node) => {
526
- const active = drag !== null && drag.pinned === node.pinned;
527
- const normalizeHalf = (half) => node.blank ? 'after' : half;
528
- return (_jsx(SessionNodeItem, { node: node, currentId: currentId, now: now, onOpen: open, onRenameRequest: onSessionRenameRequest, renderSlot: renderSlot, onReveal: node.id === revealSessionId
529
- ? () => { onSessionRevealed(node.id); }
530
- : undefined, drag: {
531
- start: () => {
532
- dropCommitted.current = false;
533
- setDrag({ accountKey: FLAT_SESSION_ORDER_KEY, sessionId: node.id, pinned: node.pinned, over: null });
534
- },
535
- active,
536
- marker: active && drag.over?.id === node.id ? drag.over.half : null,
537
- hover: (half) => {
538
- setDrag(current => current === null ? current : {
539
- ...current, over: { id: node.id, half: normalizeHalf(half) },
540
- });
541
- },
542
- drop: (half) => {
543
- if (drag !== null)
544
- commitDrag(drag, { id: node.id, half: normalizeHalf(half) });
545
- },
546
- end: () => {
547
- if (drag?.over !== null && drag?.over !== undefined)
548
- commitDrag(drag, drag.over);
549
- else
550
- setDrag(null);
551
- dropCommitted.current = false;
552
- },
553
- }, t: t }, node.id));
554
- })] }), _jsx("span", { className: css.fade })] }));
555
- }
556
- /** Flat search body: local metadata matches plus the current Host result page. */
557
- function SearchResults({ useSessions, useSessionStatus, open, onUnarchive, workspaces, archivedSessionIds, archivedFilter, query, remote, resultLimit, usePanelInfo, t, }) {
558
- const panelActive = usePanelInfo(info => info.activePanelId !== null);
559
- const list = useSessions(s => s);
560
- const statuses = useSessionStatus(s => s);
561
- const currentRemote = remote.query === query
562
- ? remote
563
- : { query, status: 'loading', items: [], hasMore: false };
564
- const results = useMemo(() => deriveSearchResults(list, workspaces, query, archivedSessionIds, archivedFilter, statuses, currentRemote, resultLimit), [list, workspaces, query, archivedSessionIds, archivedFilter, statuses, currentRemote, resultLimit]);
565
- const pending = currentRemote.status === 'loading';
566
- const currentId = panelActive
567
- ? undefined
568
- : Object.values(list.byId).find(session => (session.retainedBy.mainView ?? 0) > 0)?.id;
569
- return (_jsxs("div", { className: clsx(css.treeBody, css.wide), children: [_jsxs("div", { className: css.list, children: [_jsx("div", { className: css.searchTree, role: "tree", "aria-label": t('search.results.aria'), children: results.items.map(result => (_jsx(SearchResultItem, { result: result, currentId: currentId, onOpen: open, onUnarchive: onUnarchive, t: t }, result.id))) }), pending && (
570
- /* Two skeleton rows on an empty list, one when local matches already
571
- show and only the content hits are outstanding. */
572
- _jsx("div", { role: "status", "aria-label": t('search.pending'), children: (results.items.length === 0 ? [0, 1] : [0]).map(i => (_jsxs("div", { className: css.skeletonRow, "aria-hidden": "true", children: [_jsx("span", { className: css.skeletonDot }), _jsxs("span", { className: css.skeletonBars, children: [_jsx("span", { className: css.skeletonBar }), _jsx("span", { className: clsx(css.skeletonBar, css.skeletonBarWide) })] })] }, i))) })), !pending && results.items.length === 0 && (_jsx("div", { className: css.empty, children: t('search.noMatches') })), results.hasMore && (_jsx("div", { className: css.searchStatus, children: t('search.hasMore', { n: resultLimit }) }))] }), _jsx("span", { className: css.fade })] }));
573
- }
574
- /**
575
- * Render the browsing region.
576
- * @param props - composed slot props (shell owner share + store + injected actions).
577
- * @returns the region element tree.
578
- */
26
+ import { EXPAND_SLIDE_MS, SEARCH_DEBOUNCE_MS, SEARCH_QUERY_MAX_CODE_UNITS, mainViewSessionId, sanitizeSearchQuery, } from "./browser-shared.js";
27
+ import { ViewOptionsMenu } from "./ViewOptionsMenu.js";
28
+ import { SessionTree } from "./SessionTree.js";
29
+ import { FlatList, SearchResults } from "./ListView.js";
579
30
  export 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, }) {
580
31
  const home = useHostInfo(info => info.home);
581
32
  const shortcuts = useShortcuts(rows => rows);
@@ -617,8 +68,7 @@ export function WorkspaceBrowser({ wide, usePanelInfo, expandSidebar, useSession
617
68
  };
618
69
  const leaveArchivedOnly = () => { actions.setArchivedFilter('default'); };
619
70
  const workspaceReady = workspacePhase === 'ready' && workspaceStreamState !== 'loading';
620
- const mainSessionId = Object.values(list.byId)
621
- .find(session => (session.retainedBy.mainView ?? 0) > 0)?.id;
71
+ const mainSessionId = useMemo(() => mainViewSessionId(list), [list]);
622
72
  const currentBlank = mainSessionId !== undefined && list.byId[mainSessionId]?.blank === true
623
73
  ? mainSessionId
624
74
  : undefined;