@brimveyn/aimux 1.6.1 → 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/input/modes/types.ts +1 -0
- package/src/state/git-tree.ts +42 -16
- package/src/state/reducers/diff-cache.ts +64 -0
- package/src/state/reducers/git-mode-state.ts +91 -33
- package/src/state/reducers/git-panel-state.ts +33 -2
- package/src/state/store.ts +2 -0
- package/src/state/types.ts +58 -1
- package/src/ui/components/diff-renderer/pierre-diff.tsx +9 -31
- package/src/ui/components/diff-renderer/prepare-diff.ts +66 -0
- package/src/ui/components/diff-renderer/split-view.tsx +30 -5
- package/src/ui/components/diff-renderer/stacked-view.tsx +29 -4
- 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-pane-widget.tsx +2 -0
- package/src/ui/components/git-panel.tsx +17 -7
- package/src/ui/components/git-view.tsx +19 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brimveyn/aimux",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.2",
|
|
4
4
|
"description": "A terminal multiplexer for AI CLIs. Run Claude, Codex, OpenCode side-by-side with tabbed navigation, split panes, and persistent sessions.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"generate-themes": "bun run scripts/generate-themes.ts"
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
|
-
"@brimveyn/aimux-config": "0.4.
|
|
64
|
+
"@brimveyn/aimux-config": "0.4.2",
|
|
65
65
|
"@opentui/core": "^0.1.90",
|
|
66
66
|
"@opentui/react": "^0.1.90",
|
|
67
67
|
"@xterm/headless": "^6.0.0",
|
|
@@ -471,6 +471,7 @@ export function executeSideEffect(effect: SideEffect, ctx: SideEffectContext): v
|
|
|
471
471
|
mode: persistedGitPane?.mode ?? 'embedded',
|
|
472
472
|
position: persistedGitPane?.position ?? 'bottom',
|
|
473
473
|
ratio: persistedGitPane?.ratio ?? 0.5,
|
|
474
|
+
treeCompaction: persistedGitPane?.treeCompaction,
|
|
474
475
|
visible: persistedGitPane?.visible ?? true,
|
|
475
476
|
},
|
|
476
477
|
})
|
|
@@ -487,6 +488,24 @@ export function executeSideEffect(effect: SideEffect, ctx: SideEffectContext): v
|
|
|
487
488
|
mode: persistedGitPane?.mode ?? 'embedded',
|
|
488
489
|
position: persistedGitPane?.position ?? 'bottom',
|
|
489
490
|
ratio: persistedGitPane?.ratio ?? 0.5,
|
|
491
|
+
treeCompaction: persistedGitPane?.treeCompaction,
|
|
492
|
+
visible: persistedGitPane?.visible ?? true,
|
|
493
|
+
},
|
|
494
|
+
})
|
|
495
|
+
return
|
|
496
|
+
}
|
|
497
|
+
case 'persist-git-tree-compaction': {
|
|
498
|
+
const config = loadConfig()
|
|
499
|
+
const persistedGitPane = config.gitPane
|
|
500
|
+
saveConfig({
|
|
501
|
+
...config,
|
|
502
|
+
gitPane: {
|
|
503
|
+
diffModeRatio: persistedGitPane?.diffModeRatio,
|
|
504
|
+
fileListMode: persistedGitPane?.fileListMode,
|
|
505
|
+
mode: persistedGitPane?.mode ?? 'embedded',
|
|
506
|
+
position: persistedGitPane?.position ?? 'bottom',
|
|
507
|
+
ratio: persistedGitPane?.ratio ?? 0.5,
|
|
508
|
+
treeCompaction: effect.enabled,
|
|
490
509
|
visible: persistedGitPane?.visible ?? true,
|
|
491
510
|
},
|
|
492
511
|
})
|
package/src/app.tsx
CHANGED
|
@@ -88,10 +88,14 @@ export function App({
|
|
|
88
88
|
const userGitPane = resolvedConfig.gitPane
|
|
89
89
|
const fileListMode = userGitPane?.fileListMode ?? json.gitPane?.fileListMode ?? 'tree'
|
|
90
90
|
const diffModeRatio = userGitPane?.diffModeRatio ?? json.gitPane?.diffModeRatio ?? 0.35
|
|
91
|
+
const treeCompaction = userGitPane?.treeCompaction ?? json.gitPane?.treeCompaction ?? true
|
|
92
|
+
const prefetchRadius = userGitPane?.prefetchRadius ?? json.gitPane?.prefetchRadius ?? 5
|
|
91
93
|
const gitPaneOverrides = {
|
|
92
94
|
...json.gitPane,
|
|
93
95
|
diffModeRatio,
|
|
94
96
|
fileListMode,
|
|
97
|
+
prefetchRadius,
|
|
98
|
+
treeCompaction,
|
|
95
99
|
...(userGitPane?.visible !== undefined ? { visible: userGitPane.visible } : {}),
|
|
96
100
|
...(userGitPane?.mode !== undefined ? { mode: userGitPane.mode } : {}),
|
|
97
101
|
...(userGitPane?.position !== undefined ? { position: userGitPane.position } : {}),
|
package/src/config.ts
CHANGED
|
@@ -25,6 +25,8 @@ export const CONFIG_PATH = `${getProfileConfigDir()}/aimux.json`
|
|
|
25
25
|
export interface PersistedGitPane {
|
|
26
26
|
diffModeRatio?: number
|
|
27
27
|
fileListMode?: GitFileListMode
|
|
28
|
+
treeCompaction?: boolean
|
|
29
|
+
prefetchRadius?: number
|
|
28
30
|
visible: boolean
|
|
29
31
|
mode: 'embedded' | 'pane'
|
|
30
32
|
position: 'top' | 'bottom' | 'left' | 'right'
|
|
@@ -62,7 +64,23 @@ function isPersistedGitPane(value: unknown): value is PersistedGitPane {
|
|
|
62
64
|
const visibleOk = typeof v.visible === 'boolean'
|
|
63
65
|
const fileListModeOk =
|
|
64
66
|
v.fileListMode === undefined || v.fileListMode === 'tree' || v.fileListMode === 'flat'
|
|
65
|
-
|
|
67
|
+
const treeCompactionOk = v.treeCompaction === undefined || typeof v.treeCompaction === 'boolean'
|
|
68
|
+
const prefetchRadiusOk =
|
|
69
|
+
v.prefetchRadius === undefined ||
|
|
70
|
+
(typeof v.prefetchRadius === 'number' &&
|
|
71
|
+
Number.isFinite(v.prefetchRadius) &&
|
|
72
|
+
v.prefetchRadius >= 0 &&
|
|
73
|
+
v.prefetchRadius <= 50)
|
|
74
|
+
if (
|
|
75
|
+
!modeOk ||
|
|
76
|
+
!positionOk ||
|
|
77
|
+
!ratioOk ||
|
|
78
|
+
!diffModeRatioOk ||
|
|
79
|
+
!visibleOk ||
|
|
80
|
+
!fileListModeOk ||
|
|
81
|
+
!treeCompactionOk ||
|
|
82
|
+
!prefetchRadiusOk
|
|
83
|
+
) {
|
|
66
84
|
return false
|
|
67
85
|
}
|
|
68
86
|
// cross-field coherence: embedded => top|bottom; pane => left|right
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// FNV-1a 32-bit hash. Fast, non-crypto, used to detect stale cached diff
|
|
2
|
+
// artefacts (parsed file, syntax highlights) when the raw diff text changes.
|
|
3
|
+
export function diffHash(input: string): string {
|
|
4
|
+
let h = 0x811c9dc5
|
|
5
|
+
for (let i = 0; i < input.length; i++) {
|
|
6
|
+
h ^= input.charCodeAt(i)
|
|
7
|
+
h = (h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24))) >>> 0
|
|
8
|
+
}
|
|
9
|
+
return h.toString(16)
|
|
10
|
+
}
|
package/src/input/modes/types.ts
CHANGED
|
@@ -49,6 +49,7 @@ export type SideEffect =
|
|
|
49
49
|
| { type: 'scroll-git-diff'; delta: number }
|
|
50
50
|
| { type: 'persist-git-diff-mode-ratio'; ratio: number }
|
|
51
51
|
| { type: 'persist-git-file-list-mode'; mode: GitFileListMode }
|
|
52
|
+
| { type: 'persist-git-tree-compaction'; enabled: boolean }
|
|
52
53
|
| { type: 'git-stage'; path: string }
|
|
53
54
|
| { type: 'git-unstage'; path: string }
|
|
54
55
|
| { type: 'git-restore'; path: string }
|
package/src/state/git-tree.ts
CHANGED
|
@@ -53,7 +53,8 @@ export function gitFolderKey(section: GitFileSection, folderPath: string): strin
|
|
|
53
53
|
export function buildGitTreeRows(
|
|
54
54
|
files: GitFileEntry[],
|
|
55
55
|
collapsedFolders: Record<string, true>,
|
|
56
|
-
fileListMode: GitFileListMode = 'tree'
|
|
56
|
+
fileListMode: GitFileListMode = 'tree',
|
|
57
|
+
compact: boolean = false
|
|
57
58
|
): GitTreeRows {
|
|
58
59
|
const sections = SECTION_ORDER.map((section) => {
|
|
59
60
|
const sectionFiles = files.filter((file) => file.section === section)
|
|
@@ -68,7 +69,7 @@ export function buildGitTreeRows(
|
|
|
68
69
|
kind: 'file' as const,
|
|
69
70
|
section,
|
|
70
71
|
}))
|
|
71
|
-
: flattenSectionRows(sectionFiles, collapsedFolders),
|
|
72
|
+
: flattenSectionRows(sectionFiles, collapsedFolders, compact),
|
|
72
73
|
section,
|
|
73
74
|
}
|
|
74
75
|
})
|
|
@@ -81,10 +82,16 @@ export function getSelectedGitRow(
|
|
|
81
82
|
collapsedFolders: Record<string, true>
|
|
82
83
|
fileListMode: GitFileListMode
|
|
83
84
|
selectedEntryKey: string | null
|
|
85
|
+
compact?: boolean
|
|
84
86
|
}
|
|
85
87
|
): GitTreeRow | null {
|
|
86
88
|
if (!options.selectedEntryKey) return null
|
|
87
|
-
const { visibleRows } = buildGitTreeRows(
|
|
89
|
+
const { visibleRows } = buildGitTreeRows(
|
|
90
|
+
files,
|
|
91
|
+
options.collapsedFolders,
|
|
92
|
+
options.fileListMode,
|
|
93
|
+
options.compact ?? false
|
|
94
|
+
)
|
|
88
95
|
return visibleRows.find((row) => row.key === options.selectedEntryKey) ?? null
|
|
89
96
|
}
|
|
90
97
|
|
|
@@ -94,6 +101,7 @@ export function getSelectedGitFile(
|
|
|
94
101
|
collapsedFolders: Record<string, true>
|
|
95
102
|
fileListMode: GitFileListMode
|
|
96
103
|
selectedEntryKey: string | null
|
|
104
|
+
compact?: boolean
|
|
97
105
|
}
|
|
98
106
|
): GitFileEntry | null {
|
|
99
107
|
const row = getSelectedGitRow(files, options)
|
|
@@ -105,9 +113,10 @@ export function reconcileSelectedGitEntryKey(
|
|
|
105
113
|
collapsedFolders: Record<string, true>,
|
|
106
114
|
fileListMode: GitFileListMode,
|
|
107
115
|
selectedEntryKey: string | null | undefined,
|
|
108
|
-
preferredKeys: string[] = []
|
|
116
|
+
preferredKeys: string[] = [],
|
|
117
|
+
compact: boolean = false
|
|
109
118
|
): string | null {
|
|
110
|
-
const { visibleRows } = buildGitTreeRows(files, collapsedFolders, fileListMode)
|
|
119
|
+
const { visibleRows } = buildGitTreeRows(files, collapsedFolders, fileListMode, compact)
|
|
111
120
|
if (visibleRows.length === 0) return null
|
|
112
121
|
const candidates = [...preferredKeys, selectedEntryKey ?? '']
|
|
113
122
|
for (const key of candidates) {
|
|
@@ -122,9 +131,10 @@ export function moveGitSelection(
|
|
|
122
131
|
collapsedFolders: Record<string, true>,
|
|
123
132
|
fileListMode: GitFileListMode,
|
|
124
133
|
selectedEntryKey: string | null,
|
|
125
|
-
delta: -1 | 1
|
|
134
|
+
delta: -1 | 1,
|
|
135
|
+
compact: boolean = false
|
|
126
136
|
): string | null {
|
|
127
|
-
const { visibleRows } = buildGitTreeRows(files, collapsedFolders, fileListMode)
|
|
137
|
+
const { visibleRows } = buildGitTreeRows(files, collapsedFolders, fileListMode, compact)
|
|
128
138
|
const total = visibleRows.length
|
|
129
139
|
if (total === 0) return null
|
|
130
140
|
const current = visibleRows.findIndex((row) => row.key === selectedEntryKey)
|
|
@@ -137,9 +147,10 @@ export function moveGitFileSelection(
|
|
|
137
147
|
collapsedFolders: Record<string, true>,
|
|
138
148
|
fileListMode: GitFileListMode,
|
|
139
149
|
selectedEntryKey: string | null,
|
|
140
|
-
delta: -1 | 1
|
|
150
|
+
delta: -1 | 1,
|
|
151
|
+
compact: boolean = false
|
|
141
152
|
): string | null {
|
|
142
|
-
const { visibleRows } = buildGitTreeRows(files, collapsedFolders, fileListMode)
|
|
153
|
+
const { visibleRows } = buildGitTreeRows(files, collapsedFolders, fileListMode, compact)
|
|
143
154
|
const fileRows = visibleRows.filter((row) => row.kind === 'file')
|
|
144
155
|
const total = fileRows.length
|
|
145
156
|
if (total === 0) return null
|
|
@@ -153,13 +164,14 @@ export function moveGitFileSelection(
|
|
|
153
164
|
|
|
154
165
|
function flattenSectionRows(
|
|
155
166
|
files: GitFileEntry[],
|
|
156
|
-
collapsedFolders: Record<string, true
|
|
167
|
+
collapsedFolders: Record<string, true>,
|
|
168
|
+
compact: boolean
|
|
157
169
|
): GitTreeRow[] {
|
|
158
170
|
if (files.length === 0) return []
|
|
159
171
|
const section = files[0]?.section
|
|
160
172
|
if (!section) return []
|
|
161
173
|
const root = buildSectionTree(files, section)
|
|
162
|
-
return flattenTreeNode(root, collapsedFolders, 0)
|
|
174
|
+
return flattenTreeNode(root, collapsedFolders, 0, undefined, compact)
|
|
163
175
|
}
|
|
164
176
|
|
|
165
177
|
function buildSectionTree(files: GitFileEntry[], section: GitFileSection): GitTreeNode {
|
|
@@ -187,16 +199,28 @@ function flattenTreeNode(
|
|
|
187
199
|
node: GitTreeNode,
|
|
188
200
|
collapsedFolders: Record<string, true>,
|
|
189
201
|
depth: number,
|
|
190
|
-
parentKey
|
|
202
|
+
parentKey: string | undefined,
|
|
203
|
+
compact: boolean
|
|
191
204
|
): GitTreeRow[] {
|
|
192
205
|
const rows: GitTreeRow[] = []
|
|
193
206
|
for (const child of node.folders.values()) {
|
|
194
|
-
|
|
195
|
-
|
|
207
|
+
// Compaction: while a folder has exactly one child folder and no direct
|
|
208
|
+
// files, fold the chain into a single row ("src/keymap/keymap.ts" instead
|
|
209
|
+
// of three nested rows).
|
|
210
|
+
let current = child
|
|
211
|
+
const segments: string[] = [current.path.split('/').pop() ?? current.path]
|
|
212
|
+
while (compact && current.files.length === 0 && current.folders.size === 1) {
|
|
213
|
+
const next = current.folders.values().next().value
|
|
214
|
+
if (!next) break
|
|
215
|
+
segments.push(next.path.split('/').pop() ?? next.path)
|
|
216
|
+
current = next
|
|
217
|
+
}
|
|
218
|
+
const name = segments.join('/')
|
|
219
|
+
const key = gitFolderKey(node.section, current.path)
|
|
196
220
|
const isCollapsed = key in collapsedFolders
|
|
197
221
|
rows.push({
|
|
198
222
|
depth,
|
|
199
|
-
folderPath:
|
|
223
|
+
folderPath: current.path,
|
|
200
224
|
isCollapsed,
|
|
201
225
|
key,
|
|
202
226
|
kind: 'folder',
|
|
@@ -204,7 +228,9 @@ function flattenTreeNode(
|
|
|
204
228
|
parentKey,
|
|
205
229
|
section: node.section,
|
|
206
230
|
})
|
|
207
|
-
if (!isCollapsed)
|
|
231
|
+
if (!isCollapsed) {
|
|
232
|
+
rows.push(...flattenTreeNode(current, collapsedFolders, depth + 1, key, compact))
|
|
233
|
+
}
|
|
208
234
|
}
|
|
209
235
|
for (const file of node.files) {
|
|
210
236
|
rows.push({
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { AppState } from '../types'
|
|
2
|
+
|
|
3
|
+
export function clearDiffCacheForPath(state: AppState, path: string): AppState {
|
|
4
|
+
return clearDiffCacheForPaths(state, new Set([path]))
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// Drops every cache slice derived from the given file paths: diffs, folds,
|
|
8
|
+
// loading flags, parsed patches, and per-theme highlight tokens. Called when
|
|
9
|
+
// a file's on-disk content shifts or a file disappears from git status.
|
|
10
|
+
export function clearDiffCacheForPaths(state: AppState, paths: Set<string>): AppState {
|
|
11
|
+
if (paths.size === 0) return state
|
|
12
|
+
const nextDiffs = { ...state.gitMode.diffs }
|
|
13
|
+
const nextFolds = { ...state.gitMode.folds }
|
|
14
|
+
const nextLoading = { ...state.gitMode.loading }
|
|
15
|
+
const nextParsed = { ...state.gitMode.parsedFiles }
|
|
16
|
+
const nextHighlights = { ...state.gitMode.highlights }
|
|
17
|
+
let changed = false
|
|
18
|
+
const keyMatches = (key: string): boolean => {
|
|
19
|
+
for (const path of paths) {
|
|
20
|
+
if (key.endsWith(`:${path}`) || key.startsWith(`${path}|`) || key.includes(`:${path}|`)) {
|
|
21
|
+
return true
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return false
|
|
25
|
+
}
|
|
26
|
+
for (const key of Object.keys(nextDiffs)) {
|
|
27
|
+
const entry = nextDiffs[key]
|
|
28
|
+
if (!entry || !paths.has(entry.path)) continue
|
|
29
|
+
delete nextDiffs[key]
|
|
30
|
+
changed = true
|
|
31
|
+
}
|
|
32
|
+
for (const key of Object.keys(nextFolds)) {
|
|
33
|
+
if (!keyMatches(key)) continue
|
|
34
|
+
delete nextFolds[key]
|
|
35
|
+
changed = true
|
|
36
|
+
}
|
|
37
|
+
for (const key of Object.keys(nextLoading)) {
|
|
38
|
+
if (!keyMatches(key)) continue
|
|
39
|
+
delete nextLoading[key]
|
|
40
|
+
changed = true
|
|
41
|
+
}
|
|
42
|
+
for (const key of Object.keys(nextParsed)) {
|
|
43
|
+
if (!keyMatches(key)) continue
|
|
44
|
+
delete nextParsed[key]
|
|
45
|
+
changed = true
|
|
46
|
+
}
|
|
47
|
+
for (const key of Object.keys(nextHighlights)) {
|
|
48
|
+
if (!keyMatches(key)) continue
|
|
49
|
+
delete nextHighlights[key]
|
|
50
|
+
changed = true
|
|
51
|
+
}
|
|
52
|
+
if (!changed) return state
|
|
53
|
+
return {
|
|
54
|
+
...state,
|
|
55
|
+
gitMode: {
|
|
56
|
+
...state.gitMode,
|
|
57
|
+
diffs: nextDiffs,
|
|
58
|
+
folds: nextFolds,
|
|
59
|
+
highlights: nextHighlights,
|
|
60
|
+
loading: nextLoading,
|
|
61
|
+
parsedFiles: nextParsed,
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -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,
|
|
@@ -63,7 +37,9 @@ export function emptyGitMode(): GitModeState {
|
|
|
63
37
|
diffView: 'split',
|
|
64
38
|
folds: {},
|
|
65
39
|
headOffset: 0,
|
|
40
|
+
highlights: {},
|
|
66
41
|
loading: {},
|
|
42
|
+
parsedFiles: {},
|
|
67
43
|
pendingDeletePath: null,
|
|
68
44
|
selectedEntryKey: null,
|
|
69
45
|
}
|
|
@@ -84,7 +60,9 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
84
60
|
state.gitPanel.files,
|
|
85
61
|
state.gitMode.collapsedFolders,
|
|
86
62
|
state.gitPane.fileListMode,
|
|
87
|
-
null
|
|
63
|
+
null,
|
|
64
|
+
[],
|
|
65
|
+
state.gitPane.treeCompaction
|
|
88
66
|
),
|
|
89
67
|
},
|
|
90
68
|
}
|
|
@@ -100,7 +78,9 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
100
78
|
diffs: {},
|
|
101
79
|
folds: {},
|
|
102
80
|
headOffset: 0,
|
|
81
|
+
highlights: {},
|
|
103
82
|
loading: {},
|
|
83
|
+
parsedFiles: {},
|
|
104
84
|
},
|
|
105
85
|
}
|
|
106
86
|
}
|
|
@@ -110,7 +90,8 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
110
90
|
state.gitMode.collapsedFolders,
|
|
111
91
|
state.gitPane.fileListMode,
|
|
112
92
|
state.gitMode.selectedEntryKey,
|
|
113
|
-
action.delta
|
|
93
|
+
action.delta,
|
|
94
|
+
state.gitPane.treeCompaction
|
|
114
95
|
)
|
|
115
96
|
if (!next || next === state.gitMode.selectedEntryKey) return state
|
|
116
97
|
return {
|
|
@@ -128,7 +109,8 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
128
109
|
state.gitMode.collapsedFolders,
|
|
129
110
|
state.gitPane.fileListMode,
|
|
130
111
|
state.gitMode.selectedEntryKey,
|
|
131
|
-
action.delta
|
|
112
|
+
action.delta,
|
|
113
|
+
state.gitPane.treeCompaction
|
|
132
114
|
)
|
|
133
115
|
if (!next || next === state.gitMode.selectedEntryKey) return state
|
|
134
116
|
return {
|
|
@@ -146,7 +128,8 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
146
128
|
state.gitMode.collapsedFolders,
|
|
147
129
|
state.gitPane.fileListMode,
|
|
148
130
|
state.gitMode.selectedEntryKey,
|
|
149
|
-
[action.key]
|
|
131
|
+
[action.key],
|
|
132
|
+
state.gitPane.treeCompaction
|
|
150
133
|
)
|
|
151
134
|
if (!next || next !== action.key || next === state.gitMode.selectedEntryKey) return state
|
|
152
135
|
return {
|
|
@@ -161,6 +144,7 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
161
144
|
case 'git-mode-toggle-folder': {
|
|
162
145
|
const row = getSelectedGitRow(state.gitPanel.files, {
|
|
163
146
|
collapsedFolders: state.gitMode.collapsedFolders,
|
|
147
|
+
compact: state.gitPane.treeCompaction,
|
|
164
148
|
fileListMode: state.gitPane.fileListMode,
|
|
165
149
|
selectedEntryKey: action.key,
|
|
166
150
|
})
|
|
@@ -176,6 +160,7 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
176
160
|
case 'git-mode-toggle-selected-folder': {
|
|
177
161
|
const row = getSelectedGitRow(state.gitPanel.files, {
|
|
178
162
|
collapsedFolders: state.gitMode.collapsedFolders,
|
|
163
|
+
compact: state.gitPane.treeCompaction,
|
|
179
164
|
fileListMode: state.gitPane.fileListMode,
|
|
180
165
|
selectedEntryKey: state.gitMode.selectedEntryKey,
|
|
181
166
|
})
|
|
@@ -191,6 +176,7 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
191
176
|
case 'git-mode-collapse-selection': {
|
|
192
177
|
const row = getSelectedGitRow(state.gitPanel.files, {
|
|
193
178
|
collapsedFolders: state.gitMode.collapsedFolders,
|
|
179
|
+
compact: state.gitPane.treeCompaction,
|
|
194
180
|
fileListMode: state.gitPane.fileListMode,
|
|
195
181
|
selectedEntryKey: state.gitMode.selectedEntryKey,
|
|
196
182
|
})
|
|
@@ -217,6 +203,7 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
217
203
|
case 'git-mode-expand-selection': {
|
|
218
204
|
const row = getSelectedGitRow(state.gitPanel.files, {
|
|
219
205
|
collapsedFolders: state.gitMode.collapsedFolders,
|
|
206
|
+
compact: state.gitPane.treeCompaction,
|
|
220
207
|
fileListMode: state.gitPane.fileListMode,
|
|
221
208
|
selectedEntryKey: state.gitMode.selectedEntryKey,
|
|
222
209
|
})
|
|
@@ -236,24 +223,90 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
236
223
|
state.gitPanel.files,
|
|
237
224
|
state.gitMode.collapsedFolders,
|
|
238
225
|
fileListMode,
|
|
239
|
-
state.gitMode.selectedEntryKey
|
|
226
|
+
state.gitMode.selectedEntryKey,
|
|
227
|
+
[],
|
|
228
|
+
state.gitPane.treeCompaction
|
|
240
229
|
),
|
|
241
230
|
},
|
|
242
231
|
gitPane: { ...state.gitPane, fileListMode },
|
|
243
232
|
}
|
|
244
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
|
+
}
|
|
245
253
|
case 'git-mode-set-diff': {
|
|
246
254
|
const nextLoading = { ...state.gitMode.loading }
|
|
247
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
|
+
}
|
|
248
265
|
return {
|
|
249
266
|
...state,
|
|
250
267
|
gitMode: {
|
|
251
268
|
...state.gitMode,
|
|
252
269
|
diffs: { ...state.gitMode.diffs, [action.key]: action.diff },
|
|
270
|
+
highlights: nextHighlights,
|
|
253
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
|
+
},
|
|
254
303
|
},
|
|
255
304
|
}
|
|
256
305
|
}
|
|
306
|
+
case 'git-mode-invalidate-diffs': {
|
|
307
|
+
if (action.paths.length === 0) return state
|
|
308
|
+
return clearDiffCacheForPaths(state, new Set(action.paths))
|
|
309
|
+
}
|
|
257
310
|
case 'git-mode-set-loading': {
|
|
258
311
|
const nextLoading = { ...state.gitMode.loading }
|
|
259
312
|
if (action.loading) {
|
|
@@ -289,7 +342,9 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
289
342
|
diffs: {},
|
|
290
343
|
folds: {},
|
|
291
344
|
headOffset: next,
|
|
345
|
+
highlights: {},
|
|
292
346
|
loading: {},
|
|
347
|
+
parsedFiles: {},
|
|
293
348
|
pendingDeletePath: null,
|
|
294
349
|
},
|
|
295
350
|
}
|
|
@@ -304,7 +359,9 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
304
359
|
diffs: {},
|
|
305
360
|
folds: {},
|
|
306
361
|
headOffset: next,
|
|
362
|
+
highlights: {},
|
|
307
363
|
loading: {},
|
|
364
|
+
parsedFiles: {},
|
|
308
365
|
pendingDeletePath: null,
|
|
309
366
|
},
|
|
310
367
|
}
|
|
@@ -357,7 +414,8 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
357
414
|
state.gitMode.collapsedFolders,
|
|
358
415
|
state.gitPane.fileListMode,
|
|
359
416
|
currentKey,
|
|
360
|
-
preferredSelection
|
|
417
|
+
preferredSelection,
|
|
418
|
+
state.gitPane.treeCompaction
|
|
361
419
|
)
|
|
362
420
|
|
|
363
421
|
return {
|
|
@@ -1,6 +1,7 @@
|
|
|
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
|
|
@@ -35,6 +36,32 @@ export function emptyGitPanel(): GitPanelState {
|
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
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
|
+
|
|
38
65
|
function sameFiles(a: GitFileEntry[], b: GitFileEntry[]): boolean {
|
|
39
66
|
if (a.length !== b.length) return false
|
|
40
67
|
for (let i = 0; i < a.length; i++) {
|
|
@@ -109,7 +136,9 @@ export function reduceGitPanelState(state: AppState, action: AppAction): AppStat
|
|
|
109
136
|
sortedNext,
|
|
110
137
|
state.gitMode.collapsedFolders,
|
|
111
138
|
state.gitPane.fileListMode,
|
|
112
|
-
state.gitMode.selectedEntryKey
|
|
139
|
+
state.gitMode.selectedEntryKey,
|
|
140
|
+
[],
|
|
141
|
+
state.gitPane.treeCompaction
|
|
113
142
|
)
|
|
114
143
|
if (
|
|
115
144
|
prev.branch === next.branch &&
|
|
@@ -121,7 +150,8 @@ export function reduceGitPanelState(state: AppState, action: AppAction): AppStat
|
|
|
121
150
|
) {
|
|
122
151
|
return state
|
|
123
152
|
}
|
|
124
|
-
|
|
153
|
+
const changedPaths = diffChangedPaths(prev.files, sortedNext)
|
|
154
|
+
const base: AppState = {
|
|
125
155
|
...state,
|
|
126
156
|
gitMode: { ...state.gitMode, selectedEntryKey: nextSelectedEntryKey },
|
|
127
157
|
gitPanel: {
|
|
@@ -133,6 +163,7 @@ export function reduceGitPanelState(state: AppState, action: AppAction): AppStat
|
|
|
133
163
|
files: sortedNext,
|
|
134
164
|
},
|
|
135
165
|
}
|
|
166
|
+
return changedPaths.size === 0 ? base : clearDiffCacheForPaths(base, changedPaths)
|
|
136
167
|
}
|
|
137
168
|
case 'git-refresh-error': {
|
|
138
169
|
const prev = state.gitPanel
|