@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.
Files changed (56) hide show
  1. package/package.json +2 -2
  2. package/src/app-runtime/side-effects.ts +19 -0
  3. package/src/app.tsx +4 -0
  4. package/src/config.ts +19 -1
  5. package/src/git/diff-hash.ts +10 -0
  6. package/src/git/git-diff.ts +24 -12
  7. package/src/git/git-poller.ts +11 -3
  8. package/src/git/git-status.ts +98 -17
  9. package/src/input/modes/types.ts +1 -0
  10. package/src/pty/terminal-snapshot.ts +5 -5
  11. package/src/state/git-tree.ts +43 -17
  12. package/src/state/reducers/diff-cache.ts +64 -0
  13. package/src/state/reducers/git-mode-state.ts +135 -34
  14. package/src/state/reducers/git-panel-state.ts +39 -3
  15. package/src/state/store.ts +2 -0
  16. package/src/state/types.ts +63 -2
  17. package/src/ui/components/create-session-modal.tsx +5 -4
  18. package/src/ui/components/diff-renderer/fold-strip.tsx +3 -1
  19. package/src/ui/components/diff-renderer/pierre-diff.tsx +11 -32
  20. package/src/ui/components/diff-renderer/prepare-diff.ts +66 -0
  21. package/src/ui/components/diff-renderer/split-view.tsx +35 -6
  22. package/src/ui/components/diff-renderer/stacked-view.tsx +32 -5
  23. package/src/ui/components/diff-renderer/use-diff-prefetch.ts +191 -0
  24. package/src/ui/components/diff-renderer/use-diff-preparation.ts +106 -0
  25. package/src/ui/components/git-commit-modal.tsx +2 -1
  26. package/src/ui/components/git-pane-widget.tsx +3 -1
  27. package/src/ui/components/git-panel.tsx +92 -50
  28. package/src/ui/components/git-view.tsx +34 -5
  29. package/src/ui/components/help-modal.tsx +2 -1
  30. package/src/ui/components/input-field.tsx +2 -1
  31. package/src/ui/components/list-item.tsx +2 -1
  32. package/src/ui/components/modal-filter-bar.tsx +2 -1
  33. package/src/ui/components/modal-keybinds-overlay.tsx +2 -1
  34. package/src/ui/components/modal-shell.tsx +2 -1
  35. package/src/ui/components/new-tab-modal.tsx +2 -1
  36. package/src/ui/components/pending-chord-overlay.tsx +2 -1
  37. package/src/ui/components/session-bar.tsx +3 -1
  38. package/src/ui/components/session-picker-modal.tsx +2 -1
  39. package/src/ui/components/sidebar.tsx +8 -5
  40. package/src/ui/components/snippet-editor-modal.tsx +2 -1
  41. package/src/ui/components/snippet-picker-modal.tsx +2 -1
  42. package/src/ui/components/split-layout.tsx +2 -1
  43. package/src/ui/components/status-bar.tsx +8 -6
  44. package/src/ui/components/surface.tsx +6 -6
  45. package/src/ui/components/tab-item.tsx +14 -9
  46. package/src/ui/components/terminal-pane.tsx +7 -5
  47. package/src/ui/components/theme-picker-modal.tsx +2 -1
  48. package/src/ui/components/update-available-modal.tsx +2 -1
  49. package/src/ui/filter-themes.ts +7 -2
  50. package/src/ui/root.tsx +3 -1
  51. package/src/ui/status-bar-model.ts +13 -3
  52. package/src/ui/theme-store.ts +36 -0
  53. package/src/ui/theme.ts +4 -29
  54. package/src/ui/themes.ts +15 -63
  55. package/src/ui/house-themes.ts +0 -313
  56. package/src/ui/themes.generated.ts +0 -59794
@@ -13,7 +13,7 @@ import type {
13
13
 
14
14
  import { dispatchGlobal, runSideEffectGlobal } from '../../state/dispatch-ref'
15
15
  import { buildGitTreeRows, type GitTreeFileRow, type GitTreeFolderRow } from '../../state/git-tree'
16
- import { theme } from '../theme'
16
+ import { getCurrentTheme, useTheme } from '../theme'
17
17
 
18
18
  interface GitPanelProps {
19
19
  collapsedFolders?: Record<string, true>
@@ -23,22 +23,41 @@ interface GitPanelProps {
23
23
  selectedEntryKey?: string | null
24
24
  pathConfig?: GitPanePathConfig
25
25
  diffCountConfig?: GitPaneDiffCountConfig
26
+ headOffset?: number
27
+ compact?: boolean
26
28
  }
27
29
 
28
- const SECTION_ORDER: { key: GitFileSection; title: string }[] = [
29
- { key: 'staged', title: 'Staged Changes' },
30
- { key: 'unstaged', title: 'Changes' },
31
- { key: 'untracked', title: 'Untracked' },
32
- ]
30
+ function sectionTitle(section: GitFileSection, headOffset: number): string {
31
+ switch (section) {
32
+ case 'historical':
33
+ return `HEAD~${headOffset}`
34
+ case 'staged':
35
+ return 'Staged Changes'
36
+ case 'unstaged':
37
+ return 'Changes'
38
+ case 'untracked':
39
+ return 'Untracked'
40
+ }
41
+ }
42
+
43
+ const BASE_SECTION_ORDER: GitFileSection[] = ['staged', 'unstaged', 'untracked']
44
+ const HISTORICAL_SECTION_ORDER: GitFileSection[] = ['historical', 'untracked']
33
45
 
34
- const STATUS_COLORS: Record<GitFileEntry['status'], string> = {
35
- '?': theme.colors['gitDecoration.addedResourceForeground'],
36
- 'A': theme.colors['gitDecoration.addedResourceForeground'],
37
- 'C': theme.colors['textLink.foreground'],
38
- 'D': theme.colors['editorError.foreground'],
39
- 'M': theme.colors['editorWarning.foreground'],
40
- 'R': theme.colors['textLink.foreground'],
41
- 'U': theme.colors['editorError.foreground'],
46
+ function statusColor(status: GitFileEntry['status']): string {
47
+ const t = getCurrentTheme()
48
+ switch (status) {
49
+ case '?':
50
+ case 'A':
51
+ return t.colors['gitDecoration.addedResourceForeground']
52
+ case 'C':
53
+ case 'R':
54
+ return t.colors['textLink.foreground']
55
+ case 'D':
56
+ case 'U':
57
+ return t.colors['editorError.foreground']
58
+ case 'M':
59
+ return t.colors['editorWarning.foreground']
60
+ }
42
61
  }
43
62
 
44
63
  function displayStatus(file: GitFileEntry): string {
@@ -83,8 +102,10 @@ function renderFileLabel(
83
102
  if (!file.renamedFrom) {
84
103
  return (
85
104
  <text wrapMode="none">
86
- <span fg={theme.colors['editor.foreground']}>{basename}</span>
87
- {showDir ? <span fg={theme.colors['descriptionForeground']}> {dir}</span> : null}
105
+ <span fg={getCurrentTheme().colors['editor.foreground']}>{basename}</span>
106
+ {showDir ? (
107
+ <span fg={getCurrentTheme().colors['descriptionForeground']}> {dir}</span>
108
+ ) : null}
88
109
  </text>
89
110
  )
90
111
  }
@@ -93,13 +114,13 @@ function renderFileLabel(
93
114
  const renamedDir = stripTrailingSlash(renamed.prefix)
94
115
  return (
95
116
  <text wrapMode="none">
96
- <span fg={theme.colors['descriptionForeground']}>{renamed.basename}</span>
117
+ <span fg={getCurrentTheme().colors['descriptionForeground']}>{renamed.basename}</span>
97
118
  {showDir && renamedDir ? (
98
- <span fg={theme.colors['descriptionForeground']}> {renamedDir}</span>
119
+ <span fg={getCurrentTheme().colors['descriptionForeground']}> {renamedDir}</span>
99
120
  ) : null}
100
- <span fg={theme.colors['descriptionForeground']}> → </span>
101
- <span fg={theme.colors['editor.foreground']}>{basename}</span>
102
- {showDir ? <span fg={theme.colors['descriptionForeground']}> {dir}</span> : null}
121
+ <span fg={getCurrentTheme().colors['descriptionForeground']}> → </span>
122
+ <span fg={getCurrentTheme().colors['editor.foreground']}>{basename}</span>
123
+ {showDir ? <span fg={getCurrentTheme().colors['descriptionForeground']}> {dir}</span> : null}
103
124
  </text>
104
125
  )
105
126
  }
@@ -115,7 +136,7 @@ function renderDiffCount(
115
136
  if (!diffCountConfig.enabled) return null
116
137
  if (!hasNumstat) {
117
138
  return (
118
- <text fg={theme.colors['descriptionForeground']} bg={bg} flexShrink={0}>
139
+ <text fg={getCurrentTheme().colors['descriptionForeground']} bg={bg} flexShrink={0}>
119
140
 
120
141
  </text>
121
142
  )
@@ -123,14 +144,14 @@ function renderDiffCount(
123
144
  return (
124
145
  <box flexDirection="row" flexShrink={0}>
125
146
  <text
126
- fg={theme.colors['gitDecoration.addedResourceForeground']}
147
+ fg={getCurrentTheme().colors['gitDecoration.addedResourceForeground']}
127
148
  bg={bg}
128
149
  >{`+${padRight(file.added, addedW)}`}</text>
129
- <text fg={theme.colors['editor.lineHighlightBackground']} bg={bg}>
150
+ <text fg={getCurrentTheme().colors['editor.lineHighlightBackground']} bg={bg}>
130
151
  {' '}
131
152
  </text>
132
153
  <text
133
- fg={theme.colors['editorError.foreground']}
154
+ fg={getCurrentTheme().colors['editorError.foreground']}
134
155
  bg={bg}
135
156
  >{`−${padRight(file.removed, removedW)}`}</text>
136
157
  </box>
@@ -138,7 +159,7 @@ function renderDiffCount(
138
159
  }
139
160
 
140
161
  function renderFolderRow(row: GitTreeFolderRow, isSelected: boolean): ReactNode {
141
- const bg = isSelected ? theme.colors['list.activeSelectionBackground'] : undefined
162
+ const bg = isSelected ? getCurrentTheme().colors['list.activeSelectionBackground'] : undefined
142
163
  const onSelect = (): void => {
143
164
  dispatchGlobal({ key: row.key, type: 'git-mode-select-entry-by-key' })
144
165
  }
@@ -149,10 +170,13 @@ function renderFolderRow(row: GitTreeFolderRow, isSelected: boolean): ReactNode
149
170
  <box key={row.key} flexDirection="row" gap={1} backgroundColor={bg} onMouseDown={onSelect}>
150
171
  <box flexGrow={1} overflow="hidden" paddingLeft={row.depth * 2}>
151
172
  <box flexDirection="row" gap={1} onMouseDown={onToggle}>
152
- <text fg={theme.colors['descriptionForeground']} bg={bg}>
173
+ <text fg={getCurrentTheme().colors['descriptionForeground']} bg={bg}>
153
174
  {row.isCollapsed ? '▸' : '▾'}
154
175
  </text>
155
- <text fg={theme.colors['terminal.ansiMagenta']} bg={bg} wrapMode="none">
176
+ <text fg={getCurrentTheme().colors['textLink.foreground']} bg={bg}>
177
+ {row.isCollapsed ? '\uf07b' : '\uf07c'}
178
+ </text>
179
+ <text fg={getCurrentTheme().colors['textLink.foreground']} bg={bg} wrapMode="none">
156
180
  {row.name}
157
181
  </text>
158
182
  </box>
@@ -172,14 +196,14 @@ function renderFileRow(
172
196
  ): ReactNode {
173
197
  const file = row.file
174
198
  const hasNumstat = file.added !== null || file.removed !== null
175
- const bg = isSelected ? theme.colors['list.activeSelectionBackground'] : undefined
199
+ const bg = isSelected ? getCurrentTheme().colors['list.activeSelectionBackground'] : undefined
176
200
  const onSelect = (): void => {
177
201
  dispatchGlobal({ key: row.key, type: 'git-mode-select-entry-by-key' })
178
202
  }
179
203
  return (
180
204
  <box key={row.key} flexDirection="row" gap={1} backgroundColor={bg} onMouseDown={onSelect}>
181
205
  <box width={2} flexShrink={0} justifyContent="center">
182
- <text fg={STATUS_COLORS[file.status]} bg={bg}>
206
+ <text fg={statusColor(file.status)} bg={bg}>
183
207
  <strong>{displayStatus(file)}</strong>
184
208
  </text>
185
209
  </box>
@@ -202,7 +226,8 @@ function renderTreeSection(
202
226
  selectedEntryKey: string | null | undefined,
203
227
  showListModeToggle: boolean,
204
228
  pathConfig: GitPanePathConfig,
205
- diffCountConfig: GitPaneDiffCountConfig
229
+ diffCountConfig: GitPaneDiffCountConfig,
230
+ marginTop: number
206
231
  ): ReactNode {
207
232
  if (files.length === 0) return null
208
233
  const nextFileListMode = fileListMode === 'tree' ? 'flat' : 'tree'
@@ -211,9 +236,9 @@ function renderTreeSection(
211
236
  runSideEffectGlobal({ mode: nextFileListMode, type: 'persist-git-file-list-mode' })
212
237
  }
213
238
  return (
214
- <box key={section} flexDirection="column">
239
+ <box key={section} flexDirection="column" marginTop={marginTop}>
215
240
  <box flexDirection="row" justifyContent="space-between">
216
- <text fg={theme.colors['terminal.ansiMagenta']}>
241
+ <text fg={getCurrentTheme().colors['terminal.ansiMagenta']}>
217
242
  <strong>
218
243
  {title} ({files.length})
219
244
  </strong>
@@ -223,18 +248,18 @@ function renderTreeSection(
223
248
  <text
224
249
  fg={
225
250
  fileListMode === 'tree'
226
- ? theme.colors['textLink.foreground']
227
- : theme.colors['descriptionForeground']
251
+ ? getCurrentTheme().colors['textLink.foreground']
252
+ : getCurrentTheme().colors['descriptionForeground']
228
253
  }
229
254
  >
230
255
  tree
231
256
  </text>
232
- <text fg={theme.colors['descriptionForeground']}>|</text>
257
+ <text fg={getCurrentTheme().colors['descriptionForeground']}>|</text>
233
258
  <text
234
259
  fg={
235
260
  fileListMode === 'flat'
236
- ? theme.colors['textLink.foreground']
237
- : theme.colors['descriptionForeground']
261
+ ? getCurrentTheme().colors['textLink.foreground']
262
+ : getCurrentTheme().colors['descriptionForeground']
238
263
  }
239
264
  >
240
265
  flat
@@ -279,16 +304,25 @@ function computeStatusPlaceholder(
279
304
  hasProjectPath: boolean
280
305
  ): StatusPlaceholder | null {
281
306
  if (!hasProjectPath) {
282
- return { label: 'No active session', labelColor: theme.colors['descriptionForeground'] }
307
+ return {
308
+ label: 'No active session',
309
+ labelColor: getCurrentTheme().colors['descriptionForeground'],
310
+ }
283
311
  }
284
312
  if (gitPanel.error === 'not-a-repo') {
285
- return { label: 'Not a git repository', labelColor: theme.colors['descriptionForeground'] }
313
+ return {
314
+ label: 'Not a git repository',
315
+ labelColor: getCurrentTheme().colors['descriptionForeground'],
316
+ }
286
317
  }
287
318
  if (gitPanel.error === 'unknown') {
288
- return { label: 'Git error', labelColor: theme.colors['editorError.foreground'] }
319
+ return { label: 'Git error', labelColor: getCurrentTheme().colors['editorError.foreground'] }
289
320
  }
290
321
  if (gitPanel.files.length === 0) {
291
- return { label: 'Working tree clean', labelColor: theme.colors['descriptionForeground'] }
322
+ return {
323
+ label: 'Working tree clean',
324
+ labelColor: getCurrentTheme().colors['descriptionForeground'],
325
+ }
292
326
  }
293
327
  return null
294
328
  }
@@ -298,17 +332,21 @@ const DEFAULT_DIFF_COUNT_CONFIG: GitPaneDiffCountConfig = { enabled: true }
298
332
 
299
333
  export const GitPanel = memo(function GitPanel({
300
334
  collapsedFolders = {},
335
+ compact = false,
301
336
  diffCountConfig = DEFAULT_DIFF_COUNT_CONFIG,
302
337
  fileListMode = 'tree',
303
338
  gitPanel,
339
+ headOffset = 0,
304
340
  pathConfig = DEFAULT_PATH_CONFIG,
305
341
  projectPath,
306
342
  selectedEntryKey,
307
343
  }: GitPanelProps) {
344
+ const theme = useTheme()
345
+ const sectionOrder = headOffset > 0 ? HISTORICAL_SECTION_ORDER : BASE_SECTION_ORDER
308
346
  const scrollRef = useRef<ScrollBoxRenderable | null>(null)
309
347
  const tree = useMemo(
310
- () => buildGitTreeRows(gitPanel.files, collapsedFolders, fileListMode),
311
- [collapsedFolders, fileListMode, gitPanel.files]
348
+ () => buildGitTreeRows(gitPanel.files, collapsedFolders, fileListMode, compact),
349
+ [collapsedFolders, compact, fileListMode, gitPanel.files]
312
350
  )
313
351
  const { added: addedW, removed: removedW } = useMemo(
314
352
  () => maxDigitWidth(gitPanel.files),
@@ -350,20 +388,24 @@ export const GitPanel = memo(function GitPanel({
350
388
  viewportCulling
351
389
  contentOptions={{ flexDirection: 'column', gap: 0 }}
352
390
  >
353
- {SECTION_ORDER.map((s) => {
354
- const section = tree.sections.find((entry) => entry.section === s.key)
391
+ {sectionOrder.map((key, idx) => {
392
+ const section = tree.sections.find((entry) => entry.section === key)
393
+ const priorHasFiles = sectionOrder
394
+ .slice(0, idx)
395
+ .some((k) => (tree.sections.find((e) => e.section === k)?.files.length ?? 0) > 0)
355
396
  return renderTreeSection(
356
- s.key,
357
- s.title,
397
+ key,
398
+ sectionTitle(key, headOffset),
358
399
  section?.files ?? [],
359
400
  section?.rows ?? [],
360
401
  addedW,
361
402
  removedW,
362
403
  fileListMode,
363
404
  selectedEntryKey,
364
- s.key === toggleSection,
405
+ key === toggleSection,
365
406
  pathConfig,
366
- diffCountConfig
407
+ diffCountConfig,
408
+ priorHasFiles ? 1 : 0
367
409
  )
368
410
  })}
369
411
  </scrollbox>
@@ -4,14 +4,16 @@ import { memo, useEffect, useRef } from 'react'
4
4
  import type { DiffData, GitDiffView } from '../../state/types'
5
5
  import type { ThemeId } from '../themes'
6
6
 
7
+ import { diffHash } from '../../git/diff-hash'
7
8
  import { fetchDiff } from '../../git/git-diff'
8
9
  import { useGitPanelPolling } from '../../git/git-poller'
9
10
  import { useAppStore } from '../../state/app-store'
10
11
  import { dispatchGlobal } from '../../state/dispatch-ref'
11
12
  import { getSelectedGitFile, gitFileKey } from '../../state/git-tree'
12
13
  import { setGitDiffScroller } from '../git-view-controls'
13
- import { theme } from '../theme'
14
+ import { useTheme } from '../theme'
14
15
  import { PierreDiff, type PierreDiffHandle } from './diff-renderer'
16
+ import { useDiffPrefetch } from './diff-renderer/use-diff-prefetch'
15
17
  import { GitPanel } from './git-panel'
16
18
 
17
19
  interface DiffStageProps {
@@ -45,6 +47,7 @@ const DiffStage = memo(function DiffStage({
45
47
  themeId,
46
48
  view,
47
49
  }: DiffStageProps) {
50
+ const theme = useTheme()
48
51
  if (loading && !diff) {
49
52
  return (
50
53
  <box flexGrow={1} padding={1}>
@@ -102,6 +105,7 @@ interface GitViewProps {
102
105
  }
103
106
 
104
107
  export const GitView = memo(function GitView({ themeId }: GitViewProps) {
108
+ const theme = useTheme()
105
109
  const dimensions = useTerminalDimensions()
106
110
  const gitPane = useAppStore((s) => s.gitPane)
107
111
  const gitPanel = useAppStore((s) => s.gitPanel)
@@ -116,12 +120,13 @@ export const GitView = memo(function GitView({ themeId }: GitViewProps) {
116
120
  : undefined
117
121
  const projectPath = currentSession?.projectPath
118
122
 
119
- useGitPanelPolling({ enabled: focusMode === 'git', projectPath })
123
+ useGitPanelPolling({ enabled: focusMode === 'git', headOffset: gitMode.headOffset, projectPath })
120
124
 
121
125
  const fileBarWidth = Math.max(20, Math.floor(dimensions.width * gitPane.diffModeRatio))
122
126
 
123
127
  const selectedFile = getSelectedGitFile(gitPanel.files, {
124
128
  collapsedFolders: gitMode.collapsedFolders,
129
+ compact: gitPane.treeCompaction,
125
130
  fileListMode: gitPane.fileListMode,
126
131
  selectedEntryKey: gitMode.selectedEntryKey,
127
132
  })
@@ -145,18 +150,32 @@ export const GitView = memo(function GitView({ themeId }: GitViewProps) {
145
150
  return () => setGitDiffScroller(null)
146
151
  }, [])
147
152
 
153
+ useDiffPrefetch(gitMode.selectedEntryKey, gitPane.prefetchRadius, {
154
+ enabled: focusMode === 'git',
155
+ headOffset: gitMode.headOffset,
156
+ projectPath,
157
+ themeId,
158
+ })
159
+
148
160
  useEffect(() => {
149
161
  if (focusMode !== 'git') return
150
162
  if (!selectedFile || !projectPath) return
151
163
  if (!selectedDiffKey) return
152
164
  if (diff || loading) return
153
165
  dispatchGlobal({ key: selectedDiffKey, loading: true, type: 'git-mode-set-loading' })
154
- void fetchDiff(projectPath, selectedFile)
155
- .then((d) => dispatchGlobal({ diff: d, key: selectedDiffKey, type: 'git-mode-set-diff' }))
166
+ void fetchDiff(projectPath, selectedFile, gitMode.headOffset)
167
+ .then((d) =>
168
+ dispatchGlobal({
169
+ diff: d,
170
+ hash: diffHash(d.rawDiff),
171
+ key: selectedDiffKey,
172
+ type: 'git-mode-set-diff',
173
+ })
174
+ )
156
175
  .catch(() =>
157
176
  dispatchGlobal({ key: selectedDiffKey, loading: false, type: 'git-mode-set-loading' })
158
177
  )
159
- }, [focusMode, projectPath, selectedFile, selectedDiffKey, diff, loading])
178
+ }, [focusMode, projectPath, selectedFile, selectedDiffKey, diff, loading, gitMode.headOffset])
160
179
 
161
180
  const pendingPath = gitMode.pendingDeletePath
162
181
  const pendingIsUntracked =
@@ -204,13 +223,23 @@ export const GitView = memo(function GitView({ themeId }: GitViewProps) {
204
223
  <text fg={theme.colors['descriptionForeground']}>{gitPanel.branch}</text>
205
224
  </box>
206
225
  ) : null}
226
+ {gitMode.headOffset > 0 ? (
227
+ <box flexDirection="row" gap={1}>
228
+ <text fg={theme.colors['editorWarning.foreground']}>
229
+ <strong>HEAD~{gitMode.headOffset}</strong>
230
+ </text>
231
+ <text fg={theme.colors['descriptionForeground']}>[ newer · ] older</text>
232
+ </box>
233
+ ) : null}
207
234
  <text fg={theme.colors['editor.lineHighlightBackground']}>
208
235
  {'·'.repeat(Math.max(0, fileBarWidth - 2))}
209
236
  </text>
210
237
  <GitPanel
211
238
  collapsedFolders={gitMode.collapsedFolders}
239
+ compact={gitPane.treeCompaction}
212
240
  fileListMode={gitPane.fileListMode}
213
241
  gitPanel={gitPanel}
242
+ headOffset={gitMode.headOffset}
214
243
  projectPath={projectPath}
215
244
  selectedEntryKey={gitMode.selectedEntryKey}
216
245
  />
@@ -10,7 +10,7 @@ import {
10
10
  } from '../../input/keymap/help-entries'
11
11
  import { dispatchGlobal } from '../../state/dispatch-ref'
12
12
  import { useKeymap } from '../keymap-context'
13
- import { theme } from '../theme'
13
+ import { useTheme } from '../theme'
14
14
  import { uiTokens } from '../ui-tokens'
15
15
  import { ModalFilterBar } from './modal-filter-bar'
16
16
  import { ModalShell } from './modal-shell'
@@ -86,6 +86,7 @@ function computeWindowStart(
86
86
  }
87
87
 
88
88
  export function HelpModal({ filter, scope, selectedIndex }: HelpModalProps) {
89
+ const theme = useTheme()
89
90
  const config = useKeymap()
90
91
  const dimensions = useTerminalDimensions()
91
92
  const allEntries = useMemo(() => collectHelpEntries(config), [config])
@@ -1,4 +1,4 @@
1
- import { theme } from '../theme'
1
+ import { useTheme } from '../theme'
2
2
  import { Surface } from './surface'
3
3
 
4
4
  interface InputFieldProps {
@@ -8,6 +8,7 @@ interface InputFieldProps {
8
8
  }
9
9
 
10
10
  export function InputField({ active, cursorPos, value }: InputFieldProps) {
11
+ const theme = useTheme()
11
12
  const fg = active ? theme.colors['editor.foreground'] : theme.colors['descriptionForeground']
12
13
  if (!active) {
13
14
  return (
@@ -1,6 +1,6 @@
1
1
  import type { ReactNode } from 'react'
2
2
 
3
- import { theme } from '../theme'
3
+ import { useTheme } from '../theme'
4
4
  import { Surface } from './surface'
5
5
 
6
6
  export type ListItemDirection = 'row' | 'column'
@@ -22,6 +22,7 @@ export function ListItem({
22
22
  title,
23
23
  trailing,
24
24
  }: ListItemProps) {
25
+ const theme = useTheme()
25
26
  const isRow = direction === 'row'
26
27
  return (
27
28
  <Surface
@@ -1,4 +1,4 @@
1
- import { theme } from '../theme'
1
+ import { useTheme } from '../theme'
2
2
  import { Surface } from './surface'
3
3
 
4
4
  interface ModalFilterBarProps {
@@ -6,6 +6,7 @@ interface ModalFilterBarProps {
6
6
  }
7
7
 
8
8
  export function ModalFilterBar({ filter }: ModalFilterBarProps) {
9
+ const theme = useTheme()
9
10
  if (filter === null) {
10
11
  return null
11
12
  }
@@ -2,7 +2,7 @@ import type { ModeId } from '@brimveyn/aimux-config'
2
2
 
3
3
  import { describeBindings } from '../../input/keymap/describe-bindings'
4
4
  import { useKeymap } from '../keymap-context'
5
- import { theme } from '../theme'
5
+ import { useTheme } from '../theme'
6
6
  import { Surface } from './surface'
7
7
 
8
8
  const KEYS_COLUMN_WIDTH = 12
@@ -13,6 +13,7 @@ interface ModalKeybindsOverlayProps {
13
13
  }
14
14
 
15
15
  export function ModalKeybindsOverlay({ limit, modeId }: ModalKeybindsOverlayProps) {
16
+ const theme = useTheme()
16
17
  const config = useKeymap()
17
18
  const bindings = describeBindings(config, modeId, {
18
19
  dedupeByDescription: true,
@@ -1,7 +1,7 @@
1
1
  import type { ModeId } from '@brimveyn/aimux-config'
2
2
  import type { ReactNode } from 'react'
3
3
 
4
- import { theme } from '../theme'
4
+ import { useTheme } from '../theme'
5
5
  import { ModalKeybindsOverlay } from './modal-keybinds-overlay'
6
6
  import { Surface } from './surface'
7
7
 
@@ -24,6 +24,7 @@ export function ModalShell({
24
24
  title,
25
25
  width,
26
26
  }: ModalShellProps) {
27
+ const theme = useTheme()
27
28
  return (
28
29
  <box
29
30
  position="absolute"
@@ -1,5 +1,5 @@
1
1
  import { getAllAssistantOptions } from '../../pty/command-registry'
2
- import { theme } from '../theme'
2
+ import { useTheme } from '../theme'
3
3
  import { uiTokens } from '../ui-tokens'
4
4
  import { InputField } from './input-field'
5
5
  import { ListItem } from './list-item'
@@ -12,6 +12,7 @@ interface NewTabModalProps {
12
12
  }
13
13
 
14
14
  export function NewTabModal({ customCommands, editBuffer, selectedIndex }: NewTabModalProps) {
15
+ const theme = useTheme()
15
16
  const options = getAllAssistantOptions(customCommands)
16
17
  const selectedOption = options[selectedIndex]
17
18
  const isEditing = editBuffer !== null
@@ -2,10 +2,11 @@ import { parseKeyNotation } from '../../input/keymap/key-chord'
2
2
  import { formatChord } from '../../input/keymap/key-format'
3
3
  import { useAppStore } from '../../state/app-store'
4
4
  import { useKeymap } from '../keymap-context'
5
- import { theme } from '../theme'
5
+ import { useTheme } from '../theme'
6
6
  import { Surface } from './surface'
7
7
 
8
8
  export function PendingChordOverlay() {
9
+ const theme = useTheme()
9
10
  const pendingChords = useAppStore((s) => s.pendingChords)
10
11
  const modalOpen = useAppStore((s) => s.modal.type !== null)
11
12
  const config = useKeymap()
@@ -8,9 +8,10 @@ import { useAppStore } from '../../state/app-store'
8
8
  import { dispatchGlobal, runSideEffectGlobal } from '../../state/dispatch-ref'
9
9
  import { useBusySpinner } from '../hooks/use-busy-spinner'
10
10
  import { moveIdToIdPosition, orderSessionsForDisplay } from '../session-ordering'
11
- import { theme } from '../theme'
11
+ import { useTheme } from '../theme'
12
12
 
13
13
  export function SessionBar() {
14
+ const theme = useTheme()
14
15
  const sessions = useAppStore((s) => s.sessions)
15
16
  const currentId = useAppStore((s) => s.currentSessionId)
16
17
  const bar = useAppStore((s) => s.sessionBar)
@@ -165,6 +166,7 @@ function SessionChip({
165
166
  onRef,
166
167
  session,
167
168
  }: SessionChipProps) {
169
+ const theme = useTheme()
168
170
  const showSpinner = busy && !active
169
171
  const spinner = useBusySpinner(showSpinner)
170
172
  const indicator = showSpinner ? spinner : '●'
@@ -3,7 +3,7 @@ import type { SessionRecord } from '../../state/types'
3
3
  import { filterSessions } from '../../state/selectors'
4
4
  import { abbreviatePath } from '../path-format'
5
5
  import { orderSessionsForDisplay } from '../session-ordering'
6
- import { theme } from '../theme'
6
+ import { useTheme } from '../theme'
7
7
  import { uiTokens } from '../ui-tokens'
8
8
  import { ListItem } from './list-item'
9
9
  import { ModalFilterBar } from './modal-filter-bar'
@@ -45,6 +45,7 @@ export function SessionPickerModal({
45
45
  selectedIndex,
46
46
  sessions,
47
47
  }: SessionPickerModalProps) {
48
+ const theme = useTheme()
48
49
  const ordered = orderSessionsForDisplay(sessions)
49
50
  const baselineOrder = ordered.map((s) => s.id)
50
51
  const filtered = filterSessions(ordered, filter)
@@ -3,7 +3,7 @@ import type { ScrollBoxRenderable } from '@opentui/core'
3
3
  import { memo, useMemo, useRef } from 'react'
4
4
 
5
5
  import { useAppStore } from '../../state/app-store'
6
- import { theme } from '../theme'
6
+ import { getCurrentTheme, useTheme } from '../theme'
7
7
  import { GitPaneWidget } from './git-pane-widget'
8
8
  import { buildTabGroupInfo } from './sidebar-group-metadata'
9
9
  import { TabItem } from './tab-item'
@@ -20,6 +20,7 @@ const GUTTER_END = '╰'
20
20
  const GUTTER_PAD = '│'
21
21
 
22
22
  const SidebarTop = memo(function SidebarTop() {
23
+ const theme = useTheme()
23
24
  const sidebarWidth = useAppStore((s) => s.sidebar.width)
24
25
  const currentSessionId = useAppStore((s) => s.currentSessionId)
25
26
  const sessions = useAppStore((s) => s.sessions)
@@ -59,15 +60,15 @@ function renderGroupGutter(
59
60
  return (
60
61
  <box flexDirection="column" width={1} overflow="hidden">
61
62
  <text
62
- fg={theme.colors['terminal.ansiMagenta']}
63
- bg={isActive ? theme.colors['list.activeSelectionBackground'] : undefined}
63
+ fg={getCurrentTheme().colors['terminal.ansiMagenta']}
64
+ bg={isActive ? getCurrentTheme().colors['list.activeSelectionBackground'] : undefined}
64
65
  >
65
66
  {/* oxlint-disable-next-line no-nested-ternary */}
66
67
  {isGroupStart ? GUTTER_START : isGroupMiddle ? GUTTER_MIDDLE : GUTTER_PAD}
67
68
  </text>
68
69
  <text
69
- fg={theme.colors['terminal.ansiMagenta']}
70
- bg={isActive ? theme.colors['list.activeSelectionBackground'] : undefined}
70
+ fg={getCurrentTheme().colors['terminal.ansiMagenta']}
71
+ bg={isActive ? getCurrentTheme().colors['list.activeSelectionBackground'] : undefined}
71
72
  >
72
73
  {isGroupEnd ? GUTTER_END : GUTTER_PAD}
73
74
  </text>
@@ -80,6 +81,7 @@ interface TabsBodyProps {
80
81
  }
81
82
 
82
83
  const TabsBody = memo(function TabsBody({ onTabActivate }: TabsBodyProps) {
84
+ const theme = useTheme()
83
85
  const tabs = useAppStore((s) => s.tabs)
84
86
  const activeTabId = useAppStore((s) => s.activeTabId)
85
87
  const focusMode = useAppStore((s) => s.focusMode)
@@ -149,6 +151,7 @@ const TabsBody = memo(function TabsBody({ onTabActivate }: TabsBodyProps) {
149
151
  })
150
152
 
151
153
  export function Sidebar({ onTabActivate }: SidebarProps) {
154
+ const theme = useTheme()
152
155
  const sidebarVisible = useAppStore((s) => s.sidebar.visible)
153
156
  const sidebarWidth = useAppStore((s) => s.sidebar.width)
154
157
  const gitPane = useAppStore((s) => s.gitPane)
@@ -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'
@@ -16,6 +16,7 @@ export function SnippetEditorModal({
16
16
  snippetContent,
17
17
  snippetName,
18
18
  }: SnippetEditorModalProps) {
19
+ const theme = useTheme()
19
20
  const nameActive = activeField === 'name'
20
21
  const contentActive = activeField === 'content'
21
22
 
@@ -1,7 +1,7 @@
1
1
  import type { SnippetRecord } from '../../state/types'
2
2
 
3
3
  import { filterSnippets } from '../../state/selectors'
4
- import { theme } from '../theme'
4
+ import { useTheme } from '../theme'
5
5
  import { uiTokens } from '../ui-tokens'
6
6
  import { ListItem } from './list-item'
7
7
  import { ModalFilterBar } from './modal-filter-bar'
@@ -22,6 +22,7 @@ function truncateContent(content: string): string {
22
22
  }
23
23
 
24
24
  export function SnippetPickerModal({ filter, selectedIndex, snippets }: SnippetPickerModalProps) {
25
+ const theme = useTheme()
25
26
  const filtered = filterSnippets(snippets, filter)
26
27
 
27
28
  return (