@brimveyn/aimux 1.6.0 → 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.
- package/package.json +2 -2
- 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/pty/terminal-snapshot.ts +5 -5
- package/src/state/git-tree.ts +1 -1
- package/src/state/reducers/git-mode-state.ts +44 -1
- package/src/state/reducers/git-panel-state.ts +6 -1
- package/src/state/types.ts +5 -1
- 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 +2 -1
- package/src/ui/components/diff-renderer/split-view.tsx +5 -1
- package/src/ui/components/diff-renderer/stacked-view.tsx +3 -1
- package/src/ui/components/git-commit-modal.tsx +2 -1
- package/src/ui/components/git-pane-widget.tsx +1 -1
- package/src/ui/components/git-panel.tsx +77 -45
- package/src/ui/components/git-view.tsx +15 -4
- 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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brimveyn/aimux",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.1",
|
|
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.1",
|
|
65
65
|
"@opentui/core": "^0.1.90",
|
|
66
66
|
"@opentui/react": "^0.1.90",
|
|
67
67
|
"@xterm/headless": "^6.0.0",
|
package/src/git/git-diff.ts
CHANGED
|
@@ -6,12 +6,14 @@ function resolveStatus(entry: GitFileEntry): { status: DiffFileStatus; oldPath?:
|
|
|
6
6
|
if (entry.renamedFrom) return { oldPath: entry.renamedFrom, status: 'renamed' }
|
|
7
7
|
if (entry.section === 'untracked' || entry.status === '?') return { status: 'new' }
|
|
8
8
|
if (entry.status === 'D') return { status: 'deleted' }
|
|
9
|
-
if (entry.status === 'A' && entry.section === 'staged'
|
|
9
|
+
if (entry.status === 'A' && (entry.section === 'staged' || entry.section === 'historical')) {
|
|
10
|
+
return { status: 'new' }
|
|
11
|
+
}
|
|
10
12
|
return { status: 'modified' }
|
|
11
13
|
}
|
|
12
14
|
|
|
13
|
-
async function isBinary(cwd: string, path: string): Promise<boolean> {
|
|
14
|
-
const result = await $`git -C ${cwd} diff
|
|
15
|
+
async function isBinary(cwd: string, ref: string, path: string): Promise<boolean> {
|
|
16
|
+
const result = await $`git -C ${cwd} diff ${ref} --numstat -- ${path}`.quiet().nothrow()
|
|
15
17
|
if (result.exitCode !== 0) return false
|
|
16
18
|
const text = result.text().trim()
|
|
17
19
|
if (!text) return false
|
|
@@ -19,8 +21,8 @@ async function isBinary(cwd: string, path: string): Promise<boolean> {
|
|
|
19
21
|
return first.startsWith('-\t-\t')
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
async function readHeadSize(cwd: string, path: string): Promise<number> {
|
|
23
|
-
const result = await $`git -C ${cwd} show
|
|
24
|
+
async function readHeadSize(cwd: string, ref: string, path: string): Promise<number> {
|
|
25
|
+
const result = await $`git -C ${cwd} show ${ref}:${path}`.quiet().nothrow()
|
|
24
26
|
if (result.exitCode !== 0) return 0
|
|
25
27
|
return result.text().length
|
|
26
28
|
}
|
|
@@ -33,9 +35,14 @@ async function readWorkingSize(cwd: string, path: string): Promise<number> {
|
|
|
33
35
|
return 0
|
|
34
36
|
}
|
|
35
37
|
|
|
36
|
-
async function rawUnifiedDiff(
|
|
38
|
+
async function rawUnifiedDiff(
|
|
39
|
+
cwd: string,
|
|
40
|
+
ref: string,
|
|
41
|
+
path: string,
|
|
42
|
+
status: DiffFileStatus
|
|
43
|
+
): Promise<string> {
|
|
37
44
|
if (status === 'new') {
|
|
38
|
-
const result = await $`git -C ${cwd} diff
|
|
45
|
+
const result = await $`git -C ${cwd} diff ${ref} --no-color --no-textconv -- ${path}`
|
|
39
46
|
.quiet()
|
|
40
47
|
.nothrow()
|
|
41
48
|
if (result.exitCode === 0 && result.text().length > 0) return result.text()
|
|
@@ -47,19 +54,24 @@ async function rawUnifiedDiff(cwd: string, path: string, status: DiffFileStatus)
|
|
|
47
54
|
}
|
|
48
55
|
|
|
49
56
|
const result =
|
|
50
|
-
await $`git -C ${cwd} diff
|
|
57
|
+
await $`git -C ${cwd} diff ${ref} --unified=99999 --no-color --no-textconv -- ${path}`
|
|
51
58
|
.quiet()
|
|
52
59
|
.nothrow()
|
|
53
60
|
if (result.exitCode !== 0) return ''
|
|
54
61
|
return result.text()
|
|
55
62
|
}
|
|
56
63
|
|
|
57
|
-
export async function fetchDiff(
|
|
64
|
+
export async function fetchDiff(
|
|
65
|
+
cwd: string,
|
|
66
|
+
file: GitFileEntry,
|
|
67
|
+
headOffset: number = 0
|
|
68
|
+
): Promise<DiffData> {
|
|
58
69
|
const { oldPath, status } = resolveStatus(file)
|
|
70
|
+
const ref = headOffset > 0 ? `HEAD~${headOffset}` : 'HEAD'
|
|
59
71
|
|
|
60
|
-
if (await isBinary(cwd, file.path)) {
|
|
72
|
+
if (await isBinary(cwd, ref, file.path)) {
|
|
61
73
|
const [binarySizeBefore, binarySizeAfter] = await Promise.all([
|
|
62
|
-
readHeadSize(cwd, file.path),
|
|
74
|
+
readHeadSize(cwd, ref, file.path),
|
|
63
75
|
readWorkingSize(cwd, file.path),
|
|
64
76
|
])
|
|
65
77
|
return {
|
|
@@ -71,7 +83,7 @@ export async function fetchDiff(cwd: string, file: GitFileEntry): Promise<DiffDa
|
|
|
71
83
|
}
|
|
72
84
|
}
|
|
73
85
|
|
|
74
|
-
const rawDiff = await rawUnifiedDiff(cwd, file.path, status)
|
|
86
|
+
const rawDiff = await rawUnifiedDiff(cwd, ref, file.path, status)
|
|
75
87
|
|
|
76
88
|
const data: DiffData = {
|
|
77
89
|
path: file.path,
|
package/src/git/git-poller.ts
CHANGED
|
@@ -9,9 +9,10 @@ const MAX_INTERVAL_MS = 30_000
|
|
|
9
9
|
interface Options {
|
|
10
10
|
enabled: boolean
|
|
11
11
|
projectPath: string | undefined
|
|
12
|
+
headOffset: number
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
export function useGitPanelPolling({ enabled, projectPath }: Options): void {
|
|
15
|
+
export function useGitPanelPolling({ enabled, headOffset, projectPath }: Options): void {
|
|
15
16
|
useEffect(() => {
|
|
16
17
|
if (!enabled || !projectPath) return undefined
|
|
17
18
|
|
|
@@ -27,11 +28,18 @@ export function useGitPanelPolling({ enabled, projectPath }: Options): void {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
const tick = async () => {
|
|
30
|
-
const result = await collectGitStatus(projectPath)
|
|
31
|
+
const result = await collectGitStatus(projectPath, { headOffset })
|
|
31
32
|
if (cancelled) return
|
|
32
33
|
if (result.kind === 'ok') {
|
|
33
34
|
dispatchGlobal({ payload: result.payload, type: 'git-refresh-success' })
|
|
34
35
|
delay = BASE_INTERVAL_MS
|
|
36
|
+
} else if (result.kind === 'out-of-range') {
|
|
37
|
+
dispatchGlobal({ offset: result.maxOffset, type: 'git-mode-set-head-offset' })
|
|
38
|
+
dispatchGlobal({
|
|
39
|
+
message: `no older commit — clamped to HEAD~${result.maxOffset}`,
|
|
40
|
+
type: 'git-mode-set-message',
|
|
41
|
+
})
|
|
42
|
+
delay = BASE_INTERVAL_MS
|
|
35
43
|
} else {
|
|
36
44
|
dispatchGlobal({ kind: result.error, type: 'git-refresh-error' })
|
|
37
45
|
delay = Math.min(delay * 2, MAX_INTERVAL_MS)
|
|
@@ -45,5 +53,5 @@ export function useGitPanelPolling({ enabled, projectPath }: Options): void {
|
|
|
45
53
|
cancelled = true
|
|
46
54
|
if (timer) clearTimeout(timer)
|
|
47
55
|
}
|
|
48
|
-
}, [enabled, projectPath])
|
|
56
|
+
}, [enabled, projectPath, headOffset])
|
|
49
57
|
}
|
package/src/git/git-status.ts
CHANGED
|
@@ -16,6 +16,7 @@ interface NumstatRow {
|
|
|
16
16
|
export type GitCollectResult =
|
|
17
17
|
| { kind: 'ok'; payload: GitRefreshPayload }
|
|
18
18
|
| { kind: 'error'; error: GitPanelError }
|
|
19
|
+
| { kind: 'out-of-range'; maxOffset: number }
|
|
19
20
|
|
|
20
21
|
const STATUS_CODES = new Set(['M', 'A', 'D', 'R', 'C', 'U', '?'])
|
|
21
22
|
|
|
@@ -189,26 +190,106 @@ async function annotateUntrackedCounts(cwd: string, files: GitFileEntry[]): Prom
|
|
|
189
190
|
}
|
|
190
191
|
}
|
|
191
192
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
193
|
+
interface CollectOptions {
|
|
194
|
+
headOffset?: number
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function parseNameStatus(
|
|
198
|
+
output: string
|
|
199
|
+
): { path: string; status: GitFileStatus; renamedFrom?: string }[] {
|
|
200
|
+
const rows: { path: string; status: GitFileStatus; renamedFrom?: string }[] = []
|
|
201
|
+
for (const raw of output.split('\n')) {
|
|
202
|
+
if (!raw) continue
|
|
203
|
+
const parts = raw.split('\t')
|
|
204
|
+
const code = parts[0] ?? ''
|
|
205
|
+
const letter = code[0] ?? ''
|
|
206
|
+
const status = toStatus(letter)
|
|
207
|
+
if (!status) continue
|
|
208
|
+
if (letter === 'R' || letter === 'C') {
|
|
209
|
+
const from = parts[1]
|
|
210
|
+
const to = parts[2]
|
|
211
|
+
if (!from || !to) continue
|
|
212
|
+
rows.push({ path: to, renamedFrom: from, status })
|
|
213
|
+
} else {
|
|
214
|
+
const path = parts.slice(1).join('\t')
|
|
215
|
+
if (!path) continue
|
|
216
|
+
rows.push({ path, status })
|
|
202
217
|
}
|
|
218
|
+
}
|
|
219
|
+
return rows
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function collectAgainstHead(cwd: string): Promise<GitCollectResult> {
|
|
223
|
+
const [statusResult, unstagedDiff, stagedDiff] = await Promise.all([
|
|
224
|
+
$`git -C ${cwd} status --porcelain=v2 -b -z --untracked-files=all`.quiet().nothrow(),
|
|
225
|
+
$`git -C ${cwd} -c core.quotePath=false diff --numstat`.quiet().nothrow(),
|
|
226
|
+
$`git -C ${cwd} -c core.quotePath=false diff --cached --numstat`.quiet().nothrow(),
|
|
227
|
+
])
|
|
203
228
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
229
|
+
if (statusResult.exitCode !== 0) {
|
|
230
|
+
return { error: 'not-a-repo', kind: 'error' }
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const statusText = statusResult.text()
|
|
234
|
+
const { ahead, behind, branch } = parseBranchLines(statusText)
|
|
235
|
+
const unstagedNumstat = parseNumstat(unstagedDiff.text())
|
|
236
|
+
const stagedNumstat = parseNumstat(stagedDiff.text())
|
|
237
|
+
const files = parsePorcelainEntries(statusText, stagedNumstat, unstagedNumstat)
|
|
238
|
+
await annotateUntrackedCounts(cwd, files)
|
|
210
239
|
|
|
211
|
-
|
|
240
|
+
return { kind: 'ok', payload: { ahead, behind, branch, files } }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function collectAgainstHistorical(
|
|
244
|
+
cwd: string,
|
|
245
|
+
headOffset: number
|
|
246
|
+
): Promise<GitCollectResult> {
|
|
247
|
+
const ref = `HEAD~${headOffset}`
|
|
248
|
+
const revParse = await $`git -C ${cwd} rev-parse ${ref}`.quiet().nothrow()
|
|
249
|
+
if (revParse.exitCode !== 0) {
|
|
250
|
+
const countResult = await $`git -C ${cwd} rev-list --count HEAD`.quiet().nothrow()
|
|
251
|
+
const count = countResult.exitCode === 0 ? Number.parseInt(countResult.text().trim(), 10) : NaN
|
|
252
|
+
const maxOffset = Number.isFinite(count) && count > 0 ? count - 1 : 0
|
|
253
|
+
return { kind: 'out-of-range', maxOffset }
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const [statusResult, nameStatus, numstat, untrackedStatus] = await Promise.all([
|
|
257
|
+
$`git -C ${cwd} status --porcelain=v2 -b -z --untracked-files=all`.quiet().nothrow(),
|
|
258
|
+
$`git -C ${cwd} -c core.quotePath=false diff ${ref} --name-status`.quiet().nothrow(),
|
|
259
|
+
$`git -C ${cwd} -c core.quotePath=false diff ${ref} --numstat`.quiet().nothrow(),
|
|
260
|
+
$`git -C ${cwd} status --porcelain=v2 -z --untracked-files=all`.quiet().nothrow(),
|
|
261
|
+
])
|
|
262
|
+
|
|
263
|
+
if (statusResult.exitCode !== 0) {
|
|
264
|
+
return { error: 'not-a-repo', kind: 'error' }
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const { ahead, behind, branch } = parseBranchLines(statusResult.text())
|
|
268
|
+
const stats = parseNumstat(numstat.text())
|
|
269
|
+
const rows = parseNameStatus(nameStatus.text())
|
|
270
|
+
const files: GitFileEntry[] = rows.map((row) =>
|
|
271
|
+
buildEntry('historical', row.status, row.path, stats, row.renamedFrom)
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
// Untracked files live outside any commit — still surface them so `?` files
|
|
275
|
+
// don't vanish when the user walks history.
|
|
276
|
+
const porcelain = parsePorcelainEntries(untrackedStatus.text(), new Map(), new Map())
|
|
277
|
+
const untracked = porcelain.filter((f) => f.section === 'untracked')
|
|
278
|
+
await annotateUntrackedCounts(cwd, untracked)
|
|
279
|
+
files.push(...untracked)
|
|
280
|
+
|
|
281
|
+
return { kind: 'ok', payload: { ahead, behind, branch, files } }
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export async function collectGitStatus(
|
|
285
|
+
cwd: string,
|
|
286
|
+
options: CollectOptions = {}
|
|
287
|
+
): Promise<GitCollectResult> {
|
|
288
|
+
const headOffset = options.headOffset ?? 0
|
|
289
|
+
try {
|
|
290
|
+
return headOffset > 0
|
|
291
|
+
? await collectAgainstHistorical(cwd, headOffset)
|
|
292
|
+
: await collectAgainstHead(cwd)
|
|
212
293
|
} catch {
|
|
213
294
|
return { error: 'unknown', kind: 'error' }
|
|
214
295
|
}
|
|
@@ -2,7 +2,7 @@ import type { Terminal } from '@xterm/headless'
|
|
|
2
2
|
|
|
3
3
|
import type { TerminalLine, TerminalSnapshot, TerminalSpan } from '../state/types'
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { getCurrentTheme } from '../ui/theme'
|
|
6
6
|
|
|
7
7
|
const ANSI_PALETTE = [
|
|
8
8
|
'#000000',
|
|
@@ -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 ??
|
|
117
|
-
const resolvedBg = bg ??
|
|
116
|
+
const resolvedFg = fg ?? getCurrentTheme().colors['editor.foreground']
|
|
117
|
+
const resolvedBg = bg ?? getCurrentTheme().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 ??
|
|
124
|
-
const resolvedBg = bg ??
|
|
123
|
+
const resolvedFg = fg ?? getCurrentTheme().colors['editor.foreground']
|
|
124
|
+
const resolvedBg = bg ?? getCurrentTheme().colors['editor.background']
|
|
125
125
|
;[fg, bg] = [resolvedBg, resolvedFg]
|
|
126
126
|
}
|
|
127
127
|
|
package/src/state/git-tree.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { GitFileEntry, GitFileListMode, GitFileSection } from './types'
|
|
2
2
|
|
|
3
|
-
const SECTION_ORDER: GitFileSection[] = ['staged', 'unstaged', 'untracked']
|
|
3
|
+
const SECTION_ORDER: GitFileSection[] = ['historical', 'staged', 'unstaged', 'untracked']
|
|
4
4
|
|
|
5
5
|
interface GitTreeNode {
|
|
6
6
|
files: GitFileEntry[]
|
|
@@ -62,6 +62,7 @@ export function emptyGitMode(): GitModeState {
|
|
|
62
62
|
diffs: {},
|
|
63
63
|
diffView: 'split',
|
|
64
64
|
folds: {},
|
|
65
|
+
headOffset: 0,
|
|
65
66
|
loading: {},
|
|
66
67
|
pendingDeletePath: null,
|
|
67
68
|
selectedEntryKey: null,
|
|
@@ -90,7 +91,18 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
90
91
|
}
|
|
91
92
|
case 'exit-git-mode': {
|
|
92
93
|
if (state.focusMode !== 'git') return state
|
|
93
|
-
return {
|
|
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
|
+
}
|
|
94
106
|
}
|
|
95
107
|
case 'git-mode-move-selection': {
|
|
96
108
|
const next = moveGitSelection(
|
|
@@ -266,6 +278,37 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
266
278
|
const next = state.gitMode.diffView === 'split' ? 'stacked' : 'split'
|
|
267
279
|
return { ...state, gitMode: { ...state.gitMode, diffView: next } }
|
|
268
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
|
+
}
|
|
269
312
|
case 'git-mode-fold-adjust': {
|
|
270
313
|
const perPath = state.gitMode.folds[action.key] ?? {}
|
|
271
314
|
const prev = perPath[action.foldId] ?? { bottom: 0, top: 0 }
|
|
@@ -5,7 +5,12 @@ import { reconcileSelectedGitEntryKey } from '../git-tree'
|
|
|
5
5
|
export const GIT_PANEL_MIN_RATIO = 0.2
|
|
6
6
|
export const GIT_PANEL_MAX_RATIO = 0.8
|
|
7
7
|
|
|
8
|
-
const SECTION_RANK: Record<GitFileSection, number> = {
|
|
8
|
+
const SECTION_RANK: Record<GitFileSection, number> = {
|
|
9
|
+
historical: 0,
|
|
10
|
+
staged: 1,
|
|
11
|
+
unstaged: 2,
|
|
12
|
+
untracked: 3,
|
|
13
|
+
}
|
|
9
14
|
|
|
10
15
|
export function sortFilesBySection(files: GitFileEntry[]): GitFileEntry[] {
|
|
11
16
|
return [...files].sort((a, b) => {
|
package/src/state/types.ts
CHANGED
|
@@ -154,7 +154,7 @@ export interface GitPaneState {
|
|
|
154
154
|
|
|
155
155
|
export type GitFileStatus = 'M' | 'A' | 'D' | 'R' | 'C' | 'U' | '?'
|
|
156
156
|
|
|
157
|
-
export type GitFileSection = 'staged' | 'unstaged' | 'untracked'
|
|
157
|
+
export type GitFileSection = 'staged' | 'unstaged' | 'untracked' | 'historical'
|
|
158
158
|
|
|
159
159
|
export interface GitFileEntry {
|
|
160
160
|
path: string
|
|
@@ -204,6 +204,8 @@ export interface GitModeState {
|
|
|
204
204
|
actionMessage: string | null
|
|
205
205
|
diffView: GitDiffView
|
|
206
206
|
folds: Record<string, Record<string, FoldState>>
|
|
207
|
+
/** Working-tree-vs-HEAD~N offset. 0 = working tree vs HEAD (default). */
|
|
208
|
+
headOffset: number
|
|
207
209
|
}
|
|
208
210
|
|
|
209
211
|
interface ModalBase {
|
|
@@ -477,6 +479,8 @@ export type GitModeAction =
|
|
|
477
479
|
| { type: 'git-mode-clear-diff-cache'; path: string }
|
|
478
480
|
| { type: 'git-mode-set-message'; message: string | null }
|
|
479
481
|
| { type: 'git-mode-toggle-diff-view' }
|
|
482
|
+
| { type: 'git-mode-shift-head-offset'; delta: number }
|
|
483
|
+
| { type: 'git-mode-set-head-offset'; offset: number }
|
|
480
484
|
| {
|
|
481
485
|
type: 'git-mode-fold-adjust'
|
|
482
486
|
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)
|
|
@@ -9,7 +9,7 @@ import type { ThemeId } from '../../themes'
|
|
|
9
9
|
import { parsePatchFiles } from '../../../diff-parser'
|
|
10
10
|
import { useAppStore } from '../../../state/app-store'
|
|
11
11
|
import { dispatchGlobal } from '../../../state/dispatch-ref'
|
|
12
|
-
import {
|
|
12
|
+
import { useTheme } from '../../theme'
|
|
13
13
|
import { buildSplitRows, buildUnifiedRows, firstChangeRowOffset, gutterWidth } from './build-rows'
|
|
14
14
|
import { filetypeFromPath } from './filetype'
|
|
15
15
|
import { tokenizeSide } from './highlight'
|
|
@@ -48,6 +48,7 @@ export const PierreDiff = forwardRef<PierreDiffHandle, Props>(function PierreDif
|
|
|
48
48
|
{ cacheKey, diff, path, themeId, view },
|
|
49
49
|
ref
|
|
50
50
|
) {
|
|
51
|
+
const theme = useTheme()
|
|
51
52
|
const file = useMemo(() => {
|
|
52
53
|
const patches = parsePatchFiles(diff)
|
|
53
54
|
return patches[0]?.files[0]
|
|
@@ -13,7 +13,7 @@ import type { DiffHighlights, FoldDispatch } from './pierre-diff'
|
|
|
13
13
|
|
|
14
14
|
import { getScrollViewportDelta } from '../../../app-runtime/terminal-mouse-adapter'
|
|
15
15
|
import { scrollGitDiff } from '../../git-view-controls'
|
|
16
|
-
import {
|
|
16
|
+
import { useTheme } from '../../theme'
|
|
17
17
|
import { buildSplitRows, gutterWidth, type SplitCell, type SplitRowOrHeader } from './build-rows'
|
|
18
18
|
import { FoldStrip } from './fold-strip'
|
|
19
19
|
import { tokenToSpan } from './highlight'
|
|
@@ -43,6 +43,7 @@ export const SplitView = forwardRef<SplitViewHandle, Props>(function SplitView(
|
|
|
43
43
|
{ contentWidth, file, foldDispatch, folds, highlights },
|
|
44
44
|
ref
|
|
45
45
|
) {
|
|
46
|
+
const theme = useTheme()
|
|
46
47
|
const leftRef = useRef<ScrollBoxRenderable | null>(null)
|
|
47
48
|
const rightRef = useRef<ScrollBoxRenderable | null>(null)
|
|
48
49
|
|
|
@@ -132,6 +133,7 @@ function SideRow({
|
|
|
132
133
|
}
|
|
133
134
|
|
|
134
135
|
function HunkHeaderRow({ row }: { row: Extract<SplitRowOrHeader, { type: 'hunk-header' }> }) {
|
|
136
|
+
const theme = useTheme()
|
|
135
137
|
return (
|
|
136
138
|
<box
|
|
137
139
|
flexDirection="row"
|
|
@@ -158,6 +160,7 @@ function HalfRow({
|
|
|
158
160
|
height: number
|
|
159
161
|
tokens: ThemedToken[][]
|
|
160
162
|
}) {
|
|
163
|
+
const theme = useTheme()
|
|
161
164
|
if (cell.type === 'filler') {
|
|
162
165
|
return <box backgroundColor={theme.colors['sideBarSectionHeader.background']} height={height} />
|
|
163
166
|
}
|
|
@@ -185,6 +188,7 @@ function HalfRow({
|
|
|
185
188
|
}
|
|
186
189
|
|
|
187
190
|
function LineContent({ content, tokens }: { content: string; tokens: ThemedToken[] | undefined }) {
|
|
191
|
+
const theme = useTheme()
|
|
188
192
|
if (!tokens || tokens.length === 0) {
|
|
189
193
|
return <text fg={theme.colors['editor.foreground']}>{content}</text>
|
|
190
194
|
}
|
|
@@ -13,7 +13,7 @@ import type { DiffHighlights, FoldDispatch } from './pierre-diff'
|
|
|
13
13
|
|
|
14
14
|
import { getScrollViewportDelta } from '../../../app-runtime/terminal-mouse-adapter'
|
|
15
15
|
import { scrollGitDiff } from '../../git-view-controls'
|
|
16
|
-
import {
|
|
16
|
+
import { useTheme } from '../../theme'
|
|
17
17
|
import { buildUnifiedRows, gutterWidth, type UnifiedRowOrHeader } from './build-rows'
|
|
18
18
|
import { FoldStrip } from './fold-strip'
|
|
19
19
|
import { tokenToSpan } from './highlight'
|
|
@@ -90,6 +90,7 @@ function UnifiedRowRender({
|
|
|
90
90
|
highlights: DiffHighlights
|
|
91
91
|
row: UnifiedRowOrHeader
|
|
92
92
|
}) {
|
|
93
|
+
const theme = useTheme()
|
|
93
94
|
if (row.type === 'hunk-header') {
|
|
94
95
|
return (
|
|
95
96
|
<box
|
|
@@ -144,6 +145,7 @@ function UnifiedRowRender({
|
|
|
144
145
|
}
|
|
145
146
|
|
|
146
147
|
function LineContent({ content, tokens }: { content: string; tokens: ThemedToken[] | undefined }) {
|
|
148
|
+
const theme = useTheme()
|
|
147
149
|
if (!tokens || tokens.length === 0) {
|
|
148
150
|
return <text fg={theme.colors['editor.foreground']}>{content}</text>
|
|
149
151
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useTheme } from '../theme'
|
|
2
2
|
import { uiTokens } from '../ui-tokens'
|
|
3
3
|
import { InputField } from './input-field'
|
|
4
4
|
import { ModalShell } from './modal-shell'
|
|
@@ -11,6 +11,7 @@ interface GitCommitModalProps {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export function GitCommitModal({ activeField, body, cursorPos, title }: GitCommitModalProps) {
|
|
14
|
+
const theme = useTheme()
|
|
14
15
|
const titleActive = activeField === 'title'
|
|
15
16
|
const bodyActive = activeField === 'body'
|
|
16
17
|
|
|
@@ -23,7 +23,7 @@ export const GitPaneWidget = memo(function GitPaneWidget({ pollingEnabled }: Git
|
|
|
23
23
|
: undefined
|
|
24
24
|
const projectPath = currentSession?.projectPath
|
|
25
25
|
|
|
26
|
-
useGitPanelPolling({ enabled: pollingEnabled, projectPath })
|
|
26
|
+
useGitPanelPolling({ enabled: pollingEnabled, headOffset: 0, projectPath })
|
|
27
27
|
|
|
28
28
|
const lastGoodRef = useRef<GitPanelState | null>(null)
|
|
29
29
|
const prevProjectPathRef = useRef(projectPath)
|