@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.
Files changed (46) hide show
  1. package/package.json +2 -2
  2. package/src/git/git-diff.ts +24 -12
  3. package/src/git/git-poller.ts +11 -3
  4. package/src/git/git-status.ts +98 -17
  5. package/src/pty/terminal-snapshot.ts +5 -5
  6. package/src/state/git-tree.ts +1 -1
  7. package/src/state/reducers/git-mode-state.ts +44 -1
  8. package/src/state/reducers/git-panel-state.ts +6 -1
  9. package/src/state/types.ts +5 -1
  10. package/src/ui/components/create-session-modal.tsx +5 -4
  11. package/src/ui/components/diff-renderer/fold-strip.tsx +3 -1
  12. package/src/ui/components/diff-renderer/pierre-diff.tsx +2 -1
  13. package/src/ui/components/diff-renderer/split-view.tsx +5 -1
  14. package/src/ui/components/diff-renderer/stacked-view.tsx +3 -1
  15. package/src/ui/components/git-commit-modal.tsx +2 -1
  16. package/src/ui/components/git-pane-widget.tsx +1 -1
  17. package/src/ui/components/git-panel.tsx +77 -45
  18. package/src/ui/components/git-view.tsx +15 -4
  19. package/src/ui/components/help-modal.tsx +2 -1
  20. package/src/ui/components/input-field.tsx +2 -1
  21. package/src/ui/components/list-item.tsx +2 -1
  22. package/src/ui/components/modal-filter-bar.tsx +2 -1
  23. package/src/ui/components/modal-keybinds-overlay.tsx +2 -1
  24. package/src/ui/components/modal-shell.tsx +2 -1
  25. package/src/ui/components/new-tab-modal.tsx +2 -1
  26. package/src/ui/components/pending-chord-overlay.tsx +2 -1
  27. package/src/ui/components/session-bar.tsx +3 -1
  28. package/src/ui/components/session-picker-modal.tsx +2 -1
  29. package/src/ui/components/sidebar.tsx +8 -5
  30. package/src/ui/components/snippet-editor-modal.tsx +2 -1
  31. package/src/ui/components/snippet-picker-modal.tsx +2 -1
  32. package/src/ui/components/split-layout.tsx +2 -1
  33. package/src/ui/components/status-bar.tsx +8 -6
  34. package/src/ui/components/surface.tsx +6 -6
  35. package/src/ui/components/tab-item.tsx +14 -9
  36. package/src/ui/components/terminal-pane.tsx +7 -5
  37. package/src/ui/components/theme-picker-modal.tsx +2 -1
  38. package/src/ui/components/update-available-modal.tsx +2 -1
  39. package/src/ui/filter-themes.ts +7 -2
  40. package/src/ui/root.tsx +3 -1
  41. package/src/ui/status-bar-model.ts +13 -3
  42. package/src/ui/theme-store.ts +36 -0
  43. package/src/ui/theme.ts +4 -29
  44. package/src/ui/themes.ts +15 -63
  45. package/src/ui/house-themes.ts +0 -313
  46. 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.0",
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.0",
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",
@@ -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') return { status: 'new' }
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 HEAD --numstat -- ${path}`.quiet().nothrow()
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 HEAD:${path}`.quiet().nothrow()
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(cwd: string, path: string, status: DiffFileStatus): Promise<string> {
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 HEAD --no-color --no-textconv -- ${path}`
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 HEAD --unified=99999 --no-color --no-textconv -- ${path}`
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(cwd: string, file: GitFileEntry): Promise<DiffData> {
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,
@@ -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
  }
@@ -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
- export async function collectGitStatus(cwd: string): Promise<GitCollectResult> {
193
- try {
194
- const [statusResult, unstagedDiff, stagedDiff] = await Promise.all([
195
- $`git -C ${cwd} status --porcelain=v2 -b -z --untracked-files=all`.quiet().nothrow(),
196
- $`git -C ${cwd} -c core.quotePath=false diff --numstat`.quiet().nothrow(),
197
- $`git -C ${cwd} -c core.quotePath=false diff --cached --numstat`.quiet().nothrow(),
198
- ])
199
-
200
- if (statusResult.exitCode !== 0) {
201
- return { error: 'not-a-repo', kind: 'error' }
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
- const statusText = statusResult.text()
205
- const { ahead, behind, branch } = parseBranchLines(statusText)
206
- const unstagedNumstat = parseNumstat(unstagedDiff.text())
207
- const stagedNumstat = parseNumstat(stagedDiff.text())
208
- const files = parsePorcelainEntries(statusText, stagedNumstat, unstagedNumstat)
209
- await annotateUntrackedCounts(cwd, files)
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
- return { kind: 'ok', payload: { ahead, behind, branch, files } }
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 { theme } from '../ui/theme'
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 ?? theme.colors['editor.foreground']
117
- const resolvedBg = bg ?? theme.colors['editor.background']
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 ?? theme.colors['editor.foreground']
124
- const resolvedBg = bg ?? theme.colors['editor.background']
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
 
@@ -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 { ...state, focusMode: 'navigation' }
94
+ return {
95
+ ...state,
96
+ focusMode: 'navigation',
97
+ gitMode: {
98
+ ...state.gitMode,
99
+ actionMessage: null,
100
+ diffs: {},
101
+ folds: {},
102
+ headOffset: 0,
103
+ loading: {},
104
+ },
105
+ }
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> = { staged: 0, unstaged: 1, untracked: 2 }
8
+ const SECTION_RANK: Record<GitFileSection, number> = {
9
+ historical: 0,
10
+ staged: 1,
11
+ unstaged: 2,
12
+ untracked: 3,
13
+ }
9
14
 
10
15
  export function sortFilesBySection(files: GitFileEntry[]): GitFileEntry[] {
11
16
  return [...files].sort((a, b) => {
@@ -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 { theme } from '../theme'
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 theme.colors['editorWarning.foreground']
24
+ return getCurrentTheme().colors['editorWarning.foreground']
25
25
  }
26
26
 
27
27
  if (result.type === 'workspace') {
28
- return theme.colors['terminal.ansiMagenta']
28
+ return getCurrentTheme().colors['terminal.ansiMagenta']
29
29
  }
30
30
 
31
- return theme.colors['textLink.foreground']
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 { theme } from '../../theme'
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 { theme } from '../../theme'
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 { theme } from '../../theme'
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 { theme } from '../../theme'
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 { theme } from '../theme'
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)