@brimveyn/aimux 1.6.0 → 1.6.2
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 +2 -2
- package/src/app-runtime/side-effects.ts +19 -0
- package/src/app.tsx +4 -0
- package/src/config.ts +19 -1
- package/src/git/diff-hash.ts +10 -0
- package/src/git/git-diff.ts +24 -12
- package/src/git/git-poller.ts +11 -3
- package/src/git/git-status.ts +98 -17
- package/src/input/modes/types.ts +1 -0
- package/src/pty/terminal-snapshot.ts +5 -5
- package/src/state/git-tree.ts +43 -17
- package/src/state/reducers/diff-cache.ts +64 -0
- package/src/state/reducers/git-mode-state.ts +135 -34
- package/src/state/reducers/git-panel-state.ts +39 -3
- package/src/state/store.ts +2 -0
- package/src/state/types.ts +63 -2
- package/src/ui/components/create-session-modal.tsx +5 -4
- package/src/ui/components/diff-renderer/fold-strip.tsx +3 -1
- package/src/ui/components/diff-renderer/pierre-diff.tsx +11 -32
- package/src/ui/components/diff-renderer/prepare-diff.ts +66 -0
- package/src/ui/components/diff-renderer/split-view.tsx +35 -6
- package/src/ui/components/diff-renderer/stacked-view.tsx +32 -5
- package/src/ui/components/diff-renderer/use-diff-prefetch.ts +191 -0
- package/src/ui/components/diff-renderer/use-diff-preparation.ts +106 -0
- package/src/ui/components/git-commit-modal.tsx +2 -1
- package/src/ui/components/git-pane-widget.tsx +3 -1
- package/src/ui/components/git-panel.tsx +92 -50
- package/src/ui/components/git-view.tsx +34 -5
- package/src/ui/components/help-modal.tsx +2 -1
- package/src/ui/components/input-field.tsx +2 -1
- package/src/ui/components/list-item.tsx +2 -1
- package/src/ui/components/modal-filter-bar.tsx +2 -1
- package/src/ui/components/modal-keybinds-overlay.tsx +2 -1
- package/src/ui/components/modal-shell.tsx +2 -1
- package/src/ui/components/new-tab-modal.tsx +2 -1
- package/src/ui/components/pending-chord-overlay.tsx +2 -1
- package/src/ui/components/session-bar.tsx +3 -1
- package/src/ui/components/session-picker-modal.tsx +2 -1
- package/src/ui/components/sidebar.tsx +8 -5
- package/src/ui/components/snippet-editor-modal.tsx +2 -1
- package/src/ui/components/snippet-picker-modal.tsx +2 -1
- package/src/ui/components/split-layout.tsx +2 -1
- package/src/ui/components/status-bar.tsx +8 -6
- package/src/ui/components/surface.tsx +6 -6
- package/src/ui/components/tab-item.tsx +14 -9
- package/src/ui/components/terminal-pane.tsx +7 -5
- package/src/ui/components/theme-picker-modal.tsx +2 -1
- package/src/ui/components/update-available-modal.tsx +2 -1
- package/src/ui/filter-themes.ts +7 -2
- package/src/ui/root.tsx +3 -1
- package/src/ui/status-bar-model.ts +13 -3
- package/src/ui/theme-store.ts +36 -0
- package/src/ui/theme.ts +4 -29
- package/src/ui/themes.ts +15 -63
- package/src/ui/house-themes.ts +0 -313
- package/src/ui/themes.generated.ts +0 -59794
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
moveGitSelection,
|
|
8
8
|
reconcileSelectedGitEntryKey,
|
|
9
9
|
} from '../git-tree'
|
|
10
|
+
import { clearDiffCacheForPath, clearDiffCacheForPaths } from './diff-cache'
|
|
10
11
|
import { sortFilesBySection } from './git-panel-state'
|
|
11
12
|
|
|
12
13
|
function applyFold(state: AppState, key: string, foldId: string, next: FoldState): AppState {
|
|
@@ -28,33 +29,6 @@ function applyFold(state: AppState, key: string, foldId: string, next: FoldState
|
|
|
28
29
|
return { ...state, gitMode: { ...state.gitMode, folds: nextFolds } }
|
|
29
30
|
}
|
|
30
31
|
|
|
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
|
-
|
|
58
32
|
export function emptyGitMode(): GitModeState {
|
|
59
33
|
return {
|
|
60
34
|
actionMessage: null,
|
|
@@ -62,7 +36,10 @@ export function emptyGitMode(): GitModeState {
|
|
|
62
36
|
diffs: {},
|
|
63
37
|
diffView: 'split',
|
|
64
38
|
folds: {},
|
|
39
|
+
headOffset: 0,
|
|
40
|
+
highlights: {},
|
|
65
41
|
loading: {},
|
|
42
|
+
parsedFiles: {},
|
|
66
43
|
pendingDeletePath: null,
|
|
67
44
|
selectedEntryKey: null,
|
|
68
45
|
}
|
|
@@ -83,14 +60,29 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
83
60
|
state.gitPanel.files,
|
|
84
61
|
state.gitMode.collapsedFolders,
|
|
85
62
|
state.gitPane.fileListMode,
|
|
86
|
-
null
|
|
63
|
+
null,
|
|
64
|
+
[],
|
|
65
|
+
state.gitPane.treeCompaction
|
|
87
66
|
),
|
|
88
67
|
},
|
|
89
68
|
}
|
|
90
69
|
}
|
|
91
70
|
case 'exit-git-mode': {
|
|
92
71
|
if (state.focusMode !== 'git') return state
|
|
93
|
-
return {
|
|
72
|
+
return {
|
|
73
|
+
...state,
|
|
74
|
+
focusMode: 'navigation',
|
|
75
|
+
gitMode: {
|
|
76
|
+
...state.gitMode,
|
|
77
|
+
actionMessage: null,
|
|
78
|
+
diffs: {},
|
|
79
|
+
folds: {},
|
|
80
|
+
headOffset: 0,
|
|
81
|
+
highlights: {},
|
|
82
|
+
loading: {},
|
|
83
|
+
parsedFiles: {},
|
|
84
|
+
},
|
|
85
|
+
}
|
|
94
86
|
}
|
|
95
87
|
case 'git-mode-move-selection': {
|
|
96
88
|
const next = moveGitSelection(
|
|
@@ -98,7 +90,8 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
98
90
|
state.gitMode.collapsedFolders,
|
|
99
91
|
state.gitPane.fileListMode,
|
|
100
92
|
state.gitMode.selectedEntryKey,
|
|
101
|
-
action.delta
|
|
93
|
+
action.delta,
|
|
94
|
+
state.gitPane.treeCompaction
|
|
102
95
|
)
|
|
103
96
|
if (!next || next === state.gitMode.selectedEntryKey) return state
|
|
104
97
|
return {
|
|
@@ -116,7 +109,8 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
116
109
|
state.gitMode.collapsedFolders,
|
|
117
110
|
state.gitPane.fileListMode,
|
|
118
111
|
state.gitMode.selectedEntryKey,
|
|
119
|
-
action.delta
|
|
112
|
+
action.delta,
|
|
113
|
+
state.gitPane.treeCompaction
|
|
120
114
|
)
|
|
121
115
|
if (!next || next === state.gitMode.selectedEntryKey) return state
|
|
122
116
|
return {
|
|
@@ -134,7 +128,8 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
134
128
|
state.gitMode.collapsedFolders,
|
|
135
129
|
state.gitPane.fileListMode,
|
|
136
130
|
state.gitMode.selectedEntryKey,
|
|
137
|
-
[action.key]
|
|
131
|
+
[action.key],
|
|
132
|
+
state.gitPane.treeCompaction
|
|
138
133
|
)
|
|
139
134
|
if (!next || next !== action.key || next === state.gitMode.selectedEntryKey) return state
|
|
140
135
|
return {
|
|
@@ -149,6 +144,7 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
149
144
|
case 'git-mode-toggle-folder': {
|
|
150
145
|
const row = getSelectedGitRow(state.gitPanel.files, {
|
|
151
146
|
collapsedFolders: state.gitMode.collapsedFolders,
|
|
147
|
+
compact: state.gitPane.treeCompaction,
|
|
152
148
|
fileListMode: state.gitPane.fileListMode,
|
|
153
149
|
selectedEntryKey: action.key,
|
|
154
150
|
})
|
|
@@ -164,6 +160,7 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
164
160
|
case 'git-mode-toggle-selected-folder': {
|
|
165
161
|
const row = getSelectedGitRow(state.gitPanel.files, {
|
|
166
162
|
collapsedFolders: state.gitMode.collapsedFolders,
|
|
163
|
+
compact: state.gitPane.treeCompaction,
|
|
167
164
|
fileListMode: state.gitPane.fileListMode,
|
|
168
165
|
selectedEntryKey: state.gitMode.selectedEntryKey,
|
|
169
166
|
})
|
|
@@ -179,6 +176,7 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
179
176
|
case 'git-mode-collapse-selection': {
|
|
180
177
|
const row = getSelectedGitRow(state.gitPanel.files, {
|
|
181
178
|
collapsedFolders: state.gitMode.collapsedFolders,
|
|
179
|
+
compact: state.gitPane.treeCompaction,
|
|
182
180
|
fileListMode: state.gitPane.fileListMode,
|
|
183
181
|
selectedEntryKey: state.gitMode.selectedEntryKey,
|
|
184
182
|
})
|
|
@@ -205,6 +203,7 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
205
203
|
case 'git-mode-expand-selection': {
|
|
206
204
|
const row = getSelectedGitRow(state.gitPanel.files, {
|
|
207
205
|
collapsedFolders: state.gitMode.collapsedFolders,
|
|
206
|
+
compact: state.gitPane.treeCompaction,
|
|
208
207
|
fileListMode: state.gitPane.fileListMode,
|
|
209
208
|
selectedEntryKey: state.gitMode.selectedEntryKey,
|
|
210
209
|
})
|
|
@@ -224,24 +223,90 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
224
223
|
state.gitPanel.files,
|
|
225
224
|
state.gitMode.collapsedFolders,
|
|
226
225
|
fileListMode,
|
|
227
|
-
state.gitMode.selectedEntryKey
|
|
226
|
+
state.gitMode.selectedEntryKey,
|
|
227
|
+
[],
|
|
228
|
+
state.gitPane.treeCompaction
|
|
228
229
|
),
|
|
229
230
|
},
|
|
230
231
|
gitPane: { ...state.gitPane, fileListMode },
|
|
231
232
|
}
|
|
232
233
|
}
|
|
234
|
+
case 'git-mode-toggle-tree-compaction': {
|
|
235
|
+
const treeCompaction = !state.gitPane.treeCompaction
|
|
236
|
+
return {
|
|
237
|
+
...state,
|
|
238
|
+
gitMode: {
|
|
239
|
+
...state.gitMode,
|
|
240
|
+
pendingDeletePath: null,
|
|
241
|
+
selectedEntryKey: reconcileSelectedGitEntryKey(
|
|
242
|
+
state.gitPanel.files,
|
|
243
|
+
state.gitMode.collapsedFolders,
|
|
244
|
+
state.gitPane.fileListMode,
|
|
245
|
+
state.gitMode.selectedEntryKey,
|
|
246
|
+
[],
|
|
247
|
+
treeCompaction
|
|
248
|
+
),
|
|
249
|
+
},
|
|
250
|
+
gitPane: { ...state.gitPane, treeCompaction },
|
|
251
|
+
}
|
|
252
|
+
}
|
|
233
253
|
case 'git-mode-set-diff': {
|
|
234
254
|
const nextLoading = { ...state.gitMode.loading }
|
|
235
255
|
delete nextLoading[action.key]
|
|
256
|
+
// Drop stale derived entries (parsed + highlights) whose hash no longer matches.
|
|
257
|
+
const nextParsed = { ...state.gitMode.parsedFiles }
|
|
258
|
+
const prevParsed = nextParsed[action.key]
|
|
259
|
+
if (prevParsed && prevParsed.hash !== action.hash) delete nextParsed[action.key]
|
|
260
|
+
const nextHighlights = { ...state.gitMode.highlights }
|
|
261
|
+
for (const key of Object.keys(nextHighlights)) {
|
|
262
|
+
if (!key.startsWith(`${action.key}|`)) continue
|
|
263
|
+
if (nextHighlights[key]?.hash !== action.hash) delete nextHighlights[key]
|
|
264
|
+
}
|
|
236
265
|
return {
|
|
237
266
|
...state,
|
|
238
267
|
gitMode: {
|
|
239
268
|
...state.gitMode,
|
|
240
269
|
diffs: { ...state.gitMode.diffs, [action.key]: action.diff },
|
|
270
|
+
highlights: nextHighlights,
|
|
241
271
|
loading: nextLoading,
|
|
272
|
+
parsedFiles: nextParsed,
|
|
273
|
+
},
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
case 'git-mode-set-parsed': {
|
|
277
|
+
return {
|
|
278
|
+
...state,
|
|
279
|
+
gitMode: {
|
|
280
|
+
...state.gitMode,
|
|
281
|
+
parsedFiles: {
|
|
282
|
+
...state.gitMode.parsedFiles,
|
|
283
|
+
[action.key]: { file: action.file, hash: action.hash },
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
case 'git-mode-set-highlights': {
|
|
289
|
+
const k = `${action.key}|${action.themeId}`
|
|
290
|
+
return {
|
|
291
|
+
...state,
|
|
292
|
+
gitMode: {
|
|
293
|
+
...state.gitMode,
|
|
294
|
+
highlights: {
|
|
295
|
+
...state.gitMode.highlights,
|
|
296
|
+
[k]: {
|
|
297
|
+
add: action.add,
|
|
298
|
+
del: action.del,
|
|
299
|
+
hash: action.hash,
|
|
300
|
+
themeId: action.themeId,
|
|
301
|
+
},
|
|
302
|
+
},
|
|
242
303
|
},
|
|
243
304
|
}
|
|
244
305
|
}
|
|
306
|
+
case 'git-mode-invalidate-diffs': {
|
|
307
|
+
if (action.paths.length === 0) return state
|
|
308
|
+
return clearDiffCacheForPaths(state, new Set(action.paths))
|
|
309
|
+
}
|
|
245
310
|
case 'git-mode-set-loading': {
|
|
246
311
|
const nextLoading = { ...state.gitMode.loading }
|
|
247
312
|
if (action.loading) {
|
|
@@ -266,6 +331,41 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
266
331
|
const next = state.gitMode.diffView === 'split' ? 'stacked' : 'split'
|
|
267
332
|
return { ...state, gitMode: { ...state.gitMode, diffView: next } }
|
|
268
333
|
}
|
|
334
|
+
case 'git-mode-shift-head-offset': {
|
|
335
|
+
const next = Math.max(0, state.gitMode.headOffset + action.delta)
|
|
336
|
+
if (next === state.gitMode.headOffset) return state
|
|
337
|
+
return {
|
|
338
|
+
...state,
|
|
339
|
+
gitMode: {
|
|
340
|
+
...state.gitMode,
|
|
341
|
+
actionMessage: null,
|
|
342
|
+
diffs: {},
|
|
343
|
+
folds: {},
|
|
344
|
+
headOffset: next,
|
|
345
|
+
highlights: {},
|
|
346
|
+
loading: {},
|
|
347
|
+
parsedFiles: {},
|
|
348
|
+
pendingDeletePath: null,
|
|
349
|
+
},
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
case 'git-mode-set-head-offset': {
|
|
353
|
+
const next = Math.max(0, action.offset)
|
|
354
|
+
if (next === state.gitMode.headOffset) return state
|
|
355
|
+
return {
|
|
356
|
+
...state,
|
|
357
|
+
gitMode: {
|
|
358
|
+
...state.gitMode,
|
|
359
|
+
diffs: {},
|
|
360
|
+
folds: {},
|
|
361
|
+
headOffset: next,
|
|
362
|
+
highlights: {},
|
|
363
|
+
loading: {},
|
|
364
|
+
parsedFiles: {},
|
|
365
|
+
pendingDeletePath: null,
|
|
366
|
+
},
|
|
367
|
+
}
|
|
368
|
+
}
|
|
269
369
|
case 'git-mode-fold-adjust': {
|
|
270
370
|
const perPath = state.gitMode.folds[action.key] ?? {}
|
|
271
371
|
const prev = perPath[action.foldId] ?? { bottom: 0, top: 0 }
|
|
@@ -314,7 +414,8 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
314
414
|
state.gitMode.collapsedFolders,
|
|
315
415
|
state.gitPane.fileListMode,
|
|
316
416
|
currentKey,
|
|
317
|
-
preferredSelection
|
|
417
|
+
preferredSelection,
|
|
418
|
+
state.gitPane.treeCompaction
|
|
318
419
|
)
|
|
319
420
|
|
|
320
421
|
return {
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import type { AppAction, AppState, GitFileEntry, GitFileSection, GitPanelState } from '../types'
|
|
2
2
|
|
|
3
3
|
import { reconcileSelectedGitEntryKey } from '../git-tree'
|
|
4
|
+
import { clearDiffCacheForPaths } from './diff-cache'
|
|
4
5
|
|
|
5
6
|
export const GIT_PANEL_MIN_RATIO = 0.2
|
|
6
7
|
export const GIT_PANEL_MAX_RATIO = 0.8
|
|
7
8
|
|
|
8
|
-
const SECTION_RANK: Record<GitFileSection, number> = {
|
|
9
|
+
const SECTION_RANK: Record<GitFileSection, number> = {
|
|
10
|
+
historical: 0,
|
|
11
|
+
staged: 1,
|
|
12
|
+
unstaged: 2,
|
|
13
|
+
untracked: 3,
|
|
14
|
+
}
|
|
9
15
|
|
|
10
16
|
export function sortFilesBySection(files: GitFileEntry[]): GitFileEntry[] {
|
|
11
17
|
return [...files].sort((a, b) => {
|
|
@@ -30,6 +36,32 @@ export function emptyGitPanel(): GitPanelState {
|
|
|
30
36
|
}
|
|
31
37
|
}
|
|
32
38
|
|
|
39
|
+
function fileSignature(f: GitFileEntry): string {
|
|
40
|
+
return `${f.status}|${f.added ?? '-'}|${f.removed ?? '-'}|${f.renamedFrom ?? ''}`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Paths whose content/status shifted between two snapshots — used to drop
|
|
44
|
+
// stale cached diffs so we re-fetch instead of serving pre-edit data.
|
|
45
|
+
function diffChangedPaths(prev: GitFileEntry[], next: GitFileEntry[]): Set<string> {
|
|
46
|
+
const prevByKey = new Map<string, string>()
|
|
47
|
+
for (const f of prev) prevByKey.set(`${f.section}:${f.path}`, fileSignature(f))
|
|
48
|
+
const changed = new Set<string>()
|
|
49
|
+
const nextKeys = new Set<string>()
|
|
50
|
+
for (const f of next) {
|
|
51
|
+
const key = `${f.section}:${f.path}`
|
|
52
|
+
nextKeys.add(key)
|
|
53
|
+
const prevSig = prevByKey.get(key)
|
|
54
|
+
if (prevSig !== fileSignature(f)) changed.add(f.path)
|
|
55
|
+
}
|
|
56
|
+
for (const [key, _sig] of prevByKey) {
|
|
57
|
+
if (nextKeys.has(key)) continue
|
|
58
|
+
const [, ...rest] = key.split(':')
|
|
59
|
+
const path = rest.join(':')
|
|
60
|
+
if (path) changed.add(path)
|
|
61
|
+
}
|
|
62
|
+
return changed
|
|
63
|
+
}
|
|
64
|
+
|
|
33
65
|
function sameFiles(a: GitFileEntry[], b: GitFileEntry[]): boolean {
|
|
34
66
|
if (a.length !== b.length) return false
|
|
35
67
|
for (let i = 0; i < a.length; i++) {
|
|
@@ -104,7 +136,9 @@ export function reduceGitPanelState(state: AppState, action: AppAction): AppStat
|
|
|
104
136
|
sortedNext,
|
|
105
137
|
state.gitMode.collapsedFolders,
|
|
106
138
|
state.gitPane.fileListMode,
|
|
107
|
-
state.gitMode.selectedEntryKey
|
|
139
|
+
state.gitMode.selectedEntryKey,
|
|
140
|
+
[],
|
|
141
|
+
state.gitPane.treeCompaction
|
|
108
142
|
)
|
|
109
143
|
if (
|
|
110
144
|
prev.branch === next.branch &&
|
|
@@ -116,7 +150,8 @@ export function reduceGitPanelState(state: AppState, action: AppAction): AppStat
|
|
|
116
150
|
) {
|
|
117
151
|
return state
|
|
118
152
|
}
|
|
119
|
-
|
|
153
|
+
const changedPaths = diffChangedPaths(prev.files, sortedNext)
|
|
154
|
+
const base: AppState = {
|
|
120
155
|
...state,
|
|
121
156
|
gitMode: { ...state.gitMode, selectedEntryKey: nextSelectedEntryKey },
|
|
122
157
|
gitPanel: {
|
|
@@ -128,6 +163,7 @@ export function reduceGitPanelState(state: AppState, action: AppAction): AppStat
|
|
|
128
163
|
files: sortedNext,
|
|
129
164
|
},
|
|
130
165
|
}
|
|
166
|
+
return changedPaths.size === 0 ? base : clearDiffCacheForPaths(base, changedPaths)
|
|
131
167
|
}
|
|
132
168
|
case 'git-refresh-error': {
|
|
133
169
|
const prev = state.gitPanel
|
package/src/state/store.ts
CHANGED
package/src/state/types.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { ModeId } from '@brimveyn/aimux-config'
|
|
2
|
+
import type { ThemedToken } from 'shiki'
|
|
3
|
+
|
|
4
|
+
import type { FileDiffMetadata } from '../diff-parser'
|
|
2
5
|
|
|
3
6
|
export type BuiltinAssistantId = 'claude' | 'codex' | 'opencode' | 'terminal'
|
|
4
7
|
|
|
@@ -148,13 +151,16 @@ export interface GitPaneState {
|
|
|
148
151
|
ratio: number
|
|
149
152
|
diffModeRatio: number
|
|
150
153
|
fileListMode: GitFileListMode
|
|
154
|
+
treeCompaction: boolean
|
|
151
155
|
path: GitPanePathConfig
|
|
152
156
|
diffCount: GitPaneDiffCountConfig
|
|
157
|
+
/** Prefetch this many neighbours around the selection. 0 disables prefetch. */
|
|
158
|
+
prefetchRadius: number
|
|
153
159
|
}
|
|
154
160
|
|
|
155
161
|
export type GitFileStatus = 'M' | 'A' | 'D' | 'R' | 'C' | 'U' | '?'
|
|
156
162
|
|
|
157
|
-
export type GitFileSection = 'staged' | 'unstaged' | 'untracked'
|
|
163
|
+
export type GitFileSection = 'staged' | 'unstaged' | 'untracked' | 'historical'
|
|
158
164
|
|
|
159
165
|
export interface GitFileEntry {
|
|
160
166
|
path: string
|
|
@@ -195,15 +201,52 @@ export interface FoldState {
|
|
|
195
201
|
bottom: number
|
|
196
202
|
}
|
|
197
203
|
|
|
204
|
+
export interface ParsedDiffEntry {
|
|
205
|
+
hash: string
|
|
206
|
+
// Stored as unknown at the state boundary; concrete type is FileDiffMetadata
|
|
207
|
+
// | null, narrowed at read sites via getParsedFile().
|
|
208
|
+
file: unknown
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export interface HighlightsEntry {
|
|
212
|
+
hash: string
|
|
213
|
+
themeId: string
|
|
214
|
+
// See ParsedDiffEntry note — narrowed via getHighlights().
|
|
215
|
+
add: unknown
|
|
216
|
+
del: unknown
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function getParsedFile(entry: ParsedDiffEntry | undefined): FileDiffMetadata | null {
|
|
220
|
+
if (!entry) return null
|
|
221
|
+
return entry.file as FileDiffMetadata | null
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function getHighlightsTokens(entry: HighlightsEntry | undefined): {
|
|
225
|
+
add: ThemedToken[][]
|
|
226
|
+
del: ThemedToken[][]
|
|
227
|
+
} | null {
|
|
228
|
+
if (!entry) return null
|
|
229
|
+
return {
|
|
230
|
+
add: entry.add as ThemedToken[][],
|
|
231
|
+
del: entry.del as ThemedToken[][],
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
198
235
|
export interface GitModeState {
|
|
199
236
|
selectedEntryKey: string | null
|
|
200
237
|
collapsedFolders: Record<string, true>
|
|
201
238
|
diffs: Record<string, DiffData>
|
|
239
|
+
/** Parsed diff keyed by fileKey. Invalidated on file change or head offset shift. */
|
|
240
|
+
parsedFiles: Record<string, ParsedDiffEntry>
|
|
241
|
+
/** Tokenised highlights keyed by `${fileKey}|${themeId}`. */
|
|
242
|
+
highlights: Record<string, HighlightsEntry>
|
|
202
243
|
loading: Record<string, boolean>
|
|
203
244
|
pendingDeletePath: string | null
|
|
204
245
|
actionMessage: string | null
|
|
205
246
|
diffView: GitDiffView
|
|
206
247
|
folds: Record<string, Record<string, FoldState>>
|
|
248
|
+
/** Working-tree-vs-HEAD~N offset. 0 = working tree vs HEAD (default). */
|
|
249
|
+
headOffset: number
|
|
207
250
|
}
|
|
208
251
|
|
|
209
252
|
interface ModalBase {
|
|
@@ -471,12 +514,30 @@ export type GitModeAction =
|
|
|
471
514
|
| { type: 'git-mode-collapse-selection' }
|
|
472
515
|
| { type: 'git-mode-expand-selection' }
|
|
473
516
|
| { type: 'git-mode-toggle-file-list-mode' }
|
|
474
|
-
| { type: 'git-mode-
|
|
517
|
+
| { type: 'git-mode-toggle-tree-compaction' }
|
|
518
|
+
| { type: 'git-mode-set-diff'; key: string; diff: DiffData; hash: string }
|
|
519
|
+
| {
|
|
520
|
+
type: 'git-mode-set-parsed'
|
|
521
|
+
key: string
|
|
522
|
+
hash: string
|
|
523
|
+
file: unknown
|
|
524
|
+
}
|
|
525
|
+
| {
|
|
526
|
+
type: 'git-mode-set-highlights'
|
|
527
|
+
key: string
|
|
528
|
+
hash: string
|
|
529
|
+
themeId: string
|
|
530
|
+
add: unknown
|
|
531
|
+
del: unknown
|
|
532
|
+
}
|
|
533
|
+
| { type: 'git-mode-invalidate-diffs'; paths: string[] }
|
|
475
534
|
| { type: 'git-mode-set-loading'; key: string; loading: boolean }
|
|
476
535
|
| { type: 'git-mode-set-pending-delete'; path: string | null }
|
|
477
536
|
| { type: 'git-mode-clear-diff-cache'; path: string }
|
|
478
537
|
| { type: 'git-mode-set-message'; message: string | null }
|
|
479
538
|
| { type: 'git-mode-toggle-diff-view' }
|
|
539
|
+
| { type: 'git-mode-shift-head-offset'; delta: number }
|
|
540
|
+
| { type: 'git-mode-set-head-offset'; offset: number }
|
|
480
541
|
| {
|
|
481
542
|
type: 'git-mode-fold-adjust'
|
|
482
543
|
key: string
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { DirectoryResult } from '../../state/types'
|
|
2
2
|
|
|
3
3
|
import { abbreviatePath } from '../path-format'
|
|
4
|
-
import {
|
|
4
|
+
import { getCurrentTheme, useTheme } from '../theme'
|
|
5
5
|
import { uiTokens } from '../ui-tokens'
|
|
6
6
|
import { InputField } from './input-field'
|
|
7
7
|
import { ListItem } from './list-item'
|
|
@@ -21,14 +21,14 @@ function getDirectoryResultIcon(result: DirectoryResult): string {
|
|
|
21
21
|
|
|
22
22
|
function getDirectoryResultColor(result: DirectoryResult): string {
|
|
23
23
|
if (result.type === 'worktree') {
|
|
24
|
-
return
|
|
24
|
+
return getCurrentTheme().colors['editorWarning.foreground']
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
if (result.type === 'workspace') {
|
|
28
|
-
return
|
|
28
|
+
return getCurrentTheme().colors['terminal.ansiMagenta']
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
return
|
|
31
|
+
return getCurrentTheme().colors['textLink.foreground']
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
interface CreateSessionModalProps {
|
|
@@ -48,6 +48,7 @@ export function CreateSessionModal({
|
|
|
48
48
|
selectedIndex,
|
|
49
49
|
sessionName,
|
|
50
50
|
}: CreateSessionModalProps) {
|
|
51
|
+
const theme = useTheme()
|
|
51
52
|
const dirActive = activeField === 'directory'
|
|
52
53
|
const nameActive = activeField === 'name'
|
|
53
54
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useTheme } from '../../theme'
|
|
2
2
|
import { FOLD_STEP, type FoldInfo } from './build-rows'
|
|
3
3
|
import { type FoldDispatch } from './pierre-diff'
|
|
4
4
|
|
|
@@ -8,6 +8,7 @@ interface Props {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
function Button({ label, onPress }: { label: string; onPress: () => void }) {
|
|
11
|
+
const theme = useTheme()
|
|
11
12
|
return (
|
|
12
13
|
<box
|
|
13
14
|
paddingLeft={1}
|
|
@@ -25,6 +26,7 @@ function Spacer() {
|
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
export function FoldStrip({ dispatch, fold }: Props) {
|
|
29
|
+
const theme = useTheme()
|
|
28
30
|
const { bottomExpanded, foldId, hidden, topExpanded, total } = fold
|
|
29
31
|
const stepUp = Math.min(FOLD_STEP, hidden)
|
|
30
32
|
const stepDown = Math.min(FOLD_STEP, hidden)
|
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
import type { ScrollBoxRenderable } from '@opentui/core'
|
|
2
2
|
import type { ThemedToken } from 'shiki'
|
|
3
3
|
|
|
4
|
-
import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef
|
|
4
|
+
import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef } from 'react'
|
|
5
5
|
|
|
6
|
-
import type { FoldState } from '../../../state/types'
|
|
7
6
|
import type { ThemeId } from '../../themes'
|
|
8
7
|
|
|
9
|
-
import { parsePatchFiles } from '../../../diff-parser'
|
|
10
8
|
import { useAppStore } from '../../../state/app-store'
|
|
11
9
|
import { dispatchGlobal } from '../../../state/dispatch-ref'
|
|
12
|
-
import {
|
|
10
|
+
import { type FoldState } from '../../../state/types'
|
|
11
|
+
import { useTheme } from '../../theme'
|
|
13
12
|
import { buildSplitRows, buildUnifiedRows, firstChangeRowOffset, gutterWidth } from './build-rows'
|
|
14
|
-
import { filetypeFromPath } from './filetype'
|
|
15
|
-
import { tokenizeSide } from './highlight'
|
|
16
13
|
import { SplitView, type SplitViewHandle } from './split-view'
|
|
17
14
|
import { StackedView, type StackedViewHandle } from './stacked-view'
|
|
15
|
+
import { useDiffPreparation } from './use-diff-preparation'
|
|
18
16
|
|
|
19
17
|
export type DiffView = 'split' | 'stacked'
|
|
20
18
|
|
|
@@ -41,19 +39,16 @@ interface Props {
|
|
|
41
39
|
view: DiffView
|
|
42
40
|
}
|
|
43
41
|
|
|
44
|
-
const EMPTY_HIGHLIGHTS: DiffHighlights = { add: [], del: [] }
|
|
45
42
|
const EMPTY_FOLDS: Record<string, FoldState> = {}
|
|
46
43
|
|
|
47
44
|
export const PierreDiff = forwardRef<PierreDiffHandle, Props>(function PierreDiff(
|
|
48
45
|
{ cacheKey, diff, path, themeId, view },
|
|
49
46
|
ref
|
|
50
47
|
) {
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const filetype = useMemo(() => filetypeFromPath(path), [path])
|
|
48
|
+
const theme = useTheme()
|
|
49
|
+
const preparation = useDiffPreparation(cacheKey, diff, path, themeId)
|
|
50
|
+
const file = preparation.file ?? undefined
|
|
51
|
+
const highlights: DiffHighlights = preparation.highlights
|
|
57
52
|
|
|
58
53
|
const terminalCols = useAppStore((s) => s.layout.terminalCols)
|
|
59
54
|
const sidebarWidth = useAppStore((s) => s.sidebar.width)
|
|
@@ -77,24 +72,6 @@ export const PierreDiff = forwardRef<PierreDiffHandle, Props>(function PierreDif
|
|
|
77
72
|
[cacheKey]
|
|
78
73
|
)
|
|
79
74
|
|
|
80
|
-
const [highlights, setHighlights] = useState<DiffHighlights>(EMPTY_HIGHLIGHTS)
|
|
81
|
-
|
|
82
|
-
useEffect(() => {
|
|
83
|
-
setHighlights(EMPTY_HIGHLIGHTS)
|
|
84
|
-
if (!file || !filetype) return
|
|
85
|
-
let cancelled = false
|
|
86
|
-
void Promise.all([
|
|
87
|
-
tokenizeSide(file.additionLines, filetype, themeId),
|
|
88
|
-
tokenizeSide(file.deletionLines, filetype, themeId),
|
|
89
|
-
]).then(([add, del]) => {
|
|
90
|
-
if (cancelled) return
|
|
91
|
-
setHighlights({ add, del })
|
|
92
|
-
})
|
|
93
|
-
return () => {
|
|
94
|
-
cancelled = true
|
|
95
|
-
}
|
|
96
|
-
}, [file, filetype, themeId])
|
|
97
|
-
|
|
98
75
|
const splitRef = useRef<SplitViewHandle | null>(null)
|
|
99
76
|
const stackedRef = useRef<StackedViewHandle | null>(null)
|
|
100
77
|
|
|
@@ -140,7 +117,9 @@ export const PierreDiff = forwardRef<PierreDiffHandle, Props>(function PierreDif
|
|
|
140
117
|
if (!file) {
|
|
141
118
|
return (
|
|
142
119
|
<box flexGrow={1} padding={1}>
|
|
143
|
-
<text fg={theme.colors['descriptionForeground']}>
|
|
120
|
+
<text fg={theme.colors['descriptionForeground']}>
|
|
121
|
+
{preparation.preparing ? 'Preparing diff…' : '(could not parse diff)'}
|
|
122
|
+
</text>
|
|
144
123
|
</box>
|
|
145
124
|
)
|
|
146
125
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { ThemedToken } from 'shiki'
|
|
2
|
+
|
|
3
|
+
import { type FileDiffMetadata, parsePatchFiles } from '../../../diff-parser'
|
|
4
|
+
import { diffHash } from '../../../git/diff-hash'
|
|
5
|
+
import { filetypeFromPath } from './filetype'
|
|
6
|
+
import { tokenizeSide } from './highlight'
|
|
7
|
+
|
|
8
|
+
export interface PreparedDiff {
|
|
9
|
+
hash: string
|
|
10
|
+
file: FileDiffMetadata | null
|
|
11
|
+
filetype: string | null
|
|
12
|
+
highlights: { add: ThemedToken[][]; del: ThemedToken[][] }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface PrepareOptions {
|
|
16
|
+
signal?: AbortSignal
|
|
17
|
+
themeId: string
|
|
18
|
+
/** Skip Shiki for big files to keep prepare under a few tens of ms. */
|
|
19
|
+
skipHighlightThreshold?: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const DEFAULT_SKIP_HIGHLIGHT = 2000
|
|
23
|
+
|
|
24
|
+
function yieldToEventLoop(): Promise<void> {
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
setImmediate(resolve)
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function throwIfAborted(signal?: AbortSignal): void {
|
|
31
|
+
if (signal?.aborted) throw new DOMException('Aborted', 'AbortError')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Lifts the hot path (parse → tokenize) off the synchronous render tree. The
|
|
35
|
+
// parser still blocks briefly on very large diffs, but yielding between parse
|
|
36
|
+
// and each tokenize side keeps other UI updates responsive.
|
|
37
|
+
export async function prepareDiff(
|
|
38
|
+
diff: string,
|
|
39
|
+
path: string,
|
|
40
|
+
opts: PrepareOptions
|
|
41
|
+
): Promise<PreparedDiff> {
|
|
42
|
+
const hash = diffHash(diff)
|
|
43
|
+
throwIfAborted(opts.signal)
|
|
44
|
+
const patches = parsePatchFiles(diff)
|
|
45
|
+
const file = patches[0]?.files[0] ?? null
|
|
46
|
+
const filetype = file ? (filetypeFromPath(path) ?? null) : null
|
|
47
|
+
|
|
48
|
+
const skipThreshold = opts.skipHighlightThreshold ?? DEFAULT_SKIP_HIGHLIGHT
|
|
49
|
+
const totalLines = file ? file.additionLines.length + file.deletionLines.length : 0
|
|
50
|
+
const shouldHighlight = !!file && !!filetype && totalLines <= skipThreshold
|
|
51
|
+
|
|
52
|
+
if (!shouldHighlight) {
|
|
53
|
+
return { file, filetype, hash, highlights: { add: [], del: [] } }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
await yieldToEventLoop()
|
|
57
|
+
throwIfAborted(opts.signal)
|
|
58
|
+
const add = await tokenizeSide(file.additionLines, filetype, opts.themeId)
|
|
59
|
+
throwIfAborted(opts.signal)
|
|
60
|
+
await yieldToEventLoop()
|
|
61
|
+
throwIfAborted(opts.signal)
|
|
62
|
+
const del = await tokenizeSide(file.deletionLines, filetype, opts.themeId)
|
|
63
|
+
throwIfAborted(opts.signal)
|
|
64
|
+
|
|
65
|
+
return { file, filetype, hash, highlights: { add, del } }
|
|
66
|
+
}
|