@cerbur/clutch-dsh-worktree 0.1.4 → 0.1.6

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 (115) hide show
  1. package/README.md +89 -24
  2. package/README.zh.md +81 -23
  3. package/lib/client/WorktreeContext.d.ts.map +1 -1
  4. package/lib/client/WorktreeContext.js.map +1 -1
  5. package/lib/client/WorktreeHeroContext.d.ts +1 -1
  6. package/lib/client/WorktreeHeroContext.d.ts.map +1 -1
  7. package/lib/client/WorktreeHeroContext.js +10 -5
  8. package/lib/client/WorktreeHeroContext.js.map +1 -1
  9. package/lib/client/WorktreeSurface.d.ts +1 -1
  10. package/lib/client/WorktreeSurface.d.ts.map +1 -1
  11. package/lib/client/WorktreeSurface.js +349 -88
  12. package/lib/client/WorktreeSurface.js.map +1 -1
  13. package/lib/client/entry.d.ts.map +1 -1
  14. package/lib/client/entry.js +3 -0
  15. package/lib/client/entry.js.map +1 -1
  16. package/lib/client/locales.d.ts +28 -0
  17. package/lib/client/locales.d.ts.map +1 -1
  18. package/lib/client/locales.js +30 -2
  19. package/lib/client/locales.js.map +1 -1
  20. package/lib/client/virtual-workspace-membership.js +1 -1
  21. package/lib/client/virtual-workspace-membership.js.map +1 -1
  22. package/lib/client/worktree-connection.d.ts +2 -0
  23. package/lib/client/worktree-connection.d.ts.map +1 -1
  24. package/lib/client/worktree-connection.js +4 -0
  25. package/lib/client/worktree-connection.js.map +1 -1
  26. package/lib/client/worktree-context-store.d.ts.map +1 -1
  27. package/lib/client/worktree-context-store.js +51 -11
  28. package/lib/client/worktree-context-store.js.map +1 -1
  29. package/lib/client/worktree-error-copy.d.ts.map +1 -1
  30. package/lib/client/worktree-error-copy.js +8 -0
  31. package/lib/client/worktree-error-copy.js.map +1 -1
  32. package/lib/client/worktree-expand-state.d.ts +33 -0
  33. package/lib/client/worktree-expand-state.d.ts.map +1 -0
  34. package/lib/client/worktree-expand-state.js +83 -0
  35. package/lib/client/worktree-expand-state.js.map +1 -0
  36. package/lib/client/worktree-session-position.d.ts +2 -0
  37. package/lib/client/worktree-session-position.d.ts.map +1 -0
  38. package/lib/client/worktree-session-position.js +11 -0
  39. package/lib/client/worktree-session-position.js.map +1 -0
  40. package/lib/client/worktree-surface-dialogs.d.ts +1 -1
  41. package/lib/client/worktree-surface-dialogs.d.ts.map +1 -1
  42. package/lib/client/worktree-surface-dialogs.js +33 -13
  43. package/lib/client/worktree-surface-dialogs.js.map +1 -1
  44. package/lib/client/worktree-surface-rows.d.ts +2 -2
  45. package/lib/client/worktree-surface-rows.d.ts.map +1 -1
  46. package/lib/client/worktree-surface-rows.js +5 -5
  47. package/lib/client/worktree-surface-rows.js.map +1 -1
  48. package/lib/client/worktree-surface-selectors.d.ts +16 -0
  49. package/lib/client/worktree-surface-selectors.d.ts.map +1 -1
  50. package/lib/client/worktree-surface-selectors.js +54 -0
  51. package/lib/client/worktree-surface-selectors.js.map +1 -1
  52. package/lib/client/worktree-surface-types.d.ts +28 -1
  53. package/lib/client/worktree-surface-types.d.ts.map +1 -1
  54. package/lib/client/worktree-view-actions.d.ts +3 -0
  55. package/lib/client/worktree-view-actions.d.ts.map +1 -1
  56. package/lib/client/worktree-view-actions.js +3 -0
  57. package/lib/client/worktree-view-actions.js.map +1 -1
  58. package/lib/client/worktree-view-read.d.ts +11 -0
  59. package/lib/client/worktree-view-read.d.ts.map +1 -1
  60. package/lib/client/worktree-view-read.js +24 -0
  61. package/lib/client/worktree-view-read.js.map +1 -1
  62. package/lib/client.js +697 -137
  63. package/lib/client.js.map +1 -1
  64. package/lib/contract/index.contract.js +14 -0
  65. package/lib/contract/index.contract.js.map +1 -1
  66. package/lib/contract/index.d.ts +22 -2
  67. package/lib/contract/index.d.ts.map +1 -1
  68. package/lib/contract/index.js +5 -0
  69. package/lib/contract/index.js.map +1 -1
  70. package/lib/host/remote.d.ts.map +1 -1
  71. package/lib/host/remote.js +2 -0
  72. package/lib/host/remote.js.map +1 -1
  73. package/lib/host/service.d.ts +8 -1
  74. package/lib/host/service.d.ts.map +1 -1
  75. package/lib/host/service.js +12 -0
  76. package/lib/host/service.js.map +1 -1
  77. package/lib/index.d.ts +1 -1
  78. package/lib/index.d.ts.map +1 -1
  79. package/lib/index.js.map +1 -1
  80. package/lib/manage/manager-support.d.ts +2 -0
  81. package/lib/manage/manager-support.d.ts.map +1 -1
  82. package/lib/manage/manager-support.js +9 -0
  83. package/lib/manage/manager-support.js.map +1 -1
  84. package/lib/manage/manager-worktrees.d.ts +10 -1
  85. package/lib/manage/manager-worktrees.d.ts.map +1 -1
  86. package/lib/manage/manager-worktrees.js +121 -5
  87. package/lib/manage/manager-worktrees.js.map +1 -1
  88. package/lib/manage/manager.d.ts +8 -1
  89. package/lib/manage/manager.d.ts.map +1 -1
  90. package/lib/manage/manager.js +7 -1
  91. package/lib/manage/manager.js.map +1 -1
  92. package/lib/provider/git.d.ts +8 -0
  93. package/lib/provider/git.d.ts.map +1 -1
  94. package/lib/provider/git.js +57 -11
  95. package/lib/provider/git.js.map +1 -1
  96. package/lib/provider/index.d.ts +1 -1
  97. package/lib/provider/index.d.ts.map +1 -1
  98. package/lib/provider/index.js +1 -1
  99. package/lib/provider/index.js.map +1 -1
  100. package/lib/provider/sidecar-schema.d.ts +1 -1
  101. package/lib/provider/sidecar-schema.d.ts.map +1 -1
  102. package/lib/provider/sidecar-schema.js +33 -37
  103. package/lib/provider/sidecar-schema.js.map +1 -1
  104. package/lib/provider/sidecar.d.ts.map +1 -1
  105. package/lib/provider/sidecar.js +3 -1
  106. package/lib/provider/sidecar.js.map +1 -1
  107. package/lib/provider/types.d.ts +9 -1
  108. package/lib/provider/types.d.ts.map +1 -1
  109. package/lib/provider/types.js +2 -1
  110. package/lib/provider/types.js.map +1 -1
  111. package/lib/typert.host.js +115 -21
  112. package/lib/typert.remote-client.d.ts +5 -1
  113. package/lib/typert.remote-client.d.ts.map +1 -1
  114. package/lib/typert.remote-client.js +115 -21
  115. package/package.json +30 -30
@@ -1,16 +1,18 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore, } from 'react';
3
3
  import { IconBranchOutline16, IconCloseOutline16, IconPlusOutline16, IconSearchOutline16, } from '@deepseek-ai/dsh-client-ui-primitives';
4
4
  import { openWorktreeSession } from './navigation.js';
5
5
  import { useSidebarOverlayGeometry } from './sidebar-overlay-geometry.js';
6
+ import { isMainExpanded, isWorkspaceExpanded, isWorktreeExpanded, } from './worktree-expand-state.js';
6
7
  import { effectiveViewMode, unboundSessionIds, workspaceSessionIds } from './view-mode.js';
7
8
  import { formatWorktreeViewError } from './worktree-error-copy.js';
8
9
  import { filterVisibleSessionIds, sessionMatchesQuery, } from './session-view.js';
9
10
  import { retryWorktreeSessionBinding } from './worktree-session.js';
10
11
  import { WorktreeCreateDialog, WorktreeRemovalDialog, WorktreeSessionRenameDialog, WorktreeWorkspaceDeleteDialog, WorktreeWorkspaceRenameDialog, } from './worktree-surface-dialogs.js';
11
- import { bindingIdsFor, includesText, workspaceMatches, } from './worktree-surface-selectors.js';
12
+ import { bindingIdsFor, clearSessionGroupExpansion, includesText, isCompleteWorktreeWorkspaceSnapshot, currentSessionRevealKeys, resolveCurrentSessionLocation, workspaceMatches, } from './worktree-surface-selectors.js';
13
+ import { scrollCurrentSessionIntoView } from './worktree-session-position.js';
12
14
  import { WorktreeGroupRow, WorktreeSessionGroup, WorktreeWorkspaceRow, } from './worktree-surface-rows.js';
13
- import { executeWorktreeAction, createDefaultWorktreeName, createWorktreeRefreshGuard, filterArchivedSessionIds, loadWorktreeViews, reconcileBaseBranchSelection, resolveWorktreeMove, selectDefaultBaseBranch, stableWorkspaceIds, toRetryableWorktreeOrderError, toWorktreeViewError, WorktreeSessionBindingError, } from './worktree-view.js';
15
+ import { executeWorktreeAction, createDefaultWorktreeName, createWorktreeModalViewLoader, createWorktreeRefreshGuard, filterArchivedSessionIds, loadWorktreeViews, mergeWorktreeView, reconcileBaseBranchSelection, resolveWorktreeMove, selectDefaultBaseBranch, stableWorkspaceIds, toRetryableWorktreeOrderError, toWorktreeViewError, WorktreeSessionBindingError, } from './worktree-view.js';
14
16
  import styles from './worktree.css';
15
17
  function toNativeWorktreeViewError(error) {
16
18
  const viewError = toWorktreeViewError(error);
@@ -31,20 +33,29 @@ function useStableWorkspaceIds(workspaces) {
31
33
  * Peer Worktree navigation surface. It keeps the original DSH Workspace mode
32
34
  * intact and renders a browser-local Workspace → Worktree → Session projection.
33
35
  */
34
- export function WorktreeSurface({ useStore, actions, useSessions, useWorkspaces, t, available, manager, invalidateWorktreeContext, createWorkspace, createSessionForWorktree: createSessionCallback, createMainSession, renameWorkspace, deleteWorkspace, insertWorkspaceBefore, insertSessionBefore, insertWorktreeBefore, renameSession, forkSession, archiveSession, ensureSessionWorkspace, syncSessionWorkspaces, openSession, }) {
36
+ export function WorktreeSurface({ useStore, actions, useSessions, useWorkspaces, t, available, expandState, manager, invalidateWorktreeContext, createWorkspace, createSessionForWorktree: createSessionCallback, createMainSession, renameWorkspace, deleteWorkspace, insertWorkspaceBefore, insertSessionBefore, insertWorktreeBefore, renameSession, forkSession, archiveSession, ensureSessionWorkspace, syncSessionWorkspaces, openSession, }) {
35
37
  const preferredMode = useStore((state) => state.viewMode);
36
38
  const mode = effectiveViewMode(preferredMode, available && manager !== undefined);
37
39
  const sessions = useSessions((state) => state);
38
40
  const workspaces = useWorkspaces((state) => state);
41
+ const currentSessionId = sessions.current;
39
42
  const workspaceIds = useStableWorkspaceIds(workspaces.items);
40
43
  const [readState, setReadState] = useState(EMPTY_READ_STATE);
44
+ const expandSnapshot = useSyncExternalStore(expandState.subscribe, expandState.getSnapshot, expandState.getSnapshot);
41
45
  const readStateRef = useRef(readState);
42
46
  readStateRef.current = readState;
43
47
  const [searchQuery, setSearchQuery] = useState('');
44
- const [expandedWorkspaces, setExpandedWorkspaces] = useState({});
45
- const [expandedMains, setExpandedMains] = useState({});
46
- const [expandedWorktrees, setExpandedWorktrees] = useState({});
48
+ const [currentSessionReveal, setCurrentSessionReveal] = useState();
49
+ const searchQueryRef = useRef(searchQuery);
50
+ searchQueryRef.current = searchQuery;
51
+ const locateGenerationRef = useRef(0);
52
+ const positionedLocateGenerationRef = useRef();
47
53
  const [worktreeModalWorkspaceId, setWorktreeModalWorkspaceId] = useState();
54
+ const [modalReadError, setModalReadError] = useState();
55
+ const [modalReadLoading, setModalReadLoading] = useState(false);
56
+ const [worktreeModalMode, setWorktreeModalMode] = useState('create');
57
+ const [importCandidates, setImportCandidates] = useState({ status: 'idle', candidates: [] });
58
+ const [selectedImportPath, setSelectedImportPath] = useState();
48
59
  const [openWorkspaceMenuId, setOpenWorkspaceMenuId] = useState();
49
60
  const [openWorktreeMenuId, setOpenWorktreeMenuId] = useState();
50
61
  const [worktreeRemoval, setWorktreeRemoval] = useState();
@@ -72,6 +83,11 @@ export function WorktreeSurface({ useStore, actions, useSessions, useWorkspaces,
72
83
  const [worktreeDrag, setWorktreeDrag] = useState();
73
84
  const worktreeDropCommitted = useRef(false);
74
85
  const refreshGuard = useRef(createWorktreeRefreshGuard());
86
+ const modalReadLoader = useRef(createWorktreeModalViewLoader());
87
+ const modalReadViewRef = useRef();
88
+ const importCandidatesGuard = useRef(createWorktreeRefreshGuard());
89
+ const modalWorkspaceIdRef = useRef(worktreeModalWorkspaceId);
90
+ modalWorkspaceIdRef.current = worktreeModalWorkspaceId;
75
91
  const { ref, width, bounds } = useSidebarOverlayGeometry(mode === 'worktree');
76
92
  const collapsed = width <= 64;
77
93
  const query = searchQuery.trim().toLocaleLowerCase();
@@ -92,23 +108,59 @@ export function WorktreeSurface({ useStore, actions, useSessions, useWorkspaces,
92
108
  invalidateContext: options.invalidateContext !== false,
93
109
  invalidateWorktreeContext,
94
110
  }), (views) => {
95
- setReadState({ status: 'ready', views });
111
+ setReadState({
112
+ status: 'ready',
113
+ views: modalReadViewRef.current === undefined
114
+ ? views
115
+ : mergeWorktreeView(views, modalReadViewRef.current),
116
+ });
96
117
  }, (error) => {
97
118
  if (preserveCurrent)
98
119
  throw error;
99
120
  setReadState({
100
121
  status: 'error',
101
- views: [],
122
+ views: modalReadViewRef.current === undefined
123
+ ? []
124
+ : [modalReadViewRef.current],
102
125
  error: toWorktreeViewError(error),
103
126
  });
104
127
  });
105
128
  }, [invalidateWorktreeContext, manager, workspaceIds]);
129
+ const loadModalWorktreeView = useCallback((workspaceId) => {
130
+ setModalReadError(undefined);
131
+ setModalReadLoading(true);
132
+ if (manager === undefined) {
133
+ setModalReadLoading(false);
134
+ setModalReadError({
135
+ code: 'WORKTREE_VIEW_UNAVAILABLE',
136
+ message: '',
137
+ retryable: true,
138
+ });
139
+ return;
140
+ }
141
+ void modalReadLoader.current.load(manager, workspaceId, (view) => {
142
+ modalReadViewRef.current = view;
143
+ setModalReadLoading(false);
144
+ setReadState((current) => ({
145
+ ...current,
146
+ views: mergeWorktreeView(current.views, view),
147
+ }));
148
+ }, (error) => {
149
+ setModalReadLoading(false);
150
+ setModalReadError(toWorktreeViewError(error));
151
+ });
152
+ }, [manager]);
106
153
  useEffect(() => {
107
154
  if (mode === 'worktree') {
108
155
  void refresh({ preserveCurrent: readStateRef.current.status === 'ready' });
109
156
  }
110
157
  else {
111
158
  refreshGuard.current.invalidate();
159
+ modalReadLoader.current.invalidate();
160
+ modalReadViewRef.current = undefined;
161
+ setWorktreeModalWorkspaceId(undefined);
162
+ setModalReadError(undefined);
163
+ setModalReadLoading(false);
112
164
  setReadState(EMPTY_READ_STATE);
113
165
  }
114
166
  }, [mode, refresh]);
@@ -121,6 +173,63 @@ export function WorktreeSurface({ useStore, actions, useSessions, useWorkspaces,
121
173
  }))));
122
174
  }, [readState.status, readState.views, syncSessionWorkspaces]);
123
175
  const viewByWorkspace = useMemo(() => new Map(readState.views.map((view) => [view.workspaceId, view])), [readState.views]);
176
+ const currentSessionLocation = useMemo(() => readState.status === 'ready'
177
+ ? resolveCurrentSessionLocation(currentSessionId, workspaces.items, readState.views)
178
+ : undefined, [currentSessionId, readState.status, readState.views, workspaces.items]);
179
+ const currentRevealKeys = useMemo(() => new Set(currentSessionRevealKeys(currentSessionLocation)), [currentSessionLocation]);
180
+ const isCurrentSessionReveal = (key) => currentSessionReveal !== undefined &&
181
+ currentSessionReveal.sessionId === currentSessionId &&
182
+ currentRevealKeys.has(key) &&
183
+ currentSessionReveal.suppressedKeys[key] !== true;
184
+ useLayoutEffect(() => {
185
+ locateGenerationRef.current += 1;
186
+ positionedLocateGenerationRef.current = undefined;
187
+ if (mode !== 'worktree') {
188
+ setCurrentSessionReveal(undefined);
189
+ return;
190
+ }
191
+ if (searchQueryRef.current.trim().length > 0)
192
+ setSearchQuery('');
193
+ setCurrentSessionReveal(currentSessionId === undefined
194
+ ? undefined
195
+ : { sessionId: currentSessionId, suppressedKeys: {} });
196
+ }, [currentSessionId, mode]);
197
+ useLayoutEffect(() => {
198
+ if (mode !== 'worktree' ||
199
+ currentSessionId === undefined ||
200
+ currentSessionLocation === undefined) {
201
+ return;
202
+ }
203
+ const generation = locateGenerationRef.current;
204
+ if (positionedLocateGenerationRef.current === generation)
205
+ return;
206
+ let cancelled = false;
207
+ const frame = requestAnimationFrame(() => {
208
+ if (cancelled || generation !== locateGenerationRef.current)
209
+ return;
210
+ if (!scrollCurrentSessionIntoView(ref.current, currentSessionId))
211
+ return;
212
+ positionedLocateGenerationRef.current = generation;
213
+ });
214
+ return () => {
215
+ cancelled = true;
216
+ cancelAnimationFrame(frame);
217
+ };
218
+ }, [
219
+ currentSessionId,
220
+ currentSessionLocation,
221
+ currentSessionReveal,
222
+ expandSnapshot,
223
+ mode,
224
+ query,
225
+ readState.status,
226
+ ]);
227
+ useEffect(() => {
228
+ if (readState.status !== 'ready' ||
229
+ !isCompleteWorktreeWorkspaceSnapshot(workspaceIds, readState.views))
230
+ return;
231
+ expandState.actions.retain(workspaceIds, readState.views.flatMap((view) => view.worktrees.map((record) => record.worktreeId)));
232
+ }, [expandState, readState.status, readState.views, workspaceIds]);
124
233
  const modalWorkspace = workspaces.items.find((workspace) => workspace.workspaceId === worktreeModalWorkspaceId);
125
234
  const modalView = worktreeModalWorkspaceId === undefined
126
235
  ? undefined
@@ -131,15 +240,27 @@ export function WorktreeSurface({ useStore, actions, useSessions, useWorkspaces,
131
240
  : modalReadiness?.status === 'ready'
132
241
  ? modalView.branches.length === 0 ? 'noLocalBranch' : undefined
133
242
  : modalReadiness?.status;
134
- const modalCanCreate = modalSetupStatus === undefined && modalReadiness?.status === 'ready';
243
+ const modalCanCreate = !modalReadLoading &&
244
+ modalSetupStatus === undefined &&
245
+ modalReadiness?.status === 'ready';
135
246
  useEffect(() => {
136
247
  if (worktreeModalWorkspaceId === undefined || modalView === undefined)
137
248
  return;
138
249
  if (modalView.readiness.status !== 'ready') {
139
250
  setSelectedBranch('');
251
+ setNewBranch('');
140
252
  return;
141
253
  }
142
254
  setSelectedBranch((current) => reconcileBaseBranchSelection(current, modalView.branches));
255
+ setNewBranch((current) => {
256
+ if (current.length > 0)
257
+ return current;
258
+ const existingNames = [
259
+ ...modalView.branches.map((branch) => branch.name),
260
+ ...modalView.worktrees.map((worktree) => worktree.branch),
261
+ ];
262
+ return createDefaultWorktreeName(existingNames);
263
+ });
143
264
  }, [modalView, worktreeModalWorkspaceId]);
144
265
  const runMutation = async (operation) => {
145
266
  setActionPending(true);
@@ -155,6 +276,24 @@ export function WorktreeSurface({ useStore, actions, useSessions, useWorkspaces,
155
276
  setActionPending(false);
156
277
  }
157
278
  };
279
+ const loadImportCandidates = useCallback(async (workspaceId) => {
280
+ if (manager === undefined)
281
+ return;
282
+ setImportCandidates((current) => ({ status: 'loading', candidates: current.candidates }));
283
+ await importCandidatesGuard.current.run(() => manager.listImportCandidates({ workspaceId }), (candidates) => {
284
+ if (modalWorkspaceIdRef.current !== workspaceId)
285
+ return;
286
+ setImportCandidates({ status: 'ready', candidates });
287
+ }, (error) => {
288
+ if (modalWorkspaceIdRef.current !== workspaceId)
289
+ return;
290
+ setImportCandidates((current) => ({
291
+ status: 'error',
292
+ candidates: current.candidates,
293
+ error: toWorktreeViewError(error),
294
+ }));
295
+ });
296
+ }, [manager]);
158
297
  const workspaceRenameTrimmed = workspaceRenameDraft.trim();
159
298
  const workspaceRenameDuplicate = workspaceRenameTarget !== undefined &&
160
299
  workspaceRenameTrimmed.length > 0 &&
@@ -373,91 +512,201 @@ export function WorktreeSurface({ useStore, actions, useSessions, useWorkspaces,
373
512
  return;
374
513
  await runMutation(() => archiveSession(sessionId));
375
514
  };
515
+ const clearSessionGroups = (groupKeys) => {
516
+ if (groupKeys.length === 0)
517
+ return;
518
+ setExpandedSessionGroups((current) => clearSessionGroupExpansion(current, groupKeys));
519
+ };
520
+ const suppressCurrentSessionReveal = (key) => {
521
+ if (!isCurrentSessionReveal(key))
522
+ return false;
523
+ setCurrentSessionReveal((current) => {
524
+ if (current === undefined || current.sessionId !== currentSessionId)
525
+ return current;
526
+ return {
527
+ ...current,
528
+ suppressedKeys: { ...current.suppressedKeys, [key]: true },
529
+ };
530
+ });
531
+ return true;
532
+ };
376
533
  const toggleWorkspace = (workspaceId) => {
377
- setExpandedWorkspaces((current) => ({
378
- ...current,
379
- [workspaceId]: current[workspaceId] === false,
380
- }));
534
+ const persistedExpanded = isWorkspaceExpanded(expandSnapshot, workspaceId);
535
+ const autoExpanded = isCurrentSessionReveal('workspace:' + workspaceId);
536
+ const visuallyExpanded = persistedExpanded || autoExpanded;
537
+ if (visuallyExpanded && autoExpanded) {
538
+ suppressCurrentSessionReveal('workspace:' + workspaceId);
539
+ }
540
+ if (visuallyExpanded && !persistedExpanded) {
541
+ clearSessionGroups([
542
+ 'main:' + workspaceId,
543
+ ...(viewByWorkspace.get(workspaceId)?.worktrees.map((record) => record.worktreeId) ?? []).map((worktreeId) => 'worktree:' + worktreeId),
544
+ ]);
545
+ return;
546
+ }
547
+ expandState.actions.toggleWorkspace(workspaceId);
548
+ if (visuallyExpanded) {
549
+ clearSessionGroups([
550
+ 'main:' + workspaceId,
551
+ ...(viewByWorkspace.get(workspaceId)?.worktrees.map((record) => record.worktreeId) ?? []).map((worktreeId) => 'worktree:' + worktreeId),
552
+ ]);
553
+ }
381
554
  };
382
555
  const toggleMain = (workspaceId) => {
383
- setExpandedMains((current) => ({
384
- ...current,
385
- [workspaceId]: current[workspaceId] === false,
386
- }));
556
+ const persistedExpanded = isMainExpanded(expandSnapshot, workspaceId);
557
+ const autoExpanded = isCurrentSessionReveal('main:' + workspaceId);
558
+ const visuallyExpanded = persistedExpanded || autoExpanded;
559
+ if (visuallyExpanded && autoExpanded) {
560
+ suppressCurrentSessionReveal('main:' + workspaceId);
561
+ }
562
+ if (visuallyExpanded && !persistedExpanded) {
563
+ clearSessionGroups(['main:' + workspaceId]);
564
+ return;
565
+ }
566
+ expandState.actions.toggleMain(workspaceId);
567
+ if (visuallyExpanded)
568
+ clearSessionGroups(['main:' + workspaceId]);
387
569
  };
388
570
  const toggleWorktree = (worktreeId) => {
389
- setExpandedWorktrees((current) => ({
571
+ const persistedExpanded = isWorktreeExpanded(expandSnapshot, worktreeId);
572
+ const autoExpanded = isCurrentSessionReveal('worktree:' + worktreeId);
573
+ const visuallyExpanded = persistedExpanded || autoExpanded;
574
+ if (visuallyExpanded && autoExpanded) {
575
+ suppressCurrentSessionReveal('worktree:' + worktreeId);
576
+ }
577
+ if (visuallyExpanded && !persistedExpanded) {
578
+ clearSessionGroups(['worktree:' + worktreeId]);
579
+ return;
580
+ }
581
+ expandState.actions.toggleWorktree(worktreeId);
582
+ if (visuallyExpanded)
583
+ clearSessionGroups(['worktree:' + worktreeId]);
584
+ };
585
+ const toggleSessionGroup = (groupKey) => {
586
+ const autoExpanded = isCurrentSessionReveal('session-group:' + groupKey);
587
+ const transientExpanded = expandedSessionGroups[groupKey] === true;
588
+ if (autoExpanded) {
589
+ suppressCurrentSessionReveal('session-group:' + groupKey);
590
+ setExpandedSessionGroups((current) => {
591
+ const next = { ...current };
592
+ delete next[groupKey];
593
+ return next;
594
+ });
595
+ return;
596
+ }
597
+ setExpandedSessionGroups((current) => ({
390
598
  ...current,
391
- [worktreeId]: current[worktreeId] === false,
599
+ [groupKey]: !transientExpanded,
392
600
  }));
393
601
  };
602
+ const closeWorktreeCreator = (force = false) => {
603
+ if (actionPending && !force)
604
+ return;
605
+ modalReadLoader.current.invalidate();
606
+ modalReadViewRef.current = undefined;
607
+ importCandidatesGuard.current.invalidate();
608
+ setWorktreeModalWorkspaceId(undefined);
609
+ setModalReadError(undefined);
610
+ setModalReadLoading(false);
611
+ };
394
612
  const openWorktreeCreator = (workspace) => {
395
613
  const view = viewByWorkspace.get(workspace.workspaceId);
396
- const baseBranch = selectDefaultBaseBranch(view?.branches ?? []);
397
- const existingNames = [
398
- ...(view?.branches ?? []).map((branch) => branch.name),
399
- ...(view?.worktrees ?? []).map((worktree) => worktree.branch),
400
- ];
614
+ modalReadLoader.current.invalidate();
615
+ modalReadViewRef.current = undefined;
616
+ importCandidatesGuard.current.invalidate();
401
617
  setWorktreeModalWorkspaceId(workspace.workspaceId);
402
- setSelectedBranch(baseBranch);
403
- setNewBranch(createDefaultWorktreeName(existingNames));
618
+ setWorktreeModalMode('create');
619
+ setImportCandidates({ status: 'idle', candidates: [] });
620
+ setSelectedImportPath(undefined);
404
621
  setActionError(undefined);
622
+ setModalReadError(undefined);
623
+ setModalReadLoading(false);
624
+ if (view === undefined) {
625
+ setSelectedBranch('');
626
+ setNewBranch('');
627
+ loadModalWorktreeView(workspace.workspaceId);
628
+ return;
629
+ }
630
+ setSelectedBranch(selectDefaultBaseBranch(view.branches));
631
+ setNewBranch(createDefaultWorktreeName([
632
+ ...view.branches.map((branch) => branch.name),
633
+ ...view.worktrees.map((worktree) => worktree.branch),
634
+ ]));
405
635
  };
406
- const submitWorktree = async () => {
407
- const worktreeName = newBranch.trim();
408
- if (manager === undefined ||
409
- modalWorkspace === undefined ||
410
- !modalCanCreate ||
411
- selectedBranch.length === 0 ||
412
- worktreeName.length === 0) {
636
+ const changeWorktreeModalMode = (mode) => {
637
+ setWorktreeModalMode(mode);
638
+ if (mode === 'import' &&
639
+ worktreeModalWorkspaceId !== undefined &&
640
+ importCandidates.status === 'idle') {
641
+ void loadImportCandidates(worktreeModalWorkspaceId);
642
+ }
643
+ };
644
+ const continueWorktreeRegistration = async (registeredWorktree) => {
645
+ closeWorktreeCreator(true);
646
+ if (createSessionCallback === undefined) {
647
+ await refresh({ preserveCurrent: true });
648
+ setActionError({
649
+ code: 'WORKTREE_REGISTRATION_SESSION_UNAVAILABLE',
650
+ message: '',
651
+ retryable: true,
652
+ });
413
653
  return;
414
654
  }
415
- const input = {
416
- workspaceId: modalWorkspace.workspaceId,
417
- branch: selectedBranch,
418
- newBranch: worktreeName,
655
+ const sessionInput = {
656
+ workspaceId: registeredWorktree.workspaceId,
657
+ worktreeId: registeredWorktree.worktreeId,
658
+ cwd: registeredWorktree.absolutePath,
419
659
  };
660
+ try {
661
+ await createSessionCallback(sessionInput);
662
+ await invalidateWorktreeContext?.(registeredWorktree.workspaceId);
663
+ }
664
+ catch (error) {
665
+ if (error instanceof WorktreeSessionBindingError && error.retryable) {
666
+ setPendingSessionBinding({ ...sessionInput, sessionId: error.sessionId });
667
+ }
668
+ throw error;
669
+ }
670
+ await refresh({ preserveCurrent: true });
671
+ };
672
+ const submitWorktree = async () => {
673
+ const worktreeName = newBranch.trim();
674
+ const selectedImportCandidate = importCandidates.candidates.find((candidate) => candidate.absolutePath === selectedImportPath);
675
+ if (manager === undefined || modalWorkspace === undefined)
676
+ return;
677
+ if (worktreeModalMode === 'create' &&
678
+ (!modalCanCreate || selectedBranch.length === 0 || worktreeName.length === 0))
679
+ return;
680
+ if (worktreeModalMode === 'import' && selectedImportCandidate === undefined)
681
+ return;
420
682
  setActionPending(true);
421
683
  setActionError(undefined);
422
684
  setPendingSessionBinding(undefined);
423
685
  try {
424
- const createdWorktree = await executeWorktreeAction(manager, {
425
- type: 'createWorktree',
426
- input,
427
- });
428
- if (createdWorktree === undefined) {
686
+ const registeredWorktree = worktreeModalMode === 'create'
687
+ ? await executeWorktreeAction(manager, {
688
+ type: 'createWorktree',
689
+ input: {
690
+ workspaceId: modalWorkspace.workspaceId,
691
+ branch: selectedBranch,
692
+ newBranch: worktreeName,
693
+ },
694
+ })
695
+ : await executeWorktreeAction(manager, {
696
+ type: 'importWorktree',
697
+ input: {
698
+ workspaceId: modalWorkspace.workspaceId,
699
+ absolutePath: selectedImportCandidate.absolutePath,
700
+ },
701
+ });
702
+ if (registeredWorktree === undefined) {
429
703
  throw {
430
704
  code: 'WORKTREE_RECORD_MISSING',
431
705
  message: '',
432
706
  retryable: true,
433
707
  };
434
708
  }
435
- setWorktreeModalWorkspaceId(undefined);
436
- if (createSessionCallback === undefined) {
437
- await refresh({ preserveCurrent: true });
438
- setActionError({
439
- code: 'WORKTREE_CREATED_SESSION_UNAVAILABLE',
440
- message: '',
441
- retryable: true,
442
- });
443
- return;
444
- }
445
- const sessionInput = {
446
- workspaceId: createdWorktree.workspaceId,
447
- worktreeId: createdWorktree.worktreeId,
448
- cwd: createdWorktree.absolutePath,
449
- };
450
- try {
451
- await createSessionCallback(sessionInput);
452
- await invalidateWorktreeContext?.(createdWorktree.workspaceId);
453
- }
454
- catch (error) {
455
- if (error instanceof WorktreeSessionBindingError && error.retryable) {
456
- setPendingSessionBinding({ ...sessionInput, sessionId: error.sessionId });
457
- }
458
- throw error;
459
- }
460
- await refresh({ preserveCurrent: true });
709
+ await continueWorktreeRegistration(registeredWorktree);
461
710
  }
462
711
  catch (error) {
463
712
  setActionError(toWorktreeViewError(error));
@@ -562,15 +811,21 @@ export function WorktreeSurface({ useStore, actions, useSessions, useWorkspaces,
562
811
  const mainLabel = currentBranch === undefined
563
812
  ? t('worktree.main')
564
813
  : t('worktree.mainWithBranch', { branch: currentBranch });
565
- const expanded = expandedWorkspaces[workspace.workspaceId] !== false;
814
+ const expanded = isWorkspaceExpanded(expandSnapshot, workspace.workspaceId) ||
815
+ isCurrentSessionReveal('workspace:' + workspace.workspaceId);
566
816
  const workspaceMatchesQuery = includesText(workspace.title, query);
567
817
  const allWorkspaceSessionIds = filterArchivedSessionIds(workspaceSessionIds(workspaces, workspace.workspaceId, sessions.ids), archivedSessionIds);
568
818
  const bindings = view?.bindings ?? [];
569
819
  const boundSessionIds = new Set(bindings.map((binding) => binding.sessionId));
570
820
  const mainSessionIds = filterVisibleSessionIds(unboundSessionIds(allWorkspaceSessionIds, [...boundSessionIds]), sessions);
571
821
  const visibleMainSessionIds = mainSessionIds.filter((sessionId) => workspaceMatchesQuery || sessionMatchesQuery(sessionId, sessions, query));
572
- const mainExpanded = expandedMains[workspace.workspaceId] !== false;
822
+ const mainExpanded = isMainExpanded(expandSnapshot, workspace.workspaceId) ||
823
+ isCurrentSessionReveal('main:' + workspace.workspaceId);
573
824
  const mainGroupKey = `main:${workspace.workspaceId}`;
825
+ const sessionIds = visibleMainSessionIds;
826
+ const mainSessionGroupExpanded = expandedSessionGroups[mainGroupKey] === true ||
827
+ (sessionIds.length > 5 &&
828
+ isCurrentSessionReveal('session-group:' + mainGroupKey));
574
829
  const worktrees = view?.worktrees ?? [];
575
830
  const sameWorkspaceWorktreeDrag = worktreeDrag?.workspaceId === workspace.workspaceId;
576
831
  return (_jsxs("section", { className: styles.workspaceGroup, "data-workspace-id": workspace.workspaceId, children: [_jsx(WorktreeWorkspaceRow, { t: t, workspace: workspace, expanded: expanded, actionPending: actionPending, menuOpen: openWorkspaceMenuId === workspace.workspaceId, drag: {
@@ -623,11 +878,8 @@ export function WorktreeSurface({ useStore, actions, useSessions, useWorkspaces,
623
878
  ? undefined
624
879
  : () => {
625
880
  createMainSession(workspace.workspaceId);
626
- } }), mainExpanded && (_jsx(WorktreeSessionGroup, { t: t, groupKey: mainGroupKey, sessionIds: visibleMainSessionIds, workspaceId: workspace.workspaceId, expanded: expandedSessionGroups[mainGroupKey] === true, actionPending: actionPending, sessions: sessions, dragState: sessionDrag, onToggleExpanded: () => {
627
- setExpandedSessionGroups((current) => ({
628
- ...current,
629
- [mainGroupKey]: current[mainGroupKey] !== true,
630
- }));
881
+ } }), mainExpanded && (_jsx(WorktreeSessionGroup, { t: t, groupKey: mainGroupKey, sessionIds: sessionIds, workspaceId: workspace.workspaceId, currentSessionId: currentSessionId, expanded: mainSessionGroupExpanded, actionPending: actionPending, sessions: sessions, dragState: sessionDrag, onToggleExpanded: () => {
882
+ toggleSessionGroup(mainGroupKey);
631
883
  }, onStartDrag: (groupKey, sessionId) => {
632
884
  sessionDropCommitted.current = false;
633
885
  setSessionDrag({ groupKey, sessionId, over: null });
@@ -653,6 +905,10 @@ export function WorktreeSurface({ useStore, actions, useSessions, useWorkspaces,
653
905
  }
654
906
  const visibleWorktreeSessionIds = worktreeSessionIds.filter((sessionId) => worktreeMatchesQuery || sessionMatchesQuery(sessionId, sessions, query));
655
907
  const worktreeGroupKey = `worktree:${record.worktreeId}`;
908
+ const sessionIds = visibleWorktreeSessionIds;
909
+ const sessionGroupExpanded = expandedSessionGroups[worktreeGroupKey] === true ||
910
+ (sessionIds.length > 5 &&
911
+ isCurrentSessionReveal('session-group:' + worktreeGroupKey));
656
912
  const state = record.status === 'removed'
657
913
  ? 'warning'
658
914
  : record.health === 'repair'
@@ -663,7 +919,8 @@ export function WorktreeSurface({ useStore, actions, useSessions, useWorkspaces,
663
919
  : record.health === 'repair'
664
920
  ? t('worktree.repair')
665
921
  : t('worktree.ready');
666
- const worktreeExpanded = expandedWorktrees[record.worktreeId] !== false;
922
+ const worktreeExpanded = isWorktreeExpanded(expandSnapshot, record.worktreeId) ||
923
+ isCurrentSessionReveal('worktree:' + record.worktreeId);
667
924
  return (_jsxs("div", { className: styles.worktreeGroup, "data-worktree-id": record.worktreeId, children: [_jsx(WorktreeGroupRow, { t: t, kind: "worktree", label: record.branch, worktreeId: record.worktreeId, expanded: worktreeExpanded, icon: _jsx(IconBranchOutline16, {}), workspaceTitle: workspace.title, state: state, stateLabel: stateLabel, onToggle: () => {
668
925
  toggleWorktree(record.worktreeId);
669
926
  }, onCreateSession: record.status === 'active' && record.health !== 'repair'
@@ -718,11 +975,8 @@ export function WorktreeSurface({ useStore, actions, useSessions, useWorkspaces,
718
975
  setWorktreeDrag(undefined);
719
976
  worktreeDropCommitted.current = false;
720
977
  },
721
- } }), worktreeExpanded && (_jsx(WorktreeSessionGroup, { t: t, groupKey: worktreeGroupKey, sessionIds: visibleWorktreeSessionIds, workspaceId: workspace.workspaceId, expanded: expandedSessionGroups[worktreeGroupKey] === true, actionPending: actionPending, sessions: sessions, dragState: sessionDrag, onToggleExpanded: () => {
722
- setExpandedSessionGroups((current) => ({
723
- ...current,
724
- [worktreeGroupKey]: current[worktreeGroupKey] !== true,
725
- }));
978
+ } }), worktreeExpanded && (_jsx(WorktreeSessionGroup, { t: t, groupKey: worktreeGroupKey, sessionIds: sessionIds, workspaceId: workspace.workspaceId, currentSessionId: currentSessionId, expanded: sessionGroupExpanded, actionPending: actionPending, sessions: sessions, dragState: sessionDrag, onToggleExpanded: () => {
979
+ toggleSessionGroup(worktreeGroupKey);
726
980
  }, onStartDrag: (groupKey, sessionId) => {
727
981
  sessionDropCommitted.current = false;
728
982
  setSessionDrag({ groupKey, sessionId, over: null });
@@ -740,17 +994,24 @@ export function WorktreeSurface({ useStore, actions, useSessions, useWorkspaces,
740
994
  ? undefined
741
995
  : archiveWorktreeSession }))] }, record.worktreeId));
742
996
  })] }))] }, workspace.workspaceId));
743
- })) })) : null] })] }), _jsx("div", { className: styles.railContent, children: _jsx("button", { type: "button", className: styles.railButton, "aria-label": t('mode.exit'), onClick: () => {
744
- actions.setViewMode('workspace-session');
745
- }, children: "WT" }) }), _jsx(WorktreeSessionRenameDialog, { t: t, target: sessionRenameTarget, draft: sessionRenameDraft, pending: sessionRenamePending, error: sessionRenameError, onClose: closeSessionRename, onDraftChange: (draft) => {
997
+ })) })) : null] })] }), _jsx(WorktreeSessionRenameDialog, { t: t, target: sessionRenameTarget, draft: sessionRenameDraft, pending: sessionRenamePending, error: sessionRenameError, onClose: closeSessionRename, onDraftChange: (draft) => {
746
998
  setSessionRenameDraft(draft);
747
999
  setSessionRenameError(undefined);
748
1000
  }, onSubmit: confirmSessionRename }), _jsx(WorktreeWorkspaceRenameDialog, { t: t, target: workspaceRenameTarget, draft: workspaceRenameDraft, pending: workspaceRenamePending, duplicate: workspaceRenameDuplicate, error: workspaceRenameError, onClose: closeWorkspaceRename, onDraftChange: (draft) => {
749
1001
  setWorkspaceRenameDraft(draft);
750
1002
  setWorkspaceRenameError(undefined);
751
- }, onSubmit: confirmWorkspaceRename }), _jsx(WorktreeWorkspaceDeleteDialog, { t: t, target: workspaceDeleteTarget, pending: workspaceDeletePending, error: workspaceDeleteError, onClose: closeWorkspaceDelete, onSubmit: confirmWorkspaceDelete }), _jsx(WorktreeCreateDialog, { t: t, workspace: modalWorkspace, view: modalView, setupStatus: modalSetupStatus, canCreate: modalCanCreate, selectedBranch: selectedBranch, newBranch: newBranch, actionPending: actionPending, onClose: () => {
752
- setWorktreeModalWorkspaceId(undefined);
753
- }, onSelectedBranchChange: setSelectedBranch, onNewBranchChange: setNewBranch, onSubmit: submitWorktree }), _jsx(WorktreeRemovalDialog, { t: t, worktree: worktreeRemoval, actionPending: actionPending, onClose: () => {
1003
+ }, onSubmit: confirmWorkspaceRename }), _jsx(WorktreeWorkspaceDeleteDialog, { t: t, target: workspaceDeleteTarget, pending: workspaceDeletePending, error: workspaceDeleteError, onClose: closeWorkspaceDelete, onSubmit: confirmWorkspaceDelete }), _jsx(WorktreeCreateDialog, { t: t, workspace: modalWorkspace, view: modalView, readError: modalReadError, setupStatus: modalSetupStatus, canCreate: modalCanCreate, mode: worktreeModalMode, importCandidates: importCandidates, selectedImportPath: selectedImportPath, selectedBranch: selectedBranch, newBranch: newBranch, actionPending: actionPending, onClose: closeWorktreeCreator, onRetry: () => {
1004
+ if (worktreeModalWorkspaceId === undefined)
1005
+ return;
1006
+ modalReadViewRef.current = undefined;
1007
+ setSelectedBranch('');
1008
+ setNewBranch('');
1009
+ loadModalWorktreeView(worktreeModalWorkspaceId);
1010
+ }, onModeChange: changeWorktreeModalMode, onRetryImportCandidates: () => {
1011
+ if (worktreeModalWorkspaceId !== undefined) {
1012
+ void loadImportCandidates(worktreeModalWorkspaceId);
1013
+ }
1014
+ }, onSelectedImportPathChange: setSelectedImportPath, onSelectedBranchChange: setSelectedBranch, onNewBranchChange: setNewBranch, onSubmit: submitWorktree }), _jsx(WorktreeRemovalDialog, { t: t, worktree: worktreeRemoval, actionPending: actionPending, onClose: () => {
754
1015
  setWorktreeRemoval(undefined);
755
1016
  }, onSubmit: () => {
756
1017
  if (manager === undefined || worktreeRemoval === undefined)