@brimveyn/aimux-config 0.5.17 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brimveyn/aimux-config",
3
- "version": "0.5.17",
3
+ "version": "0.6.2",
4
4
  "description": "TypeScript configuration API for aimux — keymaps, themes, and backends with a fluent builder.",
5
5
  "keywords": [
6
6
  "aimux",
package/src/actions.ts CHANGED
@@ -80,7 +80,7 @@ export const splitHorizontal: KeyResult = r(
80
80
  )
81
81
 
82
82
  export const enterInsert: ActionFn = (ctx: ModeContext) => {
83
- if (!ctx.state.activeTabId) return null
83
+ if (!(ctx.state.activeTabId != null && ctx.state.activeTabId !== '')) return null
84
84
  return r([{ focusMode: 'terminal-input', type: 'set-focus-mode' }], [], 'terminal-input')
85
85
  }
86
86
 
@@ -88,13 +88,15 @@ export const closeModal: KeyResult = r([{ type: 'close-modal' }], [], 'navigatio
88
88
 
89
89
  export const closeOverlayModal: KeyResult = r([{ type: 'close-modal' }])
90
90
 
91
+ export const cancelNewTabModal: KeyResult = r([{ type: 'cancel-command-edit' }])
92
+
91
93
  // ---------------------------------------------------------------------------
92
94
  // Dynamic actions (need ctx at runtime — ActionFn)
93
95
  // ---------------------------------------------------------------------------
94
96
 
95
97
  export const closeTab: ActionFn = (ctx: ModeContext) => {
96
98
  const tabId = ctx.state.activeTabId
97
- if (!tabId) return null
99
+ if (!(tabId != null && tabId !== '')) return null
98
100
  return r([{ type: 'close-active-tab' }], [{ tabId, type: 'close-tab' }])
99
101
  }
100
102
 
@@ -153,7 +155,7 @@ export function focusPane(direction: 'left' | 'right' | 'up' | 'down'): KeyResul
153
155
  export function resizePane(delta: number, axis: 'horizontal' | 'vertical'): ActionFn {
154
156
  return (ctx: ModeContext) => {
155
157
  const tabId = ctx.state.activeTabId
156
- if (!tabId) return null
158
+ if (!(tabId != null && tabId !== '')) return null
157
159
  return r([{ axis, delta, tabId, type: 'resize-pane' }])
158
160
  }
159
161
  }
@@ -173,7 +175,45 @@ export function moveModalSelectionWithPreview(
173
175
  // Modal-specific actions
174
176
  // ---------------------------------------------------------------------------
175
177
 
176
- export const launchSelectedAssistant: KeyResult = r([], [{ type: 'launch-selected-assistant' }])
178
+ export const launchSelectedAssistant: ActionFn = (ctx: ModeContext) => {
179
+ if (ctx.state.modal.type === 'new-tab' && ctx.state.modal.step === 'assistant') {
180
+ return r([{ type: 'select-new-tab-assistant' }])
181
+ }
182
+ if (
183
+ ctx.state.modal.type === 'new-tab' &&
184
+ ctx.state.modal.step === 'worktree' &&
185
+ ctx.state.modal.createWorktree
186
+ ) {
187
+ return r([{ type: 'enter-new-tab-worktree-create' }])
188
+ }
189
+ return r([], [{ type: 'launch-selected-assistant' }])
190
+ }
191
+
192
+ export const toggleNewTabWorktree: KeyResult = r([{ type: 'toggle-new-tab-worktree' }])
193
+
194
+ export const deleteSelectedWorktree: ActionFn = (ctx: ModeContext) => {
195
+ const modal = ctx.state.modal
196
+ const sessionId = ctx.state.currentSessionId
197
+ if (modal.type !== 'new-tab' || modal.step !== 'worktree' || modal.createWorktree) return null
198
+ if (!(sessionId != null && sessionId !== '')) return null
199
+ const session = ctx.state.sessions.find((entry) => entry.id === sessionId)
200
+ const worktree = session?.worktrees?.[modal.selectedIndex]
201
+ if (!worktree) return null
202
+ return r(
203
+ [
204
+ { index: modal.selectedIndex, type: 'set-modal-selection-index' },
205
+ { message: null, type: 'set-new-tab-worktree-delete-state' },
206
+ ],
207
+ [
208
+ {
209
+ force: modal.worktreeDeleteConfirmId === worktree.id,
210
+ sessionId,
211
+ type: 'delete-worktree',
212
+ worktreeId: worktree.id,
213
+ },
214
+ ]
215
+ )
216
+ }
177
217
 
178
218
  export const cancelCommandEdit = (returnTo: KeyResult['transition']): KeyResult =>
179
219
  r([{ type: 'cancel-command-edit' }], [], returnTo)
@@ -298,9 +338,66 @@ export const confirmUpdateSelection: KeyResult = r(
298
338
  'navigation'
299
339
  )
300
340
 
341
+ // ---------------------------------------------------------------------------
342
+ // Worktree-move modal
343
+ // ---------------------------------------------------------------------------
344
+
345
+ export const openWorktreeMove: ActionFn = (ctx: ModeContext) => {
346
+ if (ctx.state.gitMode.headOffset > 0) {
347
+ return r([
348
+ {
349
+ message: 'disabled while viewing HEAD~N (press 0 or [ to return)',
350
+ type: 'git-mode-set-message',
351
+ },
352
+ ])
353
+ }
354
+ const session = ctx.state.sessions.find((entry) => entry.id === ctx.state.currentSessionId)
355
+ const worktrees = session?.worktrees ?? []
356
+ // From git mode, the source is the active worktree (the one being reviewed).
357
+ const source = worktrees.find((w) => w.id === session?.activeWorktreeId) ?? worktrees[0]
358
+ const hasBranch = source != null && source.branch != null && source.branch !== ''
359
+ const others = worktrees.filter((w) => w.id !== source?.id)
360
+ if (!hasBranch || source == null || others.length < 1) {
361
+ return r([{ message: 'no other worktree to move into', type: 'git-mode-set-message' }])
362
+ }
363
+ // Overlay: no mode transition — deriveModeId routes input to the picker and
364
+ // focusMode stays 'git' so the git view remains mounted underneath.
365
+ return r([{ sourceWorktreeId: source.id, type: 'open-worktree-move-modal' }])
366
+ }
367
+
368
+ export const toggleWorktreeMoveDelete: KeyResult = r([{ type: 'toggle-worktree-move-delete' }])
369
+
370
+ export const confirmWorktreeMove: ActionFn = (ctx: ModeContext) => {
371
+ const modal = ctx.state.modal
372
+ const session = ctx.state.sessions.find((entry) => entry.id === ctx.state.currentSessionId)
373
+ const worktrees = session?.worktrees ?? []
374
+ const sourceId = modal.type === 'worktree-move' ? modal.sourceWorktreeId : undefined
375
+ const source = worktrees.find((w) => w.id === sourceId)
376
+ const targets = worktrees.filter((w) => w.id !== sourceId)
377
+ const selectedIndex = modal.type === 'worktree-move' ? modal.selectedIndex : 0
378
+ const target = targets[selectedIndex]
379
+ if (!target || !session || !source || modal.type !== 'worktree-move') {
380
+ return r([{ type: 'close-modal' }])
381
+ }
382
+ // Overlay close keeps focusMode 'git' (see close-modal reducer), so the move
383
+ // result lands back in the git view that was underneath the picker.
384
+ return r(
385
+ [{ type: 'close-modal' }],
386
+ [
387
+ {
388
+ deleteSource: modal.deleteSource,
389
+ sessionId: session.id,
390
+ sourceWorktreeId: source.id,
391
+ targetWorktreeId: target.id,
392
+ type: 'move-worktree',
393
+ },
394
+ ]
395
+ )
396
+ }
397
+
301
398
  export const closePane: ActionFn = (ctx: ModeContext) => {
302
399
  const tabId = ctx.state.activeTabId
303
- if (!tabId) return null
400
+ if (!(tabId != null && tabId !== '')) return null
304
401
  return r(
305
402
  [
306
403
  { tabId, type: 'close-pane' },
@@ -345,7 +442,9 @@ export const confirmSessionRename: ActionFn = (ctx: ModeContext) => {
345
442
  ? 'modal.session-picker.filtering'
346
443
  : 'navigation'
347
444
  const effects: KeyResult['effects'] =
348
- trimmed && sessionId ? [{ name: trimmed, sessionId, type: 'rename-session' }] : []
445
+ trimmed && sessionId != null && sessionId !== ''
446
+ ? [{ name: trimmed, sessionId, type: 'rename-session' }]
447
+ : []
349
448
  return r([closeAction], effects, transition)
350
449
  }
351
450
 
@@ -354,7 +453,7 @@ export const confirmRenameTab: ActionFn = (ctx: ModeContext) => {
354
453
  const trimmed = (ctx.state.modal.editBuffer ?? '').trim()
355
454
  const tabId = ctx.state.modal.sessionTargetId
356
455
  const actions: KeyResult['actions'] = []
357
- if (trimmed && tabId) {
456
+ if (trimmed && tabId != null && tabId !== '') {
358
457
  actions.push({ tabId, title: trimmed, type: 'rename-tab' })
359
458
  }
360
459
  actions.push({ type: 'close-modal' })
@@ -387,14 +486,14 @@ export const confirmCreateSession: ActionFn = (ctx: ModeContext) => {
387
486
  }
388
487
 
389
488
  function getDefaultSessionName(projectPath?: string): string {
390
- if (!projectPath) return ''
489
+ if (!(projectPath != null && projectPath !== '')) return ''
391
490
  const segments = projectPath.split('/').filter(Boolean)
392
491
  return segments.at(-1) ?? ''
393
492
  }
394
493
 
395
494
  // Session picker escape (conditional)
396
495
  export const sessionPickerEscape: ActionFn = (ctx: ModeContext) => {
397
- if (!ctx.state.currentSessionId) return null
496
+ if (!(ctx.state.currentSessionId != null && ctx.state.currentSessionId !== '')) return null
398
497
  return r([{ type: 'close-modal' }], [], 'navigation')
399
498
  }
400
499
 
@@ -420,15 +519,19 @@ function clearPendingDelete(ctx: ModeContext): AppAction[] {
420
519
  return [{ path: null, type: 'git-mode-set-pending-delete' }]
421
520
  }
422
521
 
423
- function gitFileKey(section: string, path: string): string {
424
- return `${section}:${path}`
522
+ function gitFileKey(section: string, path: string, repoPath?: string): string {
523
+ return repoPath != null && repoPath !== ''
524
+ ? `${section}:${repoPath}:${path}`
525
+ : `${section}:${path}`
425
526
  }
426
527
 
427
528
  function selectedGitFile(ctx: ModeContext) {
428
529
  const key = ctx.state.gitMode.selectedEntryKey
429
- if (!key) return null
530
+ if (!(key != null && key !== '')) return null
430
531
  return (
431
- ctx.state.gitPanel.files.find((file) => gitFileKey(file.section, file.path) === key) ?? null
532
+ ctx.state.gitPanel.files.find(
533
+ (file) => gitFileKey(file.section, file.path, file.repoPath) === key
534
+ ) ?? null
432
535
  )
433
536
  }
434
537
 
@@ -449,17 +552,20 @@ export function selectGitFileOnly(delta: -1 | 1): ActionFn {
449
552
  }
450
553
 
451
554
  export const toggleSelectedGitFolder: ActionFn = (ctx: ModeContext) => {
452
- if (!ctx.state.gitMode.selectedEntryKey) return r([])
555
+ if (!(ctx.state.gitMode.selectedEntryKey != null && ctx.state.gitMode.selectedEntryKey !== ''))
556
+ return r([])
453
557
  return r([{ type: 'git-mode-toggle-selected-folder' }])
454
558
  }
455
559
 
456
560
  export const collapseGitSelection: ActionFn = (ctx: ModeContext) => {
457
- if (!ctx.state.gitMode.selectedEntryKey) return r([])
561
+ if (!(ctx.state.gitMode.selectedEntryKey != null && ctx.state.gitMode.selectedEntryKey !== ''))
562
+ return r([])
458
563
  return r([{ type: 'git-mode-collapse-selection' }])
459
564
  }
460
565
 
461
566
  export const expandGitSelection: ActionFn = (ctx: ModeContext) => {
462
- if (!ctx.state.gitMode.selectedEntryKey) return r([])
567
+ if (!(ctx.state.gitMode.selectedEntryKey != null && ctx.state.gitMode.selectedEntryKey !== ''))
568
+ return r([])
463
569
  return r([{ type: 'git-mode-expand-selection' }])
464
570
  }
465
571
 
@@ -485,6 +591,8 @@ export function scrollGitDiff(delta: number): KeyResult {
485
591
 
486
592
  export const toggleGitDiffView: KeyResult = r([{ type: 'git-mode-toggle-diff-view' }])
487
593
 
594
+ export const toggleGitReviewBase: KeyResult = r([{ type: 'git-mode-toggle-review-base' }])
595
+
488
596
  export function shiftGitHeadOffset(delta: number): KeyResult {
489
597
  return r([
490
598
  { delta, type: 'git-mode-shift-head-offset' },
@@ -561,7 +669,7 @@ export const gitDestructiveSelected: ActionFn = (ctx: ModeContext) => {
561
669
  export const gitToggleFoldAll: ActionFn = (ctx: ModeContext) => {
562
670
  const file = selectedGitFile(ctx)
563
671
  if (!file) return r([])
564
- const key = gitFileKey(file.section, file.path)
672
+ const key = gitFileKey(file.section, file.path, file.repoPath)
565
673
  return r([{ key, type: 'git-mode-fold-toggle-all' }])
566
674
  }
567
675
 
@@ -687,7 +795,7 @@ export const gitCommitEnterConfirm: ActionFn = (ctx: ModeContext) => {
687
795
  return r([{ type: 'git-commit-enter-confirm' }], [], 'modal.git-commit.confirm')
688
796
  }
689
797
  const sessionId = ctx.state.currentSessionId
690
- if (!sessionId) {
798
+ if (!(sessionId != null && sessionId !== '')) {
691
799
  return r([{ type: 'git-commit-enter-confirm' }], [], 'modal.git-commit.confirm')
692
800
  }
693
801
  return r(
@@ -706,7 +814,7 @@ export const gitCommitLeaveConfirm: KeyResult = r(
706
814
  export const gitCommitLeaveGenerating: ActionFn = (ctx: ModeContext) => {
707
815
  const sessionId = ctx.state.currentSessionId
708
816
  const actionsList: AppAction[] = [{ type: 'git-commit-leave-generating' }]
709
- if (sessionId) {
817
+ if (sessionId != null && sessionId !== '') {
710
818
  actionsList.push({ sessionId, type: 'auto-commit-clear' })
711
819
  }
712
820
  return r(actionsList, [], 'modal.git-commit')
package/src/backends.ts CHANGED
@@ -6,7 +6,7 @@ import type { BackendConfig } from './types'
6
6
  */
7
7
  export function claudeBackend(opts?: { model?: string }): BackendConfig {
8
8
  return {
9
- args: opts?.model ? ['--model', opts.model] : [],
9
+ args: opts?.model != null && opts.model !== '' ? ['--model', opts.model] : [],
10
10
  command: 'claude',
11
11
  }
12
12
  }
package/src/defaults.ts CHANGED
@@ -104,8 +104,10 @@ export function getDefaultKeymapConfig(): ResolvedKeymapConfig {
104
104
  .map('e', actions.gitToggleFoldAll, 'Expand/collapse all folds')
105
105
  .map('o', actions.openSelectedGitFileInEditor, 'Open in editor')
106
106
  .map('c', actions.gitCommitOpen, 'Commit')
107
+ .map('m', actions.openWorktreeMove, 'Move worktree')
107
108
  .map('p', actions.gitPush, 'Push')
108
109
  .map('v', actions.toggleGitDiffView, 'Toggle split/stacked')
110
+ .map('b', actions.toggleGitReviewBase, 'Review vs base')
109
111
  .map('t', actions.toggleGitFileListMode, 'Toggle flat/tree')
110
112
  .map('T', actions.toggleTreeCompaction, 'Toggle tree compaction')
111
113
  .map(']', actions.shiftGitHeadOffset(-1), 'Newer commit')
@@ -182,6 +184,22 @@ export function getDefaultKeymapConfig(): ResolvedKeymapConfig {
182
184
  .map('<CR>', actions.confirmUpdateSelection, 'Confirm')
183
185
  )
184
186
 
187
+ // -----------------------------------------------------------------------
188
+ // Modal: worktree-move
189
+ // -----------------------------------------------------------------------
190
+ .mode('modal.worktree-move', (m) =>
191
+ m
192
+ .map('<Esc>', actions.closeModal, 'Cancel')
193
+ .map('j', actions.moveModalSelection(1), 'Next')
194
+ .map('k', actions.moveModalSelection(-1), 'Prev')
195
+ .map('<Down>', actions.moveModalSelection(1))
196
+ .map('<Up>', actions.moveModalSelection(-1))
197
+ .map('<C-n>', actions.moveModalSelection(1))
198
+ .map('<C-p>', actions.moveModalSelection(-1))
199
+ .map('d', actions.toggleWorktreeMoveDelete, 'Toggle delete source')
200
+ .map('<CR>', actions.confirmWorktreeMove, 'Move')
201
+ )
202
+
185
203
  // -----------------------------------------------------------------------
186
204
  // Modal: rename-tab
187
205
  // -----------------------------------------------------------------------
@@ -197,8 +215,11 @@ export function getDefaultKeymapConfig(): ResolvedKeymapConfig {
197
215
  // -----------------------------------------------------------------------
198
216
  .mode('modal.new-tab.command-edit', (m) =>
199
217
  m
200
- .map('<Esc>', actions.closeModal, 'Cancel')
218
+ .map('<Esc>', actions.cancelNewTabModal, 'Cancel')
201
219
  .map('<CR>', actions.launchSelectedAssistant, 'Launch')
220
+ .map('<C-w>', actions.toggleNewTabWorktree, 'WT')
221
+ .map('<C-d>', actions.deleteSelectedWorktree, 'Delete WT')
222
+ .map('<Tab>', actions.switchField, 'Next field')
202
223
  .map('<C-n>', actions.moveModalSelection(1), 'Next')
203
224
  .map('<C-p>', actions.moveModalSelection(-1), 'Prev')
204
225
  .map('<Down>', actions.moveModalSelection(1))
@@ -76,7 +76,7 @@ export const TUI_THEMES: Record<string, TuiThemeJson> = {
76
76
  'zenburn': zenburn as TuiThemeJson,
77
77
  }
78
78
 
79
- export type ThemeId = keyof typeof TUI_THEMES & string
79
+ export type ThemeId = keyof typeof TUI_THEMES
80
80
 
81
81
  export const THEME_IDS: ThemeId[] = Object.keys(TUI_THEMES).sort() as ThemeId[]
82
82
 
@@ -12,13 +12,7 @@ const TRANSPARENT: RGBA = { a: 0, b: 0, g: 0, r: 0 }
12
12
  function rgbaFromHex(hex: string): RGBA {
13
13
  const h = hex.replace('#', '')
14
14
  // expand short forms (#rgb, #rgba)
15
- const expanded =
16
- h.length === 3 || h.length === 4
17
- ? h
18
- .split('')
19
- .map((c) => c + c)
20
- .join('')
21
- : h
15
+ const expanded = h.length === 3 || h.length === 4 ? h.replaceAll(/(.)/g, '$1$1') : h
22
16
  if (expanded.length === 6) {
23
17
  const num = parseInt(expanded, 16)
24
18
  return {
package/src/types.ts CHANGED
@@ -27,6 +27,7 @@ export type ModeId =
27
27
  | 'modal.git-commit.confirm'
28
28
  | 'modal.git-commit.generating'
29
29
  | 'modal.update-available'
30
+ | 'modal.worktree-move'
30
31
  | 'modal.ai-usage'
31
32
 
32
33
  // ─── Primitive app types ──────────────────────────────────────────────────────
@@ -82,8 +83,11 @@ export interface TerminalModeState {
82
83
 
83
84
  // ─── Layout ───────────────────────────────────────────────────────────────────
84
85
 
85
- export type LayoutLeaf = { type: 'leaf'; tabId: string }
86
- export type LayoutSplit = {
86
+ export interface LayoutLeaf {
87
+ type: 'leaf'
88
+ tabId: string
89
+ }
90
+ export interface LayoutSplit {
87
91
  type: 'split'
88
92
  direction: SplitDirection
89
93
  ratio: number
@@ -105,6 +109,7 @@ export interface PersistedTabSnapshot {
105
109
  terminalModes: TerminalModeState
106
110
  errorMessage?: string
107
111
  exitCode?: number
112
+ worktreeId?: string
108
113
  }
109
114
 
110
115
  export interface WorkspaceSnapshotV1 {
@@ -121,6 +126,23 @@ export interface WorkspaceSnapshotV1 {
121
126
  tabGroupMap?: Record<string, string>
122
127
  }
123
128
 
129
+ export type WorktreeSource = 'primary' | 'aimux-temp' | 'external'
130
+
131
+ export interface WorktreeRecord {
132
+ id: string
133
+ name: string
134
+ path: string
135
+ repoRoot: string
136
+ branch?: string
137
+ baseRef?: string
138
+ commitSha?: string
139
+ source: WorktreeSource
140
+ createdByAimux: boolean
141
+ color?: string
142
+ createdAt: string
143
+ updatedAt: string
144
+ }
145
+
124
146
  export interface SessionRecord {
125
147
  id: string
126
148
  name: string
@@ -130,6 +152,8 @@ export interface SessionRecord {
130
152
  lastOpenedAt: string
131
153
  order?: number
132
154
  workspaceSnapshot?: WorkspaceSnapshotV1
155
+ worktrees?: WorktreeRecord[]
156
+ activeWorktreeId?: string
133
157
  }
134
158
 
135
159
  export interface TabSession {
@@ -144,6 +168,7 @@ export interface TabSession {
144
168
  command: string
145
169
  errorMessage?: string
146
170
  exitCode?: number
171
+ worktreeId?: string
147
172
  }
148
173
 
149
174
  export interface SnippetRecord {
@@ -194,6 +219,7 @@ export interface GitFileEntry {
194
219
  status: GitFileStatus
195
220
  added: number | null
196
221
  removed: number | null
222
+ repoPath?: string
197
223
  }
198
224
 
199
225
  export type GitPanelError = 'not-a-repo' | 'unknown'
@@ -244,6 +270,7 @@ export interface GitModeState {
244
270
  diffView: GitDiffView
245
271
  folds: Record<string, Record<string, FoldState>>
246
272
  headOffset: number
273
+ reviewBase: boolean
247
274
  }
248
275
 
249
276
  interface ModalBase {
@@ -262,6 +289,16 @@ export interface ModalClosed extends ModalBase {
262
289
  export interface ModalNewTab extends ModalBase {
263
290
  type: 'new-tab'
264
291
  editingCommand: AssistantId | null
292
+ activeField: 'assistant' | 'branch-name' | 'target-worktree' | 'worktree-name'
293
+ branchError: string | null
294
+ branchName: string
295
+ createWorktree: boolean
296
+ selectedAssistantId: AssistantId | null
297
+ step: 'assistant' | 'worktree' | 'worktree-create'
298
+ targetWorktreeIndex: number
299
+ worktreeDeleteConfirmId: string | null
300
+ worktreeDeleteMessage: string | null
301
+ worktreeName: string
265
302
  }
266
303
  export interface ModalSessionPicker extends ModalBase {
267
304
  type: 'session-picker'
@@ -322,6 +359,13 @@ export interface ModalAIUsage extends ModalBase {
322
359
  type: 'ai-usage'
323
360
  }
324
361
 
362
+ export interface ModalWorktreeMove extends ModalBase {
363
+ type: 'worktree-move'
364
+ /** The worktree being moved (may differ from the active one, e.g. a tab menu). */
365
+ sourceWorktreeId: string
366
+ deleteSource: boolean
367
+ }
368
+
325
369
  export type ModalState =
326
370
  | ModalClosed
327
371
  | ModalNewTab
@@ -337,6 +381,7 @@ export type ModalState =
337
381
  | ModalGitCommit
338
382
  | ModalUpdateAvailable
339
383
  | ModalAIUsage
384
+ | ModalWorktreeMove
340
385
 
341
386
  export interface LayoutState {
342
387
  terminalCols: number
@@ -403,6 +448,7 @@ export interface AppState {
403
448
  gitMode: GitModeState
404
449
  autoCommit: AutoCommitState
405
450
  multiRepo: MultiRepoState
451
+ worktreeDivergence: Record<string, BranchDivergence>
406
452
  pendingChords: string[] | null
407
453
  }
408
454
 
@@ -410,6 +456,15 @@ export interface AppState {
410
456
 
411
457
  export type ModalAction =
412
458
  | { type: 'open-new-tab-modal' }
459
+ | { type: 'set-new-tab-branch-error'; message: string | null }
460
+ | {
461
+ type: 'set-new-tab-worktree-delete-state'
462
+ confirmWorktreeId?: string | null
463
+ message: string | null
464
+ }
465
+ | { type: 'enter-new-tab-worktree-create' }
466
+ | { type: 'select-new-tab-assistant'; assistantId?: AssistantId }
467
+ | { type: 'toggle-new-tab-worktree'; assistantId?: AssistantId }
413
468
  | { type: 'open-help-modal'; scope?: ModeId }
414
469
  | { type: 'open-split-picker'; direction: SplitDirection }
415
470
  | { type: 'open-session-picker' }
@@ -434,8 +489,11 @@ export type ModalAction =
434
489
  | { type: 'set-theme-entry-count'; count: number }
435
490
  | { type: 'open-theme-picker' }
436
491
  | { type: 'open-update-available-modal'; currentVersion: string; latestVersion: string }
492
+ | { type: 'set-modal-selection-index'; index: number }
437
493
  | { type: 'open-ai-usage-modal' }
438
494
  | { type: 'open-edit-custom-command'; assistantId: AssistantId }
495
+ | { type: 'open-worktree-move-modal'; sourceWorktreeId: string }
496
+ | { type: 'toggle-worktree-move-delete' }
439
497
 
440
498
  export type SessionAction =
441
499
  | { type: 'load-session'; sessionId: string; workspaceSnapshot?: WorkspaceSnapshotV1 }
@@ -445,6 +503,15 @@ export type SessionAction =
445
503
  | { type: 'delete-session-record'; sessionId: string; openSessionPicker?: boolean }
446
504
  | { type: 'reorder-sessions'; orderedIds: string[] }
447
505
  | { type: 'set-session-status'; sessionId: string; status: SessionStatus }
506
+ | { type: 'add-worktree-record'; sessionId: string; worktree: WorktreeRecord; activate?: boolean }
507
+ | { type: 'remove-worktree-record'; sessionId: string; worktreeId: string }
508
+ | { type: 'set-active-worktree'; sessionId: string; worktreeId: string }
509
+ | {
510
+ type: 'update-worktree-record'
511
+ sessionId: string
512
+ worktreeId: string
513
+ patch: Partial<WorktreeRecord>
514
+ }
448
515
 
449
516
  export type TabAction =
450
517
  | { type: 'add-tab'; tab: TabSession }
@@ -520,10 +587,17 @@ export interface GitRefreshPayload {
520
587
  files: GitFileEntry[]
521
588
  }
522
589
 
590
+ /** Commits a worktree branch is ahead/behind the ref it forked from. */
591
+ export interface BranchDivergence {
592
+ ahead: number
593
+ behind: number
594
+ }
595
+
523
596
  export type GitPanelAction =
524
597
  | { type: 'git-refresh-success'; payload: GitRefreshPayload }
525
598
  | { type: 'git-refresh-error'; kind: GitPanelError }
526
599
  | { type: 'git-panel-reset' }
600
+ | { type: 'set-worktree-divergence'; divergence: Record<string, BranchDivergence> }
527
601
 
528
602
  export type GitModeAction =
529
603
  | { type: 'enter-git-mode' }
@@ -544,6 +618,7 @@ export type GitModeAction =
544
618
  | { type: 'git-mode-set-message'; message: string | null }
545
619
  | { type: 'snippet-picker-set-message'; message: string | null }
546
620
  | { type: 'git-mode-toggle-diff-view' }
621
+ | { type: 'git-mode-toggle-review-base' }
547
622
  | { type: 'git-mode-shift-head-offset'; delta: number }
548
623
  | { type: 'git-mode-set-head-offset'; offset: number }
549
624
  | {
@@ -643,6 +718,14 @@ export type SideEffect =
643
718
  | { type: 'confirm-update-selection' }
644
719
  | { type: 'switch-session-by-index'; index: number }
645
720
  | { type: 'delete-session'; sessionId: string }
721
+ | { type: 'delete-worktree'; sessionId: string; worktreeId: string; force?: boolean }
722
+ | {
723
+ type: 'move-worktree'
724
+ sessionId: string
725
+ sourceWorktreeId: string
726
+ targetWorktreeId: string
727
+ deleteSource?: boolean
728
+ }
646
729
  | { type: 'toggle-transparent' }
647
730
  | { type: 'toggle-mode' }
648
731
  | { type: 'open-file-in-editor'; path: string }
@@ -788,7 +871,9 @@ export type GitPanePathConfig =
788
871
  | { enabled: false }
789
872
  | { enabled: true; pathFn?: (path: string) => string }
790
873
 
791
- export type GitPaneDiffCountConfig = { enabled: boolean }
874
+ export interface GitPaneDiffCountConfig {
875
+ enabled: boolean
876
+ }
792
877
 
793
878
  interface GitPaneBaseConfig {
794
879
  /** Startup override for git pane visibility. Reapplied on each launch. */