@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
|
@@ -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
|
@@ -38,7 +38,9 @@ const DEFAULT_GIT_PANE: GitPaneState = {
|
|
|
38
38
|
mode: 'embedded',
|
|
39
39
|
path: { enabled: true },
|
|
40
40
|
position: 'bottom',
|
|
41
|
+
prefetchRadius: 5,
|
|
41
42
|
ratio: 0.5,
|
|
43
|
+
treeCompaction: true,
|
|
42
44
|
visible: true,
|
|
43
45
|
}
|
|
44
46
|
|
|
@@ -65,7 +67,7 @@ export function createInitialState(
|
|
|
65
67
|
activeTabId: null,
|
|
66
68
|
currentSessionId: null,
|
|
67
69
|
customCommands,
|
|
68
|
-
focusMode: showSessionPicker ? '
|
|
70
|
+
focusMode: showSessionPicker ? 'command-edit' : 'navigation',
|
|
69
71
|
gitMode: { ...emptyGitMode(), ...overrides.gitMode },
|
|
70
72
|
gitPane: {
|
|
71
73
|
...DEFAULT_GIT_PANE,
|
|
@@ -80,7 +82,13 @@ export function createInitialState(
|
|
|
80
82
|
},
|
|
81
83
|
layoutTrees: {},
|
|
82
84
|
modal: showSessionPicker
|
|
83
|
-
? {
|
|
85
|
+
? {
|
|
86
|
+
cursorPos: 0,
|
|
87
|
+
editBuffer: '',
|
|
88
|
+
selectedIndex: 0,
|
|
89
|
+
sessionTargetId: null,
|
|
90
|
+
type: 'session-picker',
|
|
91
|
+
}
|
|
84
92
|
: emptyModal(),
|
|
85
93
|
pendingChords: null,
|
|
86
94
|
sessionBar: {
|
|
@@ -88,7 +96,7 @@ export function createInitialState(
|
|
|
88
96
|
visible: overrides.sessionBarVisible ?? true,
|
|
89
97
|
},
|
|
90
98
|
sessions,
|
|
91
|
-
|
|
99
|
+
sessionStatuses: {},
|
|
92
100
|
sidebar: {
|
|
93
101
|
maxWidth: DEFAULT_SIDEBAR_MAX_WIDTH,
|
|
94
102
|
minWidth: DEFAULT_SIDEBAR_MIN_WIDTH,
|
|
@@ -123,6 +131,8 @@ export function appReducer(state: AppState, action: AppAction): AppState {
|
|
|
123
131
|
switch (action.type) {
|
|
124
132
|
case 'set-snippets':
|
|
125
133
|
return { ...state, snippets: action.snippets }
|
|
134
|
+
case 'set-custom-commands':
|
|
135
|
+
return { ...state, customCommands: action.customCommands }
|
|
126
136
|
case 'delete-snippet': {
|
|
127
137
|
const newSnippets = state.snippets.filter((s) => s.id !== action.snippetId)
|
|
128
138
|
const filteredNew = filterSnippets(newSnippets, state.modal.editBuffer)
|
package/src/state/types.ts
CHANGED
|
@@ -1,12 +1,33 @@
|
|
|
1
1
|
import type { ModeId } from '@brimveyn/aimux-config'
|
|
2
|
+
import type { ThemedToken } from 'shiki'
|
|
3
|
+
|
|
4
|
+
import type { FileDiffMetadata } from '../diff-parser'
|
|
2
5
|
|
|
3
6
|
export type BuiltinAssistantId = 'claude' | 'codex' | 'opencode' | 'terminal'
|
|
4
7
|
|
|
5
8
|
export type AssistantId = BuiltinAssistantId | (string & {})
|
|
6
9
|
|
|
7
|
-
export type TabStatus = 'starting' | 'running' | 'disconnected' | '
|
|
10
|
+
export type TabStatus = 'starting' | 'running' | 'disconnected' | 'error'
|
|
11
|
+
|
|
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
|
+
}
|
|
8
29
|
|
|
9
|
-
export
|
|
30
|
+
export const IDLE_SESSION_STATUS: SessionStatus = { waiting: false, working: false }
|
|
10
31
|
|
|
11
32
|
export type FocusMode = 'navigation' | 'terminal-input' | 'modal' | 'command-edit' | 'git'
|
|
12
33
|
|
|
@@ -69,7 +90,7 @@ export interface PersistedTabSnapshot {
|
|
|
69
90
|
assistant: AssistantId
|
|
70
91
|
title: string
|
|
71
92
|
command: string
|
|
72
|
-
status: Exclude<
|
|
93
|
+
status: Exclude<LegacyPersistedTabStatus, 'disconnected'>
|
|
73
94
|
buffer: string
|
|
74
95
|
viewport?: TerminalSnapshot
|
|
75
96
|
terminalModes: TerminalModeState
|
|
@@ -148,8 +169,11 @@ export interface GitPaneState {
|
|
|
148
169
|
ratio: number
|
|
149
170
|
diffModeRatio: number
|
|
150
171
|
fileListMode: GitFileListMode
|
|
172
|
+
treeCompaction: boolean
|
|
151
173
|
path: GitPanePathConfig
|
|
152
174
|
diffCount: GitPaneDiffCountConfig
|
|
175
|
+
/** Prefetch this many neighbours around the selection. 0 disables prefetch. */
|
|
176
|
+
prefetchRadius: number
|
|
153
177
|
}
|
|
154
178
|
|
|
155
179
|
export type GitFileStatus = 'M' | 'A' | 'D' | 'R' | 'C' | 'U' | '?'
|
|
@@ -195,10 +219,45 @@ export interface FoldState {
|
|
|
195
219
|
bottom: number
|
|
196
220
|
}
|
|
197
221
|
|
|
222
|
+
export interface ParsedDiffEntry {
|
|
223
|
+
hash: string
|
|
224
|
+
// Stored as unknown at the state boundary; concrete type is FileDiffMetadata
|
|
225
|
+
// | null, narrowed at read sites via getParsedFile().
|
|
226
|
+
file: unknown
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export interface HighlightsEntry {
|
|
230
|
+
hash: string
|
|
231
|
+
themeId: string
|
|
232
|
+
// See ParsedDiffEntry note — narrowed via getHighlights().
|
|
233
|
+
add: unknown
|
|
234
|
+
del: unknown
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function getParsedFile(entry: ParsedDiffEntry | undefined): FileDiffMetadata | null {
|
|
238
|
+
if (!entry) return null
|
|
239
|
+
return entry.file as FileDiffMetadata | null
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function getHighlightsTokens(entry: HighlightsEntry | undefined): {
|
|
243
|
+
add: ThemedToken[][]
|
|
244
|
+
del: ThemedToken[][]
|
|
245
|
+
} | null {
|
|
246
|
+
if (!entry) return null
|
|
247
|
+
return {
|
|
248
|
+
add: entry.add as ThemedToken[][],
|
|
249
|
+
del: entry.del as ThemedToken[][],
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
198
253
|
export interface GitModeState {
|
|
199
254
|
selectedEntryKey: string | null
|
|
200
255
|
collapsedFolders: Record<string, true>
|
|
201
256
|
diffs: Record<string, DiffData>
|
|
257
|
+
/** Parsed diff keyed by fileKey. Invalidated on file change or head offset shift. */
|
|
258
|
+
parsedFiles: Record<string, ParsedDiffEntry>
|
|
259
|
+
/** Tokenised highlights keyed by `${fileKey}|${themeId}`. */
|
|
260
|
+
highlights: Record<string, HighlightsEntry>
|
|
202
261
|
loading: Record<string, boolean>
|
|
203
262
|
pendingDeletePath: string | null
|
|
204
263
|
actionMessage: string | null
|
|
@@ -223,6 +282,7 @@ export interface ModalClosed extends ModalBase {
|
|
|
223
282
|
|
|
224
283
|
export interface ModalNewTab extends ModalBase {
|
|
225
284
|
type: 'new-tab'
|
|
285
|
+
editingCommand: AssistantId | null
|
|
226
286
|
}
|
|
227
287
|
|
|
228
288
|
export interface ModalSessionPicker extends ModalBase {
|
|
@@ -269,6 +329,7 @@ export interface ModalCreateSession extends ModalBase {
|
|
|
269
329
|
pendingProjectPath: string | null
|
|
270
330
|
activeField: 'directory' | 'name'
|
|
271
331
|
nameBuffer: string
|
|
332
|
+
returnToSessionPicker: boolean
|
|
272
333
|
}
|
|
273
334
|
|
|
274
335
|
export interface ModalSnippetEditor extends ModalBase {
|
|
@@ -323,7 +384,7 @@ export interface AppState {
|
|
|
323
384
|
tabGroupMap: Record<string, string>
|
|
324
385
|
sessions: SessionRecord[]
|
|
325
386
|
currentSessionId: string | null
|
|
326
|
-
|
|
387
|
+
sessionStatuses: Record<string, SessionStatus>
|
|
327
388
|
sessionBar: SessionBarState
|
|
328
389
|
snippets: SnippetRecord[]
|
|
329
390
|
focusMode: FocusMode
|
|
@@ -342,31 +403,27 @@ export interface AppState {
|
|
|
342
403
|
export type ModalAction =
|
|
343
404
|
| { type: 'move-modal-cursor'; delta?: number; to?: 'home' | 'end' }
|
|
344
405
|
| { type: 'open-new-tab-modal' }
|
|
406
|
+
| { type: 'open-edit-custom-command'; assistantId: AssistantId }
|
|
345
407
|
| { type: 'open-help-modal'; scope?: ModeId }
|
|
346
408
|
| { type: 'open-split-picker'; direction: import('./layout-tree').SplitDirection }
|
|
347
409
|
| { type: 'open-session-picker' }
|
|
348
410
|
| { type: 'open-session-name-modal'; sessionTargetId?: string; initialName?: string }
|
|
349
411
|
| { type: 'close-modal' }
|
|
350
412
|
| { type: 'move-modal-selection'; delta: number }
|
|
351
|
-
| { type: 'begin-command-edit' }
|
|
352
413
|
| { type: 'update-command-edit'; char: string }
|
|
353
|
-
| { type: 'commit-command-edit' }
|
|
354
414
|
| { type: 'cancel-command-edit' }
|
|
355
|
-
| { type: 'open-create-session-modal' }
|
|
415
|
+
| { type: 'open-create-session-modal'; returnToSessionPicker: boolean }
|
|
356
416
|
| { type: 'set-directory-results'; results: DirectoryResult[] }
|
|
357
417
|
| { type: 'switch-create-session-field' }
|
|
358
418
|
| { type: 'select-directory' }
|
|
359
|
-
| { type: 'begin-session-filter' }
|
|
360
419
|
| { type: 'open-rename-tab-modal' }
|
|
361
420
|
| { type: 'open-snippet-picker' }
|
|
362
421
|
| { type: 'open-snippet-editor'; snippetId?: string }
|
|
363
|
-
| { type: 'begin-snippet-filter' }
|
|
364
|
-
| { type: 'begin-help-filter' }
|
|
365
|
-
| { type: 'begin-theme-filter' }
|
|
366
422
|
| { type: 'set-help-entry-count'; count: number }
|
|
367
423
|
| { type: 'set-theme-entry-count'; count: number }
|
|
368
424
|
| { type: 'open-theme-picker' }
|
|
369
425
|
| { type: 'open-update-available-modal'; currentVersion: string; latestVersion: string }
|
|
426
|
+
| { type: 'set-modal-selection-index'; index: number }
|
|
370
427
|
|
|
371
428
|
// -- Session actions --
|
|
372
429
|
export type SessionAction =
|
|
@@ -376,7 +433,7 @@ export type SessionAction =
|
|
|
376
433
|
| { type: 'rename-session-record'; sessionId: string; name: string }
|
|
377
434
|
| { type: 'delete-session-record'; sessionId: string }
|
|
378
435
|
| { type: 'reorder-sessions'; orderedIds: string[] }
|
|
379
|
-
| { type: 'set-session-
|
|
436
|
+
| { type: 'set-session-status'; sessionId: string; status: SessionStatus }
|
|
380
437
|
|
|
381
438
|
// -- Tab actions --
|
|
382
439
|
export type TabAction =
|
|
@@ -406,7 +463,6 @@ export type TabAction =
|
|
|
406
463
|
}
|
|
407
464
|
| { type: 'set-scroll-intent'; tabId: string; intent: ScrollIntent }
|
|
408
465
|
| { type: 'set-tab-activity'; tabId: string; activity?: TabActivity }
|
|
409
|
-
| { type: 'set-tab-status'; tabId: string; status: TabStatus; exitCode?: number }
|
|
410
466
|
| { type: 'set-tab-error'; tabId: string; message: string }
|
|
411
467
|
|
|
412
468
|
// -- Layout actions --
|
|
@@ -473,7 +529,23 @@ export type GitModeAction =
|
|
|
473
529
|
| { type: 'git-mode-collapse-selection' }
|
|
474
530
|
| { type: 'git-mode-expand-selection' }
|
|
475
531
|
| { type: 'git-mode-toggle-file-list-mode' }
|
|
476
|
-
| { type: 'git-mode-
|
|
532
|
+
| { type: 'git-mode-toggle-tree-compaction' }
|
|
533
|
+
| { type: 'git-mode-set-diff'; key: string; diff: DiffData; hash: string }
|
|
534
|
+
| {
|
|
535
|
+
type: 'git-mode-set-parsed'
|
|
536
|
+
key: string
|
|
537
|
+
hash: string
|
|
538
|
+
file: unknown
|
|
539
|
+
}
|
|
540
|
+
| {
|
|
541
|
+
type: 'git-mode-set-highlights'
|
|
542
|
+
key: string
|
|
543
|
+
hash: string
|
|
544
|
+
themeId: string
|
|
545
|
+
add: unknown
|
|
546
|
+
del: unknown
|
|
547
|
+
}
|
|
548
|
+
| { type: 'git-mode-invalidate-diffs'; paths: string[] }
|
|
477
549
|
| { type: 'git-mode-set-loading'; key: string; loading: boolean }
|
|
478
550
|
| { type: 'git-mode-set-pending-delete'; path: string | null }
|
|
479
551
|
| { type: 'git-mode-clear-diff-cache'; path: string }
|
|
@@ -501,6 +573,7 @@ export type GitModeAction =
|
|
|
501
573
|
export type DataAction =
|
|
502
574
|
| { type: 'set-snippets'; snippets: SnippetRecord[] }
|
|
503
575
|
| { type: 'delete-snippet'; snippetId: string }
|
|
576
|
+
| { type: 'set-custom-commands'; customCommands: Record<AssistantId, string> }
|
|
504
577
|
|
|
505
578
|
export type AppAction =
|
|
506
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
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useTheme } from '../theme'
|
|
2
|
+
|
|
3
|
+
interface BareInputProps {
|
|
4
|
+
value: string
|
|
5
|
+
cursorPos?: number
|
|
6
|
+
placeholder?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function BareInput({ cursorPos, placeholder = '', value }: BareInputProps) {
|
|
10
|
+
const theme = useTheme()
|
|
11
|
+
const fg = theme.colors['editor.foreground']
|
|
12
|
+
const bg = theme.colors['editor.background']
|
|
13
|
+
const placeholderFg = theme.colors['editorLineNumber.foreground']
|
|
14
|
+
|
|
15
|
+
if (!value) {
|
|
16
|
+
const firstChar = placeholder.charAt(0) || ' '
|
|
17
|
+
const rest = placeholder.slice(1)
|
|
18
|
+
return (
|
|
19
|
+
<text>
|
|
20
|
+
<span bg={fg} fg={bg}>
|
|
21
|
+
{firstChar}
|
|
22
|
+
</span>
|
|
23
|
+
<span fg={placeholderFg}>{rest}</span>
|
|
24
|
+
</text>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const safePos =
|
|
29
|
+
cursorPos === undefined ? value.length : Math.max(0, Math.min(value.length, cursorPos))
|
|
30
|
+
const before = value.slice(0, safePos)
|
|
31
|
+
const cursorOnEnd = safePos >= value.length
|
|
32
|
+
const cursorChar = cursorOnEnd ? ' ' : value.charAt(safePos)
|
|
33
|
+
const after = cursorOnEnd ? '' : value.slice(safePos + 1)
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<text fg={fg}>
|
|
37
|
+
{before}
|
|
38
|
+
<span bg={fg} fg={bg}>
|
|
39
|
+
{cursorChar}
|
|
40
|
+
</span>
|
|
41
|
+
{after}
|
|
42
|
+
</text>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
@@ -7,6 +7,8 @@ import { InputField } from './input-field'
|
|
|
7
7
|
import { ListItem } from './list-item'
|
|
8
8
|
import { ModalShell } from './modal-shell'
|
|
9
9
|
|
|
10
|
+
const VISIBLE_ROWS = 8
|
|
11
|
+
|
|
10
12
|
function getDirectoryResultIcon(result: DirectoryResult): string {
|
|
11
13
|
if (result.type === 'worktree') {
|
|
12
14
|
return '\u{e728}'
|
|
@@ -52,6 +54,9 @@ export function CreateSessionModal({
|
|
|
52
54
|
const dirActive = activeField === 'directory'
|
|
53
55
|
const nameActive = activeField === 'name'
|
|
54
56
|
|
|
57
|
+
const scrollOffset = Math.max(0, selectedIndex - VISIBLE_ROWS + 1)
|
|
58
|
+
const visibleResults = results.slice(scrollOffset, scrollOffset + VISIBLE_ROWS)
|
|
59
|
+
|
|
55
60
|
return (
|
|
56
61
|
<ModalShell
|
|
57
62
|
title="Create session"
|
|
@@ -66,19 +71,21 @@ export function CreateSessionModal({
|
|
|
66
71
|
</text>
|
|
67
72
|
<InputField
|
|
68
73
|
active={dirActive}
|
|
74
|
+
placeholder="Type a project name..."
|
|
69
75
|
value={
|
|
70
76
|
pendingProjectPath && !dirActive ? abbreviatePath(pendingProjectPath) : directoryQuery
|
|
71
77
|
}
|
|
72
78
|
/>
|
|
73
79
|
</box>
|
|
74
80
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
81
|
+
<box flexDirection="column" height={VISIBLE_ROWS}>
|
|
82
|
+
{results.length === 0 ? (
|
|
83
|
+
<text fg={theme.colors['descriptionForeground']}>
|
|
84
|
+
{directoryQuery.length > 0 ? 'No matches' : 'Type a project name to search...'}
|
|
85
|
+
</text>
|
|
86
|
+
) : (
|
|
87
|
+
visibleResults.map((result, index) => {
|
|
88
|
+
const active = dirActive && scrollOffset + index === selectedIndex
|
|
82
89
|
return (
|
|
83
90
|
<ListItem
|
|
84
91
|
key={result.path}
|
|
@@ -100,7 +107,8 @@ export function CreateSessionModal({
|
|
|
100
107
|
/>
|
|
101
108
|
)
|
|
102
109
|
})
|
|
103
|
-
|
|
110
|
+
)}
|
|
111
|
+
</box>
|
|
104
112
|
|
|
105
113
|
<box flexDirection="column">
|
|
106
114
|
<text
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useTheme } from '../../theme'
|
|
1
|
+
import { useBg, useTheme } from '../../theme'
|
|
2
2
|
import { FOLD_STEP, type FoldInfo } from './build-rows'
|
|
3
3
|
import { type FoldDispatch } from './pierre-diff'
|
|
4
4
|
|
|
@@ -9,13 +9,9 @@ interface Props {
|
|
|
9
9
|
|
|
10
10
|
function Button({ label, onPress }: { label: string; onPress: () => void }) {
|
|
11
11
|
const theme = useTheme()
|
|
12
|
+
const bg = useBg('sideBar.background')
|
|
12
13
|
return (
|
|
13
|
-
<box
|
|
14
|
-
paddingLeft={1}
|
|
15
|
-
paddingRight={1}
|
|
16
|
-
backgroundColor={theme.colors['sideBar.background']}
|
|
17
|
-
onMouseDown={onPress}
|
|
18
|
-
>
|
|
14
|
+
<box paddingLeft={1} paddingRight={1} backgroundColor={bg} onMouseDown={onPress}>
|
|
19
15
|
<text fg={theme.colors['textLink.foreground']}>{label}</text>
|
|
20
16
|
</box>
|
|
21
17
|
)
|
|
@@ -27,6 +23,7 @@ function Spacer() {
|
|
|
27
23
|
|
|
28
24
|
export function FoldStrip({ dispatch, fold }: Props) {
|
|
29
25
|
const theme = useTheme()
|
|
26
|
+
const headerBg = useBg('sideBarSectionHeader.background')
|
|
30
27
|
const { bottomExpanded, foldId, hidden, topExpanded, total } = fold
|
|
31
28
|
const stepUp = Math.min(FOLD_STEP, hidden)
|
|
32
29
|
const stepDown = Math.min(FOLD_STEP, hidden)
|
|
@@ -73,12 +70,7 @@ export function FoldStrip({ dispatch, fold }: Props) {
|
|
|
73
70
|
}
|
|
74
71
|
|
|
75
72
|
return (
|
|
76
|
-
<box
|
|
77
|
-
flexDirection="row"
|
|
78
|
-
backgroundColor={theme.colors['sideBarSectionHeader.background']}
|
|
79
|
-
paddingLeft={1}
|
|
80
|
-
paddingRight={1}
|
|
81
|
-
>
|
|
73
|
+
<box flexDirection="row" backgroundColor={headerBg} paddingLeft={1} paddingRight={1}>
|
|
82
74
|
<text fg={theme.colors['descriptionForeground']}>{`⋯ ${hidden} hidden `}</text>
|
|
83
75
|
{controls}
|
|
84
76
|
</box>
|
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
import type { ScrollBoxRenderable } from '@opentui/core'
|
|
2
2
|
import type { ThemedToken } from 'shiki'
|
|
3
3
|
|
|
4
|
-
import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef
|
|
4
|
+
import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef } from 'react'
|
|
5
5
|
|
|
6
|
-
import type { FoldState } from '../../../state/types'
|
|
7
6
|
import type { ThemeId } from '../../themes'
|
|
8
7
|
|
|
9
|
-
import { parsePatchFiles } from '../../../diff-parser'
|
|
10
8
|
import { useAppStore } from '../../../state/app-store'
|
|
11
9
|
import { dispatchGlobal } from '../../../state/dispatch-ref'
|
|
10
|
+
import { type FoldState } from '../../../state/types'
|
|
12
11
|
import { useTheme } from '../../theme'
|
|
13
12
|
import { buildSplitRows, buildUnifiedRows, firstChangeRowOffset, gutterWidth } from './build-rows'
|
|
14
|
-
import { filetypeFromPath } from './filetype'
|
|
15
|
-
import { tokenizeSide } from './highlight'
|
|
16
13
|
import { SplitView, type SplitViewHandle } from './split-view'
|
|
17
14
|
import { StackedView, type StackedViewHandle } from './stacked-view'
|
|
15
|
+
import { useDiffPreparation } from './use-diff-preparation'
|
|
18
16
|
|
|
19
17
|
export type DiffView = 'split' | 'stacked'
|
|
20
18
|
|
|
@@ -41,7 +39,6 @@ interface Props {
|
|
|
41
39
|
view: DiffView
|
|
42
40
|
}
|
|
43
41
|
|
|
44
|
-
const EMPTY_HIGHLIGHTS: DiffHighlights = { add: [], del: [] }
|
|
45
42
|
const EMPTY_FOLDS: Record<string, FoldState> = {}
|
|
46
43
|
|
|
47
44
|
export const PierreDiff = forwardRef<PierreDiffHandle, Props>(function PierreDiff(
|
|
@@ -49,12 +46,9 @@ export const PierreDiff = forwardRef<PierreDiffHandle, Props>(function PierreDif
|
|
|
49
46
|
ref
|
|
50
47
|
) {
|
|
51
48
|
const theme = useTheme()
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}, [diff])
|
|
56
|
-
|
|
57
|
-
const filetype = useMemo(() => filetypeFromPath(path), [path])
|
|
49
|
+
const preparation = useDiffPreparation(cacheKey, diff, path, themeId)
|
|
50
|
+
const file = preparation.file ?? undefined
|
|
51
|
+
const highlights: DiffHighlights = preparation.highlights
|
|
58
52
|
|
|
59
53
|
const terminalCols = useAppStore((s) => s.layout.terminalCols)
|
|
60
54
|
const sidebarWidth = useAppStore((s) => s.sidebar.width)
|
|
@@ -78,24 +72,6 @@ export const PierreDiff = forwardRef<PierreDiffHandle, Props>(function PierreDif
|
|
|
78
72
|
[cacheKey]
|
|
79
73
|
)
|
|
80
74
|
|
|
81
|
-
const [highlights, setHighlights] = useState<DiffHighlights>(EMPTY_HIGHLIGHTS)
|
|
82
|
-
|
|
83
|
-
useEffect(() => {
|
|
84
|
-
setHighlights(EMPTY_HIGHLIGHTS)
|
|
85
|
-
if (!file || !filetype) return
|
|
86
|
-
let cancelled = false
|
|
87
|
-
void Promise.all([
|
|
88
|
-
tokenizeSide(file.additionLines, filetype, themeId),
|
|
89
|
-
tokenizeSide(file.deletionLines, filetype, themeId),
|
|
90
|
-
]).then(([add, del]) => {
|
|
91
|
-
if (cancelled) return
|
|
92
|
-
setHighlights({ add, del })
|
|
93
|
-
})
|
|
94
|
-
return () => {
|
|
95
|
-
cancelled = true
|
|
96
|
-
}
|
|
97
|
-
}, [file, filetype, themeId])
|
|
98
|
-
|
|
99
75
|
const splitRef = useRef<SplitViewHandle | null>(null)
|
|
100
76
|
const stackedRef = useRef<StackedViewHandle | null>(null)
|
|
101
77
|
|
|
@@ -141,7 +117,9 @@ export const PierreDiff = forwardRef<PierreDiffHandle, Props>(function PierreDif
|
|
|
141
117
|
if (!file) {
|
|
142
118
|
return (
|
|
143
119
|
<box flexGrow={1} padding={1}>
|
|
144
|
-
<text fg={theme.colors['descriptionForeground']}>
|
|
120
|
+
<text fg={theme.colors['descriptionForeground']}>
|
|
121
|
+
{preparation.preparing ? 'Preparing diff…' : '(could not parse diff)'}
|
|
122
|
+
</text>
|
|
145
123
|
</box>
|
|
146
124
|
)
|
|
147
125
|
}
|