@cerbur/clutch-dsh-worktree 0.1.1 → 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 +204 -58
  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 +1015 -134
  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
@@ -1,12 +1,24 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
- import { Button, IconArchiveOutline20, IconBranchOutline16, IconChevronDownOutline14, IconChevronRightOutline14, IconCloseOutline16, IconEditOutline16, IconEllipsisOutline16, IconFolderClose16, IconFolderOpen16, IconPlusOutline16, IconSearchOutline16, IconTrashOutline16, Input, Menu, Modal, StateDot, } from '@deepseek-ai/dsh-client-ui-primitives';
3
+ import { Button, HoverCard, IconArchiveOutline20, IconBranchOutline16, IconChevronDownOutline14, IconChevronRightOutline14, IconCloseOutline16, IconEditOutline16, IconEllipsisOutline16, IconFolderClose16, IconFolderOpen16, IconPlusOutline16, IconSearchOutline16, IconTrashOutline16, Input, Menu, Modal, StateDot, } from '@deepseek-ai/dsh-client-ui-primitives';
4
4
  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
- return (_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: () => {
@@ -139,9 +180,12 @@ function WorktreeGroupRow({ t, kind, label, expanded, icon, workspaceTitle, stat
139
180
  event.stopPropagation();
140
181
  onCreateSession();
141
182
  }, children: _jsx(IconPlusOutline16, {}) }))] })] }));
183
+ if (main)
184
+ return row;
185
+ return (_jsx(HoverCard, { anchor: row, content: _jsx("div", { className: styles.worktreeHoverTitle, children: label }), openDelayMs: 500, disabled: menu?.open === true }));
142
186
  }
143
187
  /** Worktree-mode Session row with the same trailing options menu as native DSH rows. */
144
- function WorktreeSessionRow({ t, sessionId, label, drag, actionPending, onOpen, onRename, onFork, onArchive, }) {
188
+ function WorktreeSessionRow({ t, sessionId, blank, label, drag, actionPending, onOpen, onRename, onFork, onArchive, }) {
145
189
  const [menuOpen, setMenuOpen] = useState(false);
146
190
  const markerClass = drag.marker === 'before'
147
191
  ? styles.dropBefore
@@ -168,7 +212,7 @@ function WorktreeSessionRow({ t, sessionId, label, drag, actionPending, onOpen,
168
212
  disabled: onArchive === undefined || actionPending,
169
213
  },
170
214
  ];
171
- 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) => {
172
216
  event.dataTransfer.effectAllowed = 'move';
173
217
  event.dataTransfer.setData('text/plain', sessionId);
174
218
  drag.start();
@@ -183,7 +227,7 @@ function WorktreeSessionRow({ t, sessionId, label, drag, actionPending, onOpen,
183
227
  return;
184
228
  event.preventDefault();
185
229
  drag.drop(rowHalf(event));
186
- }, 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: () => {
187
231
  setMenuOpen(false);
188
232
  }, items: sessionMenuItems, onSelect: (id) => {
189
233
  setMenuOpen(false);
@@ -196,12 +240,12 @@ function WorktreeSessionRow({ t, sessionId, label, drag, actionPending, onOpen,
196
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) => {
197
241
  event.stopPropagation();
198
242
  setMenuOpen((current) => !current);
199
- }, children: _jsx(IconEllipsisOutline16, {}) })) }) })] }));
243
+ }, children: _jsx(IconEllipsisOutline16, {}) })) }) }))] }));
200
244
  }
201
245
  function WorktreeSessionGroup({ t, groupKey, sessionIds, workspaceId, expanded, actionPending, sessions, dragState, onToggleExpanded, onStartDrag, onHoverDrag, onClearDrag, onFinishDrag, onCommitDrag, onOpen, onRename, onFork, onArchive, }) {
202
246
  const visibleSessionIds = expanded ? sessionIds : sessionIds.slice(0, 5);
203
247
  const sameGroupDrag = dragState?.groupKey === groupKey;
204
- 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: {
205
249
  active: sameGroupDrag,
206
250
  marker: sameGroupDrag && dragState.over?.sessionId === sessionId
207
251
  ? dragState.over.half
@@ -236,13 +280,15 @@ function WorktreeSessionGroup({ t, groupKey, sessionIds, workspaceId, expanded,
236
280
  * Peer Worktree navigation surface. It keeps the original DSH Workspace mode
237
281
  * intact and renders a browser-local Workspace → Worktree → Session projection.
238
282
  */
239
- 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, }) {
240
284
  const preferredMode = useStore((state) => state.viewMode);
241
285
  const mode = effectiveViewMode(preferredMode, available && manager !== undefined);
242
286
  const sessions = useSessions((state) => state);
243
287
  const workspaces = useWorkspaces((state) => state);
244
288
  const workspaceIds = useStableWorkspaceIds(workspaces.items);
245
289
  const [readState, setReadState] = useState(EMPTY_READ_STATE);
290
+ const readStateRef = useRef(readState);
291
+ readStateRef.current = readState;
246
292
  const [searchQuery, setSearchQuery] = useState('');
247
293
  const [expandedWorkspaces, setExpandedWorkspaces] = useState({});
248
294
  const [expandedMains, setExpandedMains] = useState({});
@@ -272,33 +318,46 @@ export function WorktreeSurface({ useStore, actions, useSessions, useWorkspaces,
272
318
  const [sessionDrag, setSessionDrag] = useState();
273
319
  const [expandedSessionGroups, setExpandedSessionGroups] = useState({});
274
320
  const sessionDropCommitted = useRef(false);
321
+ const [worktreeDrag, setWorktreeDrag] = useState();
322
+ const worktreeDropCommitted = useRef(false);
323
+ const refreshGuard = useRef(createWorktreeRefreshGuard());
275
324
  const { ref, width, bounds } = useSidebarOverlayGeometry(mode === 'worktree');
276
325
  const collapsed = width <= 64;
277
326
  const query = searchQuery.trim().toLocaleLowerCase();
278
327
  const archivedSessionIds = workspaces.archivedSessionIds ?? [];
279
- const refresh = useCallback(async () => {
328
+ const pendingSessionArchived = pendingSessionBinding !== undefined &&
329
+ archivedSessionIds.includes(pendingSessionBinding.sessionId);
330
+ const refresh = useCallback(async (options = {}) => {
280
331
  if (manager === undefined) {
332
+ refreshGuard.current.invalidate();
281
333
  setReadState(EMPTY_READ_STATE);
282
334
  return;
283
335
  }
284
- setReadState({ status: 'loading', views: [] });
285
- try {
286
- const views = await loadWorktreeViews(manager, workspaceIds);
287
- setReadState({ status: 'ready', views });
336
+ const preserveCurrent = options.preserveCurrent === true;
337
+ if (!preserveCurrent) {
338
+ setReadState({ status: 'loading', views: [] });
288
339
  }
289
- 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;
290
348
  setReadState({
291
349
  status: 'error',
292
350
  views: [],
293
351
  error: toWorktreeViewError(error),
294
352
  });
295
- }
296
- }, [manager, workspaceIds]);
353
+ });
354
+ }, [invalidateWorktreeContext, manager, workspaceIds]);
297
355
  useEffect(() => {
298
356
  if (mode === 'worktree') {
299
- void refresh();
357
+ void refresh({ preserveCurrent: readStateRef.current.status === 'ready' });
300
358
  }
301
359
  else {
360
+ refreshGuard.current.invalidate();
302
361
  setReadState(EMPTY_READ_STATE);
303
362
  }
304
363
  }, [mode, refresh]);
@@ -315,12 +374,28 @@ export function WorktreeSurface({ useStore, actions, useSessions, useWorkspaces,
315
374
  const modalView = worktreeModalWorkspaceId === undefined
316
375
  ? undefined
317
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]);
318
393
  const runMutation = async (operation) => {
319
394
  setActionPending(true);
320
395
  setActionError(undefined);
321
396
  try {
322
397
  await operation();
323
- await refresh();
398
+ await refresh({ preserveCurrent: true });
324
399
  }
325
400
  catch (error) {
326
401
  setActionError(toWorktreeViewError(error));
@@ -480,6 +555,31 @@ export function WorktreeSurface({ useStore, actions, useSessions, useWorkspaces,
480
555
  setActionError(toWorktreeViewError(error));
481
556
  });
482
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
+ };
483
583
  const openSessionRename = (sessionId, currentTitle) => {
484
584
  setSessionRenameTarget({ sessionId, currentTitle });
485
585
  setSessionRenameDraft(currentTitle);
@@ -556,6 +656,7 @@ export function WorktreeSurface({ useStore, actions, useSessions, useWorkspaces,
556
656
  const worktreeName = newBranch.trim();
557
657
  if (manager === undefined ||
558
658
  modalWorkspace === undefined ||
659
+ !modalCanCreate ||
559
660
  selectedBranch.length === 0 ||
560
661
  worktreeName.length === 0) {
561
662
  return;
@@ -582,7 +683,7 @@ export function WorktreeSurface({ useStore, actions, useSessions, useWorkspaces,
582
683
  }
583
684
  setWorktreeModalWorkspaceId(undefined);
584
685
  if (createSessionCallback === undefined) {
585
- await refresh();
686
+ await refresh({ preserveCurrent: true });
586
687
  setActionError({
587
688
  code: 'WORKTREE_CREATED_SESSION_UNAVAILABLE',
588
689
  message: '',
@@ -597,14 +698,15 @@ export function WorktreeSurface({ useStore, actions, useSessions, useWorkspaces,
597
698
  };
598
699
  try {
599
700
  await createSessionCallback(sessionInput);
701
+ await invalidateWorktreeContext?.(createdWorktree.workspaceId);
600
702
  }
601
703
  catch (error) {
602
- if (error instanceof WorktreeSessionBindingError) {
704
+ if (error instanceof WorktreeSessionBindingError && error.retryable) {
603
705
  setPendingSessionBinding({ ...sessionInput, sessionId: error.sessionId });
604
706
  }
605
707
  throw error;
606
708
  }
607
- await refresh();
709
+ await refresh({ preserveCurrent: true });
608
710
  }
609
711
  catch (error) {
610
712
  setActionError(toWorktreeViewError(error));
@@ -627,10 +729,11 @@ export function WorktreeSurface({ useStore, actions, useSessions, useWorkspaces,
627
729
  setPendingSessionBinding(undefined);
628
730
  try {
629
731
  await createSessionCallback(input);
630
- await refresh();
732
+ await invalidateWorktreeContext?.(input.workspaceId);
733
+ await refresh({ preserveCurrent: true });
631
734
  }
632
735
  catch (error) {
633
- if (error instanceof WorktreeSessionBindingError) {
736
+ if (error instanceof WorktreeSessionBindingError && error.retryable) {
634
737
  setPendingSessionBinding({ ...input, sessionId: error.sessionId });
635
738
  }
636
739
  setActionError(toWorktreeViewError(error));
@@ -640,23 +743,31 @@ export function WorktreeSurface({ useStore, actions, useSessions, useWorkspaces,
640
743
  }
641
744
  };
642
745
  const retrySessionBinding = async () => {
643
- if (manager === undefined || pendingSessionBinding === undefined)
746
+ if (manager === undefined ||
747
+ pendingSessionBinding === undefined ||
748
+ pendingSessionArchived)
644
749
  return;
750
+ const pending = pendingSessionBinding;
645
751
  setActionPending(true);
646
752
  setActionError(undefined);
647
753
  try {
648
- await manager.bindSession({
649
- workspaceId: pendingSessionBinding.workspaceId,
650
- worktreeId: pendingSessionBinding.worktreeId,
651
- sessionId: pendingSessionBinding.sessionId,
754
+ await retryWorktreeSessionBinding({
755
+ manager,
756
+ pending,
757
+ archived: false,
758
+ ensureSessionWorkspace: (workspaceId, sessionId) => {
759
+ ensureSessionWorkspace?.(workspaceId, sessionId);
760
+ },
761
+ openSession,
652
762
  });
653
- const sessionId = pendingSessionBinding.sessionId;
654
763
  setPendingSessionBinding(undefined);
655
- ensureSessionWorkspace?.(pendingSessionBinding.workspaceId, sessionId);
656
- await refresh();
657
- openSession(sessionId);
764
+ await refresh({ preserveCurrent: true });
765
+ await invalidateWorktreeContext?.(pending.workspaceId);
658
766
  }
659
767
  catch (error) {
768
+ if (error instanceof WorktreeSessionBindingError && !error.retryable) {
769
+ setPendingSessionBinding(undefined);
770
+ }
660
771
  setActionError(toWorktreeViewError(error));
661
772
  }
662
773
  finally {
@@ -682,7 +793,7 @@ export function WorktreeSurface({ useStore, actions, useSessions, useWorkspaces,
682
793
  } }), _jsx("button", { type: "button", className: styles.iconButton, "aria-label": t('workspace.add'), onClick: () => {
683
794
  if (createWorkspace !== undefined)
684
795
  void runMutation(createWorkspace);
685
- }, 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: () => {
686
797
  void retrySessionBinding();
687
798
  }, children: t('action.retryBinding') }), _jsx("button", { type: "button", className: styles.actionButton, disabled: actionPending, onClick: () => {
688
799
  const sessionId = pendingSessionBinding.sessionId;
@@ -696,19 +807,21 @@ export function WorktreeSurface({ useStore, actions, useSessions, useWorkspaces,
696
807
  void refresh();
697
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) => {
698
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 });
699
814
  const expanded = expandedWorkspaces[workspace.workspaceId] !== false;
700
815
  const workspaceMatchesQuery = includesText(workspace.title, query);
701
816
  const allWorkspaceSessionIds = filterArchivedSessionIds(workspaceSessionIds(workspaces, workspace.workspaceId, sessions.ids), archivedSessionIds);
702
817
  const bindings = view?.bindings ?? [];
703
818
  const boundSessionIds = new Set(bindings.map((binding) => binding.sessionId));
704
- const mainSessionIds = unboundSessionIds(allWorkspaceSessionIds, [
705
- ...boundSessionIds,
706
- ]);
707
- const visibleMainSessionIds = mainSessionIds.filter((sessionId) => workspaceMatchesQuery ||
708
- includesText(sessionLabel(sessionId, sessions), query));
819
+ const mainSessionIds = filterVisibleSessionIds(unboundSessionIds(allWorkspaceSessionIds, [...boundSessionIds]), sessions);
820
+ const visibleMainSessionIds = mainSessionIds.filter((sessionId) => workspaceMatchesQuery || sessionMatchesQuery(sessionId, sessions, query));
709
821
  const mainExpanded = expandedMains[workspace.workspaceId] !== false;
710
822
  const mainGroupKey = `main:${workspace.workspaceId}`;
711
823
  const worktrees = view?.worktrees ?? [];
824
+ const sameWorkspaceWorktreeDrag = worktreeDrag?.workspaceId === workspace.workspaceId;
712
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: {
713
826
  active: workspaceDrag !== undefined,
714
827
  marker: workspaceDrag?.over?.workspaceId === workspace.workspaceId
@@ -753,7 +866,7 @@ export function WorktreeSurface({ useStore, actions, useSessions, useWorkspaces,
753
866
  openWorkspaceDelete(workspace);
754
867
  }, onMenuOpenChange: (open) => {
755
868
  setOpenWorkspaceMenuId(open ? workspace.workspaceId : undefined);
756
- } }), 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: () => {
757
870
  toggleMain(workspace.workspaceId);
758
871
  }, onCreateSession: createMainSession === undefined
759
872
  ? undefined
@@ -778,17 +891,16 @@ export function WorktreeSurface({ useStore, actions, useSessions, useWorkspaces,
778
891
  }, onCommitDrag: commitSessionDrag, onOpen: (sessionId) => {
779
892
  openWorktreeSession({ open: openSession }, sessionId);
780
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) => {
781
- 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);
782
895
  const worktreeMatchesQuery = workspaceMatchesQuery ||
783
896
  includesText(record.branch, query) ||
784
897
  includesText(record.absolutePath, query);
785
898
  if (query.length > 0 &&
786
899
  !worktreeMatchesQuery &&
787
- !worktreeSessionIds.some((sessionId) => includesText(sessionLabel(sessionId, sessions), query))) {
900
+ !worktreeSessionIds.some((sessionId) => sessionMatchesQuery(sessionId, sessions, query))) {
788
901
  return null;
789
902
  }
790
- const visibleWorktreeSessionIds = worktreeSessionIds.filter((sessionId) => worktreeMatchesQuery ||
791
- includesText(sessionLabel(sessionId, sessions), query));
903
+ const visibleWorktreeSessionIds = worktreeSessionIds.filter((sessionId) => worktreeMatchesQuery || sessionMatchesQuery(sessionId, sessions, query));
792
904
  const worktreeGroupKey = `worktree:${record.worktreeId}`;
793
905
  const state = record.status === 'removed'
794
906
  ? 'warning'
@@ -801,9 +913,9 @@ export function WorktreeSurface({ useStore, actions, useSessions, useWorkspaces,
801
913
  ? t('worktree.repair')
802
914
  : t('worktree.ready');
803
915
  const worktreeExpanded = expandedWorktrees[record.worktreeId] !== false;
804
- 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: () => {
805
917
  toggleWorktree(record.worktreeId);
806
- }, onCreateSession: record.status === 'active'
918
+ }, onCreateSession: record.status === 'active' && record.health !== 'repair'
807
919
  ? () => {
808
920
  void createSession({
809
921
  workspaceId: record.workspaceId,
@@ -824,7 +936,38 @@ export function WorktreeSurface({ useStore, actions, useSessions, useWorkspaces,
824
936
  setActionError(undefined);
825
937
  },
826
938
  }
827
- : 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: () => {
828
971
  setExpandedSessionGroups((current) => ({
829
972
  ...current,
830
973
  [worktreeGroupKey]: current[worktreeGroupKey] !== true,
@@ -874,7 +1017,7 @@ export function WorktreeSurface({ useStore, actions, useSessions, useWorkspaces,
874
1017
  }
875
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: () => {
876
1019
  void confirmWorkspaceDelete();
877
- }, 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: () => {
878
1021
  if (actionPending)
879
1022
  return;
880
1023
  setWorktreeModalWorkspaceId(undefined);
@@ -884,11 +1027,13 @@ export function WorktreeSurface({ useStore, actions, useSessions, useWorkspaces,
884
1027
  selectedBranch.length === 0 ||
885
1028
  newBranch.trim().length === 0, onClick: () => {
886
1029
  void submitWorktree();
887
- }, 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) => {
888
- setSelectedBranch(event.currentTarget.value);
889
- }, 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) => {
890
- setNewBranch(event.currentTarget.value);
891
- } })] }), 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: () => {
892
1037
  if (actionPending)
893
1038
  return;
894
1039
  setWorktreeRemoval(undefined);
@@ -906,6 +1051,7 @@ export function WorktreeSurface({ useStore, actions, useSessions, useWorkspaces,
906
1051
  worktreeId: target.worktreeId,
907
1052
  },
908
1053
  });
1054
+ await invalidateWorktreeContext?.(target.workspaceId);
909
1055
  setWorktreeRemoval(undefined);
910
1056
  });
911
1057
  }, children: t('worktree.remove') })] })) }))] }));