@brimveyn/aimux 1.6.2 → 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 +24 -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 +2 -1
- package/src/config.ts +9 -0
- 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/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 +2 -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/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 +11 -3
- package/src/state/types.ts +29 -13
- 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/split-view.tsx +11 -17
- package/src/ui/components/diff-renderer/stacked-view.tsx +7 -14
- package/src/ui/components/git-panel.tsx +10 -3
- package/src/ui/components/git-view.tsx +4 -8
- 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
|
@@ -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)
|
|
@@ -64,8 +64,8 @@ export function reduceSessionState(state: AppState, action: AppAction): AppState
|
|
|
64
64
|
const filteredNew = filterSessions(newSessions, state.modal.editBuffer)
|
|
65
65
|
const maxIndex = filteredNew.length
|
|
66
66
|
const clampedIndex = Math.min(state.modal.selectedIndex, maxIndex)
|
|
67
|
-
const
|
|
68
|
-
delete
|
|
67
|
+
const nextStatuses = { ...state.sessionStatuses }
|
|
68
|
+
delete nextStatuses[action.sessionId]
|
|
69
69
|
return {
|
|
70
70
|
...state,
|
|
71
71
|
activeTabId: action.sessionId === state.currentSessionId ? null : state.activeTabId,
|
|
@@ -79,7 +79,7 @@ export function reduceSessionState(state: AppState, action: AppAction): AppState
|
|
|
79
79
|
type: 'session-picker',
|
|
80
80
|
},
|
|
81
81
|
sessions: newSessions,
|
|
82
|
-
|
|
82
|
+
sessionStatuses: nextStatuses,
|
|
83
83
|
tabs: action.sessionId === state.currentSessionId ? [] : state.tabs,
|
|
84
84
|
}
|
|
85
85
|
}
|
|
@@ -101,11 +101,18 @@ export function reduceSessionState(state: AppState, action: AppAction): AppState
|
|
|
101
101
|
}
|
|
102
102
|
return { ...state, sessions: ordered }
|
|
103
103
|
}
|
|
104
|
-
case 'set-session-
|
|
105
|
-
|
|
104
|
+
case 'set-session-status': {
|
|
105
|
+
const prev = state.sessionStatuses[action.sessionId]
|
|
106
|
+
if (
|
|
107
|
+
prev !== undefined &&
|
|
108
|
+
prev.working === action.status.working &&
|
|
109
|
+
prev.waiting === action.status.waiting
|
|
110
|
+
) {
|
|
111
|
+
return state
|
|
112
|
+
}
|
|
106
113
|
return {
|
|
107
114
|
...state,
|
|
108
|
-
|
|
115
|
+
sessionStatuses: { ...state.sessionStatuses, [action.sessionId]: action.status },
|
|
109
116
|
}
|
|
110
117
|
}
|
|
111
118
|
default:
|
|
@@ -432,16 +432,6 @@ export function reduceTabState(state: AppState, action: AppAction): AppState | n
|
|
|
432
432
|
...state,
|
|
433
433
|
tabs: updateTab(state.tabs, action.tabId, (tab) => ({ ...tab, activity: action.activity })),
|
|
434
434
|
}
|
|
435
|
-
case 'set-tab-status':
|
|
436
|
-
return {
|
|
437
|
-
...state,
|
|
438
|
-
tabs: updateTab(state.tabs, action.tabId, (tab) => ({
|
|
439
|
-
...tab,
|
|
440
|
-
activity: action.status === 'running' ? tab.activity : undefined,
|
|
441
|
-
exitCode: action.exitCode,
|
|
442
|
-
status: action.status,
|
|
443
|
-
})),
|
|
444
|
-
}
|
|
445
435
|
case 'set-tab-error':
|
|
446
436
|
return {
|
|
447
437
|
...state,
|
package/src/state/selectors.ts
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
|
+
import type { AssistantOption } from '../pty/command-registry'
|
|
1
2
|
import type { SessionRecord, SnippetRecord } from './types'
|
|
2
3
|
|
|
4
|
+
export function filterAssistants(
|
|
5
|
+
options: AssistantOption[],
|
|
6
|
+
filter: string | null
|
|
7
|
+
): AssistantOption[] {
|
|
8
|
+
if (!filter) return options
|
|
9
|
+
const lower = filter.toLowerCase()
|
|
10
|
+
return options.filter(
|
|
11
|
+
(o) => o.label.toLowerCase().includes(lower) || o.description.toLowerCase().includes(lower)
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
|
|
3
15
|
export function filterSessions(sessions: SessionRecord[], filter: string | null): SessionRecord[] {
|
|
4
16
|
if (!filter) {
|
|
5
17
|
return sessions
|
|
@@ -20,7 +20,7 @@ export function createEmptyWorkspaceSnapshot(): WorkspaceSnapshotV1 {
|
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
function getDisconnectedStatus(status:
|
|
23
|
+
function getDisconnectedStatus(status: TabStatus): TabStatus {
|
|
24
24
|
if (status === 'running' || status === 'starting') {
|
|
25
25
|
return 'disconnected'
|
|
26
26
|
}
|
|
@@ -61,20 +61,25 @@ export function restoreTabsFromWorkspace(snapshot: WorkspaceSnapshotV1 | undefin
|
|
|
61
61
|
return []
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
return snapshot.tabs
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
64
|
+
return snapshot.tabs
|
|
65
|
+
.filter(
|
|
66
|
+
(tab): tab is typeof tab & { status: Exclude<typeof tab.status, 'exited'> } =>
|
|
67
|
+
tab.status !== 'exited'
|
|
68
|
+
)
|
|
69
|
+
.map((tab) => ({
|
|
70
|
+
activity: 'idle',
|
|
71
|
+
assistant: tab.assistant,
|
|
72
|
+
buffer: tab.buffer,
|
|
73
|
+
command: tab.command,
|
|
74
|
+
errorMessage: tab.errorMessage,
|
|
75
|
+
exitCode: tab.exitCode,
|
|
76
|
+
id: tab.id,
|
|
77
|
+
scrollIntent: tab.scrollIntent ?? DEFAULT_SCROLL_INTENT,
|
|
78
|
+
status: getDisconnectedStatus(tab.status),
|
|
79
|
+
terminalModes: tab.terminalModes,
|
|
80
|
+
title: tab.title,
|
|
81
|
+
viewport: tab.viewport,
|
|
82
|
+
}))
|
|
78
83
|
}
|
|
79
84
|
|
|
80
85
|
export function restoreLayoutTrees(
|
package/src/state/store.ts
CHANGED
|
@@ -67,7 +67,7 @@ export function createInitialState(
|
|
|
67
67
|
activeTabId: null,
|
|
68
68
|
currentSessionId: null,
|
|
69
69
|
customCommands,
|
|
70
|
-
focusMode: showSessionPicker ? '
|
|
70
|
+
focusMode: showSessionPicker ? 'command-edit' : 'navigation',
|
|
71
71
|
gitMode: { ...emptyGitMode(), ...overrides.gitMode },
|
|
72
72
|
gitPane: {
|
|
73
73
|
...DEFAULT_GIT_PANE,
|
|
@@ -82,7 +82,13 @@ export function createInitialState(
|
|
|
82
82
|
},
|
|
83
83
|
layoutTrees: {},
|
|
84
84
|
modal: showSessionPicker
|
|
85
|
-
? {
|
|
85
|
+
? {
|
|
86
|
+
cursorPos: 0,
|
|
87
|
+
editBuffer: '',
|
|
88
|
+
selectedIndex: 0,
|
|
89
|
+
sessionTargetId: null,
|
|
90
|
+
type: 'session-picker',
|
|
91
|
+
}
|
|
86
92
|
: emptyModal(),
|
|
87
93
|
pendingChords: null,
|
|
88
94
|
sessionBar: {
|
|
@@ -90,7 +96,7 @@ export function createInitialState(
|
|
|
90
96
|
visible: overrides.sessionBarVisible ?? true,
|
|
91
97
|
},
|
|
92
98
|
sessions,
|
|
93
|
-
|
|
99
|
+
sessionStatuses: {},
|
|
94
100
|
sidebar: {
|
|
95
101
|
maxWidth: DEFAULT_SIDEBAR_MAX_WIDTH,
|
|
96
102
|
minWidth: DEFAULT_SIDEBAR_MIN_WIDTH,
|
|
@@ -125,6 +131,8 @@ export function appReducer(state: AppState, action: AppAction): AppState {
|
|
|
125
131
|
switch (action.type) {
|
|
126
132
|
case 'set-snippets':
|
|
127
133
|
return { ...state, snippets: action.snippets }
|
|
134
|
+
case 'set-custom-commands':
|
|
135
|
+
return { ...state, customCommands: action.customCommands }
|
|
128
136
|
case 'delete-snippet': {
|
|
129
137
|
const newSnippets = state.snippets.filter((s) => s.id !== action.snippetId)
|
|
130
138
|
const filteredNew = filterSnippets(newSnippets, state.modal.editBuffer)
|
package/src/state/types.ts
CHANGED
|
@@ -7,9 +7,27 @@ export type BuiltinAssistantId = 'claude' | 'codex' | 'opencode' | 'terminal'
|
|
|
7
7
|
|
|
8
8
|
export type AssistantId = BuiltinAssistantId | (string & {})
|
|
9
9
|
|
|
10
|
-
export type TabStatus = 'starting' | 'running' | 'disconnected' | '
|
|
10
|
+
export type TabStatus = 'starting' | 'running' | 'disconnected' | 'error'
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Status values that may appear in legacy on-disk workspace snapshots but are
|
|
14
|
+
* not produced by the running app anymore. Filtered at restore time.
|
|
15
|
+
*/
|
|
16
|
+
export type LegacyPersistedTabStatus = TabStatus | 'exited'
|
|
17
|
+
|
|
18
|
+
export type TabActivity = 'working' | 'waiting-input' | 'idle'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Per-session status flags. Both can be true at once (e.g. one tab working,
|
|
22
|
+
* another waiting for user input) so we keep them as independent booleans
|
|
23
|
+
* rather than a single priority enum.
|
|
24
|
+
*/
|
|
25
|
+
export interface SessionStatus {
|
|
26
|
+
working: boolean
|
|
27
|
+
waiting: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const IDLE_SESSION_STATUS: SessionStatus = { waiting: false, working: false }
|
|
13
31
|
|
|
14
32
|
export type FocusMode = 'navigation' | 'terminal-input' | 'modal' | 'command-edit' | 'git'
|
|
15
33
|
|
|
@@ -72,7 +90,7 @@ export interface PersistedTabSnapshot {
|
|
|
72
90
|
assistant: AssistantId
|
|
73
91
|
title: string
|
|
74
92
|
command: string
|
|
75
|
-
status: Exclude<
|
|
93
|
+
status: Exclude<LegacyPersistedTabStatus, 'disconnected'>
|
|
76
94
|
buffer: string
|
|
77
95
|
viewport?: TerminalSnapshot
|
|
78
96
|
terminalModes: TerminalModeState
|
|
@@ -264,6 +282,7 @@ export interface ModalClosed extends ModalBase {
|
|
|
264
282
|
|
|
265
283
|
export interface ModalNewTab extends ModalBase {
|
|
266
284
|
type: 'new-tab'
|
|
285
|
+
editingCommand: AssistantId | null
|
|
267
286
|
}
|
|
268
287
|
|
|
269
288
|
export interface ModalSessionPicker extends ModalBase {
|
|
@@ -310,6 +329,7 @@ export interface ModalCreateSession extends ModalBase {
|
|
|
310
329
|
pendingProjectPath: string | null
|
|
311
330
|
activeField: 'directory' | 'name'
|
|
312
331
|
nameBuffer: string
|
|
332
|
+
returnToSessionPicker: boolean
|
|
313
333
|
}
|
|
314
334
|
|
|
315
335
|
export interface ModalSnippetEditor extends ModalBase {
|
|
@@ -364,7 +384,7 @@ export interface AppState {
|
|
|
364
384
|
tabGroupMap: Record<string, string>
|
|
365
385
|
sessions: SessionRecord[]
|
|
366
386
|
currentSessionId: string | null
|
|
367
|
-
|
|
387
|
+
sessionStatuses: Record<string, SessionStatus>
|
|
368
388
|
sessionBar: SessionBarState
|
|
369
389
|
snippets: SnippetRecord[]
|
|
370
390
|
focusMode: FocusMode
|
|
@@ -383,31 +403,27 @@ export interface AppState {
|
|
|
383
403
|
export type ModalAction =
|
|
384
404
|
| { type: 'move-modal-cursor'; delta?: number; to?: 'home' | 'end' }
|
|
385
405
|
| { type: 'open-new-tab-modal' }
|
|
406
|
+
| { type: 'open-edit-custom-command'; assistantId: AssistantId }
|
|
386
407
|
| { type: 'open-help-modal'; scope?: ModeId }
|
|
387
408
|
| { type: 'open-split-picker'; direction: import('./layout-tree').SplitDirection }
|
|
388
409
|
| { type: 'open-session-picker' }
|
|
389
410
|
| { type: 'open-session-name-modal'; sessionTargetId?: string; initialName?: string }
|
|
390
411
|
| { type: 'close-modal' }
|
|
391
412
|
| { type: 'move-modal-selection'; delta: number }
|
|
392
|
-
| { type: 'begin-command-edit' }
|
|
393
413
|
| { type: 'update-command-edit'; char: string }
|
|
394
|
-
| { type: 'commit-command-edit' }
|
|
395
414
|
| { type: 'cancel-command-edit' }
|
|
396
|
-
| { type: 'open-create-session-modal' }
|
|
415
|
+
| { type: 'open-create-session-modal'; returnToSessionPicker: boolean }
|
|
397
416
|
| { type: 'set-directory-results'; results: DirectoryResult[] }
|
|
398
417
|
| { type: 'switch-create-session-field' }
|
|
399
418
|
| { type: 'select-directory' }
|
|
400
|
-
| { type: 'begin-session-filter' }
|
|
401
419
|
| { type: 'open-rename-tab-modal' }
|
|
402
420
|
| { type: 'open-snippet-picker' }
|
|
403
421
|
| { type: 'open-snippet-editor'; snippetId?: string }
|
|
404
|
-
| { type: 'begin-snippet-filter' }
|
|
405
|
-
| { type: 'begin-help-filter' }
|
|
406
|
-
| { type: 'begin-theme-filter' }
|
|
407
422
|
| { type: 'set-help-entry-count'; count: number }
|
|
408
423
|
| { type: 'set-theme-entry-count'; count: number }
|
|
409
424
|
| { type: 'open-theme-picker' }
|
|
410
425
|
| { type: 'open-update-available-modal'; currentVersion: string; latestVersion: string }
|
|
426
|
+
| { type: 'set-modal-selection-index'; index: number }
|
|
411
427
|
|
|
412
428
|
// -- Session actions --
|
|
413
429
|
export type SessionAction =
|
|
@@ -417,7 +433,7 @@ export type SessionAction =
|
|
|
417
433
|
| { type: 'rename-session-record'; sessionId: string; name: string }
|
|
418
434
|
| { type: 'delete-session-record'; sessionId: string }
|
|
419
435
|
| { type: 'reorder-sessions'; orderedIds: string[] }
|
|
420
|
-
| { type: 'set-session-
|
|
436
|
+
| { type: 'set-session-status'; sessionId: string; status: SessionStatus }
|
|
421
437
|
|
|
422
438
|
// -- Tab actions --
|
|
423
439
|
export type TabAction =
|
|
@@ -447,7 +463,6 @@ export type TabAction =
|
|
|
447
463
|
}
|
|
448
464
|
| { type: 'set-scroll-intent'; tabId: string; intent: ScrollIntent }
|
|
449
465
|
| { type: 'set-tab-activity'; tabId: string; activity?: TabActivity }
|
|
450
|
-
| { type: 'set-tab-status'; tabId: string; status: TabStatus; exitCode?: number }
|
|
451
466
|
| { type: 'set-tab-error'; tabId: string; message: string }
|
|
452
467
|
|
|
453
468
|
// -- Layout actions --
|
|
@@ -558,6 +573,7 @@ export type GitModeAction =
|
|
|
558
573
|
export type DataAction =
|
|
559
574
|
| { type: 'set-snippets'; snippets: SnippetRecord[] }
|
|
560
575
|
| { type: 'delete-snippet'; snippetId: string }
|
|
576
|
+
| { type: 'set-custom-commands'; customCommands: Record<AssistantId, string> }
|
|
561
577
|
|
|
562
578
|
export type AppAction =
|
|
563
579
|
| ModalAction
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useKeyboard } from '@opentui/react'
|
|
2
|
+
|
|
3
|
+
import { ListItem } from './components/list-item'
|
|
4
|
+
import { ModalShell } from './components/modal-shell'
|
|
5
|
+
import { uiTokens } from './ui-tokens'
|
|
6
|
+
|
|
7
|
+
interface BreakingUpdateScreenProps {
|
|
8
|
+
onConfirm: () => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function BreakingUpdateScreen({ onConfirm }: BreakingUpdateScreenProps) {
|
|
12
|
+
useKeyboard((key) => {
|
|
13
|
+
if (key.name === 'return') {
|
|
14
|
+
key.preventDefault()
|
|
15
|
+
onConfirm()
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<ModalShell title="Breaking update detected" width={uiTokens.modalWidth.md}>
|
|
21
|
+
<box flexDirection="column" gap={1}>
|
|
22
|
+
<box flexDirection="column" gap={0}>
|
|
23
|
+
<text>{"We're sorry, but this update contains breaking protocol changes."}</text>
|
|
24
|
+
<text>The terminal manager must be restarted.</text>
|
|
25
|
+
<text>You will lose your current terminal states.</text>
|
|
26
|
+
</box>
|
|
27
|
+
<ListItem active direction="row" title={<text>OK — Restart terminal manager</text>} />
|
|
28
|
+
</box>
|
|
29
|
+
</ModalShell>
|
|
30
|
+
)
|
|
31
|
+
}
|