@cerbur/clutch-dsh-worktree 0.1.2 → 0.1.3

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 (90) hide show
  1. package/README.md +20 -5
  2. package/lib/client/WorktreeContext.d.ts +15 -0
  3. package/lib/client/WorktreeContext.d.ts.map +1 -0
  4. package/lib/client/WorktreeContext.js +19 -0
  5. package/lib/client/WorktreeContext.js.map +1 -0
  6. package/lib/client/WorktreeHeroContext.d.ts +10 -0
  7. package/lib/client/WorktreeHeroContext.d.ts.map +1 -0
  8. package/lib/client/WorktreeHeroContext.js +128 -0
  9. package/lib/client/WorktreeHeroContext.js.map +1 -0
  10. package/lib/client/WorktreeOverlay.d.ts +6 -0
  11. package/lib/client/WorktreeOverlay.d.ts.map +1 -0
  12. package/lib/client/WorktreeOverlay.js +8 -0
  13. package/lib/client/WorktreeOverlay.js.map +1 -0
  14. package/lib/client/WorktreeSurface.d.ts +3 -1
  15. package/lib/client/WorktreeSurface.d.ts.map +1 -1
  16. package/lib/client/WorktreeSurface.js +200 -57
  17. package/lib/client/WorktreeSurface.js.map +1 -1
  18. package/lib/client/entry.d.ts +1 -1
  19. package/lib/client/entry.d.ts.map +1 -1
  20. package/lib/client/entry.js +41 -14
  21. package/lib/client/entry.js.map +1 -1
  22. package/lib/client/locales.d.ts +30 -0
  23. package/lib/client/locales.d.ts.map +1 -1
  24. package/lib/client/locales.js +32 -2
  25. package/lib/client/locales.js.map +1 -1
  26. package/lib/client/session-view.d.ts +21 -0
  27. package/lib/client/session-view.d.ts.map +1 -0
  28. package/lib/client/session-view.js +26 -0
  29. package/lib/client/session-view.js.map +1 -0
  30. package/lib/client/sidebar-overlay-geometry.d.ts +1 -0
  31. package/lib/client/sidebar-overlay-geometry.d.ts.map +1 -1
  32. package/lib/client/sidebar-overlay-geometry.js +28 -17
  33. package/lib/client/sidebar-overlay-geometry.js.map +1 -1
  34. package/lib/client/virtual-workspace-membership.d.ts.map +1 -1
  35. package/lib/client/virtual-workspace-membership.js +17 -6
  36. package/lib/client/virtual-workspace-membership.js.map +1 -1
  37. package/lib/client/worktree-connection.d.ts +1 -0
  38. package/lib/client/worktree-connection.d.ts.map +1 -1
  39. package/lib/client/worktree-connection.js +4 -0
  40. package/lib/client/worktree-connection.js.map +1 -1
  41. package/lib/client/worktree-context-store.d.ts +45 -0
  42. package/lib/client/worktree-context-store.d.ts.map +1 -0
  43. package/lib/client/worktree-context-store.js +199 -0
  44. package/lib/client/worktree-context-store.js.map +1 -0
  45. package/lib/client/worktree-context.d.ts +32 -0
  46. package/lib/client/worktree-context.d.ts.map +1 -0
  47. package/lib/client/worktree-context.js +51 -0
  48. package/lib/client/worktree-context.js.map +1 -0
  49. package/lib/client/worktree-error-copy.d.ts.map +1 -1
  50. package/lib/client/worktree-error-copy.js +14 -0
  51. package/lib/client/worktree-error-copy.js.map +1 -1
  52. package/lib/client/worktree-session.d.ts +83 -0
  53. package/lib/client/worktree-session.d.ts.map +1 -0
  54. package/lib/client/worktree-session.js +185 -0
  55. package/lib/client/worktree-session.js.map +1 -0
  56. package/lib/client/worktree-view.d.ts +37 -3
  57. package/lib/client/worktree-view.d.ts.map +1 -1
  58. package/lib/client/worktree-view.js +145 -7
  59. package/lib/client/worktree-view.js.map +1 -1
  60. package/lib/client.js +1003 -133
  61. package/lib/client.js.map +1 -1
  62. package/lib/contract/index.contract.js +2 -0
  63. package/lib/contract/index.contract.js.map +1 -1
  64. package/lib/contract/index.d.ts +17 -2
  65. package/lib/contract/index.d.ts.map +1 -1
  66. package/lib/contract/index.js +2 -0
  67. package/lib/contract/index.js.map +1 -1
  68. package/lib/host/remote.d.ts.map +1 -1
  69. package/lib/host/remote.js +1 -0
  70. package/lib/host/remote.js.map +1 -1
  71. package/lib/host/service.d.ts +6 -1
  72. package/lib/host/service.d.ts.map +1 -1
  73. package/lib/host/service.js +6 -0
  74. package/lib/host/service.js.map +1 -1
  75. package/lib/manage/manager.d.ts +6 -1
  76. package/lib/manage/manager.d.ts.map +1 -1
  77. package/lib/manage/manager.js +12 -1
  78. package/lib/manage/manager.js.map +1 -1
  79. package/lib/provider/sidecar.d.ts +2 -1
  80. package/lib/provider/sidecar.d.ts.map +1 -1
  81. package/lib/provider/sidecar.js +40 -0
  82. package/lib/provider/sidecar.js.map +1 -1
  83. package/lib/provider/types.d.ts +2 -1
  84. package/lib/provider/types.d.ts.map +1 -1
  85. package/lib/provider/types.js.map +1 -1
  86. package/lib/typert.host.js +60 -18
  87. package/lib/typert.remote-client.d.ts +3 -1
  88. package/lib/typert.remote-client.d.ts.map +1 -1
  89. package/lib/typert.remote-client.js +60 -18
  90. package/package.json +6 -2
@@ -5,8 +5,20 @@ import { openWorktreeSession } from './navigation.js';
5
5
  import { useSidebarOverlayGeometry } from './sidebar-overlay-geometry.js';
6
6
  import { effectiveViewMode, unboundSessionIds, workspaceSessionIds } from './view-mode.js';
7
7
  import { formatWorktreeViewError } from './worktree-error-copy.js';
8
- import { executeWorktreeAction, createDefaultWorktreeName, filterArchivedSessionIds, loadWorktreeViews, selectDefaultBaseBranch, stableWorkspaceIds, toWorktreeViewError, WorktreeSessionBindingError, } from './worktree-view.js';
8
+ import { filterVisibleSessionIds, isBlankSession, sessionDisplayLabel, sessionMatchesQuery, } from './session-view.js';
9
+ import { retryWorktreeSessionBinding } from './worktree-session.js';
10
+ import { executeWorktreeAction, createDefaultWorktreeName, createWorktreeRefreshGuard, filterArchivedSessionIds, loadWorktreeViews, reconcileBaseBranchSelection, resolveWorktreeMove, selectDefaultBaseBranch, stableWorkspaceIds, toRetryableWorktreeOrderError, toWorktreeViewError, WorktreeSessionBindingError, worktreeSetupCommands, } from './worktree-view.js';
9
11
  import styles from './worktree.css';
12
+ function worktreeSetupMessage(status, t) {
13
+ switch (status) {
14
+ case 'noRepository':
15
+ return t('worktree.setup.noRepository');
16
+ case 'noInitialCommit':
17
+ return t('worktree.setup.noInitialCommit');
18
+ case 'noLocalBranch':
19
+ return t('worktree.setup.noLocalBranch');
20
+ }
21
+ }
10
22
  function toNativeWorktreeViewError(error) {
11
23
  const viewError = toWorktreeViewError(error);
12
24
  if (typeof error === 'object' && error !== null)
@@ -22,8 +34,8 @@ function useStableWorkspaceIds(workspaces) {
22
34
  previousRef.current = stable;
23
35
  return previousRef.current;
24
36
  }
25
- function sessionLabel(sessionId, sessions) {
26
- return sessions.byId[sessionId]?.displayTitle ?? sessionId;
37
+ function sessionLabel(sessionId, sessions, t) {
38
+ return sessionDisplayLabel(sessionId, sessions, t('session.new'));
27
39
  }
28
40
  function rowHalf(event) {
29
41
  const rect = event.currentTarget.getBoundingClientRect();
@@ -42,7 +54,7 @@ function workspaceMatches(workspace, view, sessions, query) {
42
54
  return true;
43
55
  if (includesText(workspace.title, query))
44
56
  return true;
45
- if (workspace.sessionIds.some((sessionId) => includesText(sessionLabel(sessionId, sessions), query))) {
57
+ if (workspace.sessionIds.some((sessionId) => sessionMatchesQuery(sessionId, sessions, query))) {
46
58
  return true;
47
59
  }
48
60
  if (view === undefined)
@@ -50,7 +62,7 @@ function workspaceMatches(workspace, view, sessions, query) {
50
62
  if (view.worktrees.some((record) => includesText(record.branch, query) || includesText(record.absolutePath, query))) {
51
63
  return true;
52
64
  }
53
- return view.bindings.some((binding) => includesText(sessionLabel(binding.sessionId, sessions), query));
65
+ return view.bindings.some((binding) => sessionMatchesQuery(binding.sessionId, sessions, query));
54
66
  }
55
67
  /** Workspace row using the native DSH menu, fixed action column, and drag contract. */
56
68
  function WorktreeWorkspaceRow({ t, workspace, expanded, actionPending, menuOpen, drag, onToggle, onCreateWorktree, onRename, onDelete, onMenuOpenChange, }) {
@@ -113,9 +125,38 @@ function WorktreeWorkspaceRow({ t, workspace, expanded, actionPending, menuOpen,
113
125
  }, children: _jsx(IconPlusOutline16, {}) })] })] }));
114
126
  }
115
127
  /** Shared Main/Worktree group row with parameterized icon, state, and actions. */
116
- function WorktreeGroupRow({ t, kind, label, expanded, icon, workspaceTitle, state, stateLabel, onToggle, onCreateSession, menu, }) {
128
+ function WorktreeGroupRow({ t, kind, label, worktreeId, expanded, icon, workspaceTitle, state, stateLabel, onToggle, onCreateSession, menu, drag, }) {
117
129
  const main = kind === 'main';
118
- const row = (_jsxs("div", { className: styles.worktreeRow, "data-main-group": main ? 'true' : undefined, "data-main-expanded": main ? String(expanded) : undefined, "data-menu-open": menu?.open ? 'true' : undefined, onClick: onToggle, children: [_jsx("button", { type: "button", className: `${styles.disclosureButton} ${styles.worktreeDisclosure}`, "aria-label": t(expanded ? 'worktree.collapse' : 'worktree.expand', { name: label }), "aria-expanded": expanded, onClick: (event) => {
130
+ const markerClass = drag?.marker === 'before'
131
+ ? styles.dropBefore
132
+ : drag?.marker === 'after'
133
+ ? styles.dropAfter
134
+ : '';
135
+ const dragProps = drag === undefined
136
+ ? {}
137
+ : {
138
+ draggable: true,
139
+ onDragStart: (event) => {
140
+ event.dataTransfer.effectAllowed = 'move';
141
+ event.dataTransfer.setData('text/plain', worktreeId ?? label);
142
+ drag.start();
143
+ },
144
+ onDragEnd: drag.end,
145
+ onDragOver: (event) => {
146
+ if (!drag.active)
147
+ return;
148
+ event.preventDefault();
149
+ event.dataTransfer.dropEffect = 'move';
150
+ drag.hover(rowHalf(event));
151
+ },
152
+ onDrop: (event) => {
153
+ if (!drag.active)
154
+ return;
155
+ event.preventDefault();
156
+ drag.drop(rowHalf(event));
157
+ },
158
+ };
159
+ const row = (_jsxs("div", { className: `${styles.worktreeRow} ${markerClass}`, "data-main-group": main ? 'true' : undefined, "data-main-expanded": main ? String(expanded) : undefined, "data-menu-open": menu?.open ? 'true' : undefined, "data-worktree-drag": drag?.active ? 'active' : undefined, ...dragProps, onClick: onToggle, children: [_jsx("button", { type: "button", className: `${styles.disclosureButton} ${styles.worktreeDisclosure}`, "aria-label": t(expanded ? 'worktree.collapse' : 'worktree.expand', { name: label }), "aria-expanded": expanded, onClick: (event) => {
119
160
  event.stopPropagation();
120
161
  onToggle();
121
162
  }, children: expanded ? (_jsx(IconChevronDownOutline14, { size: 18 })) : (_jsx(IconChevronRightOutline14, { size: 18 })) }), _jsx("span", { className: styles.worktreeIcon, "aria-hidden": "true", children: icon }), state !== undefined && stateLabel !== undefined && (_jsx("span", { className: styles.worktreeState, role: "img", "aria-label": stateLabel, title: stateLabel, children: _jsx(StateDot, { state: state }) })), _jsx("span", { className: styles.worktreeLabel, children: label }), _jsxs("span", { className: styles.treeActionSlot, children: [menu !== undefined && (_jsx("span", { className: styles.menuAction, children: _jsx(Menu, { open: menu.open, onClose: () => {
@@ -144,7 +185,7 @@ function WorktreeGroupRow({ t, kind, label, expanded, icon, workspaceTitle, stat
144
185
  return (_jsx(HoverCard, { anchor: row, content: _jsx("div", { className: styles.worktreeHoverTitle, children: label }), openDelayMs: 500, disabled: menu?.open === true }));
145
186
  }
146
187
  /** Worktree-mode Session row with the same trailing options menu as native DSH rows. */
147
- function WorktreeSessionRow({ t, sessionId, label, drag, actionPending, onOpen, onRename, onFork, onArchive, }) {
188
+ function WorktreeSessionRow({ t, sessionId, blank, label, drag, actionPending, onOpen, onRename, onFork, onArchive, }) {
148
189
  const [menuOpen, setMenuOpen] = useState(false);
149
190
  const markerClass = drag.marker === 'before'
150
191
  ? styles.dropBefore
@@ -171,7 +212,7 @@ function WorktreeSessionRow({ t, sessionId, label, drag, actionPending, onOpen,
171
212
  disabled: onArchive === undefined || actionPending,
172
213
  },
173
214
  ];
174
- return (_jsxs("div", { className: `${styles.treeSessionRow} ${markerClass}`, "data-session-id": sessionId, "data-session-drag": drag.active ? 'active' : undefined, "data-menu-open": menuOpen || undefined, role: "treeitem", draggable: true, onDragStart: (event) => {
215
+ return (_jsxs("div", { className: `${styles.treeSessionRow} ${markerClass}`, "data-session-id": sessionId, "data-session-blank": blank ? 'true' : undefined, "data-session-drag": drag.active ? 'active' : undefined, "data-menu-open": menuOpen || undefined, role: "treeitem", draggable: true, onDragStart: (event) => {
175
216
  event.dataTransfer.effectAllowed = 'move';
176
217
  event.dataTransfer.setData('text/plain', sessionId);
177
218
  drag.start();
@@ -186,7 +227,7 @@ function WorktreeSessionRow({ t, sessionId, label, drag, actionPending, onOpen,
186
227
  return;
187
228
  event.preventDefault();
188
229
  drag.drop(rowHalf(event));
189
- }, children: [_jsxs("button", { type: "button", className: styles.treeSessionContent, onClick: onOpen, children: [_jsx("span", { className: styles.treeGuide, "aria-hidden": "true", children: "\u2514" }), _jsx("span", { className: styles.sessionLabel, children: label })] }), _jsx("span", { className: styles.rowActions, children: _jsx(Menu, { open: menuOpen, onClose: () => {
230
+ }, children: [_jsxs("button", { type: "button", className: styles.treeSessionContent, onClick: onOpen, children: [_jsx("span", { className: styles.treeGuide, "aria-hidden": "true", children: "\u2514" }), _jsx("span", { className: styles.sessionLabel, children: label })] }), !blank && (_jsx("span", { className: styles.rowActions, children: _jsx(Menu, { open: menuOpen, onClose: () => {
190
231
  setMenuOpen(false);
191
232
  }, items: sessionMenuItems, onSelect: (id) => {
192
233
  setMenuOpen(false);
@@ -199,12 +240,12 @@ function WorktreeSessionRow({ t, sessionId, label, drag, actionPending, onOpen,
199
240
  }, portal: true, closeOnPointerLeave: true, anchor: (_jsx("button", { type: "button", className: styles.iconButton, "data-session-menu": true, "aria-label": t('session.options', { name: label }), onClick: (event) => {
200
241
  event.stopPropagation();
201
242
  setMenuOpen((current) => !current);
202
- }, children: _jsx(IconEllipsisOutline16, {}) })) }) })] }));
243
+ }, children: _jsx(IconEllipsisOutline16, {}) })) }) }))] }));
203
244
  }
204
245
  function WorktreeSessionGroup({ t, groupKey, sessionIds, workspaceId, expanded, actionPending, sessions, dragState, onToggleExpanded, onStartDrag, onHoverDrag, onClearDrag, onFinishDrag, onCommitDrag, onOpen, onRename, onFork, onArchive, }) {
205
246
  const visibleSessionIds = expanded ? sessionIds : sessionIds.slice(0, 5);
206
247
  const sameGroupDrag = dragState?.groupKey === groupKey;
207
- return (_jsxs(_Fragment, { children: [visibleSessionIds.map((sessionId) => (_jsx(WorktreeSessionRow, { t: t, sessionId: sessionId, label: sessionLabel(sessionId, sessions), drag: {
248
+ return (_jsxs(_Fragment, { children: [visibleSessionIds.map((sessionId) => (_jsx(WorktreeSessionRow, { t: t, sessionId: sessionId, blank: isBlankSession(sessionId, sessions), label: sessionLabel(sessionId, sessions, t), drag: {
208
249
  active: sameGroupDrag,
209
250
  marker: sameGroupDrag && dragState.over?.sessionId === sessionId
210
251
  ? dragState.over.half
@@ -239,13 +280,15 @@ function WorktreeSessionGroup({ t, groupKey, sessionIds, workspaceId, expanded,
239
280
  * Peer Worktree navigation surface. It keeps the original DSH Workspace mode
240
281
  * intact and renders a browser-local Workspace → Worktree → Session projection.
241
282
  */
242
- export function WorktreeSurface({ useStore, actions, useSessions, useWorkspaces, t, available, manager, createWorkspace, createSessionForWorktree: createSessionCallback, createMainSession, renameWorkspace, deleteWorkspace, insertWorkspaceBefore, insertSessionBefore, renameSession, forkSession, archiveSession, ensureSessionWorkspace, syncSessionWorkspaces, openSession, }) {
283
+ 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, }) {
243
284
  const preferredMode = useStore((state) => state.viewMode);
244
285
  const mode = effectiveViewMode(preferredMode, available && manager !== undefined);
245
286
  const sessions = useSessions((state) => state);
246
287
  const workspaces = useWorkspaces((state) => state);
247
288
  const workspaceIds = useStableWorkspaceIds(workspaces.items);
248
289
  const [readState, setReadState] = useState(EMPTY_READ_STATE);
290
+ const readStateRef = useRef(readState);
291
+ readStateRef.current = readState;
249
292
  const [searchQuery, setSearchQuery] = useState('');
250
293
  const [expandedWorkspaces, setExpandedWorkspaces] = useState({});
251
294
  const [expandedMains, setExpandedMains] = useState({});
@@ -275,33 +318,46 @@ export function WorktreeSurface({ useStore, actions, useSessions, useWorkspaces,
275
318
  const [sessionDrag, setSessionDrag] = useState();
276
319
  const [expandedSessionGroups, setExpandedSessionGroups] = useState({});
277
320
  const sessionDropCommitted = useRef(false);
321
+ const [worktreeDrag, setWorktreeDrag] = useState();
322
+ const worktreeDropCommitted = useRef(false);
323
+ const refreshGuard = useRef(createWorktreeRefreshGuard());
278
324
  const { ref, width, bounds } = useSidebarOverlayGeometry(mode === 'worktree');
279
325
  const collapsed = width <= 64;
280
326
  const query = searchQuery.trim().toLocaleLowerCase();
281
327
  const archivedSessionIds = workspaces.archivedSessionIds ?? [];
282
- const refresh = useCallback(async () => {
328
+ const pendingSessionArchived = pendingSessionBinding !== undefined &&
329
+ archivedSessionIds.includes(pendingSessionBinding.sessionId);
330
+ const refresh = useCallback(async (options = {}) => {
283
331
  if (manager === undefined) {
332
+ refreshGuard.current.invalidate();
284
333
  setReadState(EMPTY_READ_STATE);
285
334
  return;
286
335
  }
287
- setReadState({ status: 'loading', views: [] });
288
- try {
289
- const views = await loadWorktreeViews(manager, workspaceIds);
290
- setReadState({ status: 'ready', views });
336
+ const preserveCurrent = options.preserveCurrent === true;
337
+ if (!preserveCurrent) {
338
+ setReadState({ status: 'loading', views: [] });
291
339
  }
292
- catch (error) {
340
+ await refreshGuard.current.run(() => loadWorktreeViews(manager, workspaceIds, {
341
+ invalidateContext: options.invalidateContext !== false,
342
+ invalidateWorktreeContext,
343
+ }), (views) => {
344
+ setReadState({ status: 'ready', views });
345
+ }, (error) => {
346
+ if (preserveCurrent)
347
+ throw error;
293
348
  setReadState({
294
349
  status: 'error',
295
350
  views: [],
296
351
  error: toWorktreeViewError(error),
297
352
  });
298
- }
299
- }, [manager, workspaceIds]);
353
+ });
354
+ }, [invalidateWorktreeContext, manager, workspaceIds]);
300
355
  useEffect(() => {
301
356
  if (mode === 'worktree') {
302
- void refresh();
357
+ void refresh({ preserveCurrent: readStateRef.current.status === 'ready' });
303
358
  }
304
359
  else {
360
+ refreshGuard.current.invalidate();
305
361
  setReadState(EMPTY_READ_STATE);
306
362
  }
307
363
  }, [mode, refresh]);
@@ -318,12 +374,28 @@ export function WorktreeSurface({ useStore, actions, useSessions, useWorkspaces,
318
374
  const modalView = worktreeModalWorkspaceId === undefined
319
375
  ? undefined
320
376
  : viewByWorkspace.get(worktreeModalWorkspaceId);
377
+ const modalReadiness = modalView?.readiness;
378
+ const modalSetupStatus = modalView === undefined
379
+ ? undefined
380
+ : modalReadiness?.status === 'ready'
381
+ ? modalView.branches.length === 0 ? 'noLocalBranch' : undefined
382
+ : modalReadiness?.status;
383
+ const modalCanCreate = modalSetupStatus === undefined && modalReadiness?.status === 'ready';
384
+ useEffect(() => {
385
+ if (worktreeModalWorkspaceId === undefined || modalView === undefined)
386
+ return;
387
+ if (modalView.readiness.status !== 'ready') {
388
+ setSelectedBranch('');
389
+ return;
390
+ }
391
+ setSelectedBranch((current) => reconcileBaseBranchSelection(current, modalView.branches));
392
+ }, [modalView, worktreeModalWorkspaceId]);
321
393
  const runMutation = async (operation) => {
322
394
  setActionPending(true);
323
395
  setActionError(undefined);
324
396
  try {
325
397
  await operation();
326
- await refresh();
398
+ await refresh({ preserveCurrent: true });
327
399
  }
328
400
  catch (error) {
329
401
  setActionError(toWorktreeViewError(error));
@@ -483,6 +555,31 @@ export function WorktreeSurface({ useStore, actions, useSessions, useWorkspaces,
483
555
  setActionError(toWorktreeViewError(error));
484
556
  });
485
557
  };
558
+ const commitWorktreeDrag = (activeDrag, over, worktreeIds, workspaceId) => {
559
+ if (worktreeDropCommitted.current)
560
+ return;
561
+ worktreeDropCommitted.current = true;
562
+ setWorktreeDrag(undefined);
563
+ if (activeDrag.workspaceId !== workspaceId)
564
+ return;
565
+ const move = resolveWorktreeMove(worktreeIds, activeDrag.worktreeId, over.worktreeId, over.half);
566
+ if (move === undefined)
567
+ return;
568
+ if (insertWorktreeBefore === undefined) {
569
+ setActionError({
570
+ code: 'WORKTREE_ORDER_UNAVAILABLE',
571
+ message: '',
572
+ retryable: true,
573
+ });
574
+ return;
575
+ }
576
+ setActionError(undefined);
577
+ void insertWorktreeBefore(workspaceId, activeDrag.worktreeId, move.beforeWorktreeId)
578
+ .then(() => refresh({ preserveCurrent: true, invalidateContext: false }))
579
+ .catch((error) => {
580
+ setActionError(toRetryableWorktreeOrderError(error));
581
+ });
582
+ };
486
583
  const openSessionRename = (sessionId, currentTitle) => {
487
584
  setSessionRenameTarget({ sessionId, currentTitle });
488
585
  setSessionRenameDraft(currentTitle);
@@ -559,6 +656,7 @@ export function WorktreeSurface({ useStore, actions, useSessions, useWorkspaces,
559
656
  const worktreeName = newBranch.trim();
560
657
  if (manager === undefined ||
561
658
  modalWorkspace === undefined ||
659
+ !modalCanCreate ||
562
660
  selectedBranch.length === 0 ||
563
661
  worktreeName.length === 0) {
564
662
  return;
@@ -585,7 +683,7 @@ export function WorktreeSurface({ useStore, actions, useSessions, useWorkspaces,
585
683
  }
586
684
  setWorktreeModalWorkspaceId(undefined);
587
685
  if (createSessionCallback === undefined) {
588
- await refresh();
686
+ await refresh({ preserveCurrent: true });
589
687
  setActionError({
590
688
  code: 'WORKTREE_CREATED_SESSION_UNAVAILABLE',
591
689
  message: '',
@@ -600,14 +698,15 @@ export function WorktreeSurface({ useStore, actions, useSessions, useWorkspaces,
600
698
  };
601
699
  try {
602
700
  await createSessionCallback(sessionInput);
701
+ await invalidateWorktreeContext?.(createdWorktree.workspaceId);
603
702
  }
604
703
  catch (error) {
605
- if (error instanceof WorktreeSessionBindingError) {
704
+ if (error instanceof WorktreeSessionBindingError && error.retryable) {
606
705
  setPendingSessionBinding({ ...sessionInput, sessionId: error.sessionId });
607
706
  }
608
707
  throw error;
609
708
  }
610
- await refresh();
709
+ await refresh({ preserveCurrent: true });
611
710
  }
612
711
  catch (error) {
613
712
  setActionError(toWorktreeViewError(error));
@@ -630,10 +729,11 @@ export function WorktreeSurface({ useStore, actions, useSessions, useWorkspaces,
630
729
  setPendingSessionBinding(undefined);
631
730
  try {
632
731
  await createSessionCallback(input);
633
- await refresh();
732
+ await invalidateWorktreeContext?.(input.workspaceId);
733
+ await refresh({ preserveCurrent: true });
634
734
  }
635
735
  catch (error) {
636
- if (error instanceof WorktreeSessionBindingError) {
736
+ if (error instanceof WorktreeSessionBindingError && error.retryable) {
637
737
  setPendingSessionBinding({ ...input, sessionId: error.sessionId });
638
738
  }
639
739
  setActionError(toWorktreeViewError(error));
@@ -643,23 +743,31 @@ export function WorktreeSurface({ useStore, actions, useSessions, useWorkspaces,
643
743
  }
644
744
  };
645
745
  const retrySessionBinding = async () => {
646
- if (manager === undefined || pendingSessionBinding === undefined)
746
+ if (manager === undefined ||
747
+ pendingSessionBinding === undefined ||
748
+ pendingSessionArchived)
647
749
  return;
750
+ const pending = pendingSessionBinding;
648
751
  setActionPending(true);
649
752
  setActionError(undefined);
650
753
  try {
651
- await manager.bindSession({
652
- workspaceId: pendingSessionBinding.workspaceId,
653
- worktreeId: pendingSessionBinding.worktreeId,
654
- sessionId: pendingSessionBinding.sessionId,
754
+ await retryWorktreeSessionBinding({
755
+ manager,
756
+ pending,
757
+ archived: false,
758
+ ensureSessionWorkspace: (workspaceId, sessionId) => {
759
+ ensureSessionWorkspace?.(workspaceId, sessionId);
760
+ },
761
+ openSession,
655
762
  });
656
- const sessionId = pendingSessionBinding.sessionId;
657
763
  setPendingSessionBinding(undefined);
658
- ensureSessionWorkspace?.(pendingSessionBinding.workspaceId, sessionId);
659
- await refresh();
660
- openSession(sessionId);
764
+ await refresh({ preserveCurrent: true });
765
+ await invalidateWorktreeContext?.(pending.workspaceId);
661
766
  }
662
767
  catch (error) {
768
+ if (error instanceof WorktreeSessionBindingError && !error.retryable) {
769
+ setPendingSessionBinding(undefined);
770
+ }
663
771
  setActionError(toWorktreeViewError(error));
664
772
  }
665
773
  finally {
@@ -685,7 +793,7 @@ export function WorktreeSurface({ useStore, actions, useSessions, useWorkspaces,
685
793
  } }), _jsx("button", { type: "button", className: styles.iconButton, "aria-label": t('workspace.add'), onClick: () => {
686
794
  if (createWorkspace !== undefined)
687
795
  void runMutation(createWorkspace);
688
- }, children: _jsx(IconPlusOutline16, {}) })] }), _jsxs("div", { className: styles.content, tabIndex: 0, children: [actionError !== undefined && (_jsxs("div", { className: styles.error, role: "alert", "data-worktree-error": true, children: [_jsx("p", { className: styles.message, "data-error": "true", children: formatWorktreeViewError(actionError, t) }), pendingSessionBinding !== undefined && (_jsxs("div", { className: styles.recoveryActions, children: [_jsx("button", { type: "button", className: styles.actionButton, disabled: actionPending, onClick: () => {
796
+ }, children: _jsx(IconPlusOutline16, {}) })] }), _jsxs("div", { className: styles.content, tabIndex: 0, children: [actionError !== undefined && (_jsxs("div", { className: styles.error, role: "alert", "data-worktree-error": true, children: [_jsx("p", { className: styles.message, "data-error": "true", children: formatWorktreeViewError(actionError, t) }), pendingSessionBinding !== undefined && (_jsxs("div", { className: styles.recoveryActions, children: [_jsx("button", { type: "button", className: styles.actionButton, disabled: actionPending || pendingSessionArchived, onClick: () => {
689
797
  void retrySessionBinding();
690
798
  }, children: t('action.retryBinding') }), _jsx("button", { type: "button", className: styles.actionButton, disabled: actionPending, onClick: () => {
691
799
  const sessionId = pendingSessionBinding.sessionId;
@@ -699,19 +807,21 @@ export function WorktreeSurface({ useStore, actions, useSessions, useWorkspaces,
699
807
  void refresh();
700
808
  }, children: t('action.retry') })] })) : readState.status === 'ready' ? (_jsx("div", { className: styles.workspaceList, children: visibleWorkspaces.length === 0 ? (_jsx("p", { className: styles.empty, children: t('workspace.noMatches') })) : (visibleWorkspaces.map((workspace) => {
701
809
  const view = viewByWorkspace.get(workspace.workspaceId);
810
+ const currentBranch = view?.branches.find((branch) => branch.isCurrent)?.name;
811
+ const mainLabel = currentBranch === undefined
812
+ ? t('worktree.main')
813
+ : t('worktree.mainWithBranch', { branch: currentBranch });
702
814
  const expanded = expandedWorkspaces[workspace.workspaceId] !== false;
703
815
  const workspaceMatchesQuery = includesText(workspace.title, query);
704
816
  const allWorkspaceSessionIds = filterArchivedSessionIds(workspaceSessionIds(workspaces, workspace.workspaceId, sessions.ids), archivedSessionIds);
705
817
  const bindings = view?.bindings ?? [];
706
818
  const boundSessionIds = new Set(bindings.map((binding) => binding.sessionId));
707
- const mainSessionIds = unboundSessionIds(allWorkspaceSessionIds, [
708
- ...boundSessionIds,
709
- ]);
710
- const visibleMainSessionIds = mainSessionIds.filter((sessionId) => workspaceMatchesQuery ||
711
- includesText(sessionLabel(sessionId, sessions), query));
819
+ const mainSessionIds = filterVisibleSessionIds(unboundSessionIds(allWorkspaceSessionIds, [...boundSessionIds]), sessions);
820
+ const visibleMainSessionIds = mainSessionIds.filter((sessionId) => workspaceMatchesQuery || sessionMatchesQuery(sessionId, sessions, query));
712
821
  const mainExpanded = expandedMains[workspace.workspaceId] !== false;
713
822
  const mainGroupKey = `main:${workspace.workspaceId}`;
714
823
  const worktrees = view?.worktrees ?? [];
824
+ const sameWorkspaceWorktreeDrag = worktreeDrag?.workspaceId === workspace.workspaceId;
715
825
  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: {
716
826
  active: workspaceDrag !== undefined,
717
827
  marker: workspaceDrag?.over?.workspaceId === workspace.workspaceId
@@ -756,7 +866,7 @@ export function WorktreeSurface({ useStore, actions, useSessions, useWorkspaces,
756
866
  openWorkspaceDelete(workspace);
757
867
  }, onMenuOpenChange: (open) => {
758
868
  setOpenWorkspaceMenuId(open ? workspace.workspaceId : undefined);
759
- } }), expanded && (_jsxs("div", { className: styles.treeChildren, children: [_jsx(WorktreeGroupRow, { t: t, kind: "main", label: t('worktree.main'), expanded: mainExpanded, icon: _jsx(IconBranchOutline16, {}), workspaceTitle: workspace.title, onToggle: () => {
869
+ } }), expanded && (_jsxs("div", { className: styles.treeChildren, children: [_jsx(WorktreeGroupRow, { t: t, kind: "main", label: mainLabel, expanded: mainExpanded, icon: _jsx(IconBranchOutline16, {}), workspaceTitle: workspace.title, onToggle: () => {
760
870
  toggleMain(workspace.workspaceId);
761
871
  }, onCreateSession: createMainSession === undefined
762
872
  ? undefined
@@ -781,17 +891,16 @@ export function WorktreeSurface({ useStore, actions, useSessions, useWorkspaces,
781
891
  }, onCommitDrag: commitSessionDrag, onOpen: (sessionId) => {
782
892
  openWorktreeSession({ open: openSession }, sessionId);
783
893
  }, onRename: renameSession === undefined ? undefined : openSessionRename, onFork: forkSession, onArchive: archiveSession === undefined ? undefined : archiveWorktreeSession })), worktrees.length === 0 && (_jsx("p", { className: styles.emptyNested, children: t('worktree.noWorktrees') })), worktrees.map((record) => {
784
- const worktreeSessionIds = filterArchivedSessionIds(bindingIdsFor(bindings, record.worktreeId).filter((sessionId) => sessions.ids.includes(sessionId)), archivedSessionIds);
894
+ const worktreeSessionIds = filterVisibleSessionIds(filterArchivedSessionIds(bindingIdsFor(bindings, record.worktreeId).filter((sessionId) => sessions.ids.includes(sessionId)), archivedSessionIds), sessions);
785
895
  const worktreeMatchesQuery = workspaceMatchesQuery ||
786
896
  includesText(record.branch, query) ||
787
897
  includesText(record.absolutePath, query);
788
898
  if (query.length > 0 &&
789
899
  !worktreeMatchesQuery &&
790
- !worktreeSessionIds.some((sessionId) => includesText(sessionLabel(sessionId, sessions), query))) {
900
+ !worktreeSessionIds.some((sessionId) => sessionMatchesQuery(sessionId, sessions, query))) {
791
901
  return null;
792
902
  }
793
- const visibleWorktreeSessionIds = worktreeSessionIds.filter((sessionId) => worktreeMatchesQuery ||
794
- includesText(sessionLabel(sessionId, sessions), query));
903
+ const visibleWorktreeSessionIds = worktreeSessionIds.filter((sessionId) => worktreeMatchesQuery || sessionMatchesQuery(sessionId, sessions, query));
795
904
  const worktreeGroupKey = `worktree:${record.worktreeId}`;
796
905
  const state = record.status === 'removed'
797
906
  ? 'warning'
@@ -804,9 +913,9 @@ export function WorktreeSurface({ useStore, actions, useSessions, useWorkspaces,
804
913
  ? t('worktree.repair')
805
914
  : t('worktree.ready');
806
915
  const worktreeExpanded = expandedWorktrees[record.worktreeId] !== false;
807
- return (_jsxs("div", { className: styles.worktreeGroup, "data-worktree-id": record.worktreeId, children: [_jsx(WorktreeGroupRow, { t: t, kind: "worktree", label: record.branch, expanded: worktreeExpanded, icon: _jsx(IconBranchOutline16, {}), workspaceTitle: workspace.title, state: state, stateLabel: stateLabel, onToggle: () => {
916
+ 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: () => {
808
917
  toggleWorktree(record.worktreeId);
809
- }, onCreateSession: record.status === 'active'
918
+ }, onCreateSession: record.status === 'active' && record.health !== 'repair'
810
919
  ? () => {
811
920
  void createSession({
812
921
  workspaceId: record.workspaceId,
@@ -827,7 +936,38 @@ export function WorktreeSurface({ useStore, actions, useSessions, useWorkspaces,
827
936
  setActionError(undefined);
828
937
  },
829
938
  }
830
- : undefined }), worktreeExpanded && (_jsx(WorktreeSessionGroup, { t: t, groupKey: worktreeGroupKey, sessionIds: visibleWorktreeSessionIds, workspaceId: workspace.workspaceId, expanded: expandedSessionGroups[worktreeGroupKey] === true, actionPending: actionPending, sessions: sessions, dragState: sessionDrag, onToggleExpanded: () => {
939
+ : undefined, drag: {
940
+ active: sameWorkspaceWorktreeDrag,
941
+ marker: worktreeDrag?.over?.worktreeId === record.worktreeId
942
+ ? worktreeDrag.over.half
943
+ : null,
944
+ start: () => {
945
+ worktreeDropCommitted.current = false;
946
+ setWorktreeDrag({
947
+ workspaceId: workspace.workspaceId,
948
+ worktreeId: record.worktreeId,
949
+ over: null,
950
+ });
951
+ },
952
+ hover: (half) => {
953
+ setWorktreeDrag((current) => current === undefined ||
954
+ current.workspaceId !== workspace.workspaceId
955
+ ? current
956
+ : {
957
+ ...current,
958
+ over: { worktreeId: record.worktreeId, half },
959
+ });
960
+ },
961
+ drop: (half) => {
962
+ if (worktreeDrag === undefined)
963
+ return;
964
+ commitWorktreeDrag(worktreeDrag, { worktreeId: record.worktreeId, half }, worktrees.map((candidate) => candidate.worktreeId), workspace.workspaceId);
965
+ },
966
+ end: () => {
967
+ setWorktreeDrag(undefined);
968
+ worktreeDropCommitted.current = false;
969
+ },
970
+ } }), worktreeExpanded && (_jsx(WorktreeSessionGroup, { t: t, groupKey: worktreeGroupKey, sessionIds: visibleWorktreeSessionIds, workspaceId: workspace.workspaceId, expanded: expandedSessionGroups[worktreeGroupKey] === true, actionPending: actionPending, sessions: sessions, dragState: sessionDrag, onToggleExpanded: () => {
831
971
  setExpandedSessionGroups((current) => ({
832
972
  ...current,
833
973
  [worktreeGroupKey]: current[worktreeGroupKey] !== true,
@@ -877,7 +1017,7 @@ export function WorktreeSurface({ useStore, actions, useSessions, useWorkspaces,
877
1017
  }
878
1018
  } }), workspaceRenameDuplicate && (_jsx("p", { className: styles.renameError, role: "alert", children: t('workspace.duplicate') })), workspaceRenameError !== undefined && (_jsx("p", { className: styles.renameError, role: "alert", children: formatWorktreeViewError(workspaceRenameError, t) }))] })), workspaceDeleteTarget !== undefined && (_jsx(Modal, { open: workspaceDeleteTarget !== undefined, onClose: closeWorkspaceDelete, closeLabel: t('dialog.closeWorkspaceDelete'), title: t('workspace.deleteTitle'), description: t('workspace.deleteDescription', { name: workspaceDeleteTarget.title }), footer: (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "outline", disabled: workspaceDeletePending, onClick: closeWorkspaceDelete, children: t('dialog.cancel') }), _jsx(Button, { variant: "outline", disabled: workspaceDeletePending, onClick: () => {
879
1019
  void confirmWorkspaceDelete();
880
- }, children: t('dialog.delete') })] })), children: workspaceDeleteError !== undefined && (_jsx("p", { className: styles.renameError, role: "alert", children: formatWorktreeViewError(workspaceDeleteError, t) })) })), worktreeModalWorkspaceId !== undefined && modalWorkspace !== undefined && (_jsxs(Modal, { open: worktreeModalWorkspaceId !== undefined, onClose: () => {
1020
+ }, children: t('dialog.delete') })] })), children: workspaceDeleteError !== undefined && (_jsx("p", { className: styles.renameError, role: "alert", children: formatWorktreeViewError(workspaceDeleteError, t) })) })), worktreeModalWorkspaceId !== undefined && modalWorkspace !== undefined && (_jsx(Modal, { open: worktreeModalWorkspaceId !== undefined, onClose: () => {
881
1021
  if (actionPending)
882
1022
  return;
883
1023
  setWorktreeModalWorkspaceId(undefined);
@@ -887,11 +1027,13 @@ export function WorktreeSurface({ useStore, actions, useSessions, useWorkspaces,
887
1027
  selectedBranch.length === 0 ||
888
1028
  newBranch.trim().length === 0, onClick: () => {
889
1029
  void submitWorktree();
890
- }, children: t('worktree.create') })] })), children: [_jsxs("label", { className: styles.modalField, children: [t('worktree.baseBranch'), _jsxs("select", { className: styles.actionSelect, "aria-label": t('worktree.baseBranch'), value: selectedBranch, disabled: actionPending, onChange: (event) => {
891
- setSelectedBranch(event.currentTarget.value);
892
- }, children: [_jsx("option", { value: "", children: t('worktree.noLocalBranch') }), (modalView?.branches ?? []).map((branch) => (_jsxs("option", { value: branch.name, children: [branch.name, branch.isCurrent ? t('branch.current') : '', branch.checkedOut ? t('branch.checkedOut') : ''] }, branch.name)))] })] }), _jsxs("label", { className: styles.modalField, children: [t('worktree.name'), _jsx(Input, { className: styles.renameInput, "aria-label": t('worktree.name'), value: newBranch, placeholder: "dsh/12345678", disabled: actionPending, onChange: (event) => {
893
- setNewBranch(event.currentTarget.value);
894
- } })] }), modalView?.branches.length === 0 && (_jsx("p", { className: styles.empty, children: t('worktree.noBranches') }))] })), worktreeRemoval !== undefined && (_jsx(Modal, { open: worktreeRemoval !== undefined, onClose: () => {
1030
+ }, children: t('worktree.create') })] })), children: modalCanCreate ? (_jsxs(_Fragment, { children: [_jsxs("label", { className: styles.modalField, children: [t('worktree.baseBranch'), _jsx("select", { className: styles.actionSelect, "aria-label": t('worktree.baseBranch'), value: selectedBranch, disabled: actionPending, onChange: (event) => {
1031
+ setSelectedBranch(event.currentTarget.value);
1032
+ }, children: modalView?.branches.map((branch) => (_jsxs("option", { value: branch.name, children: [branch.name, branch.isCurrent ? t('branch.current') : '', branch.checkedOut ? t('branch.checkedOut') : ''] }, branch.name))) })] }), _jsxs("label", { className: styles.modalField, children: [t('worktree.name'), _jsx(Input, { className: styles.renameInput, "aria-label": t('worktree.name'), value: newBranch, placeholder: "dsh/12345678", disabled: actionPending, onChange: (event) => {
1033
+ setNewBranch(event.currentTarget.value);
1034
+ } })] })] })) : (_jsxs("div", { className: styles.gitReadiness, "data-worktree-readiness": modalSetupStatus ?? 'loading', role: "alert", children: [_jsx("p", { className: styles.message, children: modalSetupStatus === undefined
1035
+ ? t('status.loading')
1036
+ : worktreeSetupMessage(modalSetupStatus, t) }), modalSetupStatus !== undefined && (_jsx("pre", { className: styles.commandBlock, "aria-label": t('worktree.setup.commands'), children: worktreeSetupCommands(modalSetupStatus).join('\n') }))] })) })), worktreeRemoval !== undefined && (_jsx(Modal, { open: worktreeRemoval !== undefined, onClose: () => {
895
1037
  if (actionPending)
896
1038
  return;
897
1039
  setWorktreeRemoval(undefined);
@@ -909,6 +1051,7 @@ export function WorktreeSurface({ useStore, actions, useSessions, useWorkspaces,
909
1051
  worktreeId: target.worktreeId,
910
1052
  },
911
1053
  });
1054
+ await invalidateWorktreeContext?.(target.workspaceId);
912
1055
  setWorktreeRemoval(undefined);
913
1056
  });
914
1057
  }, children: t('worktree.remove') })] })) }))] }));