@brimveyn/aimux 1.14.15 → 1.14.16

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.
@@ -7,6 +7,8 @@ import { getActiveKeymap } from '../../input/keymap/keymap-ref'
7
7
  import { getAllAssistantOptions } from '../../pty/command-registry'
8
8
  import { filterThemeIds } from '../../ui/filter-themes'
9
9
  import {
10
+ type BaseRefOption,
11
+ buildBaseRefOptions,
10
12
  filterAssistants,
11
13
  filterSessions,
12
14
  filterSnippets,
@@ -44,6 +46,17 @@ function getCurrentWorktreeIndex(state: AppState): number {
44
46
  )
45
47
  }
46
48
 
49
+ function getNewTabBaseOptions(state: AppState, queryOverride?: string): BaseRefOption[] {
50
+ if (state.modal.type !== 'new-tab') return []
51
+ const worktrees =
52
+ state.sessions.find((entry) => entry.id === state.currentSessionId)?.worktrees ?? []
53
+ return buildBaseRefOptions(
54
+ worktrees,
55
+ state.modal.baseBranches,
56
+ queryOverride ?? state.modal.baseQuery
57
+ )
58
+ }
59
+
47
60
  function getSelectedNewTabAssistant(state: AppState, assistantId?: string) {
48
61
  if (assistantId != null && assistantId !== '') {
49
62
  return getAllAssistantOptions(state.customCommands).find((entry) => entry.id === assistantId)
@@ -63,6 +76,9 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
63
76
  focusMode: 'command-edit',
64
77
  modal: {
65
78
  activeField: 'assistant',
79
+ baseBranches: [],
80
+ baseQuery: '',
81
+ baseRef: '',
66
82
  branchError: null,
67
83
  branchName: '',
68
84
  createWorktree: false,
@@ -75,19 +91,25 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
75
91
  step: 'assistant',
76
92
  targetWorktreeIndex,
77
93
  type: 'new-tab',
78
- worktreeDeleteConfirmId: null,
79
- worktreeDeleteMessage: null,
94
+ worktreeDeletePrompt: null,
80
95
  worktreeName: '',
81
96
  },
82
97
  }
83
98
  }
84
99
  case 'enter-new-tab-worktree-create': {
85
100
  if (state.modal.type !== 'new-tab') return state
101
+ // Default the base to the branch of the worktree we're forking from (the
102
+ // current target), preserving the previous always-fork-from-source
103
+ // behaviour until the user picks another base.
104
+ const session = state.sessions.find((entry) => entry.id === state.currentSessionId)
105
+ const sourceWorktree = session?.worktrees?.[state.modal.targetWorktreeIndex]
86
106
  return {
87
107
  ...state,
88
108
  modal: {
89
109
  ...state.modal,
90
110
  activeField: 'worktree-name',
111
+ baseQuery: '',
112
+ baseRef: sourceWorktree?.branch ?? '',
91
113
  createWorktree: true,
92
114
  cursorPos: state.modal.worktreeName.length,
93
115
  selectedIndex: 0,
@@ -95,6 +117,18 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
95
117
  },
96
118
  }
97
119
  }
120
+ case 'set-new-tab-base-branches': {
121
+ if (state.modal.type !== 'new-tab') return state
122
+ const modalWithBranches = { ...state.modal, baseBranches: action.branches }
123
+ const withBranches: AppState = { ...state, modal: modalWithBranches }
124
+ // Backfill a default base if none resolved yet (e.g. detached source).
125
+ if (state.modal.baseRef !== '') return withBranches
126
+ const firstOption = getNewTabBaseOptions(withBranches)[0]
127
+ return {
128
+ ...withBranches,
129
+ modal: { ...modalWithBranches, baseRef: firstOption?.ref ?? '' },
130
+ }
131
+ }
98
132
  case 'enter-new-tab-template-pick': {
99
133
  if (state.modal.type !== 'new-tab') return state
100
134
  return {
@@ -125,14 +159,13 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
125
159
  },
126
160
  }
127
161
  }
128
- case 'set-new-tab-worktree-delete-state': {
162
+ case 'set-new-tab-worktree-delete-prompt': {
129
163
  if (state.modal.type !== 'new-tab') return state
130
164
  return {
131
165
  ...state,
132
166
  modal: {
133
167
  ...state.modal,
134
- worktreeDeleteConfirmId: action.confirmWorktreeId ?? null,
135
- worktreeDeleteMessage: action.message,
168
+ worktreeDeletePrompt: action.prompt,
136
169
  },
137
170
  }
138
171
  }
@@ -192,8 +225,7 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
192
225
  selectedIndex: worktreeCount === 0 ? 0 : targetWorktreeIndex,
193
226
  step: 'worktree',
194
227
  targetWorktreeIndex,
195
- worktreeDeleteConfirmId: null,
196
- worktreeDeleteMessage: null,
228
+ worktreeDeletePrompt: null,
197
229
  worktreeName: state.modal.worktreeName || `wt-${option.label}`,
198
230
  },
199
231
  }
@@ -228,8 +260,7 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
228
260
  : Math.min(state.modal.selectedIndex, Math.max(0, worktreeCount - 1)),
229
261
  step: option && createWorktree ? 'worktree' : state.modal.step,
230
262
  targetWorktreeIndex,
231
- worktreeDeleteConfirmId: null,
232
- worktreeDeleteMessage: null,
263
+ worktreeDeletePrompt: null,
233
264
  worktreeName: defaultName,
234
265
  },
235
266
  }
@@ -284,6 +315,24 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
284
315
  modal: { ...state.modal, deleteSource: !state.modal.deleteSource },
285
316
  }
286
317
  }
318
+ case 'open-worktree-delete-confirm': {
319
+ return {
320
+ ...state,
321
+ focusMode: 'modal',
322
+ modal: {
323
+ closeTabs: action.closeTabs,
324
+ editBuffer: null,
325
+ force: action.force,
326
+ reason: action.reason,
327
+ selectedIndex: 0,
328
+ sessionId: action.sessionId,
329
+ sessionTargetId: null,
330
+ type: 'worktree-delete-confirm',
331
+ worktreeId: action.worktreeId,
332
+ worktreeLabel: action.worktreeLabel,
333
+ },
334
+ }
335
+ }
287
336
  case 'open-help-modal': {
288
337
  const keymap = getActiveKeymap()
289
338
  const scope = action.scope ?? null
@@ -616,7 +665,18 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
616
665
  return state
617
666
  }
618
667
  if (state.modal.type === 'new-tab' && state.modal.step === 'worktree-create') {
619
- return state
668
+ if (state.modal.activeField !== 'base') return state
669
+ const options = getNewTabBaseOptions(state)
670
+ if (options.length === 0) return state
671
+ const next = (state.modal.selectedIndex + action.delta + options.length) % options.length
672
+ return {
673
+ ...state,
674
+ modal: {
675
+ ...state.modal,
676
+ baseRef: options[next]?.ref ?? state.modal.baseRef,
677
+ selectedIndex: next,
678
+ },
679
+ }
620
680
  }
621
681
  let optionCount: number
622
682
  if (state.modal.type === 'new-tab') {
@@ -661,8 +721,7 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
661
721
  createWorktree:
662
722
  (state.modal.selectedIndex + action.delta + optionCount) % optionCount ===
663
723
  optionCount - 1,
664
- worktreeDeleteConfirmId: null,
665
- worktreeDeleteMessage: null,
724
+ worktreeDeletePrompt: null,
666
725
  }
667
726
  : null),
668
727
  },
@@ -683,7 +742,19 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
683
742
  return state
684
743
  }
685
744
  if (state.modal.type === 'new-tab' && state.modal.step === 'worktree-create') {
686
- return state
745
+ if (state.modal.activeField !== 'base') return state
746
+ const options = getNewTabBaseOptions(state)
747
+ if (options.length === 0) return state
748
+ const clamped = Math.max(0, Math.min(options.length - 1, action.index))
749
+ if (clamped === state.modal.selectedIndex) return state
750
+ return {
751
+ ...state,
752
+ modal: {
753
+ ...state.modal,
754
+ baseRef: options[clamped]?.ref ?? state.modal.baseRef,
755
+ selectedIndex: clamped,
756
+ },
757
+ }
687
758
  }
688
759
  let optionCount: number
689
760
  if (state.modal.type === 'help') {
@@ -724,8 +795,7 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
724
795
  ...state.modal,
725
796
  createWorktree: clamped === optionCount - 1,
726
797
  selectedIndex: clamped,
727
- worktreeDeleteConfirmId: null,
728
- worktreeDeleteMessage: null,
798
+ worktreeDeletePrompt: null,
729
799
  },
730
800
  }
731
801
  }
@@ -770,6 +840,8 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
770
840
  buffer = state.modal.worktreeName
771
841
  } else if (state.modal.activeField === 'branch-name') {
772
842
  buffer = state.modal.branchName
843
+ } else if (state.modal.activeField === 'base') {
844
+ buffer = state.modal.baseQuery
773
845
  }
774
846
  }
775
847
  if (
@@ -821,6 +893,24 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
821
893
  },
822
894
  }
823
895
  }
896
+ if (
897
+ state.modal.type === 'new-tab' &&
898
+ state.modal.editingCommand === null &&
899
+ state.modal.activeField === 'base'
900
+ ) {
901
+ // Re-filter and snap the selection/base to the top match as the query changes.
902
+ const topOption = getNewTabBaseOptions(state, nextBuffer)[0]
903
+ return {
904
+ ...state,
905
+ modal: {
906
+ ...state.modal,
907
+ baseQuery: nextBuffer,
908
+ baseRef: topOption?.ref ?? state.modal.baseRef,
909
+ cursorPos: nextCursor,
910
+ selectedIndex: 0,
911
+ },
912
+ }
913
+ }
824
914
  const resetIndex =
825
915
  !isNewTabEditing &&
826
916
  (state.modal.type === 'session-picker' ||
@@ -901,8 +991,7 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
901
991
  cursorPos: state.modal.editBuffer?.length ?? 0,
902
992
  selectedIndex: Math.max(0, assistantIndex),
903
993
  step: 'assistant',
904
- worktreeDeleteConfirmId: null,
905
- worktreeDeleteMessage: null,
994
+ worktreeDeletePrompt: null,
906
995
  },
907
996
  }
908
997
  }
@@ -991,8 +1080,38 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
991
1080
  if (state.modal.type === 'new-tab') {
992
1081
  if (state.modal.step === 'assistant') return state
993
1082
  if (state.modal.step === 'worktree-create') {
994
- const nextField: typeof state.modal.activeField =
995
- state.modal.activeField === 'worktree-name' ? 'branch-name' : 'worktree-name'
1083
+ const cycle: Record<
1084
+ 'worktree-name' | 'branch-name' | 'base',
1085
+ typeof state.modal.activeField
1086
+ > = {
1087
+ 'base': 'worktree-name',
1088
+ 'branch-name': 'base',
1089
+ 'worktree-name': 'branch-name',
1090
+ }
1091
+ const nextField =
1092
+ state.modal.activeField === 'worktree-name' ||
1093
+ state.modal.activeField === 'branch-name' ||
1094
+ state.modal.activeField === 'base'
1095
+ ? cycle[state.modal.activeField]
1096
+ : 'worktree-name'
1097
+ if (nextField === 'base') {
1098
+ // Highlight the row matching the resolved base ref when entering it.
1099
+ const currentBaseRef = state.modal.baseRef
1100
+ const options = getNewTabBaseOptions(state)
1101
+ const baseIndex = Math.max(
1102
+ 0,
1103
+ options.findIndex((option) => option.ref === currentBaseRef)
1104
+ )
1105
+ return {
1106
+ ...state,
1107
+ modal: {
1108
+ ...state.modal,
1109
+ activeField: nextField,
1110
+ cursorPos: state.modal.baseQuery.length,
1111
+ selectedIndex: baseIndex,
1112
+ },
1113
+ }
1114
+ }
996
1115
  const nextValue =
997
1116
  nextField === 'branch-name' ? state.modal.branchName : state.modal.worktreeName
998
1117
  return {
@@ -18,15 +18,23 @@ export function reduceSessionState(state: AppState, action: AppAction): AppState
18
18
  const snapshot =
19
19
  action.workspaceSnapshot ??
20
20
  state.sessions.find((entry) => entry.id === action.sessionId)?.workspaceSnapshot
21
- const restored = restoreWorkspaceState(state, snapshot, {
22
- forceDisconnected: action.forceDisconnected ?? true,
23
- })
24
21
  // The session's activeWorktreeId may have been patched right before
25
22
  // this load (e.g. the cross-workspace branch of handleCycleSidebarItem
26
23
  // sets it to the worktree the user just clicked). The snapshot's
27
24
  // activeTabId still reflects the *last* worktree they were on, so
28
25
  // honor the patched worktree by filtering the restored tab list.
29
26
  const loadedSession = state.sessions.find((entry) => entry.id === action.sessionId)
27
+ const loadedWorktrees = loadedSession?.worktrees ?? []
28
+ const restored = restoreWorkspaceState(state, snapshot, {
29
+ forceDisconnected: action.forceDisconnected ?? true,
30
+ // Drop tabs bound to worktrees this session no longer owns so a stale
31
+ // id can't be caught by a later worktree delete (closing "another
32
+ // worktree's" tabs) and so corrupted catalogs self-heal on load. Only
33
+ // prune once the session has worktrees — an empty list means it hasn't
34
+ // been initialized yet, and pruning then would wrongly drop bound tabs.
35
+ validWorktreeIds:
36
+ loadedWorktrees.length > 0 ? new Set(loadedWorktrees.map((w) => w.id)) : undefined,
37
+ })
30
38
  const visible = filterTabsForActiveWorktree(restored.tabs, loadedSession)
31
39
  // Prefer the snapshot's tab; if it isn't visible under the active
32
40
  // worktree (e.g. a multi-worktree session restored onto a different
@@ -188,22 +196,6 @@ export function reduceSessionState(state: AppState, action: AppAction): AppState
188
196
  : withActiveWorktree(next, action.worktree.id)
189
197
  }),
190
198
  }
191
- case 'remove-worktree-record':
192
- return {
193
- ...state,
194
- sessions: state.sessions.map((session) => {
195
- if (session.id !== action.sessionId) return session
196
- const remaining = (session.worktrees ?? []).filter((w) => w.id !== action.worktreeId)
197
- if (remaining.length === 0) return session
198
- if (session.activeWorktreeId !== action.worktreeId) {
199
- return { ...session, updatedAt: new Date().toISOString(), worktrees: remaining }
200
- }
201
- return withActiveWorktree(
202
- { ...session, activeWorktreeId: remaining[0]?.id, worktrees: remaining },
203
- remaining[0]?.id ?? ''
204
- )
205
- }),
206
- }
207
199
  case 'set-active-worktree': {
208
200
  const sessions = state.sessions.map((session) =>
209
201
  session.id === action.sessionId ? withActiveWorktree(session, action.worktreeId) : session
@@ -469,6 +469,23 @@ export function reduceTabState(state: AppState, action: AppAction): AppState | n
469
469
  }
470
470
  return { ...state, tabs }
471
471
  }
472
+ case 'reorder-tabs': {
473
+ // Drag-and-drop reorder of the visible tab strip. `orderedTabIds` is the
474
+ // flattened new order of the currently-visible tabs (group members already
475
+ // expanded into contiguous runs). We only rewrite the slots those tabs
476
+ // occupy in `state.tabs`, leaving tabs from other worktrees anchored in
477
+ // place. Because group members arrive contiguous, splits stay intact.
478
+ const visibleSet = new Set(action.orderedTabIds)
479
+ const byId = new Map(state.tabs.map((tab) => [tab.id, tab]))
480
+ let cursor = 0
481
+ const tabs = state.tabs.map((tab) => {
482
+ if (!visibleSet.has(tab.id)) return tab
483
+ const nextId = action.orderedTabIds[cursor]
484
+ cursor++
485
+ return (nextId != null ? byId.get(nextId) : undefined) ?? tab
486
+ })
487
+ return { ...state, tabs }
488
+ }
472
489
  case 'reset-tab-session':
473
490
  return withActiveTabWorktree(
474
491
  {
@@ -1,5 +1,49 @@
1
1
  import type { AssistantOption } from '../pty/command-registry'
2
- import type { AssistantId, SessionRecord, SnippetRecord } from './types'
2
+ import type { AssistantId, SessionRecord, SnippetRecord, WorktreeRecord } from './types'
3
+
4
+ export interface BaseRefOption {
5
+ /** Git ref the new worktree is forked from. */
6
+ ref: string
7
+ label: string
8
+ kind: 'worktree' | 'branch'
9
+ /** Worktree name, for the 'worktree' kind. */
10
+ detail?: string
11
+ }
12
+
13
+ /**
14
+ * Ordered, filtered base-ref candidates for the worktree-create "Base" picker:
15
+ * branches checked out in the session's worktrees first (labelled with the
16
+ * worktree name), then the remaining local branches. A branch already surfaced
17
+ * via a worktree is not repeated. Throwaway `aimux/` branches are skipped unless
18
+ * a live worktree is on them — `git worktree remove` leaves the branch behind,
19
+ * so deleted temp worktrees would otherwise haunt the list as orphan branches.
20
+ */
21
+ export function buildBaseRefOptions(
22
+ worktrees: WorktreeRecord[],
23
+ localBranches: string[],
24
+ query: string
25
+ ): BaseRefOption[] {
26
+ const seen = new Set<string>()
27
+ const options: BaseRefOption[] = []
28
+ for (const worktree of worktrees) {
29
+ if (worktree.branch == null || worktree.branch === '' || seen.has(worktree.branch)) continue
30
+ seen.add(worktree.branch)
31
+ options.push({
32
+ detail: worktree.name,
33
+ kind: 'worktree',
34
+ label: worktree.branch,
35
+ ref: worktree.branch,
36
+ })
37
+ }
38
+ for (const branch of localBranches) {
39
+ if (seen.has(branch) || branch.startsWith('aimux/')) continue
40
+ seen.add(branch)
41
+ options.push({ kind: 'branch', label: branch, ref: branch })
42
+ }
43
+ const trimmed = query.trim().toLowerCase()
44
+ if (trimmed === '') return options
45
+ return options.filter((option) => option.label.toLowerCase().includes(trimmed))
46
+ }
3
47
 
4
48
  /**
5
49
  * 0 when the template picker should NOT show the "None" fallback (no assistant
@@ -72,6 +72,27 @@ export interface RestoreOptions {
72
72
  // will overwrite the status with daemon truth — leaving the flag on would
73
73
  // briefly flash the "Restored snapshot" hint on every j/k cycle.
74
74
  forceDisconnected?: boolean
75
+ // When provided, drop tabs pinned to a worktree id that the session no
76
+ // longer owns. Some delete paths (notably the sidebar's "Remove worktree")
77
+ // historically removed the worktree record without closing its tabs, leaving
78
+ // orphans bound to a vanished id. Those orphans are invisible (filtered out
79
+ // by the active-worktree filter) yet keep a worktree id that a *future*
80
+ // delete can collide with — exactly what closes "another worktree's" tabs.
81
+ // Pruning them on restore both repairs corrupted catalogs and prevents the
82
+ // collision. Tabs with no worktree id (legacy/unbound) are always kept.
83
+ validWorktreeIds?: ReadonlySet<string>
84
+ }
85
+
86
+ // Drop tabs bound to a worktree id the session no longer owns. Unbound tabs
87
+ // (no worktreeId) are kept — they surface under the primary worktree.
88
+ function pruneOrphanedTabs(
89
+ tabs: TabSession[],
90
+ validWorktreeIds: ReadonlySet<string> | undefined
91
+ ): TabSession[] {
92
+ if (!validWorktreeIds) return tabs
93
+ return tabs.filter(
94
+ (tab) => tab.worktreeId == null || tab.worktreeId === '' || validWorktreeIds.has(tab.worktreeId)
95
+ )
75
96
  }
76
97
 
77
98
  export function restoreTabsFromWorkspace(
@@ -84,7 +105,7 @@ export function restoreTabsFromWorkspace(
84
105
 
85
106
  const forceDisconnected = options.forceDisconnected ?? true
86
107
 
87
- return snapshot.tabs
108
+ const restored: TabSession[] = snapshot.tabs
88
109
  .filter(
89
110
  (tab): tab is typeof tab & { status: Exclude<typeof tab.status, 'exited'> } =>
90
111
  tab.status !== 'exited'
@@ -103,6 +124,7 @@ export function restoreTabsFromWorkspace(
103
124
  viewport: tab.viewport,
104
125
  worktreeId: tab.worktreeId,
105
126
  }))
127
+ return pruneOrphanedTabs(restored, options.validWorktreeIds)
106
128
  }
107
129
 
108
130
  export function restoreLayoutTrees(
@@ -47,6 +47,7 @@ export type ModalType =
47
47
  | 'update-available'
48
48
  | 'ai-usage'
49
49
  | 'worktree-move'
50
+ | 'worktree-delete-confirm'
50
51
  | null
51
52
 
52
53
  export interface TerminalSpan {
@@ -323,16 +324,21 @@ export interface ModalClosed extends ModalBase {
323
324
  export interface ModalNewTab extends ModalBase {
324
325
  type: 'new-tab'
325
326
  editingCommand: AssistantId | null
326
- activeField: 'assistant' | 'branch-name' | 'target-worktree' | 'worktree-name'
327
+ activeField: 'assistant' | 'branch-name' | 'target-worktree' | 'worktree-name' | 'base'
327
328
  branchError: string | null
328
329
  branchName: string
329
330
  createWorktree: boolean
330
331
  selectedAssistantId: AssistantId | null
331
332
  step: 'assistant' | 'worktree' | 'worktree-create' | 'template'
332
333
  targetWorktreeIndex: number
333
- worktreeDeleteConfirmId: string | null
334
- worktreeDeleteMessage: string | null
334
+ worktreeDeletePrompt: { worktreeId: string; reason: string } | null
335
335
  worktreeName: string
336
+ /** Filter text typed into the "Base" picker on the worktree-create step. */
337
+ baseQuery: string
338
+ /** Resolved base ref the new worktree is forked from (branch of a worktree or a local branch). */
339
+ baseRef: string
340
+ /** Local branches available as base refs, loaded when the create step opens. */
341
+ baseBranches: string[]
336
342
  }
337
343
 
338
344
  export interface ModalSessionPicker extends ModalBase {
@@ -417,6 +423,22 @@ export interface ModalWorktreeMove extends ModalBase {
417
423
  deleteSource: boolean
418
424
  }
419
425
 
426
+ /**
427
+ * Standalone confirmation for a recoverable worktree delete failure triggered
428
+ * outside the new-tab picker (e.g. the sidebar's "Remove worktree"). Carries the
429
+ * params needed to re-run the delete with force once confirmed.
430
+ */
431
+ export interface ModalWorktreeDeleteConfirm extends ModalBase {
432
+ type: 'worktree-delete-confirm'
433
+ sessionId: string
434
+ worktreeId: string
435
+ worktreeLabel: string
436
+ reason: string
437
+ closeTabs: boolean
438
+ /** Whether confirming force-deletes — true only after a recoverable failure. */
439
+ force: boolean
440
+ }
441
+
420
442
  export type DirectoryResultType = 'git-repo' | 'worktree' | 'workspace'
421
443
 
422
444
  export interface DirectoryResult {
@@ -440,6 +462,7 @@ export type ModalState =
440
462
  | ModalUpdateAvailable
441
463
  | ModalAIUsage
442
464
  | ModalWorktreeMove
465
+ | ModalWorktreeDeleteConfirm
443
466
 
444
467
  export interface LayoutState {
445
468
  terminalCols: number
@@ -515,10 +538,10 @@ export type ModalAction =
515
538
  | { type: 'open-new-tab-modal' }
516
539
  | { type: 'set-new-tab-branch-error'; message: string | null }
517
540
  | {
518
- type: 'set-new-tab-worktree-delete-state'
519
- confirmWorktreeId?: string | null
520
- message: string | null
541
+ type: 'set-new-tab-worktree-delete-prompt'
542
+ prompt: { worktreeId: string; reason: string } | null
521
543
  }
544
+ | { type: 'set-new-tab-base-branches'; branches: string[] }
522
545
  | { type: 'enter-new-tab-worktree-create' }
523
546
  | { type: 'enter-new-tab-template-pick' }
524
547
  | { type: 'enter-new-tab-template-shortcut' }
@@ -553,6 +576,15 @@ export type ModalAction =
553
576
  | { type: 'open-ai-usage-modal' }
554
577
  | { type: 'open-worktree-move-modal'; sourceWorktreeId: string }
555
578
  | { type: 'toggle-worktree-move-delete' }
579
+ | {
580
+ type: 'open-worktree-delete-confirm'
581
+ sessionId: string
582
+ worktreeId: string
583
+ worktreeLabel: string
584
+ reason: string
585
+ closeTabs: boolean
586
+ force: boolean
587
+ }
556
588
 
557
589
  // -- Session actions --
558
590
  export type SessionAction =
@@ -570,7 +602,6 @@ export type SessionAction =
570
602
  | { type: 'reorder-active-session'; delta: number }
571
603
  | { type: 'set-session-status'; sessionId: string; status: SessionStatus }
572
604
  | { type: 'add-worktree-record'; sessionId: string; worktree: WorktreeRecord; activate?: boolean }
573
- | { type: 'remove-worktree-record'; sessionId: string; worktreeId: string }
574
605
  | { type: 'set-active-worktree'; sessionId: string; worktreeId: string }
575
606
  | {
576
607
  type: 'update-worktree-record'
@@ -595,6 +626,7 @@ export type TabAction =
595
626
  | { type: 'set-active-tab'; tabId: string }
596
627
  | { type: 'move-active-tab'; delta: number }
597
628
  | { type: 'reorder-active-tab'; delta: number }
629
+ | { type: 'reorder-tabs'; orderedTabIds: string[] }
598
630
  | { type: 'reset-tab-session'; tabId: string }
599
631
  | { type: 'rename-tab'; tabId: string; title: string }
600
632
  | { type: 'append-tab-buffer'; tabId: string; chunk: string }
@@ -68,14 +68,24 @@ export const WorktreeRow = memo(function WorktreeRow({
68
68
  [
69
69
  'Remove worktree',
70
70
  () =>
71
+ // Always confirm first. Confirming routes through the full delete side
72
+ // effect (closes the worktree's tabs, disposes their PTYs, prunes the
73
+ // snapshot, removes the git worktree). closeTabs (not force) cleans up
74
+ // the tabs while keeping the non-force `git worktree remove`, so
75
+ // uncommitted work in a temp worktree is still protected — a dirty
76
+ // worktree re-prompts for an explicit force-delete.
71
77
  dispatchGlobal({
78
+ closeTabs: true,
79
+ force: false,
80
+ reason: 'Its assistant tabs will be closed and the worktree removed.',
72
81
  sessionId: session.id,
73
- type: 'remove-worktree-record',
82
+ type: 'open-worktree-delete-confirm',
74
83
  worktreeId: worktree.id,
84
+ worktreeLabel: worktree.branch ?? worktree.name,
75
85
  }),
76
86
  ],
77
87
  ]
78
- }, [session.id, worktree.id, worktree.source])
88
+ }, [session.id, worktree.branch, worktree.id, worktree.name, worktree.source])
79
89
 
80
90
  let bgColor: string | undefined
81
91
  if (isActiveItem) {