@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
@@ -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,40 @@ interface GitPanelProps {
23
23
  selectedEntryKey?: string | null
24
24
  pathConfig?: GitPanePathConfig
25
25
  diffCountConfig?: GitPaneDiffCountConfig
26
+ headOffset?: number
26
27
  }
27
28
 
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
- ]
29
+ function sectionTitle(section: GitFileSection, headOffset: number): string {
30
+ switch (section) {
31
+ case 'historical':
32
+ return `HEAD~${headOffset}`
33
+ case 'staged':
34
+ return 'Staged Changes'
35
+ case 'unstaged':
36
+ return 'Changes'
37
+ case 'untracked':
38
+ return 'Untracked'
39
+ }
40
+ }
41
+
42
+ const BASE_SECTION_ORDER: GitFileSection[] = ['staged', 'unstaged', 'untracked']
43
+ const HISTORICAL_SECTION_ORDER: GitFileSection[] = ['historical', 'untracked']
33
44
 
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'],
45
+ function statusColor(status: GitFileEntry['status']): string {
46
+ const t = getCurrentTheme()
47
+ switch (status) {
48
+ case '?':
49
+ case 'A':
50
+ return t.colors['gitDecoration.addedResourceForeground']
51
+ case 'C':
52
+ case 'R':
53
+ return t.colors['textLink.foreground']
54
+ case 'D':
55
+ case 'U':
56
+ return t.colors['editorError.foreground']
57
+ case 'M':
58
+ return t.colors['editorWarning.foreground']
59
+ }
42
60
  }
43
61
 
44
62
  function displayStatus(file: GitFileEntry): string {
@@ -83,8 +101,10 @@ function renderFileLabel(
83
101
  if (!file.renamedFrom) {
84
102
  return (
85
103
  <text wrapMode="none">
86
- <span fg={theme.colors['editor.foreground']}>{basename}</span>
87
- {showDir ? <span fg={theme.colors['descriptionForeground']}> {dir}</span> : null}
104
+ <span fg={getCurrentTheme().colors['editor.foreground']}>{basename}</span>
105
+ {showDir ? (
106
+ <span fg={getCurrentTheme().colors['descriptionForeground']}> {dir}</span>
107
+ ) : null}
88
108
  </text>
89
109
  )
90
110
  }
@@ -93,13 +113,13 @@ function renderFileLabel(
93
113
  const renamedDir = stripTrailingSlash(renamed.prefix)
94
114
  return (
95
115
  <text wrapMode="none">
96
- <span fg={theme.colors['descriptionForeground']}>{renamed.basename}</span>
116
+ <span fg={getCurrentTheme().colors['descriptionForeground']}>{renamed.basename}</span>
97
117
  {showDir && renamedDir ? (
98
- <span fg={theme.colors['descriptionForeground']}> {renamedDir}</span>
118
+ <span fg={getCurrentTheme().colors['descriptionForeground']}> {renamedDir}</span>
99
119
  ) : 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}
120
+ <span fg={getCurrentTheme().colors['descriptionForeground']}> → </span>
121
+ <span fg={getCurrentTheme().colors['editor.foreground']}>{basename}</span>
122
+ {showDir ? <span fg={getCurrentTheme().colors['descriptionForeground']}> {dir}</span> : null}
103
123
  </text>
104
124
  )
105
125
  }
@@ -115,7 +135,7 @@ function renderDiffCount(
115
135
  if (!diffCountConfig.enabled) return null
116
136
  if (!hasNumstat) {
117
137
  return (
118
- <text fg={theme.colors['descriptionForeground']} bg={bg} flexShrink={0}>
138
+ <text fg={getCurrentTheme().colors['descriptionForeground']} bg={bg} flexShrink={0}>
119
139
 
120
140
  </text>
121
141
  )
@@ -123,14 +143,14 @@ function renderDiffCount(
123
143
  return (
124
144
  <box flexDirection="row" flexShrink={0}>
125
145
  <text
126
- fg={theme.colors['gitDecoration.addedResourceForeground']}
146
+ fg={getCurrentTheme().colors['gitDecoration.addedResourceForeground']}
127
147
  bg={bg}
128
148
  >{`+${padRight(file.added, addedW)}`}</text>
129
- <text fg={theme.colors['editor.lineHighlightBackground']} bg={bg}>
149
+ <text fg={getCurrentTheme().colors['editor.lineHighlightBackground']} bg={bg}>
130
150
  {' '}
131
151
  </text>
132
152
  <text
133
- fg={theme.colors['editorError.foreground']}
153
+ fg={getCurrentTheme().colors['editorError.foreground']}
134
154
  bg={bg}
135
155
  >{`−${padRight(file.removed, removedW)}`}</text>
136
156
  </box>
@@ -138,7 +158,7 @@ function renderDiffCount(
138
158
  }
139
159
 
140
160
  function renderFolderRow(row: GitTreeFolderRow, isSelected: boolean): ReactNode {
141
- const bg = isSelected ? theme.colors['list.activeSelectionBackground'] : undefined
161
+ const bg = isSelected ? getCurrentTheme().colors['list.activeSelectionBackground'] : undefined
142
162
  const onSelect = (): void => {
143
163
  dispatchGlobal({ key: row.key, type: 'git-mode-select-entry-by-key' })
144
164
  }
@@ -149,10 +169,10 @@ function renderFolderRow(row: GitTreeFolderRow, isSelected: boolean): ReactNode
149
169
  <box key={row.key} flexDirection="row" gap={1} backgroundColor={bg} onMouseDown={onSelect}>
150
170
  <box flexGrow={1} overflow="hidden" paddingLeft={row.depth * 2}>
151
171
  <box flexDirection="row" gap={1} onMouseDown={onToggle}>
152
- <text fg={theme.colors['descriptionForeground']} bg={bg}>
172
+ <text fg={getCurrentTheme().colors['descriptionForeground']} bg={bg}>
153
173
  {row.isCollapsed ? '▸' : '▾'}
154
174
  </text>
155
- <text fg={theme.colors['terminal.ansiMagenta']} bg={bg} wrapMode="none">
175
+ <text fg={getCurrentTheme().colors['terminal.ansiMagenta']} bg={bg} wrapMode="none">
156
176
  {row.name}
157
177
  </text>
158
178
  </box>
@@ -172,14 +192,14 @@ function renderFileRow(
172
192
  ): ReactNode {
173
193
  const file = row.file
174
194
  const hasNumstat = file.added !== null || file.removed !== null
175
- const bg = isSelected ? theme.colors['list.activeSelectionBackground'] : undefined
195
+ const bg = isSelected ? getCurrentTheme().colors['list.activeSelectionBackground'] : undefined
176
196
  const onSelect = (): void => {
177
197
  dispatchGlobal({ key: row.key, type: 'git-mode-select-entry-by-key' })
178
198
  }
179
199
  return (
180
200
  <box key={row.key} flexDirection="row" gap={1} backgroundColor={bg} onMouseDown={onSelect}>
181
201
  <box width={2} flexShrink={0} justifyContent="center">
182
- <text fg={STATUS_COLORS[file.status]} bg={bg}>
202
+ <text fg={statusColor(file.status)} bg={bg}>
183
203
  <strong>{displayStatus(file)}</strong>
184
204
  </text>
185
205
  </box>
@@ -213,7 +233,7 @@ function renderTreeSection(
213
233
  return (
214
234
  <box key={section} flexDirection="column">
215
235
  <box flexDirection="row" justifyContent="space-between">
216
- <text fg={theme.colors['terminal.ansiMagenta']}>
236
+ <text fg={getCurrentTheme().colors['terminal.ansiMagenta']}>
217
237
  <strong>
218
238
  {title} ({files.length})
219
239
  </strong>
@@ -223,18 +243,18 @@ function renderTreeSection(
223
243
  <text
224
244
  fg={
225
245
  fileListMode === 'tree'
226
- ? theme.colors['textLink.foreground']
227
- : theme.colors['descriptionForeground']
246
+ ? getCurrentTheme().colors['textLink.foreground']
247
+ : getCurrentTheme().colors['descriptionForeground']
228
248
  }
229
249
  >
230
250
  tree
231
251
  </text>
232
- <text fg={theme.colors['descriptionForeground']}>|</text>
252
+ <text fg={getCurrentTheme().colors['descriptionForeground']}>|</text>
233
253
  <text
234
254
  fg={
235
255
  fileListMode === 'flat'
236
- ? theme.colors['textLink.foreground']
237
- : theme.colors['descriptionForeground']
256
+ ? getCurrentTheme().colors['textLink.foreground']
257
+ : getCurrentTheme().colors['descriptionForeground']
238
258
  }
239
259
  >
240
260
  flat
@@ -279,16 +299,25 @@ function computeStatusPlaceholder(
279
299
  hasProjectPath: boolean
280
300
  ): StatusPlaceholder | null {
281
301
  if (!hasProjectPath) {
282
- return { label: 'No active session', labelColor: theme.colors['descriptionForeground'] }
302
+ return {
303
+ label: 'No active session',
304
+ labelColor: getCurrentTheme().colors['descriptionForeground'],
305
+ }
283
306
  }
284
307
  if (gitPanel.error === 'not-a-repo') {
285
- return { label: 'Not a git repository', labelColor: theme.colors['descriptionForeground'] }
308
+ return {
309
+ label: 'Not a git repository',
310
+ labelColor: getCurrentTheme().colors['descriptionForeground'],
311
+ }
286
312
  }
287
313
  if (gitPanel.error === 'unknown') {
288
- return { label: 'Git error', labelColor: theme.colors['editorError.foreground'] }
314
+ return { label: 'Git error', labelColor: getCurrentTheme().colors['editorError.foreground'] }
289
315
  }
290
316
  if (gitPanel.files.length === 0) {
291
- return { label: 'Working tree clean', labelColor: theme.colors['descriptionForeground'] }
317
+ return {
318
+ label: 'Working tree clean',
319
+ labelColor: getCurrentTheme().colors['descriptionForeground'],
320
+ }
292
321
  }
293
322
  return null
294
323
  }
@@ -301,10 +330,13 @@ export const GitPanel = memo(function GitPanel({
301
330
  diffCountConfig = DEFAULT_DIFF_COUNT_CONFIG,
302
331
  fileListMode = 'tree',
303
332
  gitPanel,
333
+ headOffset = 0,
304
334
  pathConfig = DEFAULT_PATH_CONFIG,
305
335
  projectPath,
306
336
  selectedEntryKey,
307
337
  }: GitPanelProps) {
338
+ const theme = useTheme()
339
+ const sectionOrder = headOffset > 0 ? HISTORICAL_SECTION_ORDER : BASE_SECTION_ORDER
308
340
  const scrollRef = useRef<ScrollBoxRenderable | null>(null)
309
341
  const tree = useMemo(
310
342
  () => buildGitTreeRows(gitPanel.files, collapsedFolders, fileListMode),
@@ -350,18 +382,18 @@ export const GitPanel = memo(function GitPanel({
350
382
  viewportCulling
351
383
  contentOptions={{ flexDirection: 'column', gap: 0 }}
352
384
  >
353
- {SECTION_ORDER.map((s) => {
354
- const section = tree.sections.find((entry) => entry.section === s.key)
385
+ {sectionOrder.map((key) => {
386
+ const section = tree.sections.find((entry) => entry.section === key)
355
387
  return renderTreeSection(
356
- s.key,
357
- s.title,
388
+ key,
389
+ sectionTitle(key, headOffset),
358
390
  section?.files ?? [],
359
391
  section?.rows ?? [],
360
392
  addedW,
361
393
  removedW,
362
394
  fileListMode,
363
395
  selectedEntryKey,
364
- s.key === toggleSection,
396
+ key === toggleSection,
365
397
  pathConfig,
366
398
  diffCountConfig
367
399
  )
@@ -10,7 +10,7 @@ import { useAppStore } from '../../state/app-store'
10
10
  import { dispatchGlobal } from '../../state/dispatch-ref'
11
11
  import { getSelectedGitFile, gitFileKey } from '../../state/git-tree'
12
12
  import { setGitDiffScroller } from '../git-view-controls'
13
- import { theme } from '../theme'
13
+ import { useTheme } from '../theme'
14
14
  import { PierreDiff, type PierreDiffHandle } from './diff-renderer'
15
15
  import { GitPanel } from './git-panel'
16
16
 
@@ -45,6 +45,7 @@ const DiffStage = memo(function DiffStage({
45
45
  themeId,
46
46
  view,
47
47
  }: DiffStageProps) {
48
+ const theme = useTheme()
48
49
  if (loading && !diff) {
49
50
  return (
50
51
  <box flexGrow={1} padding={1}>
@@ -102,6 +103,7 @@ interface GitViewProps {
102
103
  }
103
104
 
104
105
  export const GitView = memo(function GitView({ themeId }: GitViewProps) {
106
+ const theme = useTheme()
105
107
  const dimensions = useTerminalDimensions()
106
108
  const gitPane = useAppStore((s) => s.gitPane)
107
109
  const gitPanel = useAppStore((s) => s.gitPanel)
@@ -116,7 +118,7 @@ export const GitView = memo(function GitView({ themeId }: GitViewProps) {
116
118
  : undefined
117
119
  const projectPath = currentSession?.projectPath
118
120
 
119
- useGitPanelPolling({ enabled: focusMode === 'git', projectPath })
121
+ useGitPanelPolling({ enabled: focusMode === 'git', headOffset: gitMode.headOffset, projectPath })
120
122
 
121
123
  const fileBarWidth = Math.max(20, Math.floor(dimensions.width * gitPane.diffModeRatio))
122
124
 
@@ -151,12 +153,12 @@ export const GitView = memo(function GitView({ themeId }: GitViewProps) {
151
153
  if (!selectedDiffKey) return
152
154
  if (diff || loading) return
153
155
  dispatchGlobal({ key: selectedDiffKey, loading: true, type: 'git-mode-set-loading' })
154
- void fetchDiff(projectPath, selectedFile)
156
+ void fetchDiff(projectPath, selectedFile, gitMode.headOffset)
155
157
  .then((d) => dispatchGlobal({ diff: d, key: selectedDiffKey, type: 'git-mode-set-diff' }))
156
158
  .catch(() =>
157
159
  dispatchGlobal({ key: selectedDiffKey, loading: false, type: 'git-mode-set-loading' })
158
160
  )
159
- }, [focusMode, projectPath, selectedFile, selectedDiffKey, diff, loading])
161
+ }, [focusMode, projectPath, selectedFile, selectedDiffKey, diff, loading, gitMode.headOffset])
160
162
 
161
163
  const pendingPath = gitMode.pendingDeletePath
162
164
  const pendingIsUntracked =
@@ -204,6 +206,14 @@ export const GitView = memo(function GitView({ themeId }: GitViewProps) {
204
206
  <text fg={theme.colors['descriptionForeground']}>{gitPanel.branch}</text>
205
207
  </box>
206
208
  ) : null}
209
+ {gitMode.headOffset > 0 ? (
210
+ <box flexDirection="row" gap={1}>
211
+ <text fg={theme.colors['editorWarning.foreground']}>
212
+ <strong>HEAD~{gitMode.headOffset}</strong>
213
+ </text>
214
+ <text fg={theme.colors['descriptionForeground']}>[ newer · ] older</text>
215
+ </box>
216
+ ) : null}
207
217
  <text fg={theme.colors['editor.lineHighlightBackground']}>
208
218
  {'·'.repeat(Math.max(0, fileBarWidth - 2))}
209
219
  </text>
@@ -211,6 +221,7 @@ export const GitView = memo(function GitView({ themeId }: GitViewProps) {
211
221
  collapsedFolders={gitMode.collapsedFolders}
212
222
  fileListMode={gitPane.fileListMode}
213
223
  gitPanel={gitPanel}
224
+ headOffset={gitMode.headOffset}
214
225
  projectPath={projectPath}
215
226
  selectedEntryKey={gitMode.selectedEntryKey}
216
227
  />
@@ -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 (
@@ -11,7 +11,7 @@ import {
11
11
  type PaneRect,
12
12
  type SplitDirection,
13
13
  } from '../../state/layout-tree'
14
- import { theme } from '../theme'
14
+ import { useTheme } from '../theme'
15
15
  import { TerminalPane } from './terminal-pane'
16
16
 
17
17
  const PANE_CHROME = PANE_BORDER
@@ -58,6 +58,7 @@ export function SplitLayout({
58
58
  onTerminalScrollEvent,
59
59
  tabs,
60
60
  }: SplitLayoutProps) {
61
+ const theme = useTheme()
61
62
  if (node.type === 'leaf') {
62
63
  const tab = tabs.find((t) => t.id === node.tabId)
63
64
  const isActive = node.tabId === activeTabId
@@ -4,21 +4,22 @@ import { version as APP_VERSION } from '../../../package.json'
4
4
  import { useAppStore } from '../../state/app-store'
5
5
  import { useKeymap } from '../keymap-context'
6
6
  import { getStatusBarModel } from '../status-bar-model'
7
- import { theme } from '../theme'
7
+ import { getCurrentTheme, useTheme } from '../theme'
8
8
 
9
9
  function getModeColor(focusMode: AppState['focusMode']): string {
10
+ const t = getCurrentTheme()
10
11
  switch (focusMode) {
11
12
  case 'terminal-input':
12
- return theme.colors['textLink.foreground']
13
+ return t.colors['textLink.foreground']
13
14
  case 'modal':
14
- return theme.colors['editorWarning.foreground']
15
+ return t.colors['editorWarning.foreground']
15
16
  case 'command-edit':
16
- return theme.colors['editorWarning.foreground']
17
+ return t.colors['editorWarning.foreground']
17
18
  case 'git':
18
- return theme.colors['gitDecoration.addedResourceForeground']
19
+ return t.colors['gitDecoration.addedResourceForeground']
19
20
  case 'navigation':
20
21
  default:
21
- return theme.colors['terminal.ansiMagenta']
22
+ return t.colors['terminal.ansiMagenta']
22
23
  }
23
24
  }
24
25
 
@@ -39,6 +40,7 @@ function getModeLabel(focusMode: AppState['focusMode']): string {
39
40
  }
40
41
 
41
42
  export function StatusBar() {
43
+ const theme = useTheme()
42
44
  const state = useAppStore((s) => s)
43
45
  const activeTab = state.tabs.find((tab) => tab.id === state.activeTabId)
44
46
  const config = useKeymap()