@brimveyn/aimux 1.5.4 → 1.6.0

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 (65) 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/input/modes/bridge.ts +8 -0
  13. package/src/input/modes/transitions.ts +2 -1
  14. package/src/input/modes/types.ts +4 -1
  15. package/src/pty/terminal-snapshot.ts +4 -4
  16. package/src/state/git-tree.ts +220 -0
  17. package/src/state/reducers/git-mode-state.ts +232 -35
  18. package/src/state/reducers/git-panel-state.ts +29 -3
  19. package/src/state/reducers/modal-state.ts +43 -10
  20. package/src/state/store.ts +5 -1
  21. package/src/state/types.ts +41 -5
  22. package/src/state/workspace-save.ts +2 -0
  23. package/src/ui/components/create-session-modal.tsx +23 -7
  24. package/src/ui/components/diff-renderer/build-rows.ts +349 -0
  25. package/src/ui/components/diff-renderer/filetype.ts +29 -0
  26. package/src/ui/components/diff-renderer/fold-strip.tsx +84 -0
  27. package/src/ui/components/diff-renderer/highlight.ts +48 -0
  28. package/src/ui/components/diff-renderer/index.ts +1 -0
  29. package/src/ui/components/diff-renderer/pierre-diff.tsx +170 -0
  30. package/src/ui/components/diff-renderer/split-view.tsx +207 -0
  31. package/src/ui/components/diff-renderer/stacked-view.tsx +166 -0
  32. package/src/ui/components/git-commit-modal.tsx +14 -2
  33. package/src/ui/components/git-pane-widget.tsx +5 -0
  34. package/src/ui/components/git-panel.tsx +176 -79
  35. package/src/ui/components/git-view.tsx +89 -87
  36. package/src/ui/components/help-modal.tsx +43 -11
  37. package/src/ui/components/input-field.tsx +2 -2
  38. package/src/ui/components/list-item.tsx +9 -1
  39. package/src/ui/components/modal-filter-bar.tsx +1 -1
  40. package/src/ui/components/modal-keybinds-overlay.tsx +2 -2
  41. package/src/ui/components/modal-shell.tsx +3 -3
  42. package/src/ui/components/new-tab-modal.tsx +15 -3
  43. package/src/ui/components/pending-chord-overlay.tsx +3 -3
  44. package/src/ui/components/session-bar.tsx +10 -5
  45. package/src/ui/components/session-picker-modal.tsx +26 -9
  46. package/src/ui/components/sidebar.tsx +22 -10
  47. package/src/ui/components/snippet-editor-modal.tsx +16 -2
  48. package/src/ui/components/snippet-picker-modal.tsx +11 -3
  49. package/src/ui/components/split-layout.tsx +1 -1
  50. package/src/ui/components/status-bar.tsx +12 -9
  51. package/src/ui/components/surface.tsx +5 -5
  52. package/src/ui/components/tab-item.tsx +22 -16
  53. package/src/ui/components/terminal-pane.tsx +25 -15
  54. package/src/ui/components/theme-picker-modal.tsx +111 -16
  55. package/src/ui/components/update-available-modal.tsx +11 -1
  56. package/src/ui/filter-themes.ts +10 -0
  57. package/src/ui/house-themes.ts +313 -0
  58. package/src/ui/keymap-context.ts +10 -2
  59. package/src/ui/root.tsx +26 -6
  60. package/src/ui/shiki.ts +49 -0
  61. package/src/ui/status-bar-model.ts +19 -1
  62. package/src/ui/theme.ts +22 -10
  63. package/src/ui/themes.generated.ts +59794 -0
  64. package/src/ui/themes.ts +68 -274
  65. package/src/ui/syntax.ts +0 -102
@@ -0,0 +1,10 @@
1
+ import { THEME_IDS, type ThemeId, THEMES } from './themes'
2
+
3
+ export function filterThemeIds(filter: string | null): ThemeId[] {
4
+ if (!filter) return THEME_IDS
5
+ const needle = filter.toLowerCase()
6
+ return THEME_IDS.filter((id) => {
7
+ const name = THEMES[id]?.name.toLowerCase() ?? ''
8
+ return id.toLowerCase().includes(needle) || name.includes(needle)
9
+ })
10
+ }
@@ -0,0 +1,313 @@
1
+ // Hand-curated aimux house themes, full Shiki theme shape. Colors follow the
2
+ // VSCode workbench color key namespace; `settings` is a TextMate token rule
3
+ // array shared by the in-repo diff highlighter and `shiki.loadTheme`.
4
+ /* eslint-disable perfectionist/sort-objects */
5
+
6
+ import type { Theme, ThemeColorMap } from './themes'
7
+
8
+ // --- aimux --------------------------------------------------------------
9
+ //
10
+ // Teal accents on a deep blue-black background. Designed to look at home
11
+ // inside the aimux chrome.
12
+ const AIMUX_BG = '#11151b'
13
+ const AIMUX_FG = '#edf4ff'
14
+
15
+ const AIMUX_COLORS: ThemeColorMap = {
16
+ 'contrastBorder': '#3a4c60',
17
+ 'descriptionForeground': '#6b829a',
18
+ 'diffEditor.insertedLineBackground': '#1e3d2b',
19
+ 'diffEditor.removedLineBackground': '#3b1e27',
20
+ 'editor.background': AIMUX_BG,
21
+ 'editor.foreground': AIMUX_FG,
22
+ 'editor.lineHighlightBackground': '#1a2330',
23
+ 'editor.selectionBackground': '#1f3344',
24
+ 'editorCursor.foreground': '#7cd1b8',
25
+ 'editorError.foreground': '#f38ba8',
26
+ 'editorGroup.border': '#2d3f52',
27
+ 'editorGroupHeader.tabsBackground': '#16202b',
28
+ 'editorLineNumber.foreground': '#3e506a',
29
+ 'editorWarning.foreground': '#f6c177',
30
+ 'editorWidget.background': '#0b1016',
31
+ 'errorForeground': '#f38ba8',
32
+ 'focusBorder': '#7cd1b8',
33
+ 'foreground': AIMUX_FG,
34
+ 'gitDecoration.addedResourceForeground': '#8bd5ca',
35
+ 'gitDecoration.deletedResourceForeground': '#f38ba8',
36
+ 'gitDecoration.modifiedResourceForeground': '#f6c177',
37
+ 'gitDecoration.untrackedResourceForeground': '#8bd5ca',
38
+ 'input.background': '#1c2734',
39
+ 'input.border': '#2d3f52',
40
+ 'input.foreground': AIMUX_FG,
41
+ 'list.activeSelectionBackground': '#1f3344',
42
+ 'list.activeSelectionForeground': AIMUX_FG,
43
+ 'list.hoverBackground': '#1a2330',
44
+ 'panel.background': '#16202b',
45
+ 'panel.border': '#2d3f52',
46
+ 'sideBar.background': '#16202b',
47
+ 'sideBar.foreground': AIMUX_FG,
48
+ 'sideBarSectionHeader.background': '#1c2734',
49
+ 'sideBarSectionHeader.foreground': '#b8c5d6',
50
+ 'terminal.ansiBlue': '#7cb3ff',
51
+ 'terminal.ansiCyan': '#7cd1b8',
52
+ 'terminal.ansiGreen': '#8bd5ca',
53
+ 'terminal.ansiMagenta': '#c4a7e7',
54
+ 'terminal.ansiRed': '#f38ba8',
55
+ 'terminal.ansiYellow': '#f6c177',
56
+ 'textLink.activeForeground': '#7cd1b8',
57
+ 'textLink.foreground': '#7cd1b8',
58
+ }
59
+
60
+ // --- dracula-at-night --------------------------------------------------
61
+ const DAN_BG = '#0e1419'
62
+ const DAN_FG = '#f8f8f2'
63
+
64
+ const DAN_COLORS: ThemeColorMap = {
65
+ 'contrastBorder': '#44475a',
66
+ 'descriptionForeground': '#6272a4',
67
+ 'diffEditor.insertedLineBackground': '#162b1d',
68
+ 'diffEditor.removedLineBackground': '#2a1820',
69
+ 'editor.background': DAN_BG,
70
+ 'editor.foreground': DAN_FG,
71
+ 'editor.lineHighlightBackground': '#1a2129',
72
+ 'editor.selectionBackground': '#222a33',
73
+ 'editorCursor.foreground': '#f8f8f0',
74
+ 'editorError.foreground': '#ff5555',
75
+ 'editorGroup.border': '#2a3440',
76
+ 'editorGroupHeader.tabsBackground': '#131920',
77
+ 'editorLineNumber.foreground': '#495466',
78
+ 'editorWarning.foreground': '#f1fa8c',
79
+ 'editorWidget.background': '#090d11',
80
+ 'errorForeground': '#ff5555',
81
+ 'focusBorder': '#bd93f9',
82
+ 'foreground': DAN_FG,
83
+ 'gitDecoration.addedResourceForeground': '#50fa7b',
84
+ 'gitDecoration.deletedResourceForeground': '#ff5555',
85
+ 'gitDecoration.modifiedResourceForeground': '#f1fa8c',
86
+ 'gitDecoration.untrackedResourceForeground': '#50fa7b',
87
+ 'input.background': '#1a2129',
88
+ 'input.border': '#2a3440',
89
+ 'input.foreground': DAN_FG,
90
+ 'list.activeSelectionBackground': '#222a33',
91
+ 'list.activeSelectionForeground': DAN_FG,
92
+ 'list.hoverBackground': '#1a2129',
93
+ 'panel.background': '#131920',
94
+ 'panel.border': '#2a3440',
95
+ 'sideBar.background': '#131920',
96
+ 'sideBar.foreground': DAN_FG,
97
+ 'sideBarSectionHeader.background': '#1a2129',
98
+ 'sideBarSectionHeader.foreground': '#bdc3cf',
99
+ 'terminal.ansiBlue': '#6272a4',
100
+ 'terminal.ansiCyan': '#8be9fd',
101
+ 'terminal.ansiGreen': '#50fa7b',
102
+ 'terminal.ansiMagenta': '#ff79c6',
103
+ 'terminal.ansiRed': '#ff5555',
104
+ 'terminal.ansiYellow': '#f1fa8c',
105
+ 'textLink.activeForeground': '#ff79c6',
106
+ 'textLink.foreground': '#bd93f9',
107
+ }
108
+
109
+ // --- Shared TextMate rule helpers --------------------------------------
110
+
111
+ const AIMUX_SETTINGS = [
112
+ { settings: { background: AIMUX_BG, foreground: AIMUX_FG } },
113
+ {
114
+ scope: ['comment', 'punctuation.definition.comment'],
115
+ settings: { fontStyle: 'italic', foreground: '#6b829a' },
116
+ },
117
+ { scope: ['comment.documentation'], settings: { foreground: '#8fa6bf', fontStyle: 'italic' } },
118
+ { scope: ['string', 'string.quoted'], settings: { foreground: '#f6c177' } },
119
+ { scope: ['string.template'], settings: { foreground: '#f6c177' } },
120
+ {
121
+ scope: ['string.regexp', 'string.escape', 'constant.character.escape'],
122
+ settings: { foreground: '#c4a7e7' },
123
+ },
124
+ {
125
+ scope: ['constant.numeric', 'constant.language', 'constant.language.boolean'],
126
+ settings: { foreground: '#f38ba8' },
127
+ },
128
+ { scope: ['constant.character', 'constant.other'], settings: { foreground: '#f38ba8' } },
129
+ {
130
+ scope: ['keyword', 'keyword.control', 'keyword.operator.new', 'keyword.other'],
131
+ settings: { fontStyle: 'bold', foreground: '#c4a7e7' },
132
+ },
133
+ {
134
+ scope: ['storage', 'storage.type', 'storage.modifier'],
135
+ settings: { fontStyle: 'bold', foreground: '#c4a7e7' },
136
+ },
137
+ {
138
+ scope: ['keyword.operator', 'punctuation', 'meta.brace', 'punctuation.separator'],
139
+ settings: { foreground: '#6b829a' },
140
+ },
141
+ {
142
+ scope: [
143
+ 'entity.name.function',
144
+ 'support.function',
145
+ 'meta.function-call',
146
+ 'entity.name.function.member',
147
+ ],
148
+ settings: { foreground: '#8bd5ca' },
149
+ },
150
+ {
151
+ scope: [
152
+ 'entity.name.type',
153
+ 'support.type',
154
+ 'support.class',
155
+ 'entity.name.class',
156
+ 'entity.name.interface',
157
+ 'entity.name.namespace',
158
+ ],
159
+ settings: { fontStyle: 'italic', foreground: '#7cd1b8' },
160
+ },
161
+ {
162
+ scope: ['entity.name.type.enum', 'entity.name.type.alias'],
163
+ settings: { foreground: '#7cd1b8' },
164
+ },
165
+ { scope: ['variable', 'meta.variable'], settings: { foreground: AIMUX_FG } },
166
+ {
167
+ scope: ['variable.parameter', 'meta.function.parameters'],
168
+ settings: { fontStyle: 'italic', foreground: AIMUX_FG },
169
+ },
170
+ {
171
+ scope: ['variable.other.property', 'variable.other.member', 'meta.object-literal.key'],
172
+ settings: { foreground: '#7cd1b8' },
173
+ },
174
+ {
175
+ scope: ['variable.language', 'variable.language.this'],
176
+ settings: { foreground: '#f38ba8', fontStyle: 'italic' },
177
+ },
178
+ { scope: ['entity.name.tag', 'meta.tag'], settings: { foreground: '#f38ba8' } },
179
+ {
180
+ scope: ['entity.other.attribute-name'],
181
+ settings: { fontStyle: 'italic', foreground: '#f6c177' },
182
+ },
183
+ { scope: ['support.constant'], settings: { foreground: '#f38ba8' } },
184
+ { scope: ['support.variable'], settings: { foreground: AIMUX_FG } },
185
+ // Markdown
186
+ { scope: ['markup.heading'], settings: { fontStyle: 'bold', foreground: '#7cd1b8' } },
187
+ { scope: ['markup.bold'], settings: { fontStyle: 'bold', foreground: AIMUX_FG } },
188
+ { scope: ['markup.italic'], settings: { fontStyle: 'italic', foreground: AIMUX_FG } },
189
+ { scope: ['markup.underline.link', 'markup.inline.raw'], settings: { foreground: '#8bd5ca' } },
190
+ { scope: ['markup.list'], settings: { foreground: '#c4a7e7' } },
191
+ { scope: ['markup.quote'], settings: { foreground: '#6b829a', fontStyle: 'italic' } },
192
+ { scope: ['markup.raw'], settings: { foreground: '#f6c177' } },
193
+ // JSX / TSX
194
+ {
195
+ scope: ['entity.name.tag.jsx', 'support.class.component'],
196
+ settings: { foreground: '#7cd1b8' },
197
+ },
198
+ { scope: ['meta.jsx.children'], settings: { foreground: AIMUX_FG } },
199
+ // Diff
200
+ { scope: ['markup.inserted', 'meta.diff.header.to-file'], settings: { foreground: '#8bd5ca' } },
201
+ { scope: ['markup.deleted', 'meta.diff.header.from-file'], settings: { foreground: '#f38ba8' } },
202
+ { scope: ['markup.changed'], settings: { foreground: '#f6c177' } },
203
+ { scope: ['meta.diff.range'], settings: { foreground: '#c4a7e7' } },
204
+ ]
205
+
206
+ const DAN_SETTINGS = [
207
+ { settings: { background: DAN_BG, foreground: DAN_FG } },
208
+ {
209
+ scope: ['comment', 'punctuation.definition.comment'],
210
+ settings: { fontStyle: 'italic', foreground: '#6272a4' },
211
+ },
212
+ { scope: ['comment.documentation'], settings: { fontStyle: 'italic', foreground: '#8a96b4' } },
213
+ { scope: ['string', 'string.quoted'], settings: { foreground: '#f1fa8c' } },
214
+ { scope: ['string.template'], settings: { foreground: '#f1fa8c' } },
215
+ {
216
+ scope: ['string.regexp', 'string.escape', 'constant.character.escape'],
217
+ settings: { foreground: '#ff79c6' },
218
+ },
219
+ {
220
+ scope: ['constant.numeric', 'constant.language', 'constant.language.boolean'],
221
+ settings: { foreground: '#bd93f9' },
222
+ },
223
+ { scope: ['constant.character', 'constant.other'], settings: { foreground: '#bd93f9' } },
224
+ {
225
+ scope: ['keyword', 'keyword.control', 'keyword.other'],
226
+ settings: { fontStyle: 'bold', foreground: '#ff79c6' },
227
+ },
228
+ {
229
+ scope: ['storage', 'storage.type', 'storage.modifier'],
230
+ settings: { fontStyle: 'bold italic', foreground: '#ff79c6' },
231
+ },
232
+ {
233
+ scope: ['keyword.operator', 'punctuation', 'meta.brace', 'punctuation.separator'],
234
+ settings: { foreground: '#f8f8f2' },
235
+ },
236
+ {
237
+ scope: ['entity.name.function', 'support.function', 'meta.function-call'],
238
+ settings: { foreground: '#50fa7b' },
239
+ },
240
+ {
241
+ scope: [
242
+ 'entity.name.type',
243
+ 'support.type',
244
+ 'support.class',
245
+ 'entity.name.class',
246
+ 'entity.name.interface',
247
+ ],
248
+ settings: { fontStyle: 'italic', foreground: '#8be9fd' },
249
+ },
250
+ {
251
+ scope: ['entity.name.type.enum', 'entity.name.type.alias'],
252
+ settings: { foreground: '#8be9fd' },
253
+ },
254
+ { scope: ['variable', 'meta.variable'], settings: { foreground: DAN_FG } },
255
+ {
256
+ scope: ['variable.parameter', 'meta.function.parameters'],
257
+ settings: { fontStyle: 'italic', foreground: '#ffb86c' },
258
+ },
259
+ {
260
+ scope: ['variable.other.property', 'variable.other.member', 'meta.object-literal.key'],
261
+ settings: { foreground: '#8be9fd' },
262
+ },
263
+ {
264
+ scope: ['variable.language', 'variable.language.this'],
265
+ settings: { foreground: '#bd93f9', fontStyle: 'italic' },
266
+ },
267
+ { scope: ['entity.name.tag', 'meta.tag'], settings: { foreground: '#ff79c6' } },
268
+ {
269
+ scope: ['entity.other.attribute-name'],
270
+ settings: { fontStyle: 'italic', foreground: '#50fa7b' },
271
+ },
272
+ { scope: ['support.constant'], settings: { foreground: '#bd93f9' } },
273
+ // Markdown
274
+ { scope: ['markup.heading'], settings: { fontStyle: 'bold', foreground: '#bd93f9' } },
275
+ { scope: ['markup.bold'], settings: { fontStyle: 'bold', foreground: '#ffb86c' } },
276
+ { scope: ['markup.italic'], settings: { fontStyle: 'italic', foreground: '#f1fa8c' } },
277
+ { scope: ['markup.underline.link', 'markup.inline.raw'], settings: { foreground: '#8be9fd' } },
278
+ { scope: ['markup.list'], settings: { foreground: '#ff79c6' } },
279
+ { scope: ['markup.quote'], settings: { foreground: '#6272a4', fontStyle: 'italic' } },
280
+ { scope: ['markup.raw'], settings: { foreground: '#f1fa8c' } },
281
+ // JSX / TSX
282
+ {
283
+ scope: ['entity.name.tag.jsx', 'support.class.component'],
284
+ settings: { foreground: '#8be9fd' },
285
+ },
286
+ // Diff
287
+ { scope: ['markup.inserted'], settings: { foreground: '#50fa7b' } },
288
+ { scope: ['markup.deleted'], settings: { foreground: '#ff5555' } },
289
+ { scope: ['markup.changed'], settings: { foreground: '#f1fa8c' } },
290
+ ]
291
+
292
+ export const HOUSE_THEMES: Record<string, Theme> = {
293
+ 'aimux': {
294
+ bg: AIMUX_BG,
295
+ colors: AIMUX_COLORS,
296
+ displayName: 'Aimux',
297
+ fg: AIMUX_FG,
298
+ name: 'aimux',
299
+ settings: AIMUX_SETTINGS,
300
+ type: 'dark',
301
+ },
302
+ 'dracula-at-night': {
303
+ bg: DAN_BG,
304
+ colors: DAN_COLORS,
305
+ displayName: 'Dracula At Night',
306
+ fg: DAN_FG,
307
+ name: 'dracula-at-night',
308
+ settings: DAN_SETTINGS,
309
+ type: 'dark',
310
+ },
311
+ }
312
+
313
+ export const HOUSE_THEME_IDS: string[] = Object.keys(HOUSE_THEMES)
@@ -18,17 +18,25 @@ export function useKeymap(): ResolvedKeymapConfig {
18
18
  const HELP_JOINER = ' · '
19
19
  const MAX_HELP_ENTRIES = 5
20
20
 
21
+ interface BuildHintOptions {
22
+ excludeDescriptions?: readonly string[]
23
+ }
24
+
21
25
  /** Build a single-line help string for a mode (used in modal headers and status bar). */
22
26
  export function buildHintText(
23
27
  config: ResolvedKeymapConfig,
24
28
  modeId: ModeId,
25
- limit: number = MAX_HELP_ENTRIES
29
+ limit: number = MAX_HELP_ENTRIES,
30
+ options: BuildHintOptions = {}
26
31
  ): string {
27
32
  const bindings = describeBindings(config, modeId, {
28
33
  dedupeByDescription: true,
29
34
  withDescriptionOnly: true,
30
35
  })
31
36
  if (bindings.length === 0) return ''
32
- const trimmed = bindings.slice(0, limit)
37
+ const excluded = new Set(options.excludeDescriptions ?? [])
38
+ const kept =
39
+ excluded.size > 0 ? bindings.filter((b) => !excluded.has(b.description ?? '')) : bindings
40
+ const trimmed = kept.slice(0, limit)
33
41
  return trimmed.map((b) => `${b.keysDisplay} ${b.description ?? ''}`.trim()).join(HELP_JOINER)
34
42
  }
package/src/ui/root.tsx CHANGED
@@ -134,7 +134,11 @@ function renderModal(
134
134
  )
135
135
  case 'theme-picker':
136
136
  return (
137
- <ThemePickerModal selectedIndex={modal.selectedIndex} currentThemeId={options.themeId} />
137
+ <ThemePickerModal
138
+ selectedIndex={modal.selectedIndex}
139
+ currentThemeId={options.themeId}
140
+ filter={modal.editBuffer}
141
+ />
138
142
  )
139
143
  case 'update-available':
140
144
  return (
@@ -145,7 +149,13 @@ function renderModal(
145
149
  />
146
150
  )
147
151
  case 'help':
148
- return <HelpModal filter={modal.editBuffer} selectedIndex={modal.selectedIndex} />
152
+ return (
153
+ <HelpModal
154
+ filter={modal.editBuffer}
155
+ selectedIndex={modal.selectedIndex}
156
+ scope={modal.scope}
157
+ />
158
+ )
149
159
  case 'git-commit': {
150
160
  const titleText =
151
161
  modal.activeField === 'title' ? (modal.editBuffer ?? '') : modal.contentBuffer
@@ -230,8 +240,13 @@ export function RootView({
230
240
  const inGitMode = focusMode === 'git' || modal.type === 'git-commit'
231
241
  if (inGitMode) {
232
242
  return (
233
- <box flexDirection="column" width="100%" height="100%" backgroundColor={theme.background}>
234
- <GitView />
243
+ <box
244
+ flexDirection="column"
245
+ width="100%"
246
+ height="100%"
247
+ backgroundColor={theme.colors['editor.background']}
248
+ >
249
+ <GitView themeId={themeId} />
235
250
  <StatusBar />
236
251
  <PendingChordOverlay />
237
252
  {renderModal(modal, {
@@ -249,7 +264,12 @@ export function RootView({
249
264
  }
250
265
 
251
266
  return (
252
- <box flexDirection="column" width="100%" height="100%" backgroundColor={theme.background}>
267
+ <box
268
+ flexDirection="column"
269
+ width="100%"
270
+ height="100%"
271
+ backgroundColor={theme.colors['editor.background']}
272
+ >
253
273
  {sessionBarPosition === 'top' && <SessionBar />}
254
274
  <box flexDirection="row" gap={0} padding={0} flexGrow={1}>
255
275
  <Sidebar onTabActivate={onPaneActivate} />
@@ -330,7 +350,7 @@ function GitPaneInPaneMode({ ratio }: { ratio: number }) {
330
350
  flexDirection="column"
331
351
  width={width}
332
352
  flexShrink={0}
333
- backgroundColor={theme.panel}
353
+ backgroundColor={theme.colors['sideBar.background']}
334
354
  overflow="hidden"
335
355
  >
336
356
  <GitPaneWidget pollingEnabled />
@@ -0,0 +1,49 @@
1
+ import { type BundledLanguage, type BundledTheme, createHighlighter, type Highlighter } from 'shiki'
2
+
3
+ import { THEMES } from './themes'
4
+
5
+ // Shiki's highlighter is created with at least one real shiki-bundled theme so
6
+ // its worker can warm up. House and user themes load on demand.
7
+ const WARM_THEME: BundledTheme = 'catppuccin-mocha'
8
+
9
+ let highlighterPromise: Promise<Highlighter> | null = null
10
+
11
+ export async function getShikiHighlighter(): Promise<Highlighter> {
12
+ if (!highlighterPromise) {
13
+ highlighterPromise = createHighlighter({ langs: [], themes: [WARM_THEME] })
14
+ }
15
+ return highlighterPromise
16
+ }
17
+
18
+ const loadedLangs = new Set<string>()
19
+
20
+ export async function ensureShikiLang(h: Highlighter, lang: string): Promise<boolean> {
21
+ if (loadedLangs.has(lang)) return true
22
+ try {
23
+ await h.loadLanguage(lang as BundledLanguage)
24
+ loadedLangs.add(lang)
25
+ return true
26
+ } catch {
27
+ return false
28
+ }
29
+ }
30
+
31
+ const loadedThemes = new Set<string>([WARM_THEME])
32
+
33
+ /**
34
+ * Load the theme keyed by `id` into the shiki highlighter. The same object
35
+ * powers the UI palette and the code highlighter — no synthesis, no mapping.
36
+ */
37
+ export async function ensureShikiTheme(h: Highlighter, id: string): Promise<boolean> {
38
+ if (loadedThemes.has(id)) return true
39
+ const entry = THEMES[id]
40
+ if (!entry) return false
41
+ try {
42
+ // eslint-disable-next-line typescript/no-explicit-any
43
+ await h.loadTheme(entry as any)
44
+ loadedThemes.add(id)
45
+ return true
46
+ } catch {
47
+ return false
48
+ }
49
+ }
@@ -2,16 +2,19 @@ import type { ModeId, ResolvedKeymapConfig } from '@brimveyn/aimux-config'
2
2
 
3
3
  import type { AppState, TabSession } from '../state/types'
4
4
 
5
+ import { describeBindings } from '../input/keymap/describe-bindings'
5
6
  import { buildHintText } from './keymap-context'
6
7
  import { abbreviatePath } from './path-format'
7
8
 
8
9
  export interface StatusBarModel {
9
10
  left: string
10
11
  right: string
12
+ help: string
11
13
  }
12
14
 
13
15
  const MAX_TAB_LABEL_LENGTH = 24
14
16
  const HINT_LIMIT = 6
17
+ const HELP_DESCRIPTION = 'Help'
15
18
 
16
19
  function truncateLabel(label: string): string {
17
20
  if (label.length <= MAX_TAB_LABEL_LENGTH) {
@@ -30,7 +33,17 @@ function getActiveTabLabel(tab?: TabSession): string {
30
33
  }
31
34
 
32
35
  function hintForMode(config: ResolvedKeymapConfig, modeId: ModeId): string {
33
- return buildHintText(config, modeId, HINT_LIMIT)
36
+ return buildHintText(config, modeId, HINT_LIMIT, { excludeDescriptions: [HELP_DESCRIPTION] })
37
+ }
38
+
39
+ function helpHintForMode(config: ResolvedKeymapConfig, modeId: ModeId): string {
40
+ const bindings = describeBindings(config, modeId, {
41
+ dedupeByDescription: true,
42
+ withDescriptionOnly: true,
43
+ })
44
+ const helpBinding = bindings.find((b) => b.description === HELP_DESCRIPTION)
45
+ if (!helpBinding) return ''
46
+ return `${helpBinding.keysDisplay} ${helpBinding.description ?? ''}`.trim()
34
47
  }
35
48
 
36
49
  export function getStatusBarModel(
@@ -52,24 +65,28 @@ export function getStatusBarModel(
52
65
  switch (state.focusMode) {
53
66
  case 'terminal-input':
54
67
  return {
68
+ help: '',
55
69
  left: `${getActiveTabLabel(activeTab)} ${sessionIcon} ${sessionLabel}`,
56
70
  right: hintForMode(config, 'terminal-input'),
57
71
  }
58
72
  case 'modal': {
59
73
  const modalMode = deriveModalModeId(state.modal.type)
60
74
  return {
75
+ help: '',
61
76
  left: `${sessionIcon} ${sessionLabel}`,
62
77
  right: modalMode ? hintForMode(config, modalMode) : '',
63
78
  }
64
79
  }
65
80
  case 'git':
66
81
  return {
82
+ help: helpHintForMode(config, 'git-mode'),
67
83
  left: `${sessionIcon} ${sessionLabel}`,
68
84
  right: hintForMode(config, 'git-mode'),
69
85
  }
70
86
  case 'command-edit': {
71
87
  const commandEditMode = deriveCommandEditModeId(state.modal.type)
72
88
  return {
89
+ help: '',
73
90
  left: `${sessionIcon} ${sessionLabel}`,
74
91
  right: commandEditMode ? hintForMode(config, commandEditMode) : '',
75
92
  }
@@ -77,6 +94,7 @@ export function getStatusBarModel(
77
94
  case 'navigation':
78
95
  default:
79
96
  return {
97
+ help: helpHintForMode(config, 'navigation'),
80
98
  left: `${sessionIcon} ${sessionLabel} ${getActiveTabLabel(activeTab)}`,
81
99
  right: hintForMode(config, 'navigation'),
82
100
  }
package/src/ui/theme.ts CHANGED
@@ -1,18 +1,30 @@
1
- import { type ThemeColors, type ThemeId, THEMES } from './themes'
1
+ import { type Theme, type ThemeId, THEMES } from './themes'
2
2
 
3
- export const theme: ThemeColors = { ...THEMES.aimux.colors }
3
+ const DEFAULT = THEMES['aimux']
4
+ if (!DEFAULT) throw new Error('default theme missing')
4
5
 
5
- type ThemeListener = () => void
6
- const themeListeners = new Set<ThemeListener>()
7
-
8
- export function onThemeChange(listener: ThemeListener): () => void {
9
- themeListeners.add(listener)
10
- return () => themeListeners.delete(listener)
6
+ // Mutable theme singleton. Components access `theme.colors['editor.foreground']`
7
+ // directly. On `applyTheme(id)` the colors map is replaced in place so live
8
+ // imports keep pointing at the current palette.
9
+ export const theme: Theme = {
10
+ bg: DEFAULT.bg,
11
+ colors: { ...DEFAULT.colors },
12
+ displayName: DEFAULT.displayName,
13
+ fg: DEFAULT.fg,
14
+ name: DEFAULT.name,
15
+ settings: DEFAULT.settings,
16
+ type: DEFAULT.type,
11
17
  }
12
18
 
13
19
  export function applyTheme(id: ThemeId): void {
14
20
  const entry = THEMES[id]
15
21
  if (!entry) return
16
- Object.assign(theme, entry.colors)
17
- for (const listener of themeListeners) listener()
22
+ for (const key of Object.keys(theme.colors)) delete theme.colors[key]
23
+ Object.assign(theme.colors, entry.colors)
24
+ theme.name = entry.name
25
+ theme.displayName = entry.displayName
26
+ theme.type = entry.type
27
+ theme.fg = entry.fg
28
+ theme.bg = entry.bg
29
+ theme.settings = entry.settings
18
30
  }