@brimveyn/aimux 1.5.0 → 1.6.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.
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-runtime/use-renderer-bindings.ts +1 -1
  5. package/src/app.tsx +17 -5
  6. package/src/config.ts +29 -4
  7. package/src/diff-parser/clean-last-newline.ts +5 -0
  8. package/src/diff-parser/constants.ts +13 -0
  9. package/src/diff-parser/index.ts +12 -0
  10. package/src/diff-parser/parse-line-type.ts +29 -0
  11. package/src/diff-parser/parse-patch-files.ts +369 -0
  12. package/src/diff-parser/types.ts +75 -0
  13. package/src/index.tsx +2 -2
  14. package/src/input/modes/bridge.ts +8 -0
  15. package/src/input/modes/transitions.ts +2 -1
  16. package/src/input/modes/types.ts +4 -1
  17. package/src/pty/terminal-snapshot.ts +4 -4
  18. package/src/state/git-tree.ts +220 -0
  19. package/src/state/reducers/git-mode-state.ts +232 -35
  20. package/src/state/reducers/git-panel-state.ts +29 -3
  21. package/src/state/reducers/modal-state.ts +43 -10
  22. package/src/state/store.ts +5 -1
  23. package/src/state/types.ts +41 -5
  24. package/src/state/workspace-save.ts +2 -0
  25. package/src/ui/components/create-session-modal.tsx +23 -7
  26. package/src/ui/components/diff-renderer/build-rows.ts +349 -0
  27. package/src/ui/components/diff-renderer/filetype.ts +29 -0
  28. package/src/ui/components/diff-renderer/fold-strip.tsx +84 -0
  29. package/src/ui/components/diff-renderer/highlight.ts +48 -0
  30. package/src/ui/components/diff-renderer/index.ts +1 -0
  31. package/src/ui/components/diff-renderer/pierre-diff.tsx +170 -0
  32. package/src/ui/components/diff-renderer/split-view.tsx +207 -0
  33. package/src/ui/components/diff-renderer/stacked-view.tsx +166 -0
  34. package/src/ui/components/git-commit-modal.tsx +14 -2
  35. package/src/ui/components/git-pane-widget.tsx +5 -0
  36. package/src/ui/components/git-panel.tsx +176 -79
  37. package/src/ui/components/git-view.tsx +89 -87
  38. package/src/ui/components/help-modal.tsx +43 -11
  39. package/src/ui/components/input-field.tsx +2 -2
  40. package/src/ui/components/list-item.tsx +9 -1
  41. package/src/ui/components/modal-filter-bar.tsx +1 -1
  42. package/src/ui/components/modal-keybinds-overlay.tsx +2 -2
  43. package/src/ui/components/modal-shell.tsx +3 -3
  44. package/src/ui/components/new-tab-modal.tsx +15 -3
  45. package/src/ui/components/pending-chord-overlay.tsx +3 -3
  46. package/src/ui/components/session-bar.tsx +10 -5
  47. package/src/ui/components/session-picker-modal.tsx +26 -9
  48. package/src/ui/components/sidebar.tsx +22 -10
  49. package/src/ui/components/snippet-editor-modal.tsx +16 -2
  50. package/src/ui/components/snippet-picker-modal.tsx +11 -3
  51. package/src/ui/components/split-layout.tsx +1 -1
  52. package/src/ui/components/status-bar.tsx +12 -9
  53. package/src/ui/components/surface.tsx +5 -5
  54. package/src/ui/components/tab-item.tsx +22 -16
  55. package/src/ui/components/terminal-pane.tsx +25 -15
  56. package/src/ui/components/theme-picker-modal.tsx +111 -16
  57. package/src/ui/components/update-available-modal.tsx +11 -1
  58. package/src/ui/filter-themes.ts +10 -0
  59. package/src/ui/house-themes.ts +313 -0
  60. package/src/ui/keymap-context.ts +10 -2
  61. package/src/ui/root.tsx +26 -6
  62. package/src/ui/shiki.ts +49 -0
  63. package/src/ui/status-bar-model.ts +19 -1
  64. package/src/ui/theme.ts +22 -10
  65. package/src/ui/themes.generated.ts +59794 -0
  66. package/src/ui/themes.ts +68 -274
  67. package/src/ui/syntax.ts +0 -102
@@ -0,0 +1,369 @@
1
+ // Vendored from @pierre/diffs v1.1.15 — parser only. See AIMUX-14.
2
+ /* eslint-disable eqeqeq, no-console, typescript/strict-boolean-expressions, no-else-return, unicorn/no-array-for-each */
3
+
4
+ import type {
5
+ ChangeContent,
6
+ ContextContent,
7
+ FileContents,
8
+ FileDiffMetadata,
9
+ HunkContent,
10
+ ParsedPatch,
11
+ } from './types'
12
+
13
+ import { cleanLastNewline } from './clean-last-newline'
14
+ import {
15
+ ALTERNATE_FILE_NAMES_GIT,
16
+ COMMIT_METADATA_SPLIT,
17
+ FILE_CONTEXT_BLOB,
18
+ FILENAME_HEADER_REGEX,
19
+ FILENAME_HEADER_REGEX_GIT,
20
+ GIT_DIFF_FILE_BREAK_REGEX,
21
+ HUNK_HEADER,
22
+ INDEX_LINE_METADATA,
23
+ SPLIT_WITH_NEWLINES,
24
+ UNIFIED_DIFF_FILE_BREAK_REGEX,
25
+ } from './constants'
26
+ import { parseLineType } from './parse-line-type'
27
+
28
+ interface ProcessFileOptions {
29
+ cacheKey?: string
30
+ isGitDiff?: boolean
31
+ oldFile?: FileContents
32
+ newFile?: FileContents
33
+ throwOnError?: boolean
34
+ }
35
+
36
+ function createContentGroup(
37
+ type: 'change' | 'context',
38
+ deletionLineIndex: number,
39
+ additionLineIndex: number
40
+ ): HunkContent {
41
+ if (type === 'change') {
42
+ return { additionLineIndex, additions: 0, deletionLineIndex, deletions: 0, type: 'change' }
43
+ }
44
+ return { additionLineIndex, deletionLineIndex, lines: 0, type: 'context' }
45
+ }
46
+
47
+ export function processFile(
48
+ fileDiffString: string,
49
+ {
50
+ cacheKey,
51
+ isGitDiff = GIT_DIFF_FILE_BREAK_REGEX.test(fileDiffString),
52
+ newFile,
53
+ oldFile,
54
+ throwOnError = false,
55
+ }: ProcessFileOptions = {}
56
+ ): FileDiffMetadata | undefined {
57
+ let lastHunkEnd = 0
58
+ const hunks = fileDiffString.split(FILE_CONTEXT_BLOB)
59
+ let currentFile: FileDiffMetadata | undefined
60
+ const isPartial = oldFile == null || newFile == null
61
+ let deletionLineIndex = 0
62
+ let additionLineIndex = 0
63
+ for (const hunk of hunks) {
64
+ const lines = hunk.split(SPLIT_WITH_NEWLINES)
65
+ const firstLine = lines.shift()
66
+ if (firstLine == null) {
67
+ if (throwOnError) throw Error('parsePatchContent: invalid hunk')
68
+ else console.error('parsePatchContent: invalid hunk', hunk)
69
+ continue
70
+ }
71
+ const fileHeaderMatch = firstLine.match(HUNK_HEADER)
72
+ let additionLines = 0
73
+ let deletionLines = 0
74
+ if (fileHeaderMatch == null || currentFile == null) {
75
+ if (currentFile != null) {
76
+ if (throwOnError) throw Error('parsePatchContent: Invalid hunk')
77
+ else console.error('parsePatchContent: Invalid hunk', hunk)
78
+ continue
79
+ }
80
+ currentFile = {
81
+ additionLines:
82
+ !isPartial && oldFile != null && newFile != null
83
+ ? newFile.contents.split(SPLIT_WITH_NEWLINES)
84
+ : [],
85
+ cacheKey,
86
+ deletionLines:
87
+ !isPartial && oldFile != null && newFile != null
88
+ ? oldFile.contents.split(SPLIT_WITH_NEWLINES)
89
+ : [],
90
+ hunks: [],
91
+ isPartial,
92
+ name: '',
93
+ splitLineCount: 0,
94
+ type: 'change',
95
+ unifiedLineCount: 0,
96
+ }
97
+ if (currentFile.additionLines.length === 1 && newFile?.contents === '') {
98
+ currentFile.additionLines.length = 0
99
+ }
100
+ if (currentFile.deletionLines.length === 1 && oldFile?.contents === '') {
101
+ currentFile.deletionLines.length = 0
102
+ }
103
+ lines.unshift(firstLine)
104
+ for (const line of lines) {
105
+ const filenameMatch = line.match(
106
+ isGitDiff ? FILENAME_HEADER_REGEX_GIT : FILENAME_HEADER_REGEX
107
+ )
108
+ if (line.startsWith('diff --git')) {
109
+ const match = line.trim().match(ALTERNATE_FILE_NAMES_GIT)
110
+ if (match) {
111
+ const prevName = match[1] ?? match[2]
112
+ const name = match[3] ?? match[4]
113
+ if (name != null) {
114
+ currentFile.name = name.trim()
115
+ if (prevName != null && prevName !== name) currentFile.prevName = prevName.trim()
116
+ }
117
+ }
118
+ } else if (filenameMatch != null) {
119
+ const [, type, fileName] = filenameMatch
120
+ if (fileName != null && fileName !== '/dev/null') {
121
+ if (type === '---') {
122
+ currentFile.prevName = fileName.trim()
123
+ currentFile.name = fileName.trim()
124
+ } else if (type === '+++') {
125
+ currentFile.name = fileName.trim()
126
+ }
127
+ }
128
+ } else if (isGitDiff) {
129
+ if (line.startsWith('new mode ')) currentFile.mode = line.replace('new mode', '').trim()
130
+ if (line.startsWith('old mode ')) {
131
+ currentFile.prevMode = line.replace('old mode', '').trim()
132
+ }
133
+ if (line.startsWith('new file mode')) {
134
+ currentFile.type = 'new'
135
+ currentFile.mode = line.replace('new file mode', '').trim()
136
+ }
137
+ if (line.startsWith('deleted file mode')) {
138
+ currentFile.type = 'deleted'
139
+ currentFile.mode = line.replace('deleted file mode', '').trim()
140
+ }
141
+ if (line.startsWith('similarity index')) {
142
+ if (line.startsWith('similarity index 100%')) currentFile.type = 'rename-pure'
143
+ else currentFile.type = 'rename-changed'
144
+ }
145
+ if (line.startsWith('index ')) {
146
+ const [, prevObjectId, newObjectId, mode] = line.trim().match(INDEX_LINE_METADATA) ?? []
147
+ if (prevObjectId != null) currentFile.prevObjectId = prevObjectId
148
+ if (newObjectId != null) currentFile.newObjectId = newObjectId
149
+ if (mode != null) currentFile.mode = mode
150
+ }
151
+ if (line.startsWith('rename from ')) {
152
+ currentFile.prevName = line.replace('rename from ', '').trim()
153
+ }
154
+ if (line.startsWith('rename to ')) {
155
+ currentFile.name = line.replace('rename to ', '').trim()
156
+ }
157
+ }
158
+ }
159
+ continue
160
+ }
161
+ let currentContent: HunkContent | undefined
162
+ let lastLineType: 'addition' | 'deletion' | 'context' | undefined
163
+ while (
164
+ lines.length > 0 &&
165
+ (lines[lines.length - 1] === '\n' ||
166
+ lines[lines.length - 1] === '\r' ||
167
+ lines[lines.length - 1] === '\r\n' ||
168
+ lines[lines.length - 1] === '')
169
+ ) {
170
+ lines.pop()
171
+ }
172
+ const additionStart = parseInt(fileHeaderMatch[3] ?? '')
173
+ const deletionStart = parseInt(fileHeaderMatch[1] ?? '')
174
+ deletionLineIndex = isPartial ? deletionLineIndex : deletionStart - 1
175
+ additionLineIndex = isPartial ? additionLineIndex : additionStart - 1
176
+ const hunkData = {
177
+ additionCount: parseInt(fileHeaderMatch[4] ?? '1'),
178
+ additionLineIndex,
179
+ additionLines,
180
+ additionStart,
181
+ collapsedBefore: 0,
182
+ deletionCount: parseInt(fileHeaderMatch[2] ?? '1'),
183
+ deletionLineIndex,
184
+ deletionLines,
185
+ deletionStart,
186
+ hunkContent: [] as HunkContent[],
187
+ hunkContext: fileHeaderMatch[5],
188
+ hunkSpecs: firstLine,
189
+ noEOFCRAdditions: false,
190
+ noEOFCRDeletions: false,
191
+ splitLineCount: 0,
192
+ splitLineStart: 0,
193
+ unifiedLineCount: 0,
194
+ unifiedLineStart: 0,
195
+ }
196
+ if (
197
+ isNaN(hunkData.additionCount) ||
198
+ isNaN(hunkData.deletionCount) ||
199
+ isNaN(hunkData.additionStart) ||
200
+ isNaN(hunkData.deletionStart)
201
+ ) {
202
+ if (throwOnError) throw Error('parsePatchContent: invalid hunk metadata')
203
+ else console.error('parsePatchContent: invalid hunk metadata', hunkData)
204
+ continue
205
+ }
206
+ for (const rawLine of lines) {
207
+ const parsedLine = parseLineType(rawLine)
208
+ if (parsedLine == null) {
209
+ console.error('processFile: invalid rawLine:', rawLine)
210
+ continue
211
+ }
212
+ const { line, type } = parsedLine
213
+ if (type === 'addition') {
214
+ if (currentContent == null || currentContent.type !== 'change') {
215
+ currentContent = createContentGroup('change', deletionLineIndex, additionLineIndex)
216
+ hunkData.hunkContent.push(currentContent)
217
+ }
218
+ additionLineIndex++
219
+ if (isPartial) currentFile.additionLines.push(line)
220
+ ;(currentContent as ChangeContent).additions++
221
+ additionLines++
222
+ lastLineType = 'addition'
223
+ } else if (type === 'deletion') {
224
+ if (currentContent == null || currentContent.type !== 'change') {
225
+ currentContent = createContentGroup('change', deletionLineIndex, additionLineIndex)
226
+ hunkData.hunkContent.push(currentContent)
227
+ }
228
+ deletionLineIndex++
229
+ if (isPartial) currentFile.deletionLines.push(line)
230
+ ;(currentContent as ChangeContent).deletions++
231
+ deletionLines++
232
+ lastLineType = 'deletion'
233
+ } else if (type === 'context') {
234
+ if (currentContent == null || currentContent.type !== 'context') {
235
+ currentContent = createContentGroup('context', deletionLineIndex, additionLineIndex)
236
+ hunkData.hunkContent.push(currentContent)
237
+ }
238
+ additionLineIndex++
239
+ deletionLineIndex++
240
+ if (isPartial) {
241
+ currentFile.deletionLines.push(line)
242
+ currentFile.additionLines.push(line)
243
+ }
244
+ ;(currentContent as ContextContent).lines++
245
+ lastLineType = 'context'
246
+ } else if (type === 'metadata' && currentContent != null) {
247
+ if (currentContent.type === 'context') {
248
+ hunkData.noEOFCRAdditions = true
249
+ hunkData.noEOFCRDeletions = true
250
+ } else if (lastLineType === 'deletion') hunkData.noEOFCRDeletions = true
251
+ else if (lastLineType === 'addition') hunkData.noEOFCRAdditions = true
252
+ if (isPartial && (lastLineType === 'addition' || lastLineType === 'context')) {
253
+ const lastIndex = currentFile.additionLines.length - 1
254
+ const last = currentFile.additionLines[lastIndex]
255
+ if (lastIndex >= 0 && last != null) {
256
+ currentFile.additionLines[lastIndex] = cleanLastNewline(last)
257
+ }
258
+ }
259
+ if (isPartial && (lastLineType === 'deletion' || lastLineType === 'context')) {
260
+ const lastIndex = currentFile.deletionLines.length - 1
261
+ const last = currentFile.deletionLines[lastIndex]
262
+ if (lastIndex >= 0 && last != null) {
263
+ currentFile.deletionLines[lastIndex] = cleanLastNewline(last)
264
+ }
265
+ }
266
+ }
267
+ }
268
+ hunkData.additionLines = additionLines
269
+ hunkData.deletionLines = deletionLines
270
+ hunkData.collapsedBefore = Math.max(hunkData.additionStart - 1 - lastHunkEnd, 0)
271
+ currentFile.hunks.push(hunkData)
272
+ lastHunkEnd = hunkData.additionStart + hunkData.additionCount - 1
273
+ for (const content of hunkData.hunkContent) {
274
+ if (content.type === 'context') {
275
+ hunkData.splitLineCount += content.lines
276
+ hunkData.unifiedLineCount += content.lines
277
+ } else {
278
+ hunkData.splitLineCount += Math.max(content.additions, content.deletions)
279
+ hunkData.unifiedLineCount += content.deletions + content.additions
280
+ }
281
+ }
282
+ hunkData.splitLineStart = currentFile.splitLineCount + hunkData.collapsedBefore
283
+ hunkData.unifiedLineStart = currentFile.unifiedLineCount + hunkData.collapsedBefore
284
+ currentFile.splitLineCount += hunkData.collapsedBefore + hunkData.splitLineCount
285
+ currentFile.unifiedLineCount += hunkData.collapsedBefore + hunkData.unifiedLineCount
286
+ }
287
+ if (currentFile == null) return
288
+ const lastHunk = currentFile.hunks[currentFile.hunks.length - 1]
289
+ if (
290
+ lastHunk != null &&
291
+ !isPartial &&
292
+ currentFile.additionLines.length > 0 &&
293
+ currentFile.deletionLines.length > 0
294
+ ) {
295
+ const lastHunkEnd$1 = lastHunk.additionStart + lastHunk.additionCount - 1
296
+ const totalFileLines = currentFile.additionLines.length
297
+ const collapsedAfter = Math.max(totalFileLines - lastHunkEnd$1, 0)
298
+ currentFile.splitLineCount += collapsedAfter
299
+ currentFile.unifiedLineCount += collapsedAfter
300
+ }
301
+ if (!isGitDiff) {
302
+ if (currentFile.prevName != null && currentFile.name !== currentFile.prevName) {
303
+ if (currentFile.hunks.length > 0) currentFile.type = 'rename-changed'
304
+ else currentFile.type = 'rename-pure'
305
+ } else if (newFile != null && newFile.contents === '') currentFile.type = 'deleted'
306
+ else if (oldFile != null && oldFile.contents === '') currentFile.type = 'new'
307
+ }
308
+ if (currentFile.type !== 'rename-pure' && currentFile.type !== 'rename-changed') {
309
+ currentFile.prevName = undefined
310
+ }
311
+ return currentFile
312
+ }
313
+
314
+ export function processPatch(
315
+ data: string,
316
+ cacheKeyPrefix?: string,
317
+ throwOnError = false
318
+ ): ParsedPatch {
319
+ const isGitDiff = GIT_DIFF_FILE_BREAK_REGEX.test(data)
320
+ const rawFiles = data.split(isGitDiff ? GIT_DIFF_FILE_BREAK_REGEX : UNIFIED_DIFF_FILE_BREAK_REGEX)
321
+ let patchMetadata: string | undefined
322
+ const files: FileDiffMetadata[] = []
323
+ for (const fileOrPatchMetadata of rawFiles) {
324
+ if (isGitDiff && !GIT_DIFF_FILE_BREAK_REGEX.test(fileOrPatchMetadata)) {
325
+ if (patchMetadata == null) patchMetadata = fileOrPatchMetadata
326
+ else if (throwOnError) throw Error('parsePatchContent: unknown file blob')
327
+ else console.error('parsePatchContent: unknown file blob:', fileOrPatchMetadata)
328
+ continue
329
+ } else if (!isGitDiff && !UNIFIED_DIFF_FILE_BREAK_REGEX.test(fileOrPatchMetadata)) {
330
+ if (patchMetadata == null) patchMetadata = fileOrPatchMetadata
331
+ else if (throwOnError) throw Error('parsePatchContent: unknown file blob')
332
+ else console.error('parsePatchContent: unknown file blob:', fileOrPatchMetadata)
333
+ continue
334
+ }
335
+ const currentFile = processFile(fileOrPatchMetadata, {
336
+ cacheKey: cacheKeyPrefix != null ? `${cacheKeyPrefix}-${files.length}` : undefined,
337
+ isGitDiff,
338
+ throwOnError,
339
+ })
340
+ if (currentFile != null) files.push(currentFile)
341
+ }
342
+ return { files, patchMetadata }
343
+ }
344
+
345
+ /**
346
+ * Parses a patch file string into an array of parsed patches.
347
+ */
348
+ export function parsePatchFiles(
349
+ data: string,
350
+ cacheKeyPrefix?: string,
351
+ throwOnError = false
352
+ ): ParsedPatch[] {
353
+ const patches: ParsedPatch[] = []
354
+ for (const patch of data.split(COMMIT_METADATA_SPLIT)) {
355
+ try {
356
+ patches.push(
357
+ processPatch(
358
+ patch,
359
+ cacheKeyPrefix != null ? `${cacheKeyPrefix}-${patches.length}` : undefined,
360
+ throwOnError
361
+ )
362
+ )
363
+ } catch (error) {
364
+ if (throwOnError) throw error
365
+ else console.error(error)
366
+ }
367
+ }
368
+ return patches
369
+ }
@@ -0,0 +1,75 @@
1
+ // Vendored from @pierre/diffs v1.1.15 — parser only. See AIMUX-14.
2
+ // Parser-only subset of the library's types. Renderer/shiki/hast types removed.
3
+
4
+ export type ChangeTypes = 'change' | 'rename-pure' | 'rename-changed' | 'new' | 'deleted'
5
+
6
+ export type HunkLineType = 'context' | 'expanded' | 'addition' | 'deletion' | 'metadata'
7
+
8
+ export interface ContextContent {
9
+ type: 'context'
10
+ lines: number
11
+ additionLineIndex: number
12
+ deletionLineIndex: number
13
+ }
14
+
15
+ export interface ChangeContent {
16
+ type: 'change'
17
+ deletions: number
18
+ deletionLineIndex: number
19
+ additions: number
20
+ additionLineIndex: number
21
+ }
22
+
23
+ export type HunkContent = ContextContent | ChangeContent
24
+
25
+ export interface Hunk {
26
+ collapsedBefore: number
27
+ additionStart: number
28
+ additionCount: number
29
+ additionLines: number
30
+ additionLineIndex: number
31
+ deletionStart: number
32
+ deletionCount: number
33
+ deletionLines: number
34
+ deletionLineIndex: number
35
+ hunkContent: HunkContent[]
36
+ hunkContext?: string
37
+ hunkSpecs?: string
38
+ splitLineStart: number
39
+ splitLineCount: number
40
+ unifiedLineStart: number
41
+ unifiedLineCount: number
42
+ noEOFCRDeletions: boolean
43
+ noEOFCRAdditions: boolean
44
+ }
45
+
46
+ export interface FileDiffMetadata {
47
+ name: string
48
+ prevName?: string
49
+ lang?: string
50
+ newObjectId?: string
51
+ prevObjectId?: string
52
+ mode?: string
53
+ prevMode?: string
54
+ type: ChangeTypes
55
+ hunks: Hunk[]
56
+ splitLineCount: number
57
+ unifiedLineCount: number
58
+ isPartial: boolean
59
+ deletionLines: string[]
60
+ additionLines: string[]
61
+ cacheKey?: string
62
+ }
63
+
64
+ export interface ParsedPatch {
65
+ patchMetadata?: string
66
+ files: FileDiffMetadata[]
67
+ }
68
+
69
+ export interface FileContents {
70
+ name: string
71
+ contents: string
72
+ lang?: string
73
+ header?: string
74
+ cacheKey?: string
75
+ }
package/src/index.tsx CHANGED
@@ -58,9 +58,9 @@ if (command === '--help' || command === '-h') {
58
58
 
59
59
  const renderer = await createCliRenderer({
60
60
  autoFocus: true,
61
+ consoleMode: 'disabled',
61
62
  exitOnCtrlC: false,
62
- useAlternateScreen: true,
63
- useConsole: false,
63
+ screenMode: 'alternate-screen',
64
64
  useMouse: true,
65
65
  })
66
66
 
@@ -19,6 +19,7 @@ const COMMAND_EDIT_MODE_IDS: Partial<Record<SupportedModalType, ModeId>> = {
19
19
  'session-picker': 'modal.session-picker.filtering',
20
20
  'snippet-editor': 'modal.snippet-editor',
21
21
  'snippet-picker': 'modal.snippet-picker.filtering',
22
+ 'theme-picker': 'modal.theme-picker.filtering',
22
23
  }
23
24
 
24
25
  const MODAL_MODE_IDS: Partial<Record<SupportedModalType, ModeId>> = {
@@ -32,6 +33,13 @@ const MODAL_MODE_IDS: Partial<Record<SupportedModalType, ModeId>> = {
32
33
  }
33
34
 
34
35
  export function deriveModeId(state: AppState): ModeId {
36
+ // Help renders as an overlay on top of git/navigation without flipping
37
+ // focusMode, so it needs modal-first dispatch. Filter sub-mode is tracked
38
+ // by editBuffer presence, not focusMode.
39
+ if (state.modal.type === 'help') {
40
+ return state.modal.editBuffer !== null ? 'modal.help.filtering' : 'modal.help'
41
+ }
42
+
35
43
  const directMode = DIRECT_FOCUS_MODE_IDS[state.focusMode]
36
44
  if (directMode) {
37
45
  return directMode
@@ -21,7 +21,8 @@ const TRANSITIONS: Record<ModeId, readonly ModeId[]> = {
21
21
  'modal.snippet-picker': ['navigation', 'modal.snippet-picker.filtering', 'modal.snippet-editor'],
22
22
  'modal.snippet-picker.filtering': ['modal.snippet-picker'],
23
23
  'modal.split-picker': ['navigation', 'terminal-input'],
24
- 'modal.theme-picker': ['navigation'],
24
+ 'modal.theme-picker': ['navigation', 'modal.theme-picker.filtering'],
25
+ 'modal.theme-picker.filtering': ['modal.theme-picker'],
25
26
  'modal.update-available': ['navigation'],
26
27
  'navigation': [
27
28
  'terminal-input',
@@ -1,6 +1,6 @@
1
1
  import type { KeyEvent } from '@opentui/core'
2
2
 
3
- import type { AppAction, AppState, TabSession } from '../../state/types'
3
+ import type { AppAction, AppState, GitFileListMode, TabSession } from '../../state/types'
4
4
 
5
5
  export type ModeId =
6
6
  | 'navigation'
@@ -17,6 +17,7 @@ export type ModeId =
17
17
  | 'modal.snippet-picker.filtering'
18
18
  | 'modal.snippet-editor'
19
19
  | 'modal.theme-picker'
20
+ | 'modal.theme-picker.filtering'
20
21
  | 'modal.help'
21
22
  | 'modal.help.filtering'
22
23
  | 'modal.split-picker'
@@ -46,6 +47,8 @@ export type SideEffect =
46
47
  | { type: 'split-pane'; direction: import('../../state/layout-tree').SplitDirection }
47
48
  | { type: 'confirm-split' }
48
49
  | { type: 'scroll-git-diff'; delta: number }
50
+ | { type: 'persist-git-diff-mode-ratio'; ratio: number }
51
+ | { type: 'persist-git-file-list-mode'; mode: GitFileListMode }
49
52
  | { type: 'git-stage'; path: string }
50
53
  | { type: 'git-unstage'; path: string }
51
54
  | { type: 'git-restore'; path: string }
@@ -113,15 +113,15 @@ function buildLine(
113
113
  let bg = getColorHex(current.getBgColor(), bgMode)
114
114
 
115
115
  if (current.isInverse()) {
116
- const resolvedFg = fg ?? theme.text
117
- const resolvedBg = bg ?? theme.background
116
+ const resolvedFg = fg ?? theme.colors['editor.foreground']
117
+ const resolvedBg = bg ?? theme.colors['editor.background']
118
118
  ;[fg, bg] = [resolvedBg, resolvedFg]
119
119
  }
120
120
 
121
121
  const isCursorCell = cursorVisible && cursorColumn === column
122
122
  if (isCursorCell) {
123
- const resolvedFg = fg ?? theme.text
124
- const resolvedBg = bg ?? theme.background
123
+ const resolvedFg = fg ?? theme.colors['editor.foreground']
124
+ const resolvedBg = bg ?? theme.colors['editor.background']
125
125
  ;[fg, bg] = [resolvedBg, resolvedFg]
126
126
  }
127
127