@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.
@@ -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: { editBuffer: null, selectedIndex: 0, sessionTargetId: null, type: 'new-tab' },
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: { editBuffer: null, selectedIndex: 0, sessionTargetId: null, type: 'help' },
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
- editBuffer: action.initialName ?? '',
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
- editBuffer: snippet?.name ?? '',
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: { editBuffer: null, selectedIndex: 0, sessionTargetId: null, type: 'theme-picker' },
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, editBuffer: state.modal.editBuffer ?? '' },
186
+ modal: { ...state.modal, cursorPos: buf.length, editBuffer: buf },
140
187
  }
141
188
  }
142
- case 'close-modal':
143
- return { ...state, focusMode: 'navigation', modal: emptyModal() }
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 buf =
205
- action.char === '\b'
206
- ? state.modal.editBuffer.slice(0, -1)
207
- : state.modal.editBuffer + action.char
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 { ...state, modal: { ...state.modal, editBuffer: buf, selectedIndex: resetIndex } }
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 { ...state, focusMode: 'modal', modal: { ...state.modal, editBuffer: null } }
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 { ...state, focusMode: 'modal', modal: { ...state.modal, editBuffer: null } }
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
- editBuffer: state.modal.nameBuffer,
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
- editBuffer: state.modal.contentBuffer,
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, editBuffer: state.modal.editBuffer ?? '' },
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,
@@ -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 }
@@ -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 = 'navigation' | 'terminal-input' | 'modal' | 'command-edit' | 'layout'
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.textMuted,
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.status}</strong>
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}> </text>
105
- <text fg={theme.danger}>{`−${padRight(file.removed, removedW)}`}</text>
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) => renderFileRow(file, `${section}-${i}`, addedW, removedW))}
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({ gitPanel, projectPath }: GitPanelProps) {
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) => renderSection(s.key, s.title, groups[s.key], addedW, removedW))}
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>