@brimveyn/aimux 1.12.6 → 1.13.0

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 (130) hide show
  1. package/README.md +3 -0
  2. package/package.json +4 -3
  3. package/src/app-runtime/auto-commit-driver.ts +8 -4
  4. package/src/app-runtime/auto-commit-ref.ts +1 -1
  5. package/src/app-runtime/backend-attach-runtime.ts +18 -2
  6. package/src/app-runtime/backend-runtime-events.ts +1 -1
  7. package/src/app-runtime/selection-scroll.ts +2 -2
  8. package/src/app-runtime/session-actions.ts +16 -4
  9. package/src/app-runtime/side-effects.ts +538 -83
  10. package/src/app-runtime/snippet-actions.ts +3 -3
  11. package/src/app-runtime/use-auto-commit-driver.ts +7 -4
  12. package/src/app-runtime/use-backend-runtime.ts +3 -3
  13. package/src/app-runtime/use-directory-search.ts +7 -5
  14. package/src/app-runtime/use-mouse-handlers.ts +11 -6
  15. package/src/app-runtime/use-pane-size-report.ts +1 -1
  16. package/src/app-runtime/use-renderer-bindings.ts +6 -5
  17. package/src/app.tsx +12 -7
  18. package/src/auto-commit/headless-commands.ts +4 -6
  19. package/src/daemon/daemon.ts +3 -3
  20. package/src/daemon/runtime-paths.ts +1 -1
  21. package/src/daemon/session-manager.ts +1 -1
  22. package/src/daemon/session-registry.ts +10 -3
  23. package/src/diff-parser/parse-patch-files.ts +18 -18
  24. package/src/git/command-queue.ts +1 -1
  25. package/src/git/divergence.ts +46 -0
  26. package/src/git/git-diff.ts +12 -5
  27. package/src/git/git-poller.ts +33 -11
  28. package/src/git/git-status.ts +14 -3
  29. package/src/git/move-worktree.ts +96 -0
  30. package/src/git/use-repo-discovery.ts +2 -1
  31. package/src/git/worktree-divergence-poller.ts +59 -0
  32. package/src/git/worktree.ts +99 -0
  33. package/src/index.tsx +1 -1
  34. package/src/input/keymap/describe-bindings.ts +27 -9
  35. package/src/input/keymap/key-chord.ts +4 -4
  36. package/src/input/keymap/key-format.ts +2 -2
  37. package/src/input/keymap/sequence-resolver.ts +1 -1
  38. package/src/input/keymap/trie.ts +1 -1
  39. package/src/input/modes/bridge.ts +7 -0
  40. package/src/input/modes/transitions.ts +2 -1
  41. package/src/input/modes/types.ts +11 -1
  42. package/src/input/raw-input-handler.ts +1 -1
  43. package/src/input/terminal-text-extraction.ts +9 -5
  44. package/src/integrations/claude-syntax-overlay.ts +8 -8
  45. package/src/integrations/claude-theme-sync.ts +12 -12
  46. package/src/ipc/protocol.ts +3 -3
  47. package/src/platform/clipboard.ts +4 -2
  48. package/src/platform/worktree-deps.ts +22 -0
  49. package/src/platform/worktree-paths.ts +67 -0
  50. package/src/profile-paths.ts +3 -3
  51. package/src/pty/assistant-status-detection-loop.ts +2 -2
  52. package/src/pty/assistant-status-detector.ts +15 -6
  53. package/src/pty/command-registry.ts +2 -1
  54. package/src/pty/pty-manager.ts +1 -1
  55. package/src/services/ai-usage/adapters/claude.ts +5 -5
  56. package/src/services/ai-usage/adapters/codex.ts +8 -8
  57. package/src/services/ai-usage/cache.ts +2 -2
  58. package/src/services/ai-usage/pace.ts +3 -6
  59. package/src/services/ai-usage/provider.ts +1 -1
  60. package/src/session-backend/bootstrap.ts +2 -2
  61. package/src/session-backend/local-session-backend.ts +11 -11
  62. package/src/session-backend/remote-session-backend.ts +3 -3
  63. package/src/session-backend/types.ts +2 -2
  64. package/src/snippets/expand-variables.ts +7 -7
  65. package/src/snippets/run-shell-var.ts +1 -1
  66. package/src/snippets/trigger-detector.ts +1 -1
  67. package/src/state/ai-usage-store.ts +1 -1
  68. package/src/state/git-tree.ts +8 -4
  69. package/src/state/layout-tree.ts +8 -5
  70. package/src/state/reducers/git-mode-state.ts +44 -14
  71. package/src/state/reducers/git-panel-state.ts +38 -2
  72. package/src/state/reducers/modal-state.ts +355 -21
  73. package/src/state/reducers/session-state.ts +60 -1
  74. package/src/state/reducers/tab-state.ts +151 -85
  75. package/src/state/selectors.ts +6 -4
  76. package/src/state/session-catalog.ts +20 -5
  77. package/src/state/session-persistence.ts +8 -4
  78. package/src/state/session-worktrees.ts +227 -0
  79. package/src/state/store.ts +1 -0
  80. package/src/state/toast-store.ts +95 -0
  81. package/src/state/types.ts +92 -13
  82. package/src/state/validation.ts +25 -2
  83. package/src/terminal-manager/manager-client.ts +15 -13
  84. package/src/ui/components/git/diff-renderer/fold-strip.tsx +2 -1
  85. package/src/ui/components/git/diff-renderer/pierre-diff.tsx +1 -1
  86. package/src/ui/components/git/diff-renderer/prepare-diff.ts +2 -2
  87. package/src/ui/components/git/diff-renderer/split-view.tsx +6 -4
  88. package/src/ui/components/git/diff-renderer/stacked-view.tsx +6 -4
  89. package/src/ui/components/git/diff-renderer/use-diff-prefetch.ts +19 -6
  90. package/src/ui/components/git/diff-renderer/use-diff-preparation.ts +2 -2
  91. package/src/ui/components/git/git-panel.tsx +17 -10
  92. package/src/ui/components/git/git-view.tsx +79 -19
  93. package/src/ui/components/git/image-diff/image-diff-view.tsx +2 -2
  94. package/src/ui/components/git/image-diff/terminal-image-pane.tsx +5 -4
  95. package/src/ui/components/git/pane/git-pane-context-menu.ts +2 -2
  96. package/src/ui/components/git/pane/git-pane-widget.tsx +6 -4
  97. package/src/ui/components/layout/session-bar.tsx +3 -3
  98. package/src/ui/components/layout/sidebar/sidebar.tsx +162 -36
  99. package/src/ui/components/layout/sidebar/tab-item.tsx +17 -1
  100. package/src/ui/components/layout/sidebar/use-sidebar-auto-scroll.ts +2 -2
  101. package/src/ui/components/layout/sidebar/use-sidebar-branch.ts +1 -1
  102. package/src/ui/components/layout/split-layout.tsx +1 -1
  103. package/src/ui/components/layout/terminal-pane.tsx +43 -40
  104. package/src/ui/components/modals/app/ai-usage-modal.tsx +8 -5
  105. package/src/ui/components/modals/app/help-modal.tsx +4 -2
  106. package/src/ui/components/modals/git/git-commit-modal.tsx +2 -2
  107. package/src/ui/components/modals/sessions/create-session-modal.tsx +5 -1
  108. package/src/ui/components/modals/sessions/session-picker-modal.tsx +5 -4
  109. package/src/ui/components/modals/shared/form.tsx +4 -5
  110. package/src/ui/components/modals/shared/modal-shell.tsx +3 -3
  111. package/src/ui/components/modals/shared/picker.tsx +28 -5
  112. package/src/ui/components/modals/snippets/snippet-picker-modal.tsx +11 -3
  113. package/src/ui/components/modals/tabs/new-tab-modal.tsx +173 -5
  114. package/src/ui/components/modals/worktree/worktree-move-modal.tsx +94 -0
  115. package/src/ui/components/overlays/ai-usage/ai-usage-indicator.tsx +3 -3
  116. package/src/ui/components/overlays/toast/toast-item.tsx +88 -0
  117. package/src/ui/components/overlays/toast/toast-position.ts +45 -0
  118. package/src/ui/components/overlays/toast/toast-stack.tsx +30 -0
  119. package/src/ui/components/overlays/toast/toast-viewport.tsx +24 -0
  120. package/src/ui/components/overlays/toast/use-toast-animation.ts +64 -0
  121. package/src/ui/components/primitives/input-field.tsx +1 -1
  122. package/src/ui/components/primitives/list-item.tsx +3 -3
  123. package/src/ui/filter-themes.ts +1 -1
  124. package/src/ui/root.tsx +57 -3
  125. package/src/ui/session-ordering.ts +2 -2
  126. package/src/ui/status-bar-model.ts +14 -7
  127. package/src/ui/terminal-graphics/capabilities.ts +1 -1
  128. package/src/ui/terminal-graphics/format-fallback.ts +7 -9
  129. package/src/ui/terminal-graphics/kitty.ts +2 -7
  130. package/src/update/version-check.ts +1 -1
@@ -25,21 +25,154 @@ function clampCursor(value: number, max: number): number {
25
25
  return Math.max(0, Math.min(max, value))
26
26
  }
27
27
 
28
+ function getCurrentWorktreeCount(state: AppState): number {
29
+ if (!(state.currentSessionId != null && state.currentSessionId !== '')) return 0
30
+ return state.sessions.find((entry) => entry.id === state.currentSessionId)?.worktrees?.length ?? 0
31
+ }
32
+
33
+ function getCurrentWorktreeIndex(state: AppState): number {
34
+ if (!(state.currentSessionId != null && state.currentSessionId !== '')) return 0
35
+ const session = state.sessions.find((entry) => entry.id === state.currentSessionId)
36
+ return Math.max(
37
+ 0,
38
+ (session?.worktrees ?? []).findIndex((worktree) => worktree.id === session?.activeWorktreeId)
39
+ )
40
+ }
41
+
42
+ function getSelectedNewTabAssistant(state: AppState, assistantId?: string) {
43
+ if (assistantId != null && assistantId !== '') {
44
+ return getAllAssistantOptions(state.customCommands).find((entry) => entry.id === assistantId)
45
+ }
46
+ if (state.modal.type !== 'new-tab') return
47
+ return filterAssistants(getAllAssistantOptions(state.customCommands), state.modal.editBuffer)[
48
+ state.modal.selectedIndex
49
+ ]
50
+ }
51
+
28
52
  export function reduceModalState(state: AppState, action: AppAction): AppState | null {
29
53
  switch (action.type) {
30
- case 'open-new-tab-modal':
54
+ case 'open-new-tab-modal': {
55
+ const targetWorktreeIndex = getCurrentWorktreeIndex(state)
31
56
  return {
32
57
  ...state,
33
58
  focusMode: 'command-edit',
34
59
  modal: {
60
+ activeField: 'assistant',
61
+ branchError: null,
62
+ branchName: '',
63
+ createWorktree: false,
35
64
  cursorPos: 0,
36
65
  editBuffer: '',
37
66
  editingCommand: null,
67
+ selectedAssistantId: null,
38
68
  selectedIndex: 0,
39
69
  sessionTargetId: null,
70
+ step: 'assistant',
71
+ targetWorktreeIndex,
40
72
  type: 'new-tab',
73
+ worktreeDeleteConfirmId: null,
74
+ worktreeDeleteMessage: null,
75
+ worktreeName: '',
76
+ },
77
+ }
78
+ }
79
+ case 'enter-new-tab-worktree-create': {
80
+ if (state.modal.type !== 'new-tab') return state
81
+ return {
82
+ ...state,
83
+ modal: {
84
+ ...state.modal,
85
+ activeField: 'worktree-name',
86
+ createWorktree: true,
87
+ cursorPos: state.modal.worktreeName.length,
88
+ selectedIndex: 0,
89
+ step: 'worktree-create',
41
90
  },
42
91
  }
92
+ }
93
+ case 'set-new-tab-worktree-delete-state': {
94
+ if (state.modal.type !== 'new-tab') return state
95
+ return {
96
+ ...state,
97
+ modal: {
98
+ ...state.modal,
99
+ worktreeDeleteConfirmId: action.confirmWorktreeId ?? null,
100
+ worktreeDeleteMessage: action.message,
101
+ },
102
+ }
103
+ }
104
+ case 'set-new-tab-branch-error': {
105
+ if (state.modal.type !== 'new-tab') return state
106
+ return {
107
+ ...state,
108
+ modal: {
109
+ ...state.modal,
110
+ activeField: 'branch-name',
111
+ branchError: action.message,
112
+ cursorPos: state.modal.branchName.length,
113
+ },
114
+ }
115
+ }
116
+ case 'select-new-tab-assistant': {
117
+ if (state.modal.type !== 'new-tab' || state.modal.editingCommand !== null) return state
118
+ const option = getSelectedNewTabAssistant(state, action.assistantId)
119
+ if (!option) return state
120
+ const targetWorktreeIndex = getCurrentWorktreeIndex(state)
121
+ const worktreeCount = getCurrentWorktreeCount(state)
122
+ return {
123
+ ...state,
124
+ modal: {
125
+ ...state.modal,
126
+ activeField: 'target-worktree',
127
+ branchError: null,
128
+ createWorktree: worktreeCount === 0,
129
+ cursorPos: 0,
130
+ selectedAssistantId: option.id,
131
+ selectedIndex: worktreeCount === 0 ? 0 : targetWorktreeIndex,
132
+ step: 'worktree',
133
+ targetWorktreeIndex,
134
+ worktreeDeleteConfirmId: null,
135
+ worktreeDeleteMessage: null,
136
+ worktreeName: state.modal.worktreeName || `wt-${option.label}`,
137
+ },
138
+ }
139
+ }
140
+ case 'toggle-new-tab-worktree': {
141
+ if (state.modal.type !== 'new-tab' || state.modal.editingCommand !== null) return state
142
+ const createWorktree = !state.modal.createWorktree
143
+ const option = getSelectedNewTabAssistant(state, action.assistantId)
144
+ const defaultName = state.modal.worktreeName || `wt-${option?.label ?? 'assistant'}`
145
+ const worktreeCount = getCurrentWorktreeCount(state)
146
+ const targetWorktreeIndex = getCurrentWorktreeIndex(state)
147
+ let activeField = state.modal.activeField
148
+ if (createWorktree) {
149
+ activeField = state.modal.step === 'worktree' || option ? 'target-worktree' : activeField
150
+ } else if (activeField === 'worktree-name') {
151
+ activeField = 'target-worktree'
152
+ }
153
+ const cursorPos =
154
+ createWorktree && worktreeCount <= 0
155
+ ? defaultName.length
156
+ : (state.modal.editBuffer?.length ?? 0)
157
+ return {
158
+ ...state,
159
+ modal: {
160
+ ...state.modal,
161
+ activeField,
162
+ createWorktree,
163
+ cursorPos,
164
+ selectedAssistantId: option?.id ?? state.modal.selectedAssistantId,
165
+ selectedIndex: createWorktree
166
+ ? worktreeCount
167
+ : Math.min(state.modal.selectedIndex, Math.max(0, worktreeCount - 1)),
168
+ step: option && createWorktree ? 'worktree' : state.modal.step,
169
+ targetWorktreeIndex,
170
+ worktreeDeleteConfirmId: null,
171
+ worktreeDeleteMessage: null,
172
+ worktreeName: defaultName,
173
+ },
174
+ }
175
+ }
43
176
  case 'open-edit-custom-command': {
44
177
  if (state.modal.type !== 'new-tab') return state
45
178
  const current = state.customCommands[action.assistantId] ?? ''
@@ -67,6 +200,29 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
67
200
  },
68
201
  }
69
202
  }
203
+ case 'open-worktree-move-modal': {
204
+ // Overlay: keep focusMode (git when opened via `m`, navigation when opened
205
+ // from a tab menu) so the view underneath stays mounted, like the help
206
+ // modal. deriveModeId routes input to the picker while open.
207
+ return {
208
+ ...state,
209
+ modal: {
210
+ deleteSource: false,
211
+ editBuffer: null,
212
+ selectedIndex: 0,
213
+ sessionTargetId: null,
214
+ sourceWorktreeId: action.sourceWorktreeId,
215
+ type: 'worktree-move',
216
+ },
217
+ }
218
+ }
219
+ case 'toggle-worktree-move-delete': {
220
+ if (state.modal.type !== 'worktree-move') return state
221
+ return {
222
+ ...state,
223
+ modal: { ...state.modal, deleteSource: !state.modal.deleteSource },
224
+ }
225
+ }
70
226
  case 'open-help-modal': {
71
227
  const keymap = getActiveKeymap()
72
228
  const scope = action.scope ?? null
@@ -174,9 +330,10 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
174
330
  return { ...state, modal: { ...state.modal, actionMessage: action.message } }
175
331
  }
176
332
  case 'open-snippet-editor': {
177
- const snippet = action.snippetId
178
- ? state.snippets.find((s) => s.id === action.snippetId)
179
- : undefined
333
+ const snippet =
334
+ action.snippetId != null && action.snippetId !== ''
335
+ ? state.snippets.find((s) => s.id === action.snippetId)
336
+ : undefined
180
337
  const initialName = snippet?.name ?? ''
181
338
  return {
182
339
  ...state,
@@ -365,8 +522,9 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
365
522
  }
366
523
  case 'close-modal': {
367
524
  const closingType = state.modal.type
368
- // Help is a pure overlay it never flipped focusMode, so leave it alone.
369
- if (closingType === 'help') {
525
+ // Pure overlays never flipped focusMode (they render on top of git mode),
526
+ // so leave it alone — closing returns to whatever was underneath.
527
+ if (closingType === 'help' || closingType === 'worktree-move') {
370
528
  return { ...state, modal: emptyModal() }
371
529
  }
372
530
  const nextFocus: AppState['focusMode'] = closingType === 'git-commit' ? 'git' : 'navigation'
@@ -388,19 +546,28 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
388
546
  state.modal.type !== 'theme-picker' &&
389
547
  state.modal.type !== 'create-session' &&
390
548
  state.modal.type !== 'split-picker' &&
391
- state.modal.type !== 'update-available'
549
+ state.modal.type !== 'update-available' &&
550
+ state.modal.type !== 'worktree-move'
392
551
  ) {
393
552
  return state
394
553
  }
395
554
  if (state.modal.type === 'create-session' && state.modal.activeField !== 'directory') {
396
555
  return state
397
556
  }
557
+ if (state.modal.type === 'new-tab' && state.modal.step === 'worktree-create') {
558
+ return state
559
+ }
398
560
  let optionCount: number
399
561
  if (state.modal.type === 'new-tab') {
400
- optionCount = filterAssistants(
401
- getAllAssistantOptions(state.customCommands),
402
- state.modal.editBuffer
403
- ).length
562
+ if (state.modal.step === 'worktree') {
563
+ if (state.modal.activeField === 'worktree-name') return state
564
+ optionCount = getCurrentWorktreeCount(state) + 1
565
+ } else {
566
+ optionCount = filterAssistants(
567
+ getAllAssistantOptions(state.customCommands),
568
+ state.modal.editBuffer
569
+ ).length
570
+ }
404
571
  } else if (state.modal.type === 'split-picker') {
405
572
  optionCount = getAllAssistantOptions(state.customCommands).length
406
573
  } else if (state.modal.type === 'create-session') {
@@ -412,6 +579,8 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
412
579
  optionCount = filterThemeIds(state.modal.editBuffer).length
413
580
  } else if (state.modal.type === 'update-available') {
414
581
  optionCount = 2
582
+ } else if (state.modal.type === 'worktree-move') {
583
+ optionCount = Math.max(0, getCurrentWorktreeCount(state) - 1)
415
584
  } else {
416
585
  const filtered = filterSessions(state.sessions, state.modal.editBuffer)
417
586
  optionCount = Math.max(1, filtered.length + 1)
@@ -424,6 +593,15 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
424
593
  modal: {
425
594
  ...state.modal,
426
595
  selectedIndex: (state.modal.selectedIndex + action.delta + optionCount) % optionCount,
596
+ ...(state.modal.type === 'new-tab' && state.modal.step === 'worktree'
597
+ ? {
598
+ createWorktree:
599
+ (state.modal.selectedIndex + action.delta + optionCount) % optionCount ===
600
+ optionCount - 1,
601
+ worktreeDeleteConfirmId: null,
602
+ worktreeDeleteMessage: null,
603
+ }
604
+ : null),
427
605
  },
428
606
  }
429
607
  }
@@ -436,18 +614,23 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
436
614
  state.modal.type !== 'create-session' &&
437
615
  state.modal.type !== 'split-picker' &&
438
616
  state.modal.type !== 'update-available' &&
617
+ state.modal.type !== 'worktree-move' &&
439
618
  state.modal.type !== 'help'
440
619
  ) {
441
620
  return state
442
621
  }
622
+ if (state.modal.type === 'new-tab' && state.modal.step === 'worktree-create') {
623
+ return state
624
+ }
443
625
  let optionCount: number
444
626
  if (state.modal.type === 'help') {
445
627
  optionCount = state.modal.entryCount
446
628
  } else if (state.modal.type === 'new-tab') {
447
- optionCount = filterAssistants(
448
- getAllAssistantOptions(state.customCommands),
449
- state.modal.editBuffer
450
- ).length
629
+ optionCount =
630
+ state.modal.step === 'worktree'
631
+ ? getCurrentWorktreeCount(state) + 1
632
+ : filterAssistants(getAllAssistantOptions(state.customCommands), state.modal.editBuffer)
633
+ .length
451
634
  } else if (state.modal.type === 'split-picker') {
452
635
  optionCount = getAllAssistantOptions(state.customCommands).length
453
636
  } else if (state.modal.type === 'create-session') {
@@ -458,17 +641,48 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
458
641
  optionCount = filterThemeIds(state.modal.editBuffer).length
459
642
  } else if (state.modal.type === 'update-available') {
460
643
  optionCount = 2
644
+ } else if (state.modal.type === 'worktree-move') {
645
+ optionCount = Math.max(0, getCurrentWorktreeCount(state) - 1)
461
646
  } else {
462
647
  optionCount = Math.max(1, filterSessions(state.sessions, state.modal.editBuffer).length + 1)
463
648
  }
464
649
  if (optionCount === 0) return state
465
650
  const clamped = Math.max(0, Math.min(optionCount - 1, action.index))
651
+ if (state.modal.type === 'new-tab' && state.modal.step === 'worktree') {
652
+ if (clamped === state.modal.selectedIndex) return state
653
+ return {
654
+ ...state,
655
+ modal: {
656
+ ...state.modal,
657
+ createWorktree: clamped === optionCount - 1,
658
+ selectedIndex: clamped,
659
+ worktreeDeleteConfirmId: null,
660
+ worktreeDeleteMessage: null,
661
+ },
662
+ }
663
+ }
466
664
  if (clamped === state.modal.selectedIndex) return state
467
665
  return { ...state, modal: { ...state.modal, selectedIndex: clamped } }
468
666
  }
469
667
  case 'move-modal-cursor': {
470
668
  if (state.modal.editBuffer === null) return state
471
- const len = state.modal.editBuffer.length
669
+ let editableValue = state.modal.editBuffer
670
+ if (state.modal.type === 'new-tab' && state.modal.editingCommand === null) {
671
+ if (state.modal.activeField === 'worktree-name') {
672
+ editableValue = state.modal.worktreeName
673
+ } else if (state.modal.activeField === 'branch-name') {
674
+ editableValue = state.modal.branchName
675
+ }
676
+ }
677
+ if (
678
+ state.modal.type === 'new-tab' &&
679
+ state.modal.editingCommand === null &&
680
+ state.modal.step === 'worktree' &&
681
+ state.modal.activeField === 'target-worktree'
682
+ ) {
683
+ return state
684
+ }
685
+ const len = editableValue.length
472
686
  const current = state.modal.cursorPos ?? len
473
687
  let next = current
474
688
  if (action.to === 'home') next = 0
@@ -482,7 +696,22 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
482
696
  if (state.modal.editBuffer === null) {
483
697
  return state
484
698
  }
485
- const buffer = state.modal.editBuffer
699
+ let buffer = state.modal.editBuffer
700
+ if (state.modal.type === 'new-tab' && state.modal.editingCommand === null) {
701
+ if (state.modal.activeField === 'worktree-name') {
702
+ buffer = state.modal.worktreeName
703
+ } else if (state.modal.activeField === 'branch-name') {
704
+ buffer = state.modal.branchName
705
+ }
706
+ }
707
+ if (
708
+ state.modal.type === 'new-tab' &&
709
+ state.modal.editingCommand === null &&
710
+ state.modal.step === 'worktree' &&
711
+ state.modal.activeField === 'target-worktree'
712
+ ) {
713
+ return state
714
+ }
486
715
  const cursor = clampCursor(state.modal.cursorPos ?? buffer.length, buffer.length)
487
716
  let nextBuffer: string
488
717
  let nextCursor: number
@@ -495,6 +724,35 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
495
724
  nextCursor = cursor + action.char.length
496
725
  }
497
726
  const isNewTabEditing = state.modal.type === 'new-tab' && state.modal.editingCommand !== null
727
+ if (
728
+ state.modal.type === 'new-tab' &&
729
+ state.modal.editingCommand === null &&
730
+ state.modal.activeField === 'worktree-name'
731
+ ) {
732
+ return {
733
+ ...state,
734
+ modal: {
735
+ ...state.modal,
736
+ cursorPos: nextCursor,
737
+ worktreeName: nextBuffer,
738
+ },
739
+ }
740
+ }
741
+ if (
742
+ state.modal.type === 'new-tab' &&
743
+ state.modal.editingCommand === null &&
744
+ state.modal.activeField === 'branch-name'
745
+ ) {
746
+ return {
747
+ ...state,
748
+ modal: {
749
+ ...state.modal,
750
+ branchError: null,
751
+ branchName: nextBuffer,
752
+ cursorPos: nextCursor,
753
+ },
754
+ }
755
+ }
498
756
  const resetIndex =
499
757
  !isNewTabEditing &&
500
758
  (state.modal.type === 'session-picker' ||
@@ -526,6 +784,50 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
526
784
  },
527
785
  }
528
786
  }
787
+ if (state.modal.type === 'new-tab') {
788
+ if (state.modal.step === 'worktree-create') {
789
+ const optionCount = getCurrentWorktreeCount(state) + 1
790
+ return {
791
+ ...state,
792
+ modal: {
793
+ ...state.modal,
794
+ activeField: 'target-worktree',
795
+ branchError: null,
796
+ createWorktree: false,
797
+ cursorPos: 0,
798
+ selectedIndex: Math.min(
799
+ state.modal.targetWorktreeIndex,
800
+ Math.max(0, optionCount - 1)
801
+ ),
802
+ step: 'worktree',
803
+ },
804
+ }
805
+ }
806
+ if (state.modal.step === 'worktree') {
807
+ const selectedAssistantId = state.modal.selectedAssistantId
808
+ const assistants = filterAssistants(
809
+ getAllAssistantOptions(state.customCommands),
810
+ state.modal.editBuffer
811
+ )
812
+ const assistantIndex = assistants.findIndex(
813
+ (assistant) => assistant.id === selectedAssistantId
814
+ )
815
+ return {
816
+ ...state,
817
+ modal: {
818
+ ...state.modal,
819
+ activeField: 'assistant',
820
+ createWorktree: false,
821
+ cursorPos: state.modal.editBuffer?.length ?? 0,
822
+ selectedIndex: Math.max(0, assistantIndex),
823
+ step: 'assistant',
824
+ worktreeDeleteConfirmId: null,
825
+ worktreeDeleteMessage: null,
826
+ },
827
+ }
828
+ }
829
+ return { ...state, focusMode: 'navigation', modal: emptyModal() }
830
+ }
529
831
  if (state.modal.type === 'create-session' || state.modal.type === 'snippet-editor') {
530
832
  return { ...state, focusMode: 'navigation', modal: emptyModal() }
531
833
  }
@@ -534,7 +836,6 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
534
836
  state.modal.type === 'session-picker' ||
535
837
  state.modal.type === 'snippet-picker' ||
536
838
  state.modal.type === 'theme-picker' ||
537
- state.modal.type === 'new-tab' ||
538
839
  state.modal.type === 'help'
539
840
  ) {
540
841
  return { ...state, focusMode: 'navigation', modal: emptyModal() }
@@ -607,6 +908,38 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
607
908
  },
608
909
  }
609
910
  }
911
+ if (state.modal.type === 'new-tab') {
912
+ if (state.modal.step === 'assistant') return state
913
+ if (state.modal.step === 'worktree-create') {
914
+ const nextField: typeof state.modal.activeField =
915
+ state.modal.activeField === 'worktree-name' ? 'branch-name' : 'worktree-name'
916
+ const nextValue =
917
+ nextField === 'branch-name' ? state.modal.branchName : state.modal.worktreeName
918
+ return {
919
+ ...state,
920
+ modal: {
921
+ ...state.modal,
922
+ activeField: nextField,
923
+ cursorPos: nextValue.length,
924
+ selectedIndex: 0,
925
+ },
926
+ }
927
+ }
928
+ let nextField: typeof state.modal.activeField = 'target-worktree'
929
+ if (state.modal.activeField === 'target-worktree' && state.modal.createWorktree) {
930
+ nextField = 'worktree-name'
931
+ }
932
+ const nextValue =
933
+ nextField === 'worktree-name' ? state.modal.worktreeName : (state.modal.editBuffer ?? '')
934
+ return {
935
+ ...state,
936
+ modal: {
937
+ ...state.modal,
938
+ activeField: nextField,
939
+ cursorPos: nextValue.length,
940
+ },
941
+ }
942
+ }
610
943
  return state
611
944
  }
612
945
  case 'select-directory': {
@@ -638,9 +971,10 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
638
971
  }
639
972
  }
640
973
  case 'open-rename-tab-modal': {
641
- const activeTab = state.activeTabId
642
- ? state.tabs.find((tab) => tab.id === state.activeTabId)
643
- : undefined
974
+ const activeTab =
975
+ state.activeTabId != null && state.activeTabId !== ''
976
+ ? state.tabs.find((tab) => tab.id === state.activeTabId)
977
+ : undefined
644
978
  if (!activeTab) {
645
979
  return state
646
980
  }
@@ -2,6 +2,7 @@ import type { AppAction, AppState } from '../types'
2
2
 
3
3
  import { filterSessions } from '../selectors'
4
4
  import { restoreWorkspaceState } from '../session-persistence'
5
+ import { withActiveWorktree } from '../session-worktrees'
5
6
 
6
7
  const CLOSED_MODAL = {
7
8
  editBuffer: null,
@@ -57,7 +58,7 @@ export function reduceSessionState(state: AppState, action: AppAction): AppState
57
58
  const newSessions = state.sessions.filter((session) => session.id !== action.sessionId)
58
59
  const nextStatuses = { ...state.sessionStatuses }
59
60
  delete nextStatuses[action.sessionId]
60
- if (action.openSessionPicker) {
61
+ if (action.openSessionPicker === true) {
61
62
  const filteredNew = filterSessions(newSessions, state.modal.editBuffer)
62
63
  const maxIndex = filteredNew.length
63
64
  const clampedIndex = Math.min(state.modal.selectedIndex, maxIndex)
@@ -120,6 +121,64 @@ export function reduceSessionState(state: AppState, action: AppAction): AppState
120
121
  sessionStatuses: { ...state.sessionStatuses, [action.sessionId]: action.status },
121
122
  }
122
123
  }
124
+ case 'add-worktree-record':
125
+ return {
126
+ ...state,
127
+ sessions: state.sessions.map((session) => {
128
+ if (session.id !== action.sessionId) return session
129
+ const next = {
130
+ ...session,
131
+ activeWorktreeId:
132
+ action.activate === true ? action.worktree.id : session.activeWorktreeId,
133
+ projectPath: action.activate === true ? action.worktree.path : session.projectPath,
134
+ updatedAt: new Date().toISOString(),
135
+ worktrees: [...(session.worktrees ?? []), action.worktree],
136
+ }
137
+ return next.activeWorktreeId != null && next.activeWorktreeId !== ''
138
+ ? next
139
+ : withActiveWorktree(next, action.worktree.id)
140
+ }),
141
+ }
142
+ case 'remove-worktree-record':
143
+ return {
144
+ ...state,
145
+ sessions: state.sessions.map((session) => {
146
+ if (session.id !== action.sessionId) return session
147
+ const remaining = (session.worktrees ?? []).filter((w) => w.id !== action.worktreeId)
148
+ if (remaining.length === 0) return session
149
+ if (session.activeWorktreeId !== action.worktreeId) {
150
+ return { ...session, updatedAt: new Date().toISOString(), worktrees: remaining }
151
+ }
152
+ return withActiveWorktree(
153
+ { ...session, activeWorktreeId: remaining[0]?.id, worktrees: remaining },
154
+ remaining[0]?.id ?? ''
155
+ )
156
+ }),
157
+ }
158
+ case 'set-active-worktree':
159
+ return {
160
+ ...state,
161
+ sessions: state.sessions.map((session) =>
162
+ session.id === action.sessionId ? withActiveWorktree(session, action.worktreeId) : session
163
+ ),
164
+ }
165
+ case 'update-worktree-record':
166
+ return {
167
+ ...state,
168
+ sessions: state.sessions.map((session) =>
169
+ session.id === action.sessionId
170
+ ? {
171
+ ...session,
172
+ updatedAt: new Date().toISOString(),
173
+ worktrees: session.worktrees?.map((worktree) =>
174
+ worktree.id === action.worktreeId
175
+ ? { ...worktree, ...action.patch, updatedAt: new Date().toISOString() }
176
+ : worktree
177
+ ),
178
+ }
179
+ : session
180
+ ),
181
+ }
123
182
  default:
124
183
  return null
125
184
  }