@brimveyn/aimux 1.5.4 → 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 (67) hide show
  1. package/README.md +14 -2
  2. package/package.json +5 -3
  3. package/src/app-runtime/side-effects.ts +41 -6
  4. package/src/app.tsx +17 -5
  5. package/src/config.ts +29 -4
  6. package/src/diff-parser/clean-last-newline.ts +5 -0
  7. package/src/diff-parser/constants.ts +13 -0
  8. package/src/diff-parser/index.ts +12 -0
  9. package/src/diff-parser/parse-line-type.ts +29 -0
  10. package/src/diff-parser/parse-patch-files.ts +369 -0
  11. package/src/diff-parser/types.ts +75 -0
  12. package/src/git/git-diff.ts +24 -12
  13. package/src/git/git-poller.ts +11 -3
  14. package/src/git/git-status.ts +98 -17
  15. package/src/input/modes/bridge.ts +8 -0
  16. package/src/input/modes/transitions.ts +2 -1
  17. package/src/input/modes/types.ts +4 -1
  18. package/src/pty/terminal-snapshot.ts +5 -5
  19. package/src/state/git-tree.ts +220 -0
  20. package/src/state/reducers/git-mode-state.ts +276 -36
  21. package/src/state/reducers/git-panel-state.ts +35 -4
  22. package/src/state/reducers/modal-state.ts +43 -10
  23. package/src/state/store.ts +5 -1
  24. package/src/state/types.ts +46 -6
  25. package/src/state/workspace-save.ts +2 -0
  26. package/src/ui/components/create-session-modal.tsx +25 -8
  27. package/src/ui/components/diff-renderer/build-rows.ts +349 -0
  28. package/src/ui/components/diff-renderer/filetype.ts +29 -0
  29. package/src/ui/components/diff-renderer/fold-strip.tsx +86 -0
  30. package/src/ui/components/diff-renderer/highlight.ts +48 -0
  31. package/src/ui/components/diff-renderer/index.ts +1 -0
  32. package/src/ui/components/diff-renderer/pierre-diff.tsx +171 -0
  33. package/src/ui/components/diff-renderer/split-view.tsx +211 -0
  34. package/src/ui/components/diff-renderer/stacked-view.tsx +168 -0
  35. package/src/ui/components/git-commit-modal.tsx +16 -3
  36. package/src/ui/components/git-pane-widget.tsx +6 -1
  37. package/src/ui/components/git-panel.tsx +215 -86
  38. package/src/ui/components/git-view.tsx +103 -90
  39. package/src/ui/components/help-modal.tsx +45 -12
  40. package/src/ui/components/input-field.tsx +4 -3
  41. package/src/ui/components/list-item.tsx +11 -2
  42. package/src/ui/components/modal-filter-bar.tsx +3 -2
  43. package/src/ui/components/modal-keybinds-overlay.tsx +4 -3
  44. package/src/ui/components/modal-shell.tsx +5 -4
  45. package/src/ui/components/new-tab-modal.tsx +17 -4
  46. package/src/ui/components/pending-chord-overlay.tsx +5 -4
  47. package/src/ui/components/session-bar.tsx +13 -6
  48. package/src/ui/components/session-picker-modal.tsx +28 -10
  49. package/src/ui/components/sidebar.tsx +26 -11
  50. package/src/ui/components/snippet-editor-modal.tsx +18 -3
  51. package/src/ui/components/snippet-picker-modal.tsx +13 -4
  52. package/src/ui/components/split-layout.tsx +3 -2
  53. package/src/ui/components/status-bar.tsx +15 -10
  54. package/src/ui/components/surface.tsx +6 -6
  55. package/src/ui/components/tab-item.tsx +28 -17
  56. package/src/ui/components/terminal-pane.tsx +28 -16
  57. package/src/ui/components/theme-picker-modal.tsx +113 -17
  58. package/src/ui/components/update-available-modal.tsx +13 -2
  59. package/src/ui/filter-themes.ts +15 -0
  60. package/src/ui/keymap-context.ts +10 -2
  61. package/src/ui/root.tsx +29 -7
  62. package/src/ui/shiki.ts +49 -0
  63. package/src/ui/status-bar-model.ts +32 -4
  64. package/src/ui/theme-store.ts +36 -0
  65. package/src/ui/theme.ts +4 -17
  66. package/src/ui/themes.ts +23 -277
  67. package/src/ui/syntax.ts +0 -102
@@ -1,51 +1,26 @@
1
- import type { DiffRenderable } from '@opentui/core'
2
-
1
+ import { useTerminalDimensions } from '@opentui/react'
3
2
  import { memo, useEffect, useRef } from 'react'
4
3
 
5
- import type { DiffData } from '../../state/types'
4
+ import type { DiffData, GitDiffView } from '../../state/types'
5
+ import type { ThemeId } from '../themes'
6
6
 
7
7
  import { fetchDiff } from '../../git/git-diff'
8
8
  import { useGitPanelPolling } from '../../git/git-poller'
9
9
  import { useAppStore } from '../../state/app-store'
10
10
  import { dispatchGlobal } from '../../state/dispatch-ref'
11
+ import { getSelectedGitFile, gitFileKey } from '../../state/git-tree'
11
12
  import { setGitDiffScroller } from '../git-view-controls'
12
- import { getSyntaxClient, getSyntaxStyle } from '../syntax'
13
- import { theme } from '../theme'
14
- import { fileKey, GitPanel } from './git-panel'
15
-
16
- interface CodePaneLike {
17
- scrollY: number
18
- maxScrollY: number
19
- }
20
-
21
- interface DiffRenderableInternals {
22
- leftCodeRenderable?: CodePaneLike
23
- rightCodeRenderable?: CodePaneLike
24
- }
25
-
26
- function filetypeFromPath(path: string): string | undefined {
27
- const dot = path.lastIndexOf('.')
28
- if (dot < 0) return undefined
29
- const ext = path.slice(dot + 1).toLowerCase()
30
- const map: Record<string, string> = {
31
- cjs: 'javascript',
32
- js: 'javascript',
33
- jsx: 'javascript',
34
- markdown: 'markdown',
35
- md: 'markdown',
36
- mdx: 'markdown',
37
- mjs: 'javascript',
38
- ts: 'typescript',
39
- tsx: 'typescript',
40
- zig: 'zig',
41
- }
42
- return map[ext]
43
- }
13
+ import { useTheme } from '../theme'
14
+ import { PierreDiff, type PierreDiffHandle } from './diff-renderer'
15
+ import { GitPanel } from './git-panel'
44
16
 
45
17
  interface DiffStageProps {
46
18
  diff: DiffData | undefined
19
+ diffKey: string | null
47
20
  loading: boolean
48
- diffRef: React.RefObject<DiffRenderable | null>
21
+ diffRef: React.RefObject<PierreDiffHandle | null>
22
+ themeId: ThemeId
23
+ view: GitDiffView
49
24
  }
50
25
 
51
26
  function placeholderText(diff: DiffData): string | null {
@@ -62,25 +37,33 @@ function placeholderText(diff: DiffData): string | null {
62
37
  return null
63
38
  }
64
39
 
65
- const DiffStage = memo(function DiffStage({ diff, diffRef, loading }: DiffStageProps) {
40
+ const DiffStage = memo(function DiffStage({
41
+ diff,
42
+ diffKey,
43
+ diffRef,
44
+ loading,
45
+ themeId,
46
+ view,
47
+ }: DiffStageProps) {
48
+ const theme = useTheme()
66
49
  if (loading && !diff) {
67
50
  return (
68
51
  <box flexGrow={1} padding={1}>
69
- <text fg={theme.textMuted}>Loading diff…</text>
52
+ <text fg={theme.colors['descriptionForeground']}>Loading diff…</text>
70
53
  </box>
71
54
  )
72
55
  }
73
56
  if (!diff) {
74
57
  return (
75
58
  <box flexGrow={1} padding={1}>
76
- <text fg={theme.textMuted}>Select a file.</text>
59
+ <text fg={theme.colors['descriptionForeground']}>Select a file.</text>
77
60
  </box>
78
61
  )
79
62
  }
80
63
  if (diff.errorMessage) {
81
64
  return (
82
65
  <box flexGrow={1} padding={1}>
83
- <text fg={theme.danger}>{diff.errorMessage}</text>
66
+ <text fg={theme.colors['editorError.foreground']}>{diff.errorMessage}</text>
84
67
  </box>
85
68
  )
86
69
  }
@@ -89,74 +72,77 @@ const DiffStage = memo(function DiffStage({ diff, diffRef, loading }: DiffStageP
89
72
  if (placeholder) {
90
73
  return (
91
74
  <box flexGrow={1} padding={1}>
92
- <text fg={theme.textMuted}>{placeholder}</text>
75
+ <text fg={theme.colors['descriptionForeground']}>{placeholder}</text>
93
76
  </box>
94
77
  )
95
78
  }
96
79
 
97
- const filetype = filetypeFromPath(diff.path)
98
-
99
80
  return (
100
81
  <box flexDirection="column" flexGrow={1} overflow="hidden">
101
82
  {diff.oldPath ? (
102
83
  <box paddingLeft={1} paddingRight={1}>
103
- <text fg={theme.textMuted}>
84
+ <text fg={theme.colors['descriptionForeground']}>
104
85
  renamed: {diff.oldPath} → {diff.path}
105
86
  </text>
106
87
  </box>
107
88
  ) : null}
108
- <diff
89
+ <PierreDiff
90
+ cacheKey={diffKey ?? diff.path}
109
91
  ref={diffRef}
110
92
  diff={diff.rawDiff}
111
- view="split"
112
- syncScroll
113
- showLineNumbers
114
- wrapMode="none"
115
- filetype={filetype}
116
- treeSitterClient={filetype ? getSyntaxClient() : undefined}
117
- syntaxStyle={filetype ? getSyntaxStyle() : undefined}
118
- addedBg={theme.diffAddBg}
119
- removedBg={theme.diffRemoveBg}
120
- addedSignColor={theme.success}
121
- removedSignColor={theme.danger}
122
- flexGrow={1}
93
+ path={diff.path}
94
+ themeId={themeId}
95
+ view={view}
123
96
  />
124
97
  </box>
125
98
  )
126
99
  })
127
100
 
128
- export const GitView = memo(function GitView() {
129
- const sidebarWidth = useAppStore((s) => s.sidebar.width)
101
+ interface GitViewProps {
102
+ themeId: ThemeId
103
+ }
104
+
105
+ export const GitView = memo(function GitView({ themeId }: GitViewProps) {
106
+ const theme = useTheme()
107
+ const dimensions = useTerminalDimensions()
108
+ const gitPane = useAppStore((s) => s.gitPane)
130
109
  const gitPanel = useAppStore((s) => s.gitPanel)
131
110
  const gitMode = useAppStore((s) => s.gitMode)
132
111
  const currentSessionId = useAppStore((s) => s.currentSessionId)
133
112
  const sessions = useAppStore((s) => s.sessions)
134
113
  const focusMode = useAppStore((s) => s.focusMode)
135
- const diffRef = useRef<DiffRenderable | null>(null)
114
+ const diffRef = useRef<PierreDiffHandle | null>(null)
136
115
 
137
116
  const currentSession = currentSessionId
138
117
  ? sessions.find((s) => s.id === currentSessionId)
139
118
  : undefined
140
119
  const projectPath = currentSession?.projectPath
141
120
 
142
- useGitPanelPolling({ enabled: focusMode === 'git', projectPath })
121
+ useGitPanelPolling({ enabled: focusMode === 'git', headOffset: gitMode.headOffset, projectPath })
122
+
123
+ const fileBarWidth = Math.max(20, Math.floor(dimensions.width * gitPane.diffModeRatio))
143
124
 
144
- const selectedFile = gitPanel.files[gitMode.selectedFileIndex]
145
- const diff = selectedFile ? gitMode.diffs[selectedFile.path] : undefined
146
- const loading = selectedFile ? !!gitMode.loading[selectedFile.path] : false
125
+ const selectedFile = getSelectedGitFile(gitPanel.files, {
126
+ collapsedFolders: gitMode.collapsedFolders,
127
+ fileListMode: gitPane.fileListMode,
128
+ selectedEntryKey: gitMode.selectedEntryKey,
129
+ })
130
+ const selectedDiffKey = selectedFile ? gitFileKey(selectedFile) : null
131
+ const diff = selectedDiffKey ? gitMode.diffs[selectedDiffKey] : undefined
132
+ const loading = selectedDiffKey ? !!gitMode.loading[selectedDiffKey] : false
147
133
 
148
134
  useEffect(() => {
149
135
  setGitDiffScroller((delta: number) => {
150
- const node = diffRef.current as unknown as DiffRenderableInternals | null
136
+ const node = diffRef.current
151
137
  if (!node) return
152
- const left = node.leftCodeRenderable
153
- const right = node.rightCodeRenderable
154
- if (!left || !right) return
155
- const cap = Math.max(left.maxScrollY, right.maxScrollY)
156
- const base = left.scrollY
138
+ const left = node.leftScroll
139
+ const right = node.rightScroll
140
+ if (!left) return
141
+ const cap = Math.max(left.scrollHeight - left.viewport.height, 0)
142
+ const base = left.scrollTop
157
143
  const nextScroll = Math.max(0, Math.min(cap, base + delta))
158
- left.scrollY = nextScroll
159
- right.scrollY = nextScroll
144
+ left.scrollTop = nextScroll
145
+ if (right && right !== left) right.scrollTop = nextScroll
160
146
  })
161
147
  return () => setGitDiffScroller(null)
162
148
  }, [])
@@ -164,13 +150,15 @@ export const GitView = memo(function GitView() {
164
150
  useEffect(() => {
165
151
  if (focusMode !== 'git') return
166
152
  if (!selectedFile || !projectPath) return
167
- const path = selectedFile.path
153
+ if (!selectedDiffKey) return
168
154
  if (diff || loading) return
169
- dispatchGlobal({ loading: true, path, type: 'git-mode-set-loading' })
170
- void fetchDiff(projectPath, selectedFile)
171
- .then((d) => dispatchGlobal({ diff: d, path, type: 'git-mode-set-diff' }))
172
- .catch(() => dispatchGlobal({ loading: false, path, type: 'git-mode-set-loading' }))
173
- }, [focusMode, projectPath, selectedFile, diff, loading])
155
+ dispatchGlobal({ key: selectedDiffKey, loading: true, type: 'git-mode-set-loading' })
156
+ void fetchDiff(projectPath, selectedFile, gitMode.headOffset)
157
+ .then((d) => dispatchGlobal({ diff: d, key: selectedDiffKey, type: 'git-mode-set-diff' }))
158
+ .catch(() =>
159
+ dispatchGlobal({ key: selectedDiffKey, loading: false, type: 'git-mode-set-loading' })
160
+ )
161
+ }, [focusMode, projectPath, selectedFile, selectedDiffKey, diff, loading, gitMode.headOffset])
174
162
 
175
163
  const pendingPath = gitMode.pendingDeletePath
176
164
  const pendingIsUntracked =
@@ -178,7 +166,7 @@ export const GitView = memo(function GitView() {
178
166
  selectedFile?.path === pendingPath &&
179
167
  selectedFile.section === 'untracked'
180
168
  let pendingHint: string | null = null
181
- if (pendingPath !== null) {
169
+ if (pendingPath !== null && selectedFile) {
182
170
  pendingHint = pendingIsUntracked
183
171
  ? 'press d again to delete file'
184
172
  : 'press d again to discard changes'
@@ -187,13 +175,13 @@ export const GitView = memo(function GitView() {
187
175
  let footerNode: React.ReactNode = null
188
176
  if (pendingHint) {
189
177
  footerNode = (
190
- <text fg={theme.warning}>
178
+ <text fg={theme.colors['editorWarning.foreground']}>
191
179
  <strong>{pendingHint}</strong>
192
180
  </text>
193
181
  )
194
182
  } else if (actionMessage) {
195
183
  footerNode = actionMessage.split('\n').map((line, idx) => (
196
- <text key={idx} fg={theme.accent}>
184
+ <text key={idx} fg={theme.colors['textLink.foreground']}>
197
185
  {line}
198
186
  </text>
199
187
  ))
@@ -203,32 +191,57 @@ export const GitView = memo(function GitView() {
203
191
  <box flexDirection="column" flexGrow={1}>
204
192
  <box flexDirection="row" flexGrow={1}>
205
193
  <box
206
- width={sidebarWidth}
194
+ width={fileBarWidth}
207
195
  flexDirection="column"
208
- backgroundColor={theme.panel}
196
+ backgroundColor={theme.colors['sideBar.background']}
209
197
  padding={0}
210
198
  gap={0}
211
199
  >
212
- <text fg={theme.accent}>
200
+ <text fg={theme.colors['textLink.foreground']}>
213
201
  <strong>aimux · git</strong>
214
202
  </text>
215
203
  {gitPanel.branch ? (
216
204
  <box flexDirection="row">
217
- <text fg={theme.accent}>{'\u{e702}'} </text>
218
- <text fg={theme.textMuted}>{gitPanel.branch}</text>
205
+ <text fg={theme.colors['textLink.foreground']}>{'\u{e702}'} </text>
206
+ <text fg={theme.colors['descriptionForeground']}>{gitPanel.branch}</text>
207
+ </box>
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>
219
215
  </box>
220
216
  ) : null}
221
- <text fg={theme.dim}>{'·'.repeat(Math.max(0, sidebarWidth - 2))}</text>
217
+ <text fg={theme.colors['editor.lineHighlightBackground']}>
218
+ {'·'.repeat(Math.max(0, fileBarWidth - 2))}
219
+ </text>
222
220
  <GitPanel
221
+ collapsedFolders={gitMode.collapsedFolders}
222
+ fileListMode={gitPane.fileListMode}
223
223
  gitPanel={gitPanel}
224
+ headOffset={gitMode.headOffset}
224
225
  projectPath={projectPath}
225
- selectedFileKey={selectedFile ? fileKey(selectedFile) : null}
226
+ selectedEntryKey={gitMode.selectedEntryKey}
226
227
  />
227
228
  </box>
228
- <DiffStage diff={diff} diffRef={diffRef} loading={loading} />
229
+ <DiffStage
230
+ diff={diff}
231
+ diffKey={selectedDiffKey}
232
+ diffRef={diffRef}
233
+ loading={loading}
234
+ themeId={themeId}
235
+ view={gitMode.diffView}
236
+ />
229
237
  </box>
230
238
  {footerNode ? (
231
- <box paddingLeft={1} paddingRight={1} backgroundColor={theme.panel} flexDirection="column">
239
+ <box
240
+ paddingLeft={1}
241
+ paddingRight={1}
242
+ backgroundColor={theme.colors['sideBar.background']}
243
+ flexDirection="column"
244
+ >
232
245
  {footerNode}
233
246
  </box>
234
247
  ) : null}
@@ -1,10 +1,16 @@
1
+ import type { ModeId } from '@brimveyn/aimux-config'
2
+
1
3
  import { useTerminalDimensions } from '@opentui/react'
2
4
  import { useLayoutEffect, useMemo, useRef } from 'react'
3
5
 
4
- import { collectHelpEntries, type HelpEntry } from '../../input/keymap/help-entries'
6
+ import {
7
+ collectHelpEntries,
8
+ HELP_MODE_LABELS,
9
+ type HelpEntry,
10
+ } from '../../input/keymap/help-entries'
5
11
  import { dispatchGlobal } from '../../state/dispatch-ref'
6
12
  import { useKeymap } from '../keymap-context'
7
- import { theme } from '../theme'
13
+ import { useTheme } from '../theme'
8
14
  import { uiTokens } from '../ui-tokens'
9
15
  import { ModalFilterBar } from './modal-filter-bar'
10
16
  import { ModalShell } from './modal-shell'
@@ -12,6 +18,7 @@ import { ModalShell } from './modal-shell'
12
18
  interface HelpModalProps {
13
19
  filter: string | null
14
20
  selectedIndex: number
21
+ scope: ModeId | null
15
22
  }
16
23
 
17
24
  function matchesFilter(entry: HelpEntry, needle: string): boolean {
@@ -78,14 +85,24 @@ function computeWindowStart(
78
85
  return start
79
86
  }
80
87
 
81
- export function HelpModal({ filter, selectedIndex }: HelpModalProps) {
88
+ export function HelpModal({ filter, scope, selectedIndex }: HelpModalProps) {
89
+ const theme = useTheme()
82
90
  const config = useKeymap()
83
91
  const dimensions = useTerminalDimensions()
84
92
  const allEntries = useMemo(() => collectHelpEntries(config), [config])
93
+ const scoped = useMemo(
94
+ () => (scope ? allEntries.filter((e) => e.mode === scope) : allEntries),
95
+ [allEntries, scope]
96
+ )
85
97
  const filtered = useMemo(
86
- () => allEntries.filter((e) => matchesFilter(e, filter ?? '')),
87
- [allEntries, filter]
98
+ () => scoped.filter((e) => matchesFilter(e, filter ?? '')),
99
+ [scoped, filter]
88
100
  )
101
+ const title = useMemo(() => {
102
+ if (!scope) return 'Keybindings'
103
+ const label = HELP_MODE_LABELS.find((m) => m.modeId === scope)?.label ?? scope
104
+ return `${label} — keybindings`
105
+ }, [scope])
89
106
  const rows = useMemo(() => buildRows(filtered), [filtered])
90
107
 
91
108
  useLayoutEffect(() => {
@@ -113,12 +130,12 @@ export function HelpModal({ filter, selectedIndex }: HelpModalProps) {
113
130
 
114
131
  return (
115
132
  <ModalShell
116
- title="Keybindings"
133
+ title={title}
117
134
  keybindsModeId="modal.help"
118
135
  width={uiTokens.modalWidth.lg}
119
136
  footer={
120
137
  <box flexDirection="column" gap={0}>
121
- <text fg={theme.dim}>
138
+ <text fg={theme.colors['editor.lineHighlightBackground']}>
122
139
  {filtered.length === 0
123
140
  ? ''
124
141
  : ` ${effectiveIndex + 1} / ${filtered.length}${filter ? '' : ' — type / to filter'}`}
@@ -128,7 +145,7 @@ export function HelpModal({ filter, selectedIndex }: HelpModalProps) {
128
145
  }
129
146
  >
130
147
  {filtered.length === 0 ? (
131
- <text fg={theme.textMuted}>
148
+ <text fg={theme.colors['descriptionForeground']}>
132
149
  {filter ? 'No matching bindings.' : 'No bindings registered.'}
133
150
  </text>
134
151
  ) : (
@@ -138,14 +155,14 @@ export function HelpModal({ filter, selectedIndex }: HelpModalProps) {
138
155
  if (row.kind === 'header') {
139
156
  return (
140
157
  <box key={`h-${rowIndex}`} paddingLeft={1} paddingTop={i === 0 ? 0 : 1}>
141
- <text fg={theme.warning} wrapMode="none">
158
+ <text fg={theme.colors['editorWarning.foreground']} wrapMode="none">
142
159
  <strong>{row.label}</strong>
143
160
  </text>
144
161
  </box>
145
162
  )
146
163
  }
147
164
  const active = row.entryIndex === effectiveIndex
148
- const bg = active ? theme.panelHighlight : undefined
165
+ const bg = active ? theme.colors['list.activeSelectionBackground'] : undefined
149
166
  return (
150
167
  <box
151
168
  key={`e-${rowIndex}`}
@@ -155,12 +172,28 @@ export function HelpModal({ filter, selectedIndex }: HelpModalProps) {
155
172
  backgroundColor={bg}
156
173
  >
157
174
  <box width={KEYS_COLUMN_WIDTH} flexShrink={0}>
158
- <text fg={active ? theme.accent : theme.accentAlt} bg={bg} wrapMode="none">
175
+ <text
176
+ fg={
177
+ active
178
+ ? theme.colors['textLink.foreground']
179
+ : theme.colors['terminal.ansiMagenta']
180
+ }
181
+ bg={bg}
182
+ wrapMode="none"
183
+ >
159
184
  {row.entry.keysDisplay}
160
185
  </text>
161
186
  </box>
162
187
  <box flexGrow={1} overflow="hidden">
163
- <text fg={active ? theme.text : theme.textMuted} bg={bg} wrapMode="none">
188
+ <text
189
+ fg={
190
+ active
191
+ ? theme.colors['editor.foreground']
192
+ : theme.colors['descriptionForeground']
193
+ }
194
+ bg={bg}
195
+ wrapMode="none"
196
+ >
164
197
  {row.entry.description ?? ''}
165
198
  </text>
166
199
  </box>
@@ -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,7 +8,8 @@ interface InputFieldProps {
8
8
  }
9
9
 
10
10
  export function InputField({ active, cursorPos, value }: InputFieldProps) {
11
- const fg = active ? theme.text : theme.textMuted
11
+ const theme = useTheme()
12
+ const fg = active ? theme.colors['editor.foreground'] : theme.colors['descriptionForeground']
12
13
  if (!active) {
13
14
  return (
14
15
  <Surface tone="input" padding={1}>
@@ -29,7 +30,7 @@ export function InputField({ active, cursorPos, value }: InputFieldProps) {
29
30
  <Surface tone="inputActive" padding={1}>
30
31
  <text fg={fg}>
31
32
  {before}
32
- <span bg={theme.text} fg={theme.background}>
33
+ <span bg={theme.colors['editor.foreground']} fg={theme.colors['editor.background']}>
33
34
  {cursorDisplay}
34
35
  </span>
35
36
  {trailing}
@@ -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
@@ -33,7 +34,15 @@ export function ListItem({
33
34
  <box flexDirection="row">
34
35
  {isRow ? null : (
35
36
  <>
36
- <text fg={active ? theme.accent : theme.dim}>{active ? '›' : '·'}</text>
37
+ <text
38
+ fg={
39
+ active
40
+ ? theme.colors['textLink.foreground']
41
+ : theme.colors['editor.lineHighlightBackground']
42
+ }
43
+ >
44
+ {active ? '›' : '·'}
45
+ </text>
37
46
  <text> </text>
38
47
  </>
39
48
  )}
@@ -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,13 +6,14 @@ 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
  }
12
13
 
13
14
  return (
14
15
  <Surface tone="input" paddingLeft={1} paddingRight={1} paddingTop={1} paddingBottom={1}>
15
- <text fg={theme.text}>/{filter}_</text>
16
+ <text fg={theme.colors['editor.foreground']}>/{filter}_</text>
16
17
  </Surface>
17
18
  )
18
19
  }
@@ -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,
@@ -28,9 +29,9 @@ export function ModalKeybindsOverlay({ limit, modeId }: ModalKeybindsOverlayProp
28
29
  {entries.map((binding) => (
29
30
  <box key={`${binding.keys}-${binding.description}`} flexDirection="row">
30
31
  <box width={KEYS_COLUMN_WIDTH}>
31
- <text fg={theme.accentAlt}>{binding.keysDisplay}</text>
32
+ <text fg={theme.colors['terminal.ansiMagenta']}>{binding.keysDisplay}</text>
32
33
  </box>
33
- <text fg={theme.textMuted}>{binding.description ?? ''}</text>
34
+ <text fg={theme.colors['descriptionForeground']}>{binding.description ?? ''}</text>
34
35
  </box>
35
36
  ))}
36
37
  </Surface>
@@ -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"
@@ -40,14 +41,14 @@ export function ModalShell({
40
41
  left={0}
41
42
  width="100%"
42
43
  height="100%"
43
- backgroundColor={theme.overlay}
44
+ backgroundColor={theme.colors['editorWidget.background']}
44
45
  opacity={0.7}
45
46
  />
46
47
  <Surface tone="elevated" padding={1} gap={1} width={width}>
47
48
  <box width="100%" flexDirection="column" gap={listGap}>
48
49
  <box flexDirection="column">
49
- <text fg={theme.accentAlt}>{title}</text>
50
- {subtitle ? <text fg={theme.textMuted}>{subtitle}</text> : null}
50
+ <text fg={theme.colors['terminal.ansiMagenta']}>{title}</text>
51
+ {subtitle ? <text fg={theme.colors['descriptionForeground']}>{subtitle}</text> : null}
51
52
  </box>
52
53
  {children}
53
54
  {footer ? <box>{footer}</box> : null}
@@ -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
@@ -34,11 +35,23 @@ export function NewTabModal({ customCommands, editBuffer, selectedIndex }: NewTa
34
35
  <ListItem
35
36
  key={option.id}
36
37
  active={active}
37
- title={<text fg={active ? theme.text : theme.textMuted}>{option.label}</text>}
38
+ title={
39
+ <text
40
+ fg={
41
+ active
42
+ ? theme.colors['editor.foreground']
43
+ : theme.colors['descriptionForeground']
44
+ }
45
+ >
46
+ {option.label}
47
+ </text>
48
+ }
38
49
  subtitle={
39
50
  <box flexDirection="column">
40
- <text fg={theme.textMuted}>{option.description}</text>
41
- {customCmd ? <text fg={theme.accent}>{customCmd}</text> : null}
51
+ <text fg={theme.colors['descriptionForeground']}>{option.description}</text>
52
+ {customCmd ? (
53
+ <text fg={theme.colors['textLink.foreground']}>{customCmd}</text>
54
+ ) : null}
42
55
  </box>
43
56
  }
44
57
  />
@@ -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()
@@ -19,9 +20,9 @@ export function PendingChordOverlay() {
19
20
  <box position="absolute" bottom={modalOpen ? 10 : 2} right={1}>
20
21
  <Surface tone="elevated" paddingLeft={1} paddingRight={1}>
21
22
  <box flexDirection="row">
22
- <text fg={theme.textMuted}>pending: </text>
23
- <text fg={theme.accent}>{display}</text>
24
- <text fg={theme.textMuted}> …</text>
23
+ <text fg={theme.colors['descriptionForeground']}>pending: </text>
24
+ <text fg={theme.colors['textLink.foreground']}>{display}</text>
25
+ <text fg={theme.colors['descriptionForeground']}> …</text>
25
26
  </box>
26
27
  </Surface>
27
28
  </box>