@brimveyn/aimux 1.12.2 → 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.
- package/README.md +3 -0
- package/package.json +4 -3
- package/src/app-runtime/auto-commit-driver.ts +8 -4
- package/src/app-runtime/auto-commit-ref.ts +1 -1
- package/src/app-runtime/backend-attach-runtime.ts +18 -2
- package/src/app-runtime/backend-runtime-events.ts +1 -1
- package/src/app-runtime/pty-write.ts +52 -0
- package/src/app-runtime/selection-scroll.ts +2 -2
- package/src/app-runtime/session-actions.ts +16 -4
- package/src/app-runtime/side-effects.ts +598 -102
- package/src/app-runtime/snippet-actions.ts +25 -11
- package/src/app-runtime/use-auto-commit-driver.ts +7 -4
- package/src/app-runtime/use-backend-runtime.ts +3 -3
- package/src/app-runtime/use-directory-search.ts +7 -5
- package/src/app-runtime/use-mouse-handlers.ts +11 -6
- package/src/app-runtime/use-pane-size-report.ts +1 -1
- package/src/app-runtime/use-renderer-bindings.ts +143 -8
- package/src/app.tsx +25 -9
- package/src/auto-commit/headless-commands.ts +4 -6
- package/src/daemon/daemon.ts +3 -3
- package/src/daemon/runtime-paths.ts +1 -1
- package/src/daemon/session-manager.ts +1 -1
- package/src/daemon/session-registry.ts +10 -3
- package/src/diff-parser/parse-patch-files.ts +18 -18
- package/src/git/command-queue.ts +1 -1
- package/src/git/divergence.ts +46 -0
- package/src/git/git-diff.ts +12 -5
- package/src/git/git-poller.ts +33 -11
- package/src/git/git-status.ts +14 -3
- package/src/git/move-worktree.ts +96 -0
- package/src/git/use-repo-discovery.ts +2 -1
- package/src/git/worktree-divergence-poller.ts +59 -0
- package/src/git/worktree.ts +99 -0
- package/src/index.tsx +1 -1
- package/src/input/keymap/describe-bindings.ts +27 -9
- package/src/input/keymap/key-chord.ts +4 -4
- package/src/input/keymap/key-format.ts +2 -2
- package/src/input/keymap/sequence-resolver.ts +1 -1
- package/src/input/keymap/trie.ts +1 -1
- package/src/input/modes/bridge.ts +7 -0
- package/src/input/modes/transitions.ts +2 -1
- package/src/input/modes/types.ts +13 -1
- package/src/input/raw-input-handler.ts +34 -1
- package/src/input/terminal-text-extraction.ts +9 -5
- package/src/integrations/claude-syntax-overlay.ts +8 -8
- package/src/integrations/claude-theme-sync.ts +12 -12
- package/src/ipc/protocol.ts +3 -3
- package/src/platform/clipboard.ts +18 -2
- package/src/platform/worktree-deps.ts +22 -0
- package/src/platform/worktree-paths.ts +67 -0
- package/src/profile-paths.ts +3 -3
- package/src/pty/assistant-status-detection-loop.ts +2 -2
- package/src/pty/assistant-status-detector.ts +15 -6
- package/src/pty/command-registry.ts +8 -1
- package/src/pty/pty-manager.ts +1 -1
- package/src/services/ai-usage/adapters/claude.ts +5 -5
- package/src/services/ai-usage/adapters/codex.ts +8 -8
- package/src/services/ai-usage/cache.ts +2 -2
- package/src/services/ai-usage/pace.ts +3 -6
- package/src/services/ai-usage/provider.ts +1 -1
- package/src/session-backend/bootstrap.ts +2 -2
- package/src/session-backend/local-session-backend.ts +11 -11
- package/src/session-backend/remote-session-backend.ts +3 -3
- package/src/session-backend/types.ts +2 -2
- package/src/snippets/expand-variables.ts +112 -0
- package/src/snippets/run-shell-var.ts +93 -0
- package/src/snippets/trigger-detector.ts +105 -0
- package/src/state/ai-usage-store.ts +1 -1
- package/src/state/git-tree.ts +8 -4
- package/src/state/layout-tree.ts +8 -5
- package/src/state/reducers/git-mode-state.ts +44 -14
- package/src/state/reducers/git-panel-state.ts +38 -2
- package/src/state/reducers/modal-state.ts +385 -24
- package/src/state/reducers/session-state.ts +60 -1
- package/src/state/reducers/tab-state.ts +151 -85
- package/src/state/selectors.ts +6 -4
- package/src/state/session-catalog.ts +20 -5
- package/src/state/session-persistence.ts +8 -4
- package/src/state/session-worktrees.ts +227 -0
- package/src/state/snippet-catalog.ts +65 -4
- package/src/state/store.ts +1 -0
- package/src/state/toast-store.ts +95 -0
- package/src/state/types.ts +108 -16
- package/src/state/validation.ts +47 -3
- package/src/terminal-manager/manager-client.ts +15 -13
- package/src/ui/components/git/diff-renderer/fold-strip.tsx +2 -1
- package/src/ui/components/git/diff-renderer/pierre-diff.tsx +1 -1
- package/src/ui/components/git/diff-renderer/prepare-diff.ts +2 -2
- package/src/ui/components/git/diff-renderer/split-view.tsx +6 -4
- package/src/ui/components/git/diff-renderer/stacked-view.tsx +6 -4
- package/src/ui/components/git/diff-renderer/use-diff-prefetch.ts +19 -6
- package/src/ui/components/git/diff-renderer/use-diff-preparation.ts +2 -2
- package/src/ui/components/git/git-panel.tsx +17 -10
- package/src/ui/components/git/git-view.tsx +79 -19
- package/src/ui/components/git/image-diff/image-diff-view.tsx +2 -2
- package/src/ui/components/git/image-diff/terminal-image-pane.tsx +5 -4
- package/src/ui/components/git/pane/git-pane-context-menu.ts +2 -2
- package/src/ui/components/git/pane/git-pane-widget.tsx +6 -4
- package/src/ui/components/layout/session-bar.tsx +3 -3
- package/src/ui/components/layout/sidebar/sidebar.tsx +162 -36
- package/src/ui/components/layout/sidebar/tab-item.tsx +17 -1
- package/src/ui/components/layout/sidebar/use-sidebar-auto-scroll.ts +2 -2
- package/src/ui/components/layout/sidebar/use-sidebar-branch.ts +1 -1
- package/src/ui/components/layout/split-layout.tsx +1 -1
- package/src/ui/components/layout/terminal-pane.tsx +43 -40
- package/src/ui/components/modals/app/ai-usage-modal.tsx +8 -5
- package/src/ui/components/modals/app/help-modal.tsx +4 -2
- package/src/ui/components/modals/git/git-commit-modal.tsx +2 -2
- package/src/ui/components/modals/sessions/create-session-modal.tsx +5 -1
- package/src/ui/components/modals/sessions/session-picker-modal.tsx +5 -4
- package/src/ui/components/modals/shared/form.tsx +4 -5
- package/src/ui/components/modals/shared/modal-shell.tsx +3 -3
- package/src/ui/components/modals/shared/picker.tsx +28 -5
- package/src/ui/components/modals/snippets/snippet-editor-modal.tsx +10 -6
- package/src/ui/components/modals/snippets/snippet-picker-modal.tsx +25 -6
- package/src/ui/components/modals/tabs/new-tab-modal.tsx +173 -5
- package/src/ui/components/modals/worktree/worktree-move-modal.tsx +94 -0
- package/src/ui/components/overlays/ai-usage/ai-usage-indicator.tsx +3 -3
- package/src/ui/components/overlays/toast/toast-item.tsx +88 -0
- package/src/ui/components/overlays/toast/toast-position.ts +45 -0
- package/src/ui/components/overlays/toast/toast-stack.tsx +30 -0
- package/src/ui/components/overlays/toast/toast-viewport.tsx +24 -0
- package/src/ui/components/overlays/toast/use-toast-animation.ts +64 -0
- package/src/ui/components/primitives/input-field.tsx +1 -1
- package/src/ui/components/primitives/list-item.tsx +3 -3
- package/src/ui/filter-themes.ts +1 -1
- package/src/ui/root.tsx +65 -14
- package/src/ui/session-ordering.ts +2 -2
- package/src/ui/status-bar-model.ts +14 -7
- package/src/ui/terminal-graphics/capabilities.ts +1 -1
- package/src/ui/terminal-graphics/format-fallback.ts +7 -9
- package/src/ui/terminal-graphics/kitty.ts +2 -7
- 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',
|
|
90
|
+
},
|
|
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,
|
|
41
113
|
},
|
|
42
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
|
|
@@ -160,6 +316,7 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
160
316
|
...state,
|
|
161
317
|
focusMode: 'command-edit',
|
|
162
318
|
modal: {
|
|
319
|
+
actionMessage: null,
|
|
163
320
|
cursorPos: 0,
|
|
164
321
|
editBuffer: '',
|
|
165
322
|
selectedIndex: 0,
|
|
@@ -167,10 +324,16 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
167
324
|
type: 'snippet-picker',
|
|
168
325
|
},
|
|
169
326
|
}
|
|
327
|
+
case 'snippet-picker-set-message': {
|
|
328
|
+
if (state.modal.type !== 'snippet-picker') return state
|
|
329
|
+
if (state.modal.actionMessage === action.message) return state
|
|
330
|
+
return { ...state, modal: { ...state.modal, actionMessage: action.message } }
|
|
331
|
+
}
|
|
170
332
|
case 'open-snippet-editor': {
|
|
171
|
-
const snippet =
|
|
172
|
-
|
|
173
|
-
|
|
333
|
+
const snippet =
|
|
334
|
+
action.snippetId != null && action.snippetId !== ''
|
|
335
|
+
? state.snippets.find((s) => s.id === action.snippetId)
|
|
336
|
+
: undefined
|
|
174
337
|
const initialName = snippet?.name ?? ''
|
|
175
338
|
return {
|
|
176
339
|
...state,
|
|
@@ -180,8 +343,10 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
180
343
|
contentBuffer: snippet?.content ?? '',
|
|
181
344
|
cursorPos: initialName.length,
|
|
182
345
|
editBuffer: initialName,
|
|
346
|
+
nameBuffer: initialName,
|
|
183
347
|
selectedIndex: 0,
|
|
184
348
|
sessionTargetId: snippet?.id ?? null,
|
|
349
|
+
triggerBuffer: snippet?.trigger ?? '',
|
|
185
350
|
type: 'snippet-editor',
|
|
186
351
|
},
|
|
187
352
|
}
|
|
@@ -357,8 +522,9 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
357
522
|
}
|
|
358
523
|
case 'close-modal': {
|
|
359
524
|
const closingType = state.modal.type
|
|
360
|
-
//
|
|
361
|
-
|
|
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') {
|
|
362
528
|
return { ...state, modal: emptyModal() }
|
|
363
529
|
}
|
|
364
530
|
const nextFocus: AppState['focusMode'] = closingType === 'git-commit' ? 'git' : 'navigation'
|
|
@@ -380,19 +546,28 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
380
546
|
state.modal.type !== 'theme-picker' &&
|
|
381
547
|
state.modal.type !== 'create-session' &&
|
|
382
548
|
state.modal.type !== 'split-picker' &&
|
|
383
|
-
state.modal.type !== 'update-available'
|
|
549
|
+
state.modal.type !== 'update-available' &&
|
|
550
|
+
state.modal.type !== 'worktree-move'
|
|
384
551
|
) {
|
|
385
552
|
return state
|
|
386
553
|
}
|
|
387
554
|
if (state.modal.type === 'create-session' && state.modal.activeField !== 'directory') {
|
|
388
555
|
return state
|
|
389
556
|
}
|
|
557
|
+
if (state.modal.type === 'new-tab' && state.modal.step === 'worktree-create') {
|
|
558
|
+
return state
|
|
559
|
+
}
|
|
390
560
|
let optionCount: number
|
|
391
561
|
if (state.modal.type === 'new-tab') {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
state
|
|
395
|
-
|
|
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
|
+
}
|
|
396
571
|
} else if (state.modal.type === 'split-picker') {
|
|
397
572
|
optionCount = getAllAssistantOptions(state.customCommands).length
|
|
398
573
|
} else if (state.modal.type === 'create-session') {
|
|
@@ -404,6 +579,8 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
404
579
|
optionCount = filterThemeIds(state.modal.editBuffer).length
|
|
405
580
|
} else if (state.modal.type === 'update-available') {
|
|
406
581
|
optionCount = 2
|
|
582
|
+
} else if (state.modal.type === 'worktree-move') {
|
|
583
|
+
optionCount = Math.max(0, getCurrentWorktreeCount(state) - 1)
|
|
407
584
|
} else {
|
|
408
585
|
const filtered = filterSessions(state.sessions, state.modal.editBuffer)
|
|
409
586
|
optionCount = Math.max(1, filtered.length + 1)
|
|
@@ -416,6 +593,15 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
416
593
|
modal: {
|
|
417
594
|
...state.modal,
|
|
418
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),
|
|
419
605
|
},
|
|
420
606
|
}
|
|
421
607
|
}
|
|
@@ -428,18 +614,23 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
428
614
|
state.modal.type !== 'create-session' &&
|
|
429
615
|
state.modal.type !== 'split-picker' &&
|
|
430
616
|
state.modal.type !== 'update-available' &&
|
|
617
|
+
state.modal.type !== 'worktree-move' &&
|
|
431
618
|
state.modal.type !== 'help'
|
|
432
619
|
) {
|
|
433
620
|
return state
|
|
434
621
|
}
|
|
622
|
+
if (state.modal.type === 'new-tab' && state.modal.step === 'worktree-create') {
|
|
623
|
+
return state
|
|
624
|
+
}
|
|
435
625
|
let optionCount: number
|
|
436
626
|
if (state.modal.type === 'help') {
|
|
437
627
|
optionCount = state.modal.entryCount
|
|
438
628
|
} else if (state.modal.type === 'new-tab') {
|
|
439
|
-
optionCount =
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
629
|
+
optionCount =
|
|
630
|
+
state.modal.step === 'worktree'
|
|
631
|
+
? getCurrentWorktreeCount(state) + 1
|
|
632
|
+
: filterAssistants(getAllAssistantOptions(state.customCommands), state.modal.editBuffer)
|
|
633
|
+
.length
|
|
443
634
|
} else if (state.modal.type === 'split-picker') {
|
|
444
635
|
optionCount = getAllAssistantOptions(state.customCommands).length
|
|
445
636
|
} else if (state.modal.type === 'create-session') {
|
|
@@ -450,17 +641,48 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
450
641
|
optionCount = filterThemeIds(state.modal.editBuffer).length
|
|
451
642
|
} else if (state.modal.type === 'update-available') {
|
|
452
643
|
optionCount = 2
|
|
644
|
+
} else if (state.modal.type === 'worktree-move') {
|
|
645
|
+
optionCount = Math.max(0, getCurrentWorktreeCount(state) - 1)
|
|
453
646
|
} else {
|
|
454
647
|
optionCount = Math.max(1, filterSessions(state.sessions, state.modal.editBuffer).length + 1)
|
|
455
648
|
}
|
|
456
649
|
if (optionCount === 0) return state
|
|
457
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
|
+
}
|
|
458
664
|
if (clamped === state.modal.selectedIndex) return state
|
|
459
665
|
return { ...state, modal: { ...state.modal, selectedIndex: clamped } }
|
|
460
666
|
}
|
|
461
667
|
case 'move-modal-cursor': {
|
|
462
668
|
if (state.modal.editBuffer === null) return state
|
|
463
|
-
|
|
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
|
|
464
686
|
const current = state.modal.cursorPos ?? len
|
|
465
687
|
let next = current
|
|
466
688
|
if (action.to === 'home') next = 0
|
|
@@ -474,7 +696,22 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
474
696
|
if (state.modal.editBuffer === null) {
|
|
475
697
|
return state
|
|
476
698
|
}
|
|
477
|
-
|
|
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
|
+
}
|
|
478
715
|
const cursor = clampCursor(state.modal.cursorPos ?? buffer.length, buffer.length)
|
|
479
716
|
let nextBuffer: string
|
|
480
717
|
let nextCursor: number
|
|
@@ -487,6 +724,35 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
487
724
|
nextCursor = cursor + action.char.length
|
|
488
725
|
}
|
|
489
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
|
+
}
|
|
490
756
|
const resetIndex =
|
|
491
757
|
!isNewTabEditing &&
|
|
492
758
|
(state.modal.type === 'session-picker' ||
|
|
@@ -518,6 +784,50 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
518
784
|
},
|
|
519
785
|
}
|
|
520
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
|
+
}
|
|
521
831
|
if (state.modal.type === 'create-session' || state.modal.type === 'snippet-editor') {
|
|
522
832
|
return { ...state, focusMode: 'navigation', modal: emptyModal() }
|
|
523
833
|
}
|
|
@@ -526,7 +836,6 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
526
836
|
state.modal.type === 'session-picker' ||
|
|
527
837
|
state.modal.type === 'snippet-picker' ||
|
|
528
838
|
state.modal.type === 'theme-picker' ||
|
|
529
|
-
state.modal.type === 'new-tab' ||
|
|
530
839
|
state.modal.type === 'help'
|
|
531
840
|
) {
|
|
532
841
|
return { ...state, focusMode: 'navigation', modal: emptyModal() }
|
|
@@ -553,14 +862,33 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
553
862
|
}
|
|
554
863
|
}
|
|
555
864
|
if (state.modal.type === 'snippet-editor') {
|
|
556
|
-
const
|
|
557
|
-
|
|
865
|
+
const current = state.modal.editBuffer ?? ''
|
|
866
|
+
// Save current edit buffer back to the field it belongs to.
|
|
867
|
+
const updatedBuffers = {
|
|
868
|
+
contentBuffer:
|
|
869
|
+
state.modal.activeField === 'content' ? current : state.modal.contentBuffer,
|
|
870
|
+
nameBuffer: state.modal.activeField === 'name' ? current : state.modal.nameBuffer,
|
|
871
|
+
triggerBuffer:
|
|
872
|
+
state.modal.activeField === 'trigger' ? current : state.modal.triggerBuffer,
|
|
873
|
+
}
|
|
874
|
+
const cycle: Record<'name' | 'trigger' | 'content', 'name' | 'trigger' | 'content'> = {
|
|
875
|
+
content: 'name',
|
|
876
|
+
name: 'trigger',
|
|
877
|
+
trigger: 'content',
|
|
878
|
+
}
|
|
879
|
+
const nextField = cycle[state.modal.activeField]
|
|
880
|
+
const nextEditByField = {
|
|
881
|
+
content: updatedBuffers.contentBuffer,
|
|
882
|
+
name: updatedBuffers.nameBuffer,
|
|
883
|
+
trigger: updatedBuffers.triggerBuffer,
|
|
884
|
+
}
|
|
885
|
+
const nextEdit = nextEditByField[nextField]
|
|
558
886
|
return {
|
|
559
887
|
...state,
|
|
560
888
|
modal: {
|
|
561
889
|
...state.modal,
|
|
890
|
+
...updatedBuffers,
|
|
562
891
|
activeField: nextField,
|
|
563
|
-
contentBuffer: state.modal.editBuffer ?? '',
|
|
564
892
|
cursorPos: nextEdit.length,
|
|
565
893
|
editBuffer: nextEdit,
|
|
566
894
|
},
|
|
@@ -580,6 +908,38 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
580
908
|
},
|
|
581
909
|
}
|
|
582
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
|
+
}
|
|
583
943
|
return state
|
|
584
944
|
}
|
|
585
945
|
case 'select-directory': {
|
|
@@ -611,9 +971,10 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
611
971
|
}
|
|
612
972
|
}
|
|
613
973
|
case 'open-rename-tab-modal': {
|
|
614
|
-
const activeTab =
|
|
615
|
-
|
|
616
|
-
|
|
974
|
+
const activeTab =
|
|
975
|
+
state.activeTabId != null && state.activeTabId !== ''
|
|
976
|
+
? state.tabs.find((tab) => tab.id === state.activeTabId)
|
|
977
|
+
: undefined
|
|
617
978
|
if (!activeTab) {
|
|
618
979
|
return state
|
|
619
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
|
}
|