@brimveyn/aimux 1.1.0 → 1.2.5
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/package.json +3 -4
- package/src/app-runtime/side-effects.ts +120 -0
- package/src/app-runtime/use-renderer-bindings.ts +6 -0
- package/src/app.tsx +6 -0
- package/src/git/command-queue.ts +7 -0
- package/src/git/git-diff.ts +83 -0
- package/src/git/git-poller.ts +4 -4
- package/src/git/git-status.ts +32 -0
- package/src/index.tsx +0 -0
- package/src/input/modes/bridge.ts +2 -0
- package/src/input/modes/handlers/shared.ts +16 -0
- package/src/input/modes/transitions.ts +3 -0
- package/src/input/modes/types.ts +9 -0
- package/src/state/dispatch-ref.ts +13 -0
- package/src/state/reducers/git-mode-state.ts +136 -0
- package/src/state/reducers/git-panel-state.ts +15 -3
- package/src/state/reducers/modal-state.ts +129 -22
- package/src/state/store.ts +5 -0
- package/src/state/types.ts +56 -1
- package/src/ui/components/git-commit-modal.tsx +42 -0
- package/src/ui/components/git-panel.tsx +42 -13
- package/src/ui/components/git-view.tsx +237 -0
- package/src/ui/components/input-field.tsx +26 -5
- package/src/ui/components/status-bar.tsx +8 -0
- package/src/ui/git-view-controls.ts +11 -0
- package/src/ui/root.tsx +36 -2
- package/src/ui/status-bar-model.ts +18 -0
- package/src/ui/syntax.ts +102 -0
- package/src/ui/theme.ts +9 -0
- package/src/ui/themes.ts +24 -0
- package/src/ui/components/pending-chord-indicator.tsx +0 -41
|
@@ -10,6 +10,7 @@ const THEME_COUNT = THEME_IDS.length
|
|
|
10
10
|
|
|
11
11
|
function emptyModal() {
|
|
12
12
|
return {
|
|
13
|
+
cursorPos: 0,
|
|
13
14
|
editBuffer: null,
|
|
14
15
|
selectedIndex: 0,
|
|
15
16
|
sessionTargetId: null,
|
|
@@ -19,25 +20,42 @@ function emptyModal() {
|
|
|
19
20
|
|
|
20
21
|
export { emptyModal }
|
|
21
22
|
|
|
23
|
+
function clampCursor(value: number, max: number): number {
|
|
24
|
+
return Math.max(0, Math.min(max, value))
|
|
25
|
+
}
|
|
26
|
+
|
|
22
27
|
export function reduceModalState(state: AppState, action: AppAction): AppState | null {
|
|
23
28
|
switch (action.type) {
|
|
24
29
|
case 'open-new-tab-modal':
|
|
25
30
|
return {
|
|
26
31
|
...state,
|
|
27
32
|
focusMode: 'modal',
|
|
28
|
-
modal: {
|
|
33
|
+
modal: {
|
|
34
|
+
cursorPos: 0,
|
|
35
|
+
editBuffer: null,
|
|
36
|
+
selectedIndex: 0,
|
|
37
|
+
sessionTargetId: null,
|
|
38
|
+
type: 'new-tab',
|
|
39
|
+
},
|
|
29
40
|
}
|
|
30
41
|
case 'open-help-modal':
|
|
31
42
|
return {
|
|
32
43
|
...state,
|
|
33
44
|
focusMode: 'modal',
|
|
34
|
-
modal: {
|
|
45
|
+
modal: {
|
|
46
|
+
cursorPos: 0,
|
|
47
|
+
editBuffer: null,
|
|
48
|
+
selectedIndex: 0,
|
|
49
|
+
sessionTargetId: null,
|
|
50
|
+
type: 'help',
|
|
51
|
+
},
|
|
35
52
|
}
|
|
36
53
|
case 'open-split-picker':
|
|
37
54
|
return {
|
|
38
55
|
...state,
|
|
39
56
|
focusMode: 'modal',
|
|
40
57
|
modal: {
|
|
58
|
+
cursorPos: 0,
|
|
41
59
|
editBuffer: null,
|
|
42
60
|
selectedIndex: 0,
|
|
43
61
|
sessionTargetId: null,
|
|
@@ -50,29 +68,34 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
50
68
|
...state,
|
|
51
69
|
focusMode: 'modal',
|
|
52
70
|
modal: {
|
|
71
|
+
cursorPos: 0,
|
|
53
72
|
editBuffer: null,
|
|
54
73
|
selectedIndex: 0,
|
|
55
74
|
sessionTargetId: null,
|
|
56
75
|
type: 'session-picker',
|
|
57
76
|
},
|
|
58
77
|
}
|
|
59
|
-
case 'open-session-name-modal':
|
|
78
|
+
case 'open-session-name-modal': {
|
|
79
|
+
const initialName = action.initialName ?? ''
|
|
60
80
|
return {
|
|
61
81
|
...state,
|
|
62
82
|
focusMode: 'command-edit',
|
|
63
83
|
modal: {
|
|
64
|
-
|
|
84
|
+
cursorPos: initialName.length,
|
|
85
|
+
editBuffer: initialName,
|
|
65
86
|
selectedIndex: 0,
|
|
66
87
|
sessionTargetId: action.sessionTargetId ?? null,
|
|
67
88
|
type: 'session-name',
|
|
68
89
|
},
|
|
69
90
|
}
|
|
91
|
+
}
|
|
70
92
|
case 'open-create-session-modal':
|
|
71
93
|
return {
|
|
72
94
|
...state,
|
|
73
95
|
focusMode: 'command-edit',
|
|
74
96
|
modal: {
|
|
75
97
|
activeField: 'directory',
|
|
98
|
+
cursorPos: 0,
|
|
76
99
|
directoryResults: [],
|
|
77
100
|
editBuffer: '',
|
|
78
101
|
nameBuffer: '',
|
|
@@ -100,6 +123,7 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
100
123
|
...state,
|
|
101
124
|
focusMode: 'modal',
|
|
102
125
|
modal: {
|
|
126
|
+
cursorPos: 0,
|
|
103
127
|
editBuffer: null,
|
|
104
128
|
selectedIndex: 0,
|
|
105
129
|
sessionTargetId: null,
|
|
@@ -110,13 +134,15 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
110
134
|
const snippet = action.snippetId
|
|
111
135
|
? state.snippets.find((s) => s.id === action.snippetId)
|
|
112
136
|
: undefined
|
|
137
|
+
const initialName = snippet?.name ?? ''
|
|
113
138
|
return {
|
|
114
139
|
...state,
|
|
115
140
|
focusMode: 'command-edit',
|
|
116
141
|
modal: {
|
|
117
142
|
activeField: 'name',
|
|
118
143
|
contentBuffer: snippet?.content ?? '',
|
|
119
|
-
|
|
144
|
+
cursorPos: initialName.length,
|
|
145
|
+
editBuffer: initialName,
|
|
120
146
|
selectedIndex: 0,
|
|
121
147
|
sessionTargetId: snippet?.id ?? null,
|
|
122
148
|
type: 'snippet-editor',
|
|
@@ -127,20 +153,44 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
127
153
|
return {
|
|
128
154
|
...state,
|
|
129
155
|
focusMode: 'modal',
|
|
130
|
-
modal: {
|
|
156
|
+
modal: {
|
|
157
|
+
cursorPos: 0,
|
|
158
|
+
editBuffer: null,
|
|
159
|
+
selectedIndex: 0,
|
|
160
|
+
sessionTargetId: null,
|
|
161
|
+
type: 'theme-picker',
|
|
162
|
+
},
|
|
163
|
+
}
|
|
164
|
+
case 'open-git-commit-modal':
|
|
165
|
+
return {
|
|
166
|
+
...state,
|
|
167
|
+
focusMode: 'command-edit',
|
|
168
|
+
modal: {
|
|
169
|
+
activeField: 'title',
|
|
170
|
+
contentBuffer: '',
|
|
171
|
+
cursorPos: 0,
|
|
172
|
+
editBuffer: '',
|
|
173
|
+
selectedIndex: 0,
|
|
174
|
+
sessionTargetId: null,
|
|
175
|
+
type: 'git-commit',
|
|
176
|
+
},
|
|
131
177
|
}
|
|
132
178
|
case 'begin-snippet-filter': {
|
|
133
179
|
if (state.modal.type !== 'snippet-picker') {
|
|
134
180
|
return state
|
|
135
181
|
}
|
|
182
|
+
const buf = state.modal.editBuffer ?? ''
|
|
136
183
|
return {
|
|
137
184
|
...state,
|
|
138
185
|
focusMode: 'command-edit',
|
|
139
|
-
modal: { ...state.modal,
|
|
186
|
+
modal: { ...state.modal, cursorPos: buf.length, editBuffer: buf },
|
|
140
187
|
}
|
|
141
188
|
}
|
|
142
|
-
case 'close-modal':
|
|
143
|
-
|
|
189
|
+
case 'close-modal': {
|
|
190
|
+
const nextFocus: AppState['focusMode'] =
|
|
191
|
+
state.modal.type === 'git-commit' ? 'git' : 'navigation'
|
|
192
|
+
return { ...state, focusMode: nextFocus, modal: emptyModal() }
|
|
193
|
+
}
|
|
144
194
|
case 'move-modal-selection': {
|
|
145
195
|
if (
|
|
146
196
|
state.modal.type !== 'new-tab' &&
|
|
@@ -180,6 +230,18 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
180
230
|
},
|
|
181
231
|
}
|
|
182
232
|
}
|
|
233
|
+
case 'move-modal-cursor': {
|
|
234
|
+
if (state.modal.editBuffer === null) return state
|
|
235
|
+
const len = state.modal.editBuffer.length
|
|
236
|
+
const current = state.modal.cursorPos ?? len
|
|
237
|
+
let next = current
|
|
238
|
+
if (action.to === 'home') next = 0
|
|
239
|
+
else if (action.to === 'end') next = len
|
|
240
|
+
else if (typeof action.delta === 'number') next = current + action.delta
|
|
241
|
+
next = clampCursor(next, len)
|
|
242
|
+
if (next === current) return state
|
|
243
|
+
return { ...state, modal: { ...state.modal, cursorPos: next } }
|
|
244
|
+
}
|
|
183
245
|
case 'begin-command-edit': {
|
|
184
246
|
if (state.modal.type !== 'new-tab' && state.modal.type !== 'session-name') {
|
|
185
247
|
return state
|
|
@@ -194,22 +256,38 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
194
256
|
return {
|
|
195
257
|
...state,
|
|
196
258
|
focusMode: 'command-edit',
|
|
197
|
-
modal: { ...state.modal, editBuffer: currentCmd },
|
|
259
|
+
modal: { ...state.modal, cursorPos: currentCmd.length, editBuffer: currentCmd },
|
|
198
260
|
}
|
|
199
261
|
}
|
|
200
262
|
case 'update-command-edit': {
|
|
201
263
|
if (state.modal.editBuffer === null) {
|
|
202
264
|
return state
|
|
203
265
|
}
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
266
|
+
const buffer = state.modal.editBuffer
|
|
267
|
+
const cursor = clampCursor(state.modal.cursorPos ?? buffer.length, buffer.length)
|
|
268
|
+
let nextBuffer: string
|
|
269
|
+
let nextCursor: number
|
|
270
|
+
if (action.char === '\b') {
|
|
271
|
+
if (cursor === 0) return state
|
|
272
|
+
nextBuffer = buffer.slice(0, cursor - 1) + buffer.slice(cursor)
|
|
273
|
+
nextCursor = cursor - 1
|
|
274
|
+
} else {
|
|
275
|
+
nextBuffer = buffer.slice(0, cursor) + action.char + buffer.slice(cursor)
|
|
276
|
+
nextCursor = cursor + action.char.length
|
|
277
|
+
}
|
|
208
278
|
const resetIndex =
|
|
209
279
|
state.modal.type === 'session-picker' || state.modal.type === 'snippet-picker'
|
|
210
280
|
? 0
|
|
211
281
|
: state.modal.selectedIndex
|
|
212
|
-
return {
|
|
282
|
+
return {
|
|
283
|
+
...state,
|
|
284
|
+
modal: {
|
|
285
|
+
...state.modal,
|
|
286
|
+
cursorPos: nextCursor,
|
|
287
|
+
editBuffer: nextBuffer,
|
|
288
|
+
selectedIndex: resetIndex,
|
|
289
|
+
},
|
|
290
|
+
}
|
|
213
291
|
}
|
|
214
292
|
case 'commit-command-edit': {
|
|
215
293
|
if (state.modal.editBuffer === null) {
|
|
@@ -241,7 +319,11 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
241
319
|
const option = allOpts[state.modal.selectedIndex]
|
|
242
320
|
const assistantId = option?.id
|
|
243
321
|
if (!assistantId) {
|
|
244
|
-
return {
|
|
322
|
+
return {
|
|
323
|
+
...state,
|
|
324
|
+
focusMode: 'modal',
|
|
325
|
+
modal: { ...state.modal, cursorPos: 0, editBuffer: null },
|
|
326
|
+
}
|
|
245
327
|
}
|
|
246
328
|
const trimmed = state.modal.editBuffer.trim()
|
|
247
329
|
const newCustomCommands = { ...state.customCommands }
|
|
@@ -254,7 +336,7 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
254
336
|
...state,
|
|
255
337
|
customCommands: newCustomCommands,
|
|
256
338
|
focusMode: 'modal',
|
|
257
|
-
modal: { ...state.modal, editBuffer: null },
|
|
339
|
+
modal: { ...state.modal, cursorPos: 0, editBuffer: null },
|
|
258
340
|
}
|
|
259
341
|
}
|
|
260
342
|
case 'cancel-command-edit': {
|
|
@@ -265,33 +347,55 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
265
347
|
return {
|
|
266
348
|
...state,
|
|
267
349
|
focusMode: 'modal',
|
|
268
|
-
modal: { ...state.modal, editBuffer: null, selectedIndex: 0 },
|
|
350
|
+
modal: { ...state.modal, cursorPos: 0, editBuffer: null, selectedIndex: 0 },
|
|
269
351
|
}
|
|
270
352
|
}
|
|
271
|
-
return {
|
|
353
|
+
return {
|
|
354
|
+
...state,
|
|
355
|
+
focusMode: 'modal',
|
|
356
|
+
modal: { ...state.modal, cursorPos: 0, editBuffer: null },
|
|
357
|
+
}
|
|
272
358
|
}
|
|
273
359
|
case 'switch-create-session-field': {
|
|
274
360
|
if (state.modal.type === 'create-session') {
|
|
275
361
|
const nextField = state.modal.activeField === 'directory' ? 'name' : 'directory'
|
|
362
|
+
const nextEdit = state.modal.nameBuffer
|
|
276
363
|
return {
|
|
277
364
|
...state,
|
|
278
365
|
modal: {
|
|
279
366
|
...state.modal,
|
|
280
367
|
activeField: nextField,
|
|
281
|
-
|
|
368
|
+
cursorPos: nextEdit.length,
|
|
369
|
+
editBuffer: nextEdit,
|
|
282
370
|
nameBuffer: state.modal.editBuffer ?? '',
|
|
283
371
|
},
|
|
284
372
|
}
|
|
285
373
|
}
|
|
286
374
|
if (state.modal.type === 'snippet-editor') {
|
|
287
375
|
const nextField = state.modal.activeField === 'name' ? 'content' : 'name'
|
|
376
|
+
const nextEdit = state.modal.contentBuffer
|
|
377
|
+
return {
|
|
378
|
+
...state,
|
|
379
|
+
modal: {
|
|
380
|
+
...state.modal,
|
|
381
|
+
activeField: nextField,
|
|
382
|
+
contentBuffer: state.modal.editBuffer ?? '',
|
|
383
|
+
cursorPos: nextEdit.length,
|
|
384
|
+
editBuffer: nextEdit,
|
|
385
|
+
},
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (state.modal.type === 'git-commit') {
|
|
389
|
+
const nextField = state.modal.activeField === 'title' ? 'body' : 'title'
|
|
390
|
+
const nextEdit = state.modal.contentBuffer
|
|
288
391
|
return {
|
|
289
392
|
...state,
|
|
290
393
|
modal: {
|
|
291
394
|
...state.modal,
|
|
292
395
|
activeField: nextField,
|
|
293
396
|
contentBuffer: state.modal.editBuffer ?? '',
|
|
294
|
-
|
|
397
|
+
cursorPos: nextEdit.length,
|
|
398
|
+
editBuffer: nextEdit,
|
|
295
399
|
},
|
|
296
400
|
}
|
|
297
401
|
}
|
|
@@ -315,6 +419,7 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
315
419
|
modal: {
|
|
316
420
|
...state.modal,
|
|
317
421
|
activeField: 'name',
|
|
422
|
+
cursorPos: autoName.length,
|
|
318
423
|
editBuffer: autoName,
|
|
319
424
|
nameBuffer:
|
|
320
425
|
state.modal.activeField === 'directory'
|
|
@@ -328,10 +433,11 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
328
433
|
if (state.modal.type !== 'session-picker') {
|
|
329
434
|
return state
|
|
330
435
|
}
|
|
436
|
+
const buf = state.modal.editBuffer ?? ''
|
|
331
437
|
return {
|
|
332
438
|
...state,
|
|
333
439
|
focusMode: 'command-edit',
|
|
334
|
-
modal: { ...state.modal,
|
|
440
|
+
modal: { ...state.modal, cursorPos: buf.length, editBuffer: buf },
|
|
335
441
|
}
|
|
336
442
|
}
|
|
337
443
|
case 'open-rename-tab-modal': {
|
|
@@ -345,6 +451,7 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
345
451
|
...state,
|
|
346
452
|
focusMode: 'command-edit',
|
|
347
453
|
modal: {
|
|
454
|
+
cursorPos: activeTab.title.length,
|
|
348
455
|
editBuffer: activeTab.title,
|
|
349
456
|
selectedIndex: 0,
|
|
350
457
|
sessionTargetId: activeTab.id,
|
package/src/state/store.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AppAction, AppState, SessionRecord, SnippetRecord } from './types'
|
|
2
2
|
|
|
3
|
+
import { emptyGitMode, reduceGitModeState } from './reducers/git-mode-state'
|
|
3
4
|
import { emptyGitPanel, reduceGitPanelState } from './reducers/git-panel-state'
|
|
4
5
|
import { emptyModal, reduceModalState } from './reducers/modal-state'
|
|
5
6
|
import { reduceSessionState } from './reducers/session-state'
|
|
@@ -30,6 +31,7 @@ export function createInitialState(
|
|
|
30
31
|
currentSessionId: null,
|
|
31
32
|
customCommands,
|
|
32
33
|
focusMode: showSessionPicker ? 'modal' : 'navigation',
|
|
34
|
+
gitMode: emptyGitMode(),
|
|
33
35
|
gitPanel: emptyGitPanel(),
|
|
34
36
|
layout: {
|
|
35
37
|
terminalCols: DEFAULT_TERMINAL_COLS,
|
|
@@ -71,6 +73,9 @@ export function appReducer(state: AppState, action: AppAction): AppState {
|
|
|
71
73
|
const gitPanelState = reduceGitPanelState(state, action)
|
|
72
74
|
if (gitPanelState) return gitPanelState
|
|
73
75
|
|
|
76
|
+
const gitModeState = reduceGitModeState(state, action)
|
|
77
|
+
if (gitModeState) return gitModeState
|
|
78
|
+
|
|
74
79
|
switch (action.type) {
|
|
75
80
|
case 'set-snippets':
|
|
76
81
|
return { ...state, snippets: action.snippets }
|
package/src/state/types.ts
CHANGED
|
@@ -6,7 +6,13 @@ export type TabStatus = 'starting' | 'running' | 'disconnected' | 'exited' | 'er
|
|
|
6
6
|
|
|
7
7
|
export type TabActivity = 'busy' | 'idle'
|
|
8
8
|
|
|
9
|
-
export type FocusMode =
|
|
9
|
+
export type FocusMode =
|
|
10
|
+
| 'navigation'
|
|
11
|
+
| 'terminal-input'
|
|
12
|
+
| 'modal'
|
|
13
|
+
| 'command-edit'
|
|
14
|
+
| 'layout'
|
|
15
|
+
| 'git'
|
|
10
16
|
|
|
11
17
|
export type ModalType =
|
|
12
18
|
| 'new-tab'
|
|
@@ -19,6 +25,7 @@ export type ModalType =
|
|
|
19
25
|
| 'theme-picker'
|
|
20
26
|
| 'help'
|
|
21
27
|
| 'split-picker'
|
|
28
|
+
| 'git-commit'
|
|
22
29
|
| null
|
|
23
30
|
|
|
24
31
|
export interface TerminalSpan {
|
|
@@ -135,10 +142,31 @@ export interface GitPanelState {
|
|
|
135
142
|
error: GitPanelError | null
|
|
136
143
|
}
|
|
137
144
|
|
|
145
|
+
export type DiffFileStatus = 'modified' | 'new' | 'deleted' | 'binary' | 'renamed'
|
|
146
|
+
|
|
147
|
+
export interface DiffData {
|
|
148
|
+
path: string
|
|
149
|
+
status: DiffFileStatus
|
|
150
|
+
oldPath?: string
|
|
151
|
+
rawDiff: string
|
|
152
|
+
binarySizeBefore?: number
|
|
153
|
+
binarySizeAfter?: number
|
|
154
|
+
errorMessage?: string
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface GitModeState {
|
|
158
|
+
selectedFileIndex: number
|
|
159
|
+
diffs: Record<string, DiffData>
|
|
160
|
+
loading: Record<string, boolean>
|
|
161
|
+
pendingDeletePath: string | null
|
|
162
|
+
actionMessage: string | null
|
|
163
|
+
}
|
|
164
|
+
|
|
138
165
|
interface ModalBase {
|
|
139
166
|
selectedIndex: number
|
|
140
167
|
editBuffer: string | null
|
|
141
168
|
sessionTargetId: string | null
|
|
169
|
+
cursorPos?: number
|
|
142
170
|
}
|
|
143
171
|
|
|
144
172
|
export interface ModalClosed extends ModalBase {
|
|
@@ -180,6 +208,12 @@ export interface ModalSplitPicker extends ModalBase {
|
|
|
180
208
|
splitDirection: import('./layout-tree').SplitDirection
|
|
181
209
|
}
|
|
182
210
|
|
|
211
|
+
export interface ModalGitCommit extends ModalBase {
|
|
212
|
+
type: 'git-commit'
|
|
213
|
+
activeField: 'title' | 'body'
|
|
214
|
+
contentBuffer: string
|
|
215
|
+
}
|
|
216
|
+
|
|
183
217
|
export interface ModalCreateSession extends ModalBase {
|
|
184
218
|
type: 'create-session'
|
|
185
219
|
directoryResults: DirectoryResult[]
|
|
@@ -213,6 +247,7 @@ export type ModalState =
|
|
|
213
247
|
| ModalSplitPicker
|
|
214
248
|
| ModalCreateSession
|
|
215
249
|
| ModalSnippetEditor
|
|
250
|
+
| ModalGitCommit
|
|
216
251
|
|
|
217
252
|
export interface LayoutState {
|
|
218
253
|
terminalCols: number
|
|
@@ -239,12 +274,14 @@ export interface AppState {
|
|
|
239
274
|
layout: LayoutState
|
|
240
275
|
customCommands: Record<AssistantId, string>
|
|
241
276
|
gitPanel: GitPanelState
|
|
277
|
+
gitMode: GitModeState
|
|
242
278
|
/** Chord prefix the sequence resolver is currently waiting on, or null when idle. */
|
|
243
279
|
pendingChords: string[] | null
|
|
244
280
|
}
|
|
245
281
|
|
|
246
282
|
// -- Modal actions --
|
|
247
283
|
export type ModalAction =
|
|
284
|
+
| { type: 'move-modal-cursor'; delta?: number; to?: 'home' | 'end' }
|
|
248
285
|
| { type: 'open-new-tab-modal' }
|
|
249
286
|
| { type: 'open-help-modal' }
|
|
250
287
|
| { type: 'open-split-picker'; direction: import('./layout-tree').SplitDirection }
|
|
@@ -352,6 +389,23 @@ export type GitPanelAction =
|
|
|
352
389
|
| { type: 'git-refresh-error'; kind: GitPanelError }
|
|
353
390
|
| { type: 'git-panel-reset' }
|
|
354
391
|
|
|
392
|
+
export type GitModeAction =
|
|
393
|
+
| { type: 'enter-git-mode' }
|
|
394
|
+
| { type: 'exit-git-mode' }
|
|
395
|
+
| { type: 'git-mode-select-file'; delta: -1 | 1 }
|
|
396
|
+
| { type: 'git-mode-set-diff'; path: string; diff: DiffData }
|
|
397
|
+
| { type: 'git-mode-set-loading'; path: string; loading: boolean }
|
|
398
|
+
| { type: 'git-mode-set-pending-delete'; path: string | null }
|
|
399
|
+
| { type: 'git-mode-clear-diff-cache'; path: string }
|
|
400
|
+
| { type: 'git-mode-set-message'; message: string | null }
|
|
401
|
+
| {
|
|
402
|
+
type: 'git-mode-optimistic-move'
|
|
403
|
+
path: string
|
|
404
|
+
fromSection: GitFileSection
|
|
405
|
+
toSection: GitFileSection | null
|
|
406
|
+
}
|
|
407
|
+
| { type: 'open-git-commit-modal' }
|
|
408
|
+
|
|
355
409
|
// -- Data actions --
|
|
356
410
|
export type DataAction =
|
|
357
411
|
| { type: 'set-snippets'; snippets: SnippetRecord[] }
|
|
@@ -365,3 +419,4 @@ export type AppAction =
|
|
|
365
419
|
| UIAction
|
|
366
420
|
| DataAction
|
|
367
421
|
| GitPanelAction
|
|
422
|
+
| GitModeAction
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { theme } from '../theme'
|
|
2
|
+
import { uiTokens } from '../ui-tokens'
|
|
3
|
+
import { InputField } from './input-field'
|
|
4
|
+
import { ModalShell } from './modal-shell'
|
|
5
|
+
|
|
6
|
+
interface GitCommitModalProps {
|
|
7
|
+
activeField: 'title' | 'body'
|
|
8
|
+
title: string
|
|
9
|
+
body: string
|
|
10
|
+
cursorPos: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function GitCommitModal({ activeField, body, cursorPos, title }: GitCommitModalProps) {
|
|
14
|
+
const titleActive = activeField === 'title'
|
|
15
|
+
const bodyActive = activeField === 'body'
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<ModalShell
|
|
19
|
+
title="Commit"
|
|
20
|
+
help="Tab switch · ←→ move cursor · Enter newline (body) · Ctrl+Enter commit · Esc cancel"
|
|
21
|
+
width={uiTokens.modalWidth.xl}
|
|
22
|
+
>
|
|
23
|
+
<box flexDirection="column">
|
|
24
|
+
<text fg={titleActive ? theme.text : theme.textMuted}>Title</text>
|
|
25
|
+
<InputField
|
|
26
|
+
active={titleActive}
|
|
27
|
+
cursorPos={titleActive ? cursorPos : undefined}
|
|
28
|
+
value={title}
|
|
29
|
+
/>
|
|
30
|
+
</box>
|
|
31
|
+
|
|
32
|
+
<box flexDirection="column">
|
|
33
|
+
<text fg={bodyActive ? theme.text : theme.textMuted}>Body (optional)</text>
|
|
34
|
+
<InputField
|
|
35
|
+
active={bodyActive}
|
|
36
|
+
cursorPos={bodyActive ? cursorPos : undefined}
|
|
37
|
+
value={body}
|
|
38
|
+
/>
|
|
39
|
+
</box>
|
|
40
|
+
</ModalShell>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
@@ -7,6 +7,11 @@ import { theme } from '../theme'
|
|
|
7
7
|
interface GitPanelProps {
|
|
8
8
|
gitPanel: GitPanelState
|
|
9
9
|
projectPath: string | undefined
|
|
10
|
+
selectedFileKey?: string | null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function fileKey(file: Pick<GitFileEntry, 'path' | 'section'>): string {
|
|
14
|
+
return `${file.section}:${file.path}`
|
|
10
15
|
}
|
|
11
16
|
|
|
12
17
|
const SECTION_ORDER: { key: GitFileSection; title: string }[] = [
|
|
@@ -16,7 +21,7 @@ const SECTION_ORDER: { key: GitFileSection; title: string }[] = [
|
|
|
16
21
|
]
|
|
17
22
|
|
|
18
23
|
const STATUS_COLORS: Record<GitFileEntry['status'], string> = {
|
|
19
|
-
'?': theme.
|
|
24
|
+
'?': theme.success,
|
|
20
25
|
'A': theme.success,
|
|
21
26
|
'C': theme.accent,
|
|
22
27
|
'D': theme.danger,
|
|
@@ -25,6 +30,11 @@ const STATUS_COLORS: Record<GitFileEntry['status'], string> = {
|
|
|
25
30
|
'U': theme.danger,
|
|
26
31
|
}
|
|
27
32
|
|
|
33
|
+
function displayStatus(file: GitFileEntry): string {
|
|
34
|
+
if (file.section === 'untracked') return 'A'
|
|
35
|
+
return file.status
|
|
36
|
+
}
|
|
37
|
+
|
|
28
38
|
function groupBySection(files: GitFileEntry[]): Record<GitFileSection, GitFileEntry[]> {
|
|
29
39
|
const groups: Record<GitFileSection, GitFileEntry[]> = {
|
|
30
40
|
staged: [],
|
|
@@ -87,25 +97,29 @@ function renderFileRow(
|
|
|
87
97
|
file: GitFileEntry,
|
|
88
98
|
key: string,
|
|
89
99
|
addedW: number,
|
|
90
|
-
removedW: number
|
|
100
|
+
removedW: number,
|
|
101
|
+
isSelected: boolean
|
|
91
102
|
): ReactNode {
|
|
92
103
|
const hasNumstat = file.added !== null || file.removed !== null
|
|
104
|
+
const bg = isSelected ? theme.panelHighlight : undefined
|
|
93
105
|
return (
|
|
94
|
-
<box key={key} flexDirection="row" gap={1} paddingLeft={1}>
|
|
95
|
-
<text fg={STATUS_COLORS[file.status]}>
|
|
96
|
-
<strong>{file
|
|
106
|
+
<box key={key} flexDirection="row" gap={1} paddingLeft={1} backgroundColor={bg}>
|
|
107
|
+
<text fg={STATUS_COLORS[file.status]} bg={bg}>
|
|
108
|
+
<strong>{displayStatus(file)}</strong>
|
|
97
109
|
</text>
|
|
98
110
|
<box flexGrow={1} overflow="hidden">
|
|
99
111
|
{renderPath(file)}
|
|
100
112
|
</box>
|
|
101
113
|
{hasNumstat ? (
|
|
102
114
|
<box flexDirection="row" flexShrink={0}>
|
|
103
|
-
<text fg={theme.success}>{`+${padRight(file.added, addedW)}`}</text>
|
|
104
|
-
<text fg={theme.dim}
|
|
105
|
-
|
|
115
|
+
<text fg={theme.success} bg={bg}>{`+${padRight(file.added, addedW)}`}</text>
|
|
116
|
+
<text fg={theme.dim} bg={bg}>
|
|
117
|
+
{' '}
|
|
118
|
+
</text>
|
|
119
|
+
<text fg={theme.danger} bg={bg}>{`−${padRight(file.removed, removedW)}`}</text>
|
|
106
120
|
</box>
|
|
107
121
|
) : (
|
|
108
|
-
<text fg={theme.textMuted} flexShrink={0}>
|
|
122
|
+
<text fg={theme.textMuted} bg={bg} flexShrink={0}>
|
|
109
123
|
—
|
|
110
124
|
</text>
|
|
111
125
|
)}
|
|
@@ -118,7 +132,8 @@ function renderSection(
|
|
|
118
132
|
title: string,
|
|
119
133
|
files: GitFileEntry[],
|
|
120
134
|
addedW: number,
|
|
121
|
-
removedW: number
|
|
135
|
+
removedW: number,
|
|
136
|
+
selectedFileKey: string | null | undefined
|
|
122
137
|
): ReactNode {
|
|
123
138
|
if (files.length === 0) return null
|
|
124
139
|
return (
|
|
@@ -128,7 +143,15 @@ function renderSection(
|
|
|
128
143
|
{title} ({files.length})
|
|
129
144
|
</strong>
|
|
130
145
|
</text>
|
|
131
|
-
{files.map((file, i) =>
|
|
146
|
+
{files.map((file, i) =>
|
|
147
|
+
renderFileRow(
|
|
148
|
+
file,
|
|
149
|
+
`${section}-${i}`,
|
|
150
|
+
addedW,
|
|
151
|
+
removedW,
|
|
152
|
+
!!selectedFileKey && fileKey(file) === selectedFileKey
|
|
153
|
+
)
|
|
154
|
+
)}
|
|
132
155
|
</box>
|
|
133
156
|
)
|
|
134
157
|
}
|
|
@@ -167,7 +190,11 @@ function computeStatusPlaceholder(
|
|
|
167
190
|
return null
|
|
168
191
|
}
|
|
169
192
|
|
|
170
|
-
export const GitPanel = memo(function GitPanel({
|
|
193
|
+
export const GitPanel = memo(function GitPanel({
|
|
194
|
+
gitPanel,
|
|
195
|
+
projectPath,
|
|
196
|
+
selectedFileKey,
|
|
197
|
+
}: GitPanelProps) {
|
|
171
198
|
const groups = useMemo(() => groupBySection(gitPanel.files), [gitPanel.files])
|
|
172
199
|
const { added: addedW, removed: removedW } = useMemo(
|
|
173
200
|
() => maxDigitWidth(gitPanel.files),
|
|
@@ -192,7 +219,9 @@ export const GitPanel = memo(function GitPanel({ gitPanel, projectPath }: GitPan
|
|
|
192
219
|
viewportCulling
|
|
193
220
|
contentOptions={{ flexDirection: 'column', gap: 0 }}
|
|
194
221
|
>
|
|
195
|
-
{SECTION_ORDER.map((s) =>
|
|
222
|
+
{SECTION_ORDER.map((s) =>
|
|
223
|
+
renderSection(s.key, s.title, groups[s.key], addedW, removedW, selectedFileKey)
|
|
224
|
+
)}
|
|
196
225
|
</scrollbox>
|
|
197
226
|
)}
|
|
198
227
|
</box>
|