@brimveyn/aimux 1.6.1 → 1.7.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 +2 -2
- package/src/app-runtime/backend-attach-runtime.ts +6 -0
- package/src/app-runtime/backend-runtime-events.ts +21 -21
- package/src/app-runtime/side-effects.ts +43 -12
- package/src/app-runtime/use-backend-runtime.ts +2 -20
- package/src/app-runtime/use-directory-search.ts +6 -1
- package/src/app.tsx +6 -1
- package/src/config.ts +28 -1
- package/src/daemon/daemon.ts +197 -15
- package/src/daemon/session-manager.ts +9 -0
- package/src/daemon/session-registry.ts +5 -5
- package/src/git/diff-hash.ts +10 -0
- package/src/index.tsx +10 -2
- package/src/input/keymap/help-entries.ts +5 -5
- package/src/input/modes/bridge.ts +5 -8
- package/src/input/modes/transitions.ts +15 -23
- package/src/input/modes/types.ts +3 -5
- package/src/ipc/protocol.ts +49 -5
- package/src/platform/project-search.ts +45 -12
- package/src/pty/assistant-status-detection-loop.ts +192 -0
- package/src/pty/assistant-status-detector.ts +226 -0
- package/src/pty/pty-manager.ts +3 -37
- package/src/session-backend/bootstrap.ts +4 -1
- package/src/session-backend/local-session-backend.ts +43 -56
- package/src/session-backend/remote-session-backend.ts +15 -0
- package/src/session-backend/types.ts +10 -1
- package/src/state/git-tree.ts +42 -16
- package/src/state/reducers/diff-cache.ts +64 -0
- package/src/state/reducers/git-mode-state.ts +91 -33
- package/src/state/reducers/git-panel-state.ts +33 -2
- package/src/state/reducers/modal-state.ts +91 -134
- package/src/state/reducers/session-state.ts +13 -6
- package/src/state/reducers/tab-state.ts +0 -10
- package/src/state/selectors.ts +12 -0
- package/src/state/session-persistence.ts +20 -15
- package/src/state/store.ts +13 -3
- package/src/state/types.ts +87 -14
- package/src/ui/breaking-update-screen.tsx +31 -0
- package/src/ui/components/bare-input.tsx +44 -0
- package/src/ui/components/create-session-modal.tsx +16 -8
- package/src/ui/components/diff-renderer/fold-strip.tsx +5 -13
- package/src/ui/components/diff-renderer/pierre-diff.tsx +9 -31
- package/src/ui/components/diff-renderer/prepare-diff.ts +66 -0
- package/src/ui/components/diff-renderer/split-view.tsx +35 -16
- package/src/ui/components/diff-renderer/stacked-view.tsx +30 -12
- package/src/ui/components/diff-renderer/use-diff-prefetch.ts +191 -0
- package/src/ui/components/diff-renderer/use-diff-preparation.ts +106 -0
- package/src/ui/components/git-pane-widget.tsx +2 -0
- package/src/ui/components/git-panel.tsx +27 -10
- package/src/ui/components/git-view.tsx +23 -9
- package/src/ui/components/help-modal.tsx +45 -160
- package/src/ui/components/input-field.tsx +6 -2
- package/src/ui/components/list-item.tsx +36 -28
- package/src/ui/components/modal-shell.tsx +51 -13
- package/src/ui/components/new-tab-modal.tsx +78 -45
- package/src/ui/components/picker.tsx +179 -0
- package/src/ui/components/session-bar.tsx +47 -21
- package/src/ui/components/session-picker-modal.tsx +58 -56
- package/src/ui/components/sidebar.tsx +49 -22
- package/src/ui/components/snippet-picker-modal.tsx +43 -34
- package/src/ui/components/status-bar.tsx +3 -2
- package/src/ui/components/surface.tsx +11 -8
- package/src/ui/components/tab-item.tsx +38 -22
- package/src/ui/components/terminal-pane.tsx +8 -8
- package/src/ui/components/theme-picker-modal.tsx +51 -91
- package/src/ui/root.tsx +14 -23
- package/src/ui/status-bar-model.ts +5 -5
- package/src/ui/theme-store.ts +26 -2
- package/src/ui/theme.ts +9 -1
- package/src/ui/components/modal-filter-bar.tsx +0 -19
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
moveGitSelection,
|
|
8
8
|
reconcileSelectedGitEntryKey,
|
|
9
9
|
} from '../git-tree'
|
|
10
|
+
import { clearDiffCacheForPath, clearDiffCacheForPaths } from './diff-cache'
|
|
10
11
|
import { sortFilesBySection } from './git-panel-state'
|
|
11
12
|
|
|
12
13
|
function applyFold(state: AppState, key: string, foldId: string, next: FoldState): AppState {
|
|
@@ -28,33 +29,6 @@ function applyFold(state: AppState, key: string, foldId: string, next: FoldState
|
|
|
28
29
|
return { ...state, gitMode: { ...state.gitMode, folds: nextFolds } }
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
function clearDiffCacheForPath(state: AppState, path: string): AppState {
|
|
32
|
-
const nextDiffs = { ...state.gitMode.diffs }
|
|
33
|
-
const nextFolds = { ...state.gitMode.folds }
|
|
34
|
-
const nextLoading = { ...state.gitMode.loading }
|
|
35
|
-
let changed = false
|
|
36
|
-
for (const key of Object.keys(nextDiffs)) {
|
|
37
|
-
if (nextDiffs[key]?.path !== path) continue
|
|
38
|
-
delete nextDiffs[key]
|
|
39
|
-
changed = true
|
|
40
|
-
}
|
|
41
|
-
for (const key of Object.keys(nextFolds)) {
|
|
42
|
-
if (!key.endsWith(`:${path}`)) continue
|
|
43
|
-
delete nextFolds[key]
|
|
44
|
-
changed = true
|
|
45
|
-
}
|
|
46
|
-
for (const key of Object.keys(nextLoading)) {
|
|
47
|
-
if (!key.endsWith(`:${path}`)) continue
|
|
48
|
-
delete nextLoading[key]
|
|
49
|
-
changed = true
|
|
50
|
-
}
|
|
51
|
-
if (!changed) return state
|
|
52
|
-
return {
|
|
53
|
-
...state,
|
|
54
|
-
gitMode: { ...state.gitMode, diffs: nextDiffs, folds: nextFolds, loading: nextLoading },
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
32
|
export function emptyGitMode(): GitModeState {
|
|
59
33
|
return {
|
|
60
34
|
actionMessage: null,
|
|
@@ -63,7 +37,9 @@ export function emptyGitMode(): GitModeState {
|
|
|
63
37
|
diffView: 'split',
|
|
64
38
|
folds: {},
|
|
65
39
|
headOffset: 0,
|
|
40
|
+
highlights: {},
|
|
66
41
|
loading: {},
|
|
42
|
+
parsedFiles: {},
|
|
67
43
|
pendingDeletePath: null,
|
|
68
44
|
selectedEntryKey: null,
|
|
69
45
|
}
|
|
@@ -84,7 +60,9 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
84
60
|
state.gitPanel.files,
|
|
85
61
|
state.gitMode.collapsedFolders,
|
|
86
62
|
state.gitPane.fileListMode,
|
|
87
|
-
null
|
|
63
|
+
null,
|
|
64
|
+
[],
|
|
65
|
+
state.gitPane.treeCompaction
|
|
88
66
|
),
|
|
89
67
|
},
|
|
90
68
|
}
|
|
@@ -100,7 +78,9 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
100
78
|
diffs: {},
|
|
101
79
|
folds: {},
|
|
102
80
|
headOffset: 0,
|
|
81
|
+
highlights: {},
|
|
103
82
|
loading: {},
|
|
83
|
+
parsedFiles: {},
|
|
104
84
|
},
|
|
105
85
|
}
|
|
106
86
|
}
|
|
@@ -110,7 +90,8 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
110
90
|
state.gitMode.collapsedFolders,
|
|
111
91
|
state.gitPane.fileListMode,
|
|
112
92
|
state.gitMode.selectedEntryKey,
|
|
113
|
-
action.delta
|
|
93
|
+
action.delta,
|
|
94
|
+
state.gitPane.treeCompaction
|
|
114
95
|
)
|
|
115
96
|
if (!next || next === state.gitMode.selectedEntryKey) return state
|
|
116
97
|
return {
|
|
@@ -128,7 +109,8 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
128
109
|
state.gitMode.collapsedFolders,
|
|
129
110
|
state.gitPane.fileListMode,
|
|
130
111
|
state.gitMode.selectedEntryKey,
|
|
131
|
-
action.delta
|
|
112
|
+
action.delta,
|
|
113
|
+
state.gitPane.treeCompaction
|
|
132
114
|
)
|
|
133
115
|
if (!next || next === state.gitMode.selectedEntryKey) return state
|
|
134
116
|
return {
|
|
@@ -146,7 +128,8 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
146
128
|
state.gitMode.collapsedFolders,
|
|
147
129
|
state.gitPane.fileListMode,
|
|
148
130
|
state.gitMode.selectedEntryKey,
|
|
149
|
-
[action.key]
|
|
131
|
+
[action.key],
|
|
132
|
+
state.gitPane.treeCompaction
|
|
150
133
|
)
|
|
151
134
|
if (!next || next !== action.key || next === state.gitMode.selectedEntryKey) return state
|
|
152
135
|
return {
|
|
@@ -161,6 +144,7 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
161
144
|
case 'git-mode-toggle-folder': {
|
|
162
145
|
const row = getSelectedGitRow(state.gitPanel.files, {
|
|
163
146
|
collapsedFolders: state.gitMode.collapsedFolders,
|
|
147
|
+
compact: state.gitPane.treeCompaction,
|
|
164
148
|
fileListMode: state.gitPane.fileListMode,
|
|
165
149
|
selectedEntryKey: action.key,
|
|
166
150
|
})
|
|
@@ -176,6 +160,7 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
176
160
|
case 'git-mode-toggle-selected-folder': {
|
|
177
161
|
const row = getSelectedGitRow(state.gitPanel.files, {
|
|
178
162
|
collapsedFolders: state.gitMode.collapsedFolders,
|
|
163
|
+
compact: state.gitPane.treeCompaction,
|
|
179
164
|
fileListMode: state.gitPane.fileListMode,
|
|
180
165
|
selectedEntryKey: state.gitMode.selectedEntryKey,
|
|
181
166
|
})
|
|
@@ -191,6 +176,7 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
191
176
|
case 'git-mode-collapse-selection': {
|
|
192
177
|
const row = getSelectedGitRow(state.gitPanel.files, {
|
|
193
178
|
collapsedFolders: state.gitMode.collapsedFolders,
|
|
179
|
+
compact: state.gitPane.treeCompaction,
|
|
194
180
|
fileListMode: state.gitPane.fileListMode,
|
|
195
181
|
selectedEntryKey: state.gitMode.selectedEntryKey,
|
|
196
182
|
})
|
|
@@ -217,6 +203,7 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
217
203
|
case 'git-mode-expand-selection': {
|
|
218
204
|
const row = getSelectedGitRow(state.gitPanel.files, {
|
|
219
205
|
collapsedFolders: state.gitMode.collapsedFolders,
|
|
206
|
+
compact: state.gitPane.treeCompaction,
|
|
220
207
|
fileListMode: state.gitPane.fileListMode,
|
|
221
208
|
selectedEntryKey: state.gitMode.selectedEntryKey,
|
|
222
209
|
})
|
|
@@ -236,24 +223,90 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
236
223
|
state.gitPanel.files,
|
|
237
224
|
state.gitMode.collapsedFolders,
|
|
238
225
|
fileListMode,
|
|
239
|
-
state.gitMode.selectedEntryKey
|
|
226
|
+
state.gitMode.selectedEntryKey,
|
|
227
|
+
[],
|
|
228
|
+
state.gitPane.treeCompaction
|
|
240
229
|
),
|
|
241
230
|
},
|
|
242
231
|
gitPane: { ...state.gitPane, fileListMode },
|
|
243
232
|
}
|
|
244
233
|
}
|
|
234
|
+
case 'git-mode-toggle-tree-compaction': {
|
|
235
|
+
const treeCompaction = !state.gitPane.treeCompaction
|
|
236
|
+
return {
|
|
237
|
+
...state,
|
|
238
|
+
gitMode: {
|
|
239
|
+
...state.gitMode,
|
|
240
|
+
pendingDeletePath: null,
|
|
241
|
+
selectedEntryKey: reconcileSelectedGitEntryKey(
|
|
242
|
+
state.gitPanel.files,
|
|
243
|
+
state.gitMode.collapsedFolders,
|
|
244
|
+
state.gitPane.fileListMode,
|
|
245
|
+
state.gitMode.selectedEntryKey,
|
|
246
|
+
[],
|
|
247
|
+
treeCompaction
|
|
248
|
+
),
|
|
249
|
+
},
|
|
250
|
+
gitPane: { ...state.gitPane, treeCompaction },
|
|
251
|
+
}
|
|
252
|
+
}
|
|
245
253
|
case 'git-mode-set-diff': {
|
|
246
254
|
const nextLoading = { ...state.gitMode.loading }
|
|
247
255
|
delete nextLoading[action.key]
|
|
256
|
+
// Drop stale derived entries (parsed + highlights) whose hash no longer matches.
|
|
257
|
+
const nextParsed = { ...state.gitMode.parsedFiles }
|
|
258
|
+
const prevParsed = nextParsed[action.key]
|
|
259
|
+
if (prevParsed && prevParsed.hash !== action.hash) delete nextParsed[action.key]
|
|
260
|
+
const nextHighlights = { ...state.gitMode.highlights }
|
|
261
|
+
for (const key of Object.keys(nextHighlights)) {
|
|
262
|
+
if (!key.startsWith(`${action.key}|`)) continue
|
|
263
|
+
if (nextHighlights[key]?.hash !== action.hash) delete nextHighlights[key]
|
|
264
|
+
}
|
|
248
265
|
return {
|
|
249
266
|
...state,
|
|
250
267
|
gitMode: {
|
|
251
268
|
...state.gitMode,
|
|
252
269
|
diffs: { ...state.gitMode.diffs, [action.key]: action.diff },
|
|
270
|
+
highlights: nextHighlights,
|
|
253
271
|
loading: nextLoading,
|
|
272
|
+
parsedFiles: nextParsed,
|
|
273
|
+
},
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
case 'git-mode-set-parsed': {
|
|
277
|
+
return {
|
|
278
|
+
...state,
|
|
279
|
+
gitMode: {
|
|
280
|
+
...state.gitMode,
|
|
281
|
+
parsedFiles: {
|
|
282
|
+
...state.gitMode.parsedFiles,
|
|
283
|
+
[action.key]: { file: action.file, hash: action.hash },
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
case 'git-mode-set-highlights': {
|
|
289
|
+
const k = `${action.key}|${action.themeId}`
|
|
290
|
+
return {
|
|
291
|
+
...state,
|
|
292
|
+
gitMode: {
|
|
293
|
+
...state.gitMode,
|
|
294
|
+
highlights: {
|
|
295
|
+
...state.gitMode.highlights,
|
|
296
|
+
[k]: {
|
|
297
|
+
add: action.add,
|
|
298
|
+
del: action.del,
|
|
299
|
+
hash: action.hash,
|
|
300
|
+
themeId: action.themeId,
|
|
301
|
+
},
|
|
302
|
+
},
|
|
254
303
|
},
|
|
255
304
|
}
|
|
256
305
|
}
|
|
306
|
+
case 'git-mode-invalidate-diffs': {
|
|
307
|
+
if (action.paths.length === 0) return state
|
|
308
|
+
return clearDiffCacheForPaths(state, new Set(action.paths))
|
|
309
|
+
}
|
|
257
310
|
case 'git-mode-set-loading': {
|
|
258
311
|
const nextLoading = { ...state.gitMode.loading }
|
|
259
312
|
if (action.loading) {
|
|
@@ -289,7 +342,9 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
289
342
|
diffs: {},
|
|
290
343
|
folds: {},
|
|
291
344
|
headOffset: next,
|
|
345
|
+
highlights: {},
|
|
292
346
|
loading: {},
|
|
347
|
+
parsedFiles: {},
|
|
293
348
|
pendingDeletePath: null,
|
|
294
349
|
},
|
|
295
350
|
}
|
|
@@ -304,7 +359,9 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
304
359
|
diffs: {},
|
|
305
360
|
folds: {},
|
|
306
361
|
headOffset: next,
|
|
362
|
+
highlights: {},
|
|
307
363
|
loading: {},
|
|
364
|
+
parsedFiles: {},
|
|
308
365
|
pendingDeletePath: null,
|
|
309
366
|
},
|
|
310
367
|
}
|
|
@@ -357,7 +414,8 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
357
414
|
state.gitMode.collapsedFolders,
|
|
358
415
|
state.gitPane.fileListMode,
|
|
359
416
|
currentKey,
|
|
360
|
-
preferredSelection
|
|
417
|
+
preferredSelection,
|
|
418
|
+
state.gitPane.treeCompaction
|
|
361
419
|
)
|
|
362
420
|
|
|
363
421
|
return {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AppAction, AppState, GitFileEntry, GitFileSection, GitPanelState } from '../types'
|
|
2
2
|
|
|
3
3
|
import { reconcileSelectedGitEntryKey } from '../git-tree'
|
|
4
|
+
import { clearDiffCacheForPaths } from './diff-cache'
|
|
4
5
|
|
|
5
6
|
export const GIT_PANEL_MIN_RATIO = 0.2
|
|
6
7
|
export const GIT_PANEL_MAX_RATIO = 0.8
|
|
@@ -35,6 +36,32 @@ export function emptyGitPanel(): GitPanelState {
|
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
function fileSignature(f: GitFileEntry): string {
|
|
40
|
+
return `${f.status}|${f.added ?? '-'}|${f.removed ?? '-'}|${f.renamedFrom ?? ''}`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Paths whose content/status shifted between two snapshots — used to drop
|
|
44
|
+
// stale cached diffs so we re-fetch instead of serving pre-edit data.
|
|
45
|
+
function diffChangedPaths(prev: GitFileEntry[], next: GitFileEntry[]): Set<string> {
|
|
46
|
+
const prevByKey = new Map<string, string>()
|
|
47
|
+
for (const f of prev) prevByKey.set(`${f.section}:${f.path}`, fileSignature(f))
|
|
48
|
+
const changed = new Set<string>()
|
|
49
|
+
const nextKeys = new Set<string>()
|
|
50
|
+
for (const f of next) {
|
|
51
|
+
const key = `${f.section}:${f.path}`
|
|
52
|
+
nextKeys.add(key)
|
|
53
|
+
const prevSig = prevByKey.get(key)
|
|
54
|
+
if (prevSig !== fileSignature(f)) changed.add(f.path)
|
|
55
|
+
}
|
|
56
|
+
for (const [key, _sig] of prevByKey) {
|
|
57
|
+
if (nextKeys.has(key)) continue
|
|
58
|
+
const [, ...rest] = key.split(':')
|
|
59
|
+
const path = rest.join(':')
|
|
60
|
+
if (path) changed.add(path)
|
|
61
|
+
}
|
|
62
|
+
return changed
|
|
63
|
+
}
|
|
64
|
+
|
|
38
65
|
function sameFiles(a: GitFileEntry[], b: GitFileEntry[]): boolean {
|
|
39
66
|
if (a.length !== b.length) return false
|
|
40
67
|
for (let i = 0; i < a.length; i++) {
|
|
@@ -109,7 +136,9 @@ export function reduceGitPanelState(state: AppState, action: AppAction): AppStat
|
|
|
109
136
|
sortedNext,
|
|
110
137
|
state.gitMode.collapsedFolders,
|
|
111
138
|
state.gitPane.fileListMode,
|
|
112
|
-
state.gitMode.selectedEntryKey
|
|
139
|
+
state.gitMode.selectedEntryKey,
|
|
140
|
+
[],
|
|
141
|
+
state.gitPane.treeCompaction
|
|
113
142
|
)
|
|
114
143
|
if (
|
|
115
144
|
prev.branch === next.branch &&
|
|
@@ -121,7 +150,8 @@ export function reduceGitPanelState(state: AppState, action: AppAction): AppStat
|
|
|
121
150
|
) {
|
|
122
151
|
return state
|
|
123
152
|
}
|
|
124
|
-
|
|
153
|
+
const changedPaths = diffChangedPaths(prev.files, sortedNext)
|
|
154
|
+
const base: AppState = {
|
|
125
155
|
...state,
|
|
126
156
|
gitMode: { ...state.gitMode, selectedEntryKey: nextSelectedEntryKey },
|
|
127
157
|
gitPanel: {
|
|
@@ -133,6 +163,7 @@ export function reduceGitPanelState(state: AppState, action: AppAction): AppStat
|
|
|
133
163
|
files: sortedNext,
|
|
134
164
|
},
|
|
135
165
|
}
|
|
166
|
+
return changedPaths.size === 0 ? base : clearDiffCacheForPaths(base, changedPaths)
|
|
136
167
|
}
|
|
137
168
|
case 'git-refresh-error': {
|
|
138
169
|
const prev = state.gitPanel
|
|
@@ -6,7 +6,7 @@ import { collectHelpEntries } from '../../input/keymap/help-entries'
|
|
|
6
6
|
import { getActiveKeymap } from '../../input/keymap/keymap-ref'
|
|
7
7
|
import { getAllAssistantOptions } from '../../pty/command-registry'
|
|
8
8
|
import { filterThemeIds } from '../../ui/filter-themes'
|
|
9
|
-
import { filterSessions, filterSnippets } from '../selectors'
|
|
9
|
+
import { filterAssistants, filterSessions, filterSnippets } from '../selectors'
|
|
10
10
|
|
|
11
11
|
function emptyModal() {
|
|
12
12
|
return {
|
|
@@ -29,15 +29,30 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
29
29
|
case 'open-new-tab-modal':
|
|
30
30
|
return {
|
|
31
31
|
...state,
|
|
32
|
-
focusMode: '
|
|
32
|
+
focusMode: 'command-edit',
|
|
33
33
|
modal: {
|
|
34
34
|
cursorPos: 0,
|
|
35
|
-
editBuffer:
|
|
35
|
+
editBuffer: '',
|
|
36
|
+
editingCommand: null,
|
|
36
37
|
selectedIndex: 0,
|
|
37
38
|
sessionTargetId: null,
|
|
38
39
|
type: 'new-tab',
|
|
39
40
|
},
|
|
40
41
|
}
|
|
42
|
+
case 'open-edit-custom-command': {
|
|
43
|
+
if (state.modal.type !== 'new-tab') return state
|
|
44
|
+
const current = state.customCommands[action.assistantId] ?? ''
|
|
45
|
+
return {
|
|
46
|
+
...state,
|
|
47
|
+
focusMode: 'command-edit',
|
|
48
|
+
modal: {
|
|
49
|
+
...state.modal,
|
|
50
|
+
cursorPos: current.length,
|
|
51
|
+
editBuffer: current,
|
|
52
|
+
editingCommand: action.assistantId,
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
}
|
|
41
56
|
case 'open-help-modal': {
|
|
42
57
|
const keymap = getActiveKeymap()
|
|
43
58
|
const scope = action.scope ?? null
|
|
@@ -47,7 +62,7 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
47
62
|
...state,
|
|
48
63
|
modal: {
|
|
49
64
|
cursorPos: 0,
|
|
50
|
-
editBuffer:
|
|
65
|
+
editBuffer: '',
|
|
51
66
|
entryCount: scopedEntries.length,
|
|
52
67
|
scope,
|
|
53
68
|
selectedIndex: 0,
|
|
@@ -72,10 +87,10 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
72
87
|
case 'open-session-picker':
|
|
73
88
|
return {
|
|
74
89
|
...state,
|
|
75
|
-
focusMode: '
|
|
90
|
+
focusMode: 'command-edit',
|
|
76
91
|
modal: {
|
|
77
92
|
cursorPos: 0,
|
|
78
|
-
editBuffer:
|
|
93
|
+
editBuffer: '',
|
|
79
94
|
selectedIndex: 0,
|
|
80
95
|
sessionTargetId: null,
|
|
81
96
|
type: 'session-picker',
|
|
@@ -106,6 +121,7 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
106
121
|
editBuffer: '',
|
|
107
122
|
nameBuffer: '',
|
|
108
123
|
pendingProjectPath: null,
|
|
124
|
+
returnToSessionPicker: action.returnToSessionPicker,
|
|
109
125
|
selectedIndex: 0,
|
|
110
126
|
sessionTargetId: null,
|
|
111
127
|
type: 'create-session',
|
|
@@ -127,10 +143,10 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
127
143
|
case 'open-snippet-picker':
|
|
128
144
|
return {
|
|
129
145
|
...state,
|
|
130
|
-
focusMode: '
|
|
146
|
+
focusMode: 'command-edit',
|
|
131
147
|
modal: {
|
|
132
148
|
cursorPos: 0,
|
|
133
|
-
editBuffer:
|
|
149
|
+
editBuffer: '',
|
|
134
150
|
selectedIndex: 0,
|
|
135
151
|
sessionTargetId: null,
|
|
136
152
|
type: 'snippet-picker',
|
|
@@ -158,10 +174,10 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
158
174
|
case 'open-theme-picker':
|
|
159
175
|
return {
|
|
160
176
|
...state,
|
|
161
|
-
focusMode: '
|
|
177
|
+
focusMode: 'command-edit',
|
|
162
178
|
modal: {
|
|
163
179
|
cursorPos: 0,
|
|
164
|
-
editBuffer:
|
|
180
|
+
editBuffer: '',
|
|
165
181
|
entryCount: 0,
|
|
166
182
|
selectedIndex: 0,
|
|
167
183
|
sessionTargetId: null,
|
|
@@ -196,17 +212,6 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
196
212
|
type: 'git-commit',
|
|
197
213
|
},
|
|
198
214
|
}
|
|
199
|
-
case 'begin-snippet-filter': {
|
|
200
|
-
if (state.modal.type !== 'snippet-picker') {
|
|
201
|
-
return state
|
|
202
|
-
}
|
|
203
|
-
const buf = state.modal.editBuffer ?? ''
|
|
204
|
-
return {
|
|
205
|
-
...state,
|
|
206
|
-
focusMode: 'command-edit',
|
|
207
|
-
modal: { ...state.modal, cursorPos: buf.length, editBuffer: buf },
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
215
|
case 'set-help-entry-count': {
|
|
211
216
|
if (state.modal.type !== 'help') return state
|
|
212
217
|
if (state.modal.entryCount === action.count) {
|
|
@@ -221,16 +226,6 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
221
226
|
modal: { ...state.modal, entryCount: action.count, selectedIndex: clamped },
|
|
222
227
|
}
|
|
223
228
|
}
|
|
224
|
-
case 'begin-help-filter': {
|
|
225
|
-
if (state.modal.type !== 'help') {
|
|
226
|
-
return state
|
|
227
|
-
}
|
|
228
|
-
const buf = state.modal.editBuffer ?? ''
|
|
229
|
-
return {
|
|
230
|
-
...state,
|
|
231
|
-
modal: { ...state.modal, cursorPos: buf.length, editBuffer: buf },
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
229
|
case 'set-theme-entry-count': {
|
|
235
230
|
if (state.modal.type !== 'theme-picker') return state
|
|
236
231
|
const clamped = Math.min(state.modal.selectedIndex, Math.max(0, action.count - 1))
|
|
@@ -242,17 +237,6 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
242
237
|
modal: { ...state.modal, entryCount: action.count, selectedIndex: clamped },
|
|
243
238
|
}
|
|
244
239
|
}
|
|
245
|
-
case 'begin-theme-filter': {
|
|
246
|
-
if (state.modal.type !== 'theme-picker') {
|
|
247
|
-
return state
|
|
248
|
-
}
|
|
249
|
-
const buf = state.modal.editBuffer ?? ''
|
|
250
|
-
return {
|
|
251
|
-
...state,
|
|
252
|
-
focusMode: 'command-edit',
|
|
253
|
-
modal: { ...state.modal, cursorPos: buf.length, editBuffer: buf },
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
240
|
case 'close-modal': {
|
|
257
241
|
const closingType = state.modal.type
|
|
258
242
|
// Help is a pure overlay — it never flipped focusMode, so leave it alone.
|
|
@@ -286,7 +270,12 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
286
270
|
return state
|
|
287
271
|
}
|
|
288
272
|
let optionCount: number
|
|
289
|
-
if (state.modal.type === 'new-tab'
|
|
273
|
+
if (state.modal.type === 'new-tab') {
|
|
274
|
+
optionCount = filterAssistants(
|
|
275
|
+
getAllAssistantOptions(state.customCommands),
|
|
276
|
+
state.modal.editBuffer
|
|
277
|
+
).length
|
|
278
|
+
} else if (state.modal.type === 'split-picker') {
|
|
290
279
|
optionCount = getAllAssistantOptions(state.customCommands).length
|
|
291
280
|
} else if (state.modal.type === 'create-session') {
|
|
292
281
|
optionCount = state.modal.directoryResults.length
|
|
@@ -312,6 +301,45 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
312
301
|
},
|
|
313
302
|
}
|
|
314
303
|
}
|
|
304
|
+
case 'set-modal-selection-index': {
|
|
305
|
+
if (
|
|
306
|
+
state.modal.type !== 'new-tab' &&
|
|
307
|
+
state.modal.type !== 'session-picker' &&
|
|
308
|
+
state.modal.type !== 'snippet-picker' &&
|
|
309
|
+
state.modal.type !== 'theme-picker' &&
|
|
310
|
+
state.modal.type !== 'create-session' &&
|
|
311
|
+
state.modal.type !== 'split-picker' &&
|
|
312
|
+
state.modal.type !== 'update-available' &&
|
|
313
|
+
state.modal.type !== 'help'
|
|
314
|
+
) {
|
|
315
|
+
return state
|
|
316
|
+
}
|
|
317
|
+
let optionCount: number
|
|
318
|
+
if (state.modal.type === 'help') {
|
|
319
|
+
optionCount = state.modal.entryCount
|
|
320
|
+
} else if (state.modal.type === 'new-tab') {
|
|
321
|
+
optionCount = filterAssistants(
|
|
322
|
+
getAllAssistantOptions(state.customCommands),
|
|
323
|
+
state.modal.editBuffer
|
|
324
|
+
).length
|
|
325
|
+
} else if (state.modal.type === 'split-picker') {
|
|
326
|
+
optionCount = getAllAssistantOptions(state.customCommands).length
|
|
327
|
+
} else if (state.modal.type === 'create-session') {
|
|
328
|
+
optionCount = state.modal.directoryResults.length
|
|
329
|
+
} else if (state.modal.type === 'snippet-picker') {
|
|
330
|
+
optionCount = filterSnippets(state.snippets, state.modal.editBuffer).length
|
|
331
|
+
} else if (state.modal.type === 'theme-picker') {
|
|
332
|
+
optionCount = filterThemeIds(state.modal.editBuffer).length
|
|
333
|
+
} else if (state.modal.type === 'update-available') {
|
|
334
|
+
optionCount = 2
|
|
335
|
+
} else {
|
|
336
|
+
optionCount = Math.max(1, filterSessions(state.sessions, state.modal.editBuffer).length + 1)
|
|
337
|
+
}
|
|
338
|
+
if (optionCount === 0) return state
|
|
339
|
+
const clamped = Math.max(0, Math.min(optionCount - 1, action.index))
|
|
340
|
+
if (clamped === state.modal.selectedIndex) return state
|
|
341
|
+
return { ...state, modal: { ...state.modal, selectedIndex: clamped } }
|
|
342
|
+
}
|
|
315
343
|
case 'move-modal-cursor': {
|
|
316
344
|
if (state.modal.editBuffer === null) return state
|
|
317
345
|
const len = state.modal.editBuffer.length
|
|
@@ -324,23 +352,6 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
324
352
|
if (next === current) return state
|
|
325
353
|
return { ...state, modal: { ...state.modal, cursorPos: next } }
|
|
326
354
|
}
|
|
327
|
-
case 'begin-command-edit': {
|
|
328
|
-
if (state.modal.type !== 'new-tab' && state.modal.type !== 'session-name') {
|
|
329
|
-
return state
|
|
330
|
-
}
|
|
331
|
-
if (state.modal.type === 'session-name') {
|
|
332
|
-
return { ...state, focusMode: 'command-edit' }
|
|
333
|
-
}
|
|
334
|
-
const allOptions = getAllAssistantOptions(state.customCommands)
|
|
335
|
-
const option = allOptions[state.modal.selectedIndex]
|
|
336
|
-
const assistantId = option?.id
|
|
337
|
-
const currentCmd = (assistantId && state.customCommands[assistantId]) ?? option?.command ?? ''
|
|
338
|
-
return {
|
|
339
|
-
...state,
|
|
340
|
-
focusMode: 'command-edit',
|
|
341
|
-
modal: { ...state.modal, cursorPos: currentCmd.length, editBuffer: currentCmd },
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
355
|
case 'update-command-edit': {
|
|
345
356
|
if (state.modal.editBuffer === null) {
|
|
346
357
|
return state
|
|
@@ -357,11 +368,14 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
357
368
|
nextBuffer = buffer.slice(0, cursor) + action.char + buffer.slice(cursor)
|
|
358
369
|
nextCursor = cursor + action.char.length
|
|
359
370
|
}
|
|
371
|
+
const isNewTabEditing = state.modal.type === 'new-tab' && state.modal.editingCommand !== null
|
|
360
372
|
const resetIndex =
|
|
361
|
-
|
|
362
|
-
state.modal.type === '
|
|
363
|
-
|
|
364
|
-
|
|
373
|
+
!isNewTabEditing &&
|
|
374
|
+
(state.modal.type === 'session-picker' ||
|
|
375
|
+
state.modal.type === 'snippet-picker' ||
|
|
376
|
+
state.modal.type === 'theme-picker' ||
|
|
377
|
+
state.modal.type === 'new-tab' ||
|
|
378
|
+
state.modal.type === 'help')
|
|
365
379
|
? 0
|
|
366
380
|
: state.modal.selectedIndex
|
|
367
381
|
return {
|
|
@@ -374,76 +388,30 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
374
388
|
},
|
|
375
389
|
}
|
|
376
390
|
}
|
|
377
|
-
case '
|
|
378
|
-
if (state.modal.
|
|
379
|
-
return state
|
|
380
|
-
}
|
|
381
|
-
if (state.modal.type === 'create-session') {
|
|
382
|
-
return state
|
|
383
|
-
}
|
|
384
|
-
if (state.modal.type === 'snippet-editor') {
|
|
385
|
-
return state
|
|
386
|
-
}
|
|
387
|
-
if (state.modal.type === 'rename-tab') {
|
|
388
|
-
return state
|
|
389
|
-
}
|
|
390
|
-
if (state.modal.type === 'session-name') {
|
|
391
|
-
return state
|
|
392
|
-
}
|
|
393
|
-
if (state.modal.type === 'session-picker' || state.modal.type === 'snippet-picker') {
|
|
394
|
-
return {
|
|
395
|
-
...state,
|
|
396
|
-
focusMode: 'modal',
|
|
397
|
-
modal: { ...state.modal, selectedIndex: 0 },
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
if (state.modal.type !== 'new-tab') {
|
|
401
|
-
return state
|
|
402
|
-
}
|
|
403
|
-
const allOpts = getAllAssistantOptions(state.customCommands)
|
|
404
|
-
const option = allOpts[state.modal.selectedIndex]
|
|
405
|
-
const assistantId = option?.id
|
|
406
|
-
if (!assistantId) {
|
|
391
|
+
case 'cancel-command-edit': {
|
|
392
|
+
if (state.modal.type === 'new-tab' && state.modal.editingCommand !== null) {
|
|
407
393
|
return {
|
|
408
394
|
...state,
|
|
409
|
-
|
|
410
|
-
|
|
395
|
+
modal: {
|
|
396
|
+
...state.modal,
|
|
397
|
+
cursorPos: 0,
|
|
398
|
+
editBuffer: '',
|
|
399
|
+
editingCommand: null,
|
|
400
|
+
},
|
|
411
401
|
}
|
|
412
402
|
}
|
|
413
|
-
const trimmed = state.modal.editBuffer.trim()
|
|
414
|
-
const newCustomCommands = { ...state.customCommands }
|
|
415
|
-
if (trimmed) {
|
|
416
|
-
newCustomCommands[assistantId] = trimmed
|
|
417
|
-
} else {
|
|
418
|
-
delete newCustomCommands[assistantId]
|
|
419
|
-
}
|
|
420
|
-
return {
|
|
421
|
-
...state,
|
|
422
|
-
customCommands: newCustomCommands,
|
|
423
|
-
focusMode: 'modal',
|
|
424
|
-
modal: { ...state.modal, cursorPos: 0, editBuffer: null },
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
case 'cancel-command-edit': {
|
|
428
403
|
if (state.modal.type === 'create-session' || state.modal.type === 'snippet-editor') {
|
|
429
404
|
return { ...state, focusMode: 'navigation', modal: emptyModal() }
|
|
430
405
|
}
|
|
431
|
-
|
|
432
|
-
return {
|
|
433
|
-
...state,
|
|
434
|
-
modal: { ...state.modal, cursorPos: 0, editBuffer: null, selectedIndex: 0 },
|
|
435
|
-
}
|
|
436
|
-
}
|
|
406
|
+
// Pickers and overlays with auto-filter — Esc closes the modal entirely.
|
|
437
407
|
if (
|
|
438
408
|
state.modal.type === 'session-picker' ||
|
|
439
409
|
state.modal.type === 'snippet-picker' ||
|
|
440
|
-
state.modal.type === 'theme-picker'
|
|
410
|
+
state.modal.type === 'theme-picker' ||
|
|
411
|
+
state.modal.type === 'new-tab' ||
|
|
412
|
+
state.modal.type === 'help'
|
|
441
413
|
) {
|
|
442
|
-
return {
|
|
443
|
-
...state,
|
|
444
|
-
focusMode: 'modal',
|
|
445
|
-
modal: { ...state.modal, cursorPos: 0, editBuffer: null, selectedIndex: 0 },
|
|
446
|
-
}
|
|
414
|
+
return { ...state, focusMode: 'navigation', modal: emptyModal() }
|
|
447
415
|
}
|
|
448
416
|
return {
|
|
449
417
|
...state,
|
|
@@ -524,17 +492,6 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
524
492
|
},
|
|
525
493
|
}
|
|
526
494
|
}
|
|
527
|
-
case 'begin-session-filter': {
|
|
528
|
-
if (state.modal.type !== 'session-picker') {
|
|
529
|
-
return state
|
|
530
|
-
}
|
|
531
|
-
const buf = state.modal.editBuffer ?? ''
|
|
532
|
-
return {
|
|
533
|
-
...state,
|
|
534
|
-
focusMode: 'command-edit',
|
|
535
|
-
modal: { ...state.modal, cursorPos: buf.length, editBuffer: buf },
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
495
|
case 'open-rename-tab-modal': {
|
|
539
496
|
const activeTab = state.activeTabId
|
|
540
497
|
? state.tabs.find((tab) => tab.id === state.activeTabId)
|