@brimveyn/aimux 1.5.4 → 1.6.1

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.
Files changed (67) hide show
  1. package/README.md +14 -2
  2. package/package.json +5 -3
  3. package/src/app-runtime/side-effects.ts +41 -6
  4. package/src/app.tsx +17 -5
  5. package/src/config.ts +29 -4
  6. package/src/diff-parser/clean-last-newline.ts +5 -0
  7. package/src/diff-parser/constants.ts +13 -0
  8. package/src/diff-parser/index.ts +12 -0
  9. package/src/diff-parser/parse-line-type.ts +29 -0
  10. package/src/diff-parser/parse-patch-files.ts +369 -0
  11. package/src/diff-parser/types.ts +75 -0
  12. package/src/git/git-diff.ts +24 -12
  13. package/src/git/git-poller.ts +11 -3
  14. package/src/git/git-status.ts +98 -17
  15. package/src/input/modes/bridge.ts +8 -0
  16. package/src/input/modes/transitions.ts +2 -1
  17. package/src/input/modes/types.ts +4 -1
  18. package/src/pty/terminal-snapshot.ts +5 -5
  19. package/src/state/git-tree.ts +220 -0
  20. package/src/state/reducers/git-mode-state.ts +276 -36
  21. package/src/state/reducers/git-panel-state.ts +35 -4
  22. package/src/state/reducers/modal-state.ts +43 -10
  23. package/src/state/store.ts +5 -1
  24. package/src/state/types.ts +46 -6
  25. package/src/state/workspace-save.ts +2 -0
  26. package/src/ui/components/create-session-modal.tsx +25 -8
  27. package/src/ui/components/diff-renderer/build-rows.ts +349 -0
  28. package/src/ui/components/diff-renderer/filetype.ts +29 -0
  29. package/src/ui/components/diff-renderer/fold-strip.tsx +86 -0
  30. package/src/ui/components/diff-renderer/highlight.ts +48 -0
  31. package/src/ui/components/diff-renderer/index.ts +1 -0
  32. package/src/ui/components/diff-renderer/pierre-diff.tsx +171 -0
  33. package/src/ui/components/diff-renderer/split-view.tsx +211 -0
  34. package/src/ui/components/diff-renderer/stacked-view.tsx +168 -0
  35. package/src/ui/components/git-commit-modal.tsx +16 -3
  36. package/src/ui/components/git-pane-widget.tsx +6 -1
  37. package/src/ui/components/git-panel.tsx +215 -86
  38. package/src/ui/components/git-view.tsx +103 -90
  39. package/src/ui/components/help-modal.tsx +45 -12
  40. package/src/ui/components/input-field.tsx +4 -3
  41. package/src/ui/components/list-item.tsx +11 -2
  42. package/src/ui/components/modal-filter-bar.tsx +3 -2
  43. package/src/ui/components/modal-keybinds-overlay.tsx +4 -3
  44. package/src/ui/components/modal-shell.tsx +5 -4
  45. package/src/ui/components/new-tab-modal.tsx +17 -4
  46. package/src/ui/components/pending-chord-overlay.tsx +5 -4
  47. package/src/ui/components/session-bar.tsx +13 -6
  48. package/src/ui/components/session-picker-modal.tsx +28 -10
  49. package/src/ui/components/sidebar.tsx +26 -11
  50. package/src/ui/components/snippet-editor-modal.tsx +18 -3
  51. package/src/ui/components/snippet-picker-modal.tsx +13 -4
  52. package/src/ui/components/split-layout.tsx +3 -2
  53. package/src/ui/components/status-bar.tsx +15 -10
  54. package/src/ui/components/surface.tsx +6 -6
  55. package/src/ui/components/tab-item.tsx +28 -17
  56. package/src/ui/components/terminal-pane.tsx +28 -16
  57. package/src/ui/components/theme-picker-modal.tsx +113 -17
  58. package/src/ui/components/update-available-modal.tsx +13 -2
  59. package/src/ui/filter-themes.ts +15 -0
  60. package/src/ui/keymap-context.ts +10 -2
  61. package/src/ui/root.tsx +29 -7
  62. package/src/ui/shiki.ts +49 -0
  63. package/src/ui/status-bar-model.ts +32 -4
  64. package/src/ui/theme-store.ts +36 -0
  65. package/src/ui/theme.ts +4 -17
  66. package/src/ui/themes.ts +23 -277
  67. package/src/ui/syntax.ts +0 -102
@@ -1,14 +1,71 @@
1
- import type { AppAction, AppState, GitFileEntry, GitModeState } from '../types'
1
+ import type { AppAction, AppState, FoldState, GitFileEntry, GitModeState } from '../types'
2
2
 
3
+ import {
4
+ getSelectedGitRow,
5
+ gitFileKey,
6
+ moveGitFileSelection,
7
+ moveGitSelection,
8
+ reconcileSelectedGitEntryKey,
9
+ } from '../git-tree'
3
10
  import { sortFilesBySection } from './git-panel-state'
4
11
 
12
+ function applyFold(state: AppState, key: string, foldId: string, next: FoldState): AppState {
13
+ const perPath = state.gitMode.folds[key] ?? {}
14
+ const prev = perPath[foldId] ?? { bottom: 0, top: 0 }
15
+ if (prev.top === next.top && prev.bottom === next.bottom) return state
16
+ const nextPerPath = { ...perPath }
17
+ if (next.top === 0 && next.bottom === 0) {
18
+ delete nextPerPath[foldId]
19
+ } else {
20
+ nextPerPath[foldId] = next
21
+ }
22
+ const nextFolds = { ...state.gitMode.folds }
23
+ if (Object.keys(nextPerPath).length === 0) {
24
+ delete nextFolds[key]
25
+ } else {
26
+ nextFolds[key] = nextPerPath
27
+ }
28
+ return { ...state, gitMode: { ...state.gitMode, folds: nextFolds } }
29
+ }
30
+
31
+ function clearDiffCacheForPath(state: AppState, path: string): AppState {
32
+ const nextDiffs = { ...state.gitMode.diffs }
33
+ const nextFolds = { ...state.gitMode.folds }
34
+ const nextLoading = { ...state.gitMode.loading }
35
+ let changed = false
36
+ for (const key of Object.keys(nextDiffs)) {
37
+ if (nextDiffs[key]?.path !== path) continue
38
+ delete nextDiffs[key]
39
+ changed = true
40
+ }
41
+ for (const key of Object.keys(nextFolds)) {
42
+ if (!key.endsWith(`:${path}`)) continue
43
+ delete nextFolds[key]
44
+ changed = true
45
+ }
46
+ for (const key of Object.keys(nextLoading)) {
47
+ if (!key.endsWith(`:${path}`)) continue
48
+ delete nextLoading[key]
49
+ changed = true
50
+ }
51
+ if (!changed) return state
52
+ return {
53
+ ...state,
54
+ gitMode: { ...state.gitMode, diffs: nextDiffs, folds: nextFolds, loading: nextLoading },
55
+ }
56
+ }
57
+
5
58
  export function emptyGitMode(): GitModeState {
6
59
  return {
7
60
  actionMessage: null,
61
+ collapsedFolders: {},
8
62
  diffs: {},
63
+ diffView: 'split',
64
+ folds: {},
65
+ headOffset: 0,
9
66
  loading: {},
10
67
  pendingDeletePath: null,
11
- selectedFileIndex: 0,
68
+ selectedEntryKey: null,
12
69
  }
13
70
  }
14
71
 
@@ -23,36 +80,176 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
23
80
  ...state.gitMode,
24
81
  actionMessage: null,
25
82
  pendingDeletePath: null,
26
- selectedFileIndex: 0,
83
+ selectedEntryKey: reconcileSelectedGitEntryKey(
84
+ state.gitPanel.files,
85
+ state.gitMode.collapsedFolders,
86
+ state.gitPane.fileListMode,
87
+ null
88
+ ),
27
89
  },
28
90
  }
29
91
  }
30
92
  case 'exit-git-mode': {
31
93
  if (state.focusMode !== 'git') return state
32
- return { ...state, focusMode: 'navigation' }
94
+ return {
95
+ ...state,
96
+ focusMode: 'navigation',
97
+ gitMode: {
98
+ ...state.gitMode,
99
+ actionMessage: null,
100
+ diffs: {},
101
+ folds: {},
102
+ headOffset: 0,
103
+ loading: {},
104
+ },
105
+ }
106
+ }
107
+ case 'git-mode-move-selection': {
108
+ const next = moveGitSelection(
109
+ state.gitPanel.files,
110
+ state.gitMode.collapsedFolders,
111
+ state.gitPane.fileListMode,
112
+ state.gitMode.selectedEntryKey,
113
+ action.delta
114
+ )
115
+ if (!next || next === state.gitMode.selectedEntryKey) return state
116
+ return {
117
+ ...state,
118
+ gitMode: {
119
+ ...state.gitMode,
120
+ pendingDeletePath: null,
121
+ selectedEntryKey: next,
122
+ },
123
+ }
124
+ }
125
+ case 'git-mode-move-file-selection': {
126
+ const next = moveGitFileSelection(
127
+ state.gitPanel.files,
128
+ state.gitMode.collapsedFolders,
129
+ state.gitPane.fileListMode,
130
+ state.gitMode.selectedEntryKey,
131
+ action.delta
132
+ )
133
+ if (!next || next === state.gitMode.selectedEntryKey) return state
134
+ return {
135
+ ...state,
136
+ gitMode: {
137
+ ...state.gitMode,
138
+ pendingDeletePath: null,
139
+ selectedEntryKey: next,
140
+ },
141
+ }
142
+ }
143
+ case 'git-mode-select-entry-by-key': {
144
+ const next = reconcileSelectedGitEntryKey(
145
+ state.gitPanel.files,
146
+ state.gitMode.collapsedFolders,
147
+ state.gitPane.fileListMode,
148
+ state.gitMode.selectedEntryKey,
149
+ [action.key]
150
+ )
151
+ if (!next || next !== action.key || next === state.gitMode.selectedEntryKey) return state
152
+ return {
153
+ ...state,
154
+ gitMode: {
155
+ ...state.gitMode,
156
+ pendingDeletePath: null,
157
+ selectedEntryKey: next,
158
+ },
159
+ }
33
160
  }
34
- case 'git-mode-select-file': {
35
- const total = state.gitPanel.files.length
36
- if (total === 0) return state
37
- const next = (state.gitMode.selectedFileIndex + action.delta + total) % total
38
- if (next === state.gitMode.selectedFileIndex) return state
161
+ case 'git-mode-toggle-folder': {
162
+ const row = getSelectedGitRow(state.gitPanel.files, {
163
+ collapsedFolders: state.gitMode.collapsedFolders,
164
+ fileListMode: state.gitPane.fileListMode,
165
+ selectedEntryKey: action.key,
166
+ })
167
+ if (!row || row.kind !== 'folder') return state
168
+ const nextCollapsed = { ...state.gitMode.collapsedFolders }
169
+ if (action.key in nextCollapsed) {
170
+ delete nextCollapsed[action.key]
171
+ } else {
172
+ nextCollapsed[action.key] = true
173
+ }
174
+ return { ...state, gitMode: { ...state.gitMode, collapsedFolders: nextCollapsed } }
175
+ }
176
+ case 'git-mode-toggle-selected-folder': {
177
+ const row = getSelectedGitRow(state.gitPanel.files, {
178
+ collapsedFolders: state.gitMode.collapsedFolders,
179
+ fileListMode: state.gitPane.fileListMode,
180
+ selectedEntryKey: state.gitMode.selectedEntryKey,
181
+ })
182
+ if (!row || row.kind !== 'folder') return state
183
+ const nextCollapsed = { ...state.gitMode.collapsedFolders }
184
+ if (row.key in nextCollapsed) {
185
+ delete nextCollapsed[row.key]
186
+ } else {
187
+ nextCollapsed[row.key] = true
188
+ }
189
+ return { ...state, gitMode: { ...state.gitMode, collapsedFolders: nextCollapsed } }
190
+ }
191
+ case 'git-mode-collapse-selection': {
192
+ const row = getSelectedGitRow(state.gitPanel.files, {
193
+ collapsedFolders: state.gitMode.collapsedFolders,
194
+ fileListMode: state.gitPane.fileListMode,
195
+ selectedEntryKey: state.gitMode.selectedEntryKey,
196
+ })
197
+ if (!row) return state
198
+ if (row.kind === 'folder' && !row.isCollapsed) {
199
+ return {
200
+ ...state,
201
+ gitMode: {
202
+ ...state.gitMode,
203
+ collapsedFolders: { ...state.gitMode.collapsedFolders, [row.key]: true },
204
+ },
205
+ }
206
+ }
207
+ if (!row.parentKey || row.parentKey === state.gitMode.selectedEntryKey) return state
39
208
  return {
40
209
  ...state,
41
210
  gitMode: {
42
211
  ...state.gitMode,
43
212
  pendingDeletePath: null,
44
- selectedFileIndex: next,
213
+ selectedEntryKey: row.parentKey,
45
214
  },
46
215
  }
47
216
  }
217
+ case 'git-mode-expand-selection': {
218
+ const row = getSelectedGitRow(state.gitPanel.files, {
219
+ collapsedFolders: state.gitMode.collapsedFolders,
220
+ fileListMode: state.gitPane.fileListMode,
221
+ selectedEntryKey: state.gitMode.selectedEntryKey,
222
+ })
223
+ if (!row || row.kind !== 'folder' || !row.isCollapsed) return state
224
+ const nextCollapsed = { ...state.gitMode.collapsedFolders }
225
+ delete nextCollapsed[row.key]
226
+ return { ...state, gitMode: { ...state.gitMode, collapsedFolders: nextCollapsed } }
227
+ }
228
+ case 'git-mode-toggle-file-list-mode': {
229
+ const fileListMode = state.gitPane.fileListMode === 'tree' ? 'flat' : 'tree'
230
+ return {
231
+ ...state,
232
+ gitMode: {
233
+ ...state.gitMode,
234
+ pendingDeletePath: null,
235
+ selectedEntryKey: reconcileSelectedGitEntryKey(
236
+ state.gitPanel.files,
237
+ state.gitMode.collapsedFolders,
238
+ fileListMode,
239
+ state.gitMode.selectedEntryKey
240
+ ),
241
+ },
242
+ gitPane: { ...state.gitPane, fileListMode },
243
+ }
244
+ }
48
245
  case 'git-mode-set-diff': {
49
246
  const nextLoading = { ...state.gitMode.loading }
50
- delete nextLoading[action.path]
247
+ delete nextLoading[action.key]
51
248
  return {
52
249
  ...state,
53
250
  gitMode: {
54
251
  ...state.gitMode,
55
- diffs: { ...state.gitMode.diffs, [action.path]: action.diff },
252
+ diffs: { ...state.gitMode.diffs, [action.key]: action.diff },
56
253
  loading: nextLoading,
57
254
  },
58
255
  }
@@ -60,9 +257,9 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
60
257
  case 'git-mode-set-loading': {
61
258
  const nextLoading = { ...state.gitMode.loading }
62
259
  if (action.loading) {
63
- nextLoading[action.path] = true
260
+ nextLoading[action.key] = true
64
261
  } else {
65
- delete nextLoading[action.path]
262
+ delete nextLoading[action.key]
66
263
  }
67
264
  return { ...state, gitMode: { ...state.gitMode, loading: nextLoading } }
68
265
  }
@@ -71,17 +268,64 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
71
268
  return { ...state, gitMode: { ...state.gitMode, pendingDeletePath: action.path } }
72
269
  }
73
270
  case 'git-mode-clear-diff-cache': {
74
- if (!(action.path in state.gitMode.diffs)) return state
75
- const nextDiffs = { ...state.gitMode.diffs }
76
- delete nextDiffs[action.path]
77
- return { ...state, gitMode: { ...state.gitMode, diffs: nextDiffs } }
271
+ return clearDiffCacheForPath(state, action.path)
78
272
  }
79
273
  case 'git-mode-set-message': {
80
274
  if (state.gitMode.actionMessage === action.message) return state
81
275
  return { ...state, gitMode: { ...state.gitMode, actionMessage: action.message } }
82
276
  }
277
+ case 'git-mode-toggle-diff-view': {
278
+ const next = state.gitMode.diffView === 'split' ? 'stacked' : 'split'
279
+ return { ...state, gitMode: { ...state.gitMode, diffView: next } }
280
+ }
281
+ case 'git-mode-shift-head-offset': {
282
+ const next = Math.max(0, state.gitMode.headOffset + action.delta)
283
+ if (next === state.gitMode.headOffset) return state
284
+ return {
285
+ ...state,
286
+ gitMode: {
287
+ ...state.gitMode,
288
+ actionMessage: null,
289
+ diffs: {},
290
+ folds: {},
291
+ headOffset: next,
292
+ loading: {},
293
+ pendingDeletePath: null,
294
+ },
295
+ }
296
+ }
297
+ case 'git-mode-set-head-offset': {
298
+ const next = Math.max(0, action.offset)
299
+ if (next === state.gitMode.headOffset) return state
300
+ return {
301
+ ...state,
302
+ gitMode: {
303
+ ...state.gitMode,
304
+ diffs: {},
305
+ folds: {},
306
+ headOffset: next,
307
+ loading: {},
308
+ pendingDeletePath: null,
309
+ },
310
+ }
311
+ }
312
+ case 'git-mode-fold-adjust': {
313
+ const perPath = state.gitMode.folds[action.key] ?? {}
314
+ const prev = perPath[action.foldId] ?? { bottom: 0, top: 0 }
315
+ const nextSide = Math.max(0, prev[action.side] + action.delta)
316
+ const nextFold =
317
+ action.side === 'top'
318
+ ? { bottom: prev.bottom, top: nextSide }
319
+ : { bottom: nextSide, top: prev.top }
320
+ return applyFold(state, action.key, action.foldId, nextFold)
321
+ }
322
+ case 'git-mode-fold-set': {
323
+ const top = Math.max(0, action.top)
324
+ const bottom = Math.max(0, action.bottom)
325
+ return applyFold(state, action.key, action.foldId, { bottom, top })
326
+ }
83
327
  case 'git-mode-optimistic-move': {
84
- const currentIdx = state.gitMode.selectedFileIndex
328
+ const currentKey = state.gitMode.selectedEntryKey
85
329
  const files = state.gitPanel.files
86
330
  const idx = files.findIndex((f) => f.path === action.path && f.section === action.fromSection)
87
331
  if (idx < 0) return state
@@ -103,29 +347,25 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
103
347
  }
104
348
  }
105
349
  nextFiles = sortFilesBySection(nextFiles)
106
-
107
- const newLen = nextFiles.length
108
- let nextIdx: number
109
- if (newLen === 0) {
110
- nextIdx = 0
111
- } else {
112
- const movedIdx =
113
- toSection === null
114
- ? -1
115
- : nextFiles.findIndex((f) => f.path === action.path && f.section === toSection)
116
- if (movedIdx === currentIdx) {
117
- nextIdx = (currentIdx + 1) % newLen
118
- } else {
119
- nextIdx = Math.min(currentIdx, newLen - 1)
120
- }
121
- }
350
+ const movedCurrentKey = gitFileKey({ path: action.path, section: action.fromSection })
351
+ const preferredSelection =
352
+ currentKey === movedCurrentKey && toSection !== null
353
+ ? [gitFileKey({ path: action.path, section: toSection })]
354
+ : []
355
+ const nextSelectedEntryKey = reconcileSelectedGitEntryKey(
356
+ nextFiles,
357
+ state.gitMode.collapsedFolders,
358
+ state.gitPane.fileListMode,
359
+ currentKey,
360
+ preferredSelection
361
+ )
122
362
 
123
363
  return {
124
364
  ...state,
125
365
  gitMode: {
126
366
  ...state.gitMode,
127
367
  pendingDeletePath: null,
128
- selectedFileIndex: nextIdx,
368
+ selectedEntryKey: nextSelectedEntryKey,
129
369
  },
130
370
  gitPanel: { ...state.gitPanel, files: nextFiles },
131
371
  }
@@ -1,9 +1,16 @@
1
1
  import type { AppAction, AppState, GitFileEntry, GitFileSection, GitPanelState } from '../types'
2
2
 
3
+ import { reconcileSelectedGitEntryKey } from '../git-tree'
4
+
3
5
  export const GIT_PANEL_MIN_RATIO = 0.2
4
6
  export const GIT_PANEL_MAX_RATIO = 0.8
5
7
 
6
- const SECTION_RANK: Record<GitFileSection, number> = { staged: 0, unstaged: 1, untracked: 2 }
8
+ const SECTION_RANK: Record<GitFileSection, number> = {
9
+ historical: 0,
10
+ staged: 1,
11
+ unstaged: 2,
12
+ untracked: 3,
13
+ }
7
14
 
8
15
  export function sortFilesBySection(files: GitFileEntry[]): GitFileEntry[] {
9
16
  return [...files].sort((a, b) => {
@@ -63,6 +70,11 @@ export function reduceGitPanelState(state: AppState, action: AppAction): AppStat
63
70
  if (nextRatio === state.gitPane.ratio) return state
64
71
  return { ...state, gitPane: { ...state.gitPane, ratio: nextRatio } }
65
72
  }
73
+ case 'resize-git-diff-pane': {
74
+ const nextRatio = clampRatio(state.gitPane.diffModeRatio + action.delta)
75
+ if (nextRatio === state.gitPane.diffModeRatio) return state
76
+ return { ...state, gitPane: { ...state.gitPane, diffModeRatio: nextRatio } }
77
+ }
66
78
  case 'set-git-pane-mode': {
67
79
  if (state.gitPane.mode === action.mode) return state
68
80
  const isEmbedded = action.mode === 'embedded'
@@ -93,17 +105,25 @@ export function reduceGitPanelState(state: AppState, action: AppAction): AppStat
93
105
  const prev = state.gitPanel
94
106
  const next = action.payload
95
107
  const sortedNext = sortFilesBySection(next.files)
108
+ const nextSelectedEntryKey = reconcileSelectedGitEntryKey(
109
+ sortedNext,
110
+ state.gitMode.collapsedFolders,
111
+ state.gitPane.fileListMode,
112
+ state.gitMode.selectedEntryKey
113
+ )
96
114
  if (
97
115
  prev.branch === next.branch &&
98
116
  prev.ahead === next.ahead &&
99
117
  prev.behind === next.behind &&
100
118
  prev.error === null &&
101
- sameFiles(prev.files, sortedNext)
119
+ sameFiles(prev.files, sortedNext) &&
120
+ nextSelectedEntryKey === state.gitMode.selectedEntryKey
102
121
  ) {
103
122
  return state
104
123
  }
105
124
  return {
106
125
  ...state,
126
+ gitMode: { ...state.gitMode, selectedEntryKey: nextSelectedEntryKey },
107
127
  gitPanel: {
108
128
  ...prev,
109
129
  ahead: next.ahead,
@@ -116,9 +136,16 @@ export function reduceGitPanelState(state: AppState, action: AppAction): AppStat
116
136
  }
117
137
  case 'git-refresh-error': {
118
138
  const prev = state.gitPanel
119
- if (prev.error === action.kind && prev.files.length === 0) return state
139
+ if (
140
+ prev.error === action.kind &&
141
+ prev.files.length === 0 &&
142
+ state.gitMode.selectedEntryKey === null
143
+ ) {
144
+ return state
145
+ }
120
146
  return {
121
147
  ...state,
148
+ gitMode: { ...state.gitMode, pendingDeletePath: null, selectedEntryKey: null },
122
149
  gitPanel: { ...prev, error: action.kind, files: [] },
123
150
  }
124
151
  }
@@ -133,7 +160,11 @@ export function reduceGitPanelState(state: AppState, action: AppAction): AppStat
133
160
  ) {
134
161
  return state
135
162
  }
136
- return { ...state, gitPanel: emptyGitPanel() }
163
+ return {
164
+ ...state,
165
+ gitMode: { ...state.gitMode, pendingDeletePath: null, selectedEntryKey: null },
166
+ gitPanel: emptyGitPanel(),
167
+ }
137
168
  }
138
169
  default:
139
170
  return null
@@ -5,11 +5,9 @@ import type { AppAction, AppState } from '../types'
5
5
  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
- import { THEME_IDS } from '../../ui/themes'
8
+ import { filterThemeIds } from '../../ui/filter-themes'
9
9
  import { filterSessions, filterSnippets } from '../selectors'
10
10
 
11
- const THEME_COUNT = THEME_IDS.length
12
-
13
11
  function emptyModal() {
14
12
  return {
15
13
  cursorPos: 0,
@@ -42,14 +40,16 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
42
40
  }
43
41
  case 'open-help-modal': {
44
42
  const keymap = getActiveKeymap()
45
- const entryCount = keymap ? collectHelpEntries(keymap).length : 0
43
+ const scope = action.scope ?? null
44
+ const allEntries = keymap ? collectHelpEntries(keymap) : []
45
+ const scopedEntries = scope ? allEntries.filter((e) => e.mode === scope) : allEntries
46
46
  return {
47
47
  ...state,
48
- focusMode: 'modal',
49
48
  modal: {
50
49
  cursorPos: 0,
51
50
  editBuffer: null,
52
- entryCount,
51
+ entryCount: scopedEntries.length,
52
+ scope,
53
53
  selectedIndex: 0,
54
54
  sessionTargetId: null,
55
55
  type: 'help',
@@ -162,6 +162,7 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
162
162
  modal: {
163
163
  cursorPos: 0,
164
164
  editBuffer: null,
165
+ entryCount: 0,
165
166
  selectedIndex: 0,
166
167
  sessionTargetId: null,
167
168
  type: 'theme-picker',
@@ -225,6 +226,27 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
225
226
  return state
226
227
  }
227
228
  const buf = state.modal.editBuffer ?? ''
229
+ return {
230
+ ...state,
231
+ modal: { ...state.modal, cursorPos: buf.length, editBuffer: buf },
232
+ }
233
+ }
234
+ case 'set-theme-entry-count': {
235
+ if (state.modal.type !== 'theme-picker') return state
236
+ const clamped = Math.min(state.modal.selectedIndex, Math.max(0, action.count - 1))
237
+ if (state.modal.entryCount === action.count && clamped === state.modal.selectedIndex) {
238
+ return state
239
+ }
240
+ return {
241
+ ...state,
242
+ modal: { ...state.modal, entryCount: action.count, selectedIndex: clamped },
243
+ }
244
+ }
245
+ case 'begin-theme-filter': {
246
+ if (state.modal.type !== 'theme-picker') {
247
+ return state
248
+ }
249
+ const buf = state.modal.editBuffer ?? ''
228
250
  return {
229
251
  ...state,
230
252
  focusMode: 'command-edit',
@@ -232,8 +254,12 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
232
254
  }
233
255
  }
234
256
  case 'close-modal': {
235
- const nextFocus: AppState['focusMode'] =
236
- state.modal.type === 'git-commit' ? 'git' : 'navigation'
257
+ const closingType = state.modal.type
258
+ // Help is a pure overlay — it never flipped focusMode, so leave it alone.
259
+ if (closingType === 'help') {
260
+ return { ...state, modal: emptyModal() }
261
+ }
262
+ const nextFocus: AppState['focusMode'] = closingType === 'git-commit' ? 'git' : 'navigation'
237
263
  return { ...state, focusMode: nextFocus, modal: emptyModal() }
238
264
  }
239
265
  case 'move-modal-selection': {
@@ -268,7 +294,7 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
268
294
  const filtered = filterSnippets(state.snippets, state.modal.editBuffer)
269
295
  optionCount = filtered.length
270
296
  } else if (state.modal.type === 'theme-picker') {
271
- optionCount = THEME_COUNT
297
+ optionCount = filterThemeIds(state.modal.editBuffer).length
272
298
  } else if (state.modal.type === 'update-available') {
273
299
  optionCount = 2
274
300
  } else {
@@ -334,6 +360,7 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
334
360
  const resetIndex =
335
361
  state.modal.type === 'session-picker' ||
336
362
  state.modal.type === 'snippet-picker' ||
363
+ state.modal.type === 'theme-picker' ||
337
364
  state.modal.type === 'help'
338
365
  ? 0
339
366
  : state.modal.selectedIndex
@@ -401,10 +428,16 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
401
428
  if (state.modal.type === 'create-session' || state.modal.type === 'snippet-editor') {
402
429
  return { ...state, focusMode: 'navigation', modal: emptyModal() }
403
430
  }
431
+ if (state.modal.type === 'help') {
432
+ return {
433
+ ...state,
434
+ modal: { ...state.modal, cursorPos: 0, editBuffer: null, selectedIndex: 0 },
435
+ }
436
+ }
404
437
  if (
405
438
  state.modal.type === 'session-picker' ||
406
439
  state.modal.type === 'snippet-picker' ||
407
- state.modal.type === 'help'
440
+ state.modal.type === 'theme-picker'
408
441
  ) {
409
442
  return {
410
443
  ...state,
@@ -1,6 +1,7 @@
1
1
  import type {
2
2
  AppAction,
3
3
  AppState,
4
+ GitModeState,
4
5
  GitPaneMode,
5
6
  GitPanePosition,
6
7
  GitPaneState,
@@ -24,6 +25,7 @@ const DEFAULT_TERMINAL_COLS = 80
24
25
  const DEFAULT_TERMINAL_ROWS = 24
25
26
 
26
27
  export interface InitialStateOverrides {
28
+ gitMode?: Partial<GitModeState>
27
29
  gitPane?: Partial<GitPaneState>
28
30
  sessionBarVisible?: boolean
29
31
  sessionBarPosition?: SessionBarPosition
@@ -31,6 +33,8 @@ export interface InitialStateOverrides {
31
33
 
32
34
  const DEFAULT_GIT_PANE: GitPaneState = {
33
35
  diffCount: { enabled: true },
36
+ diffModeRatio: 0.35,
37
+ fileListMode: 'tree',
34
38
  mode: 'embedded',
35
39
  path: { enabled: true },
36
40
  position: 'bottom',
@@ -62,7 +66,7 @@ export function createInitialState(
62
66
  currentSessionId: null,
63
67
  customCommands,
64
68
  focusMode: showSessionPicker ? 'modal' : 'navigation',
65
- gitMode: emptyGitMode(),
69
+ gitMode: { ...emptyGitMode(), ...overrides.gitMode },
66
70
  gitPane: {
67
71
  ...DEFAULT_GIT_PANE,
68
72
  ...overrides.gitPane,