@brimveyn/aimux 1.7.1 → 1.7.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 (54) hide show
  1. package/package.json +3 -4
  2. package/src/app-runtime/pty-write.ts +8 -3
  3. package/src/app-runtime/side-effects.ts +10 -2
  4. package/src/app-runtime/use-renderer-bindings.ts +2 -1
  5. package/src/app.tsx +5 -8
  6. package/src/config.ts +2 -10
  7. package/src/input/modes/bridge.ts +1 -1
  8. package/src/input/modes/types.ts +6 -1
  9. package/src/input/raw-input-handler.ts +12 -3
  10. package/src/pty/terminal-snapshot.ts +4 -4
  11. package/src/state/reducers/modal-state.ts +3 -2
  12. package/src/state/reducers/session-state.ts +0 -7
  13. package/src/state/types.ts +7 -1
  14. package/src/ui/components/bare-input.tsx +5 -5
  15. package/src/ui/components/context-menu-box.tsx +21 -0
  16. package/src/ui/components/context-menu-overlay.tsx +114 -0
  17. package/src/ui/components/create-session-modal.tsx +12 -41
  18. package/src/ui/components/diff-renderer/fold-strip.tsx +7 -7
  19. package/src/ui/components/diff-renderer/highlight.ts +6 -10
  20. package/src/ui/components/diff-renderer/pierre-diff.tsx +3 -3
  21. package/src/ui/components/diff-renderer/prepare-diff.ts +2 -3
  22. package/src/ui/components/diff-renderer/split-view.tsx +22 -29
  23. package/src/ui/components/diff-renderer/stacked-view.tsx +18 -31
  24. package/src/ui/components/diff-renderer/use-diff-prefetch.ts +0 -1
  25. package/src/ui/components/diff-renderer/use-diff-preparation.ts +1 -1
  26. package/src/ui/components/git-commit-modal.tsx +4 -16
  27. package/src/ui/components/git-panel.tsx +37 -73
  28. package/src/ui/components/git-view.tsx +17 -19
  29. package/src/ui/components/help-modal.tsx +5 -13
  30. package/src/ui/components/input-field.tsx +5 -7
  31. package/src/ui/components/list-item.tsx +3 -11
  32. package/src/ui/components/modal-keybinds-overlay.tsx +4 -4
  33. package/src/ui/components/modal-shell.tsx +6 -11
  34. package/src/ui/components/new-tab-modal.tsx +7 -15
  35. package/src/ui/components/pending-chord-overlay.tsx +5 -5
  36. package/src/ui/components/picker.tsx +6 -6
  37. package/src/ui/components/session-bar.tsx +34 -20
  38. package/src/ui/components/session-picker-modal.tsx +7 -20
  39. package/src/ui/components/sidebar.tsx +36 -35
  40. package/src/ui/components/snippet-editor-modal.tsx +4 -18
  41. package/src/ui/components/snippet-picker-modal.tsx +5 -9
  42. package/src/ui/components/split-layout.tsx +3 -3
  43. package/src/ui/components/status-bar.tsx +12 -13
  44. package/src/ui/components/surface.tsx +8 -11
  45. package/src/ui/components/tab-item.tsx +54 -29
  46. package/src/ui/components/terminal-pane.tsx +59 -25
  47. package/src/ui/components/theme-picker-modal.tsx +7 -21
  48. package/src/ui/components/update-available-modal.tsx +3 -13
  49. package/src/ui/context-menu/controller.ts +34 -0
  50. package/src/ui/root.tsx +5 -2
  51. package/src/ui/shiki.ts +29 -21
  52. package/src/ui/theme-store.ts +110 -21
  53. package/src/ui/theme.ts +6 -3
  54. package/src/ui/themes.ts +18 -13
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brimveyn/aimux",
3
- "version": "1.7.1",
3
+ "version": "1.7.2",
4
4
  "description": "A terminal multiplexer for AI CLIs. Run Claude, Codex, OpenCode side-by-side with tabbed navigation, split panes, and persistent sessions.",
5
5
  "keywords": [
6
6
  "ai",
@@ -57,11 +57,10 @@
57
57
  "format": "oxfmt --write .",
58
58
  "format:check": "oxfmt --check .",
59
59
  "knip": "knip-bun",
60
- "bump": "bun run scripts/bump.ts",
61
- "generate-themes": "bun run scripts/generate-themes.ts"
60
+ "bump": "bun run scripts/bump.ts"
62
61
  },
63
62
  "dependencies": {
64
- "@brimveyn/aimux-config": "0.4.4",
63
+ "@brimveyn/aimux-config": "0.4.5",
65
64
  "@opentui/core": "^0.1.90",
66
65
  "@opentui/react": "^0.1.90",
67
66
  "@xterm/headless": "^6.0.0",
@@ -3,6 +3,10 @@ import type { AppAction, TabSession } from '../state/types'
3
3
 
4
4
  import { buildPtyPastePayload } from '../input/paste'
5
5
 
6
+ export interface PtyWriteOptions {
7
+ autoBottom?: boolean
8
+ }
9
+
6
10
  function shouldScrollViewportToBottom(tab: TabSession): boolean {
7
11
  const viewport = tab.viewport
8
12
  return viewport !== undefined && viewport.viewportY < viewport.baseY
@@ -13,9 +17,10 @@ export function writeToTab(
13
17
  tabId: string,
14
18
  tab: TabSession | undefined,
15
19
  input: string,
16
- dispatch?: (action: AppAction) => void
20
+ dispatch?: (action: AppAction) => void,
21
+ options?: PtyWriteOptions
17
22
  ): void {
18
- if (tab && shouldScrollViewportToBottom(tab)) {
23
+ if ((options?.autoBottom ?? true) && tab && shouldScrollViewportToBottom(tab)) {
19
24
  backend.scrollViewportToBottom(tabId)
20
25
  dispatch?.({ intent: { kind: 'bottom' }, tabId, type: 'set-scroll-intent' })
21
26
  }
@@ -31,5 +36,5 @@ export function writePasteToTab(
31
36
  dispatch?: (action: AppAction) => void
32
37
  ): void {
33
38
  const payload = buildPtyPastePayload(text, tab?.terminalModes.bracketedPasteMode ?? false)
34
- writeToTab(backend, tabId, tab, payload, dispatch)
39
+ writeToTab(backend, tabId, tab, payload, dispatch, { autoBottom: true })
35
40
  }
@@ -385,6 +385,10 @@ export function executeSideEffect(effect: SideEffect, ctx: SideEffectContext): v
385
385
  handleSelectedSessionDelete(ctx)
386
386
  return
387
387
  }
388
+ case 'delete-session': {
389
+ handleDeleteSessionEffect(state, backend, dispatch, effect.sessionId)
390
+ return
391
+ }
388
392
  case 'open-rename-selected-session': {
389
393
  openSelectedSessionRename(ctx)
390
394
  return
@@ -453,8 +457,12 @@ export function executeSideEffect(effect: SideEffect, ctx: SideEffectContext): v
453
457
  return
454
458
  }
455
459
  case 'split-pane': {
456
- const customCommand = state.customCommands.terminal
457
- const tab = createTabSession('terminal', customCommand, state.customCommands)
460
+ const sourceTab = effect.sourceTabId
461
+ ? state.tabs.find((t) => t.id === effect.sourceTabId)
462
+ : undefined
463
+ const assistant = sourceTab?.assistant ?? 'terminal'
464
+ const customCommand = state.customCommands[assistant]
465
+ const tab = createTabSession(assistant, customCommand, state.customCommands)
458
466
  executeSplitPane(ctx, effect.direction, tab)
459
467
  return
460
468
  }
@@ -60,7 +60,8 @@ export function useRendererBindings({
60
60
  activeTabRef.current?.terminalModes.bracketedPasteMode ?? false,
61
61
  getFocusMode: () => focusModeRef.current,
62
62
  handleTerminalShortcut,
63
- writeToPty: (tabId, data) => writeToTab(backend, tabId, activeTabRef.current, data, dispatch),
63
+ writeToPty: (tabId, data, options) =>
64
+ writeToTab(backend, tabId, activeTabRef.current, data, dispatch, options),
64
65
  })
65
66
 
66
67
  const handlePasteEvent = (event: { bytes: Uint8Array; defaultPrevented?: boolean }) => {
package/src/app.tsx CHANGED
@@ -38,7 +38,7 @@ import { appReducer, createInitialState } from './state/store'
38
38
  import { KeymapContext } from './ui/keymap-context'
39
39
  import { RootView } from './ui/root'
40
40
  import { applyTheme, setTransparent } from './ui/theme'
41
- import { isKnownThemeId, registerUserThemes, type ThemeId } from './ui/themes'
41
+ import { isKnownThemeId, type ThemeId } from './ui/themes'
42
42
  import {
43
43
  fetchLatestNpmVersion,
44
44
  getCurrentPackageVersion,
@@ -66,15 +66,12 @@ export function App({
66
66
  const renderer = useRenderer()
67
67
  const dimensions = useTerminalDimensions()
68
68
  const [themeId, setThemeId] = useState<ThemeId>(() => {
69
- registerUserThemes(resolvedConfig.themes)
70
69
  const config = loadConfig()
71
70
  const persisted = config.themeId && isKnownThemeId(config.themeId) ? config.themeId : undefined
72
- const fromConfig =
73
- resolvedConfig.theme && isKnownThemeId(resolvedConfig.theme)
74
- ? resolvedConfig.theme
75
- : undefined
76
- const initial = persisted ?? fromConfig ?? 'aimux'
77
- applyTheme(initial)
71
+ const fromConfig: ThemeId =
72
+ resolvedConfig.theme?.mode === 'light' ? 'aimux-light' : 'aimux-dark'
73
+ const initial: ThemeId = persisted ?? fromConfig
74
+ applyTheme(initial, resolvedConfig.theme?.paletteOverrides)
78
75
  setTransparent(config.themeTransparent ?? false)
79
76
  return initial
80
77
  })
package/src/config.ts CHANGED
@@ -5,19 +5,11 @@ import type { GitFileListMode, SessionBarPosition, WorkspaceSnapshotV1 } from '.
5
5
  import { logDebug } from './debug/input-log'
6
6
  import { getProfileConfigDir } from './profile-paths'
7
7
  import { isWorkspaceSnapshotV1 } from './state/validation'
8
- import { THEME_IDS, type ThemeId } from './ui/themes'
9
-
10
- const LEGACY_THEME_ALIASES: Record<string, ThemeId> = {
11
- 'everforest': 'everforest-dark',
12
- 'gruvbox-dark': 'gruvbox-dark-hard',
13
- 'kanagawa': 'kanagawa-wave',
14
- 'one-dark': 'one-dark-pro',
15
- }
8
+ import { migrateThemeId as resolveLegacyThemeId, type ThemeId } from './ui/themes'
16
9
 
17
10
  function migrateThemeId(value: unknown): ThemeId | undefined {
18
11
  if (typeof value !== 'string') return undefined
19
- if (THEME_IDS.includes(value as ThemeId)) return value as ThemeId
20
- return LEGACY_THEME_ALIASES[value]
12
+ return resolveLegacyThemeId(value)
21
13
  }
22
14
 
23
15
  export const CONFIG_PATH = `${getProfileConfigDir()}/aimux.json`
@@ -19,11 +19,11 @@ const COMMAND_EDIT_MODE_IDS: Partial<Record<SupportedModalType, ModeId>> = {
19
19
  'session-picker': 'modal.session-picker.filtering',
20
20
  'snippet-editor': 'modal.snippet-editor',
21
21
  'snippet-picker': 'modal.snippet-picker.filtering',
22
+ 'split-picker': 'modal.split-picker',
22
23
  'theme-picker': 'modal.theme-picker.filtering',
23
24
  }
24
25
 
25
26
  const MODAL_MODE_IDS: Partial<Record<SupportedModalType, ModeId>> = {
26
- 'split-picker': 'modal.split-picker',
27
27
  'update-available': 'modal.update-available',
28
28
  }
29
29
 
@@ -40,7 +40,11 @@ export type SideEffect =
40
40
  | { type: 'apply-theme'; action: 'confirm' }
41
41
  | { type: 'apply-theme'; action: 'preview'; delta: 1 | -1 }
42
42
  | { type: 'rename-session'; sessionId: string; name: string }
43
- | { type: 'split-pane'; direction: import('../../state/layout-tree').SplitDirection }
43
+ | {
44
+ type: 'split-pane'
45
+ direction: import('../../state/layout-tree').SplitDirection
46
+ sourceTabId?: string
47
+ }
44
48
  | { type: 'confirm-split' }
45
49
  | { type: 'scroll-git-diff'; delta: number }
46
50
  | { type: 'persist-git-diff-mode-ratio'; ratio: number }
@@ -54,6 +58,7 @@ export type SideEffect =
54
58
  | { type: 'git-push' }
55
59
  | { type: 'confirm-update-selection' }
56
60
  | { type: 'switch-session-by-index'; index: number }
61
+ | { type: 'delete-session'; sessionId: string }
57
62
  | { type: 'toggle-transparent' }
58
63
 
59
64
  export interface KeyResult {
@@ -1,3 +1,4 @@
1
+ import type { PtyWriteOptions } from '../app-runtime/pty-write'
1
2
  import type { FocusMode } from '../state/types'
2
3
 
3
4
  import { logInputDebug } from '../debug/input-log'
@@ -10,6 +11,8 @@ const KITTY_MOD_SUPER = 8
10
11
  const KITTY_MOD_HYPER = 16
11
12
  const KITTY_MOD_META = 32
12
13
  const KITTY_HOST_MOD_MASK = KITTY_MOD_SUPER | KITTY_MOD_HYPER | KITTY_MOD_META
14
+ const FOCUS_IN_SEQUENCE = `${ESC}[I`
15
+ const FOCUS_OUT_SEQUENCE = `${ESC}[O`
13
16
 
14
17
  function normalizeControlSequence(sequence: string): string | null {
15
18
  const match = KITTY_CTRL_RE.exec(sequence)
@@ -79,7 +82,7 @@ export function createRawInputHandler(deps: {
79
82
  getFocusMode: () => FocusMode
80
83
  getActiveTabId: () => string | null
81
84
  getBracketedPasteModeEnabled: () => boolean
82
- writeToPty: (tabId: string, data: string) => void
85
+ writeToPty: (tabId: string, data: string, options?: PtyWriteOptions) => void
83
86
  /**
84
87
  * Dispatch a configured terminal-input shortcut.
85
88
  * Returns true if the chord was consumed by the keymap, false otherwise.
@@ -88,6 +91,10 @@ export function createRawInputHandler(deps: {
88
91
  }): (sequence: string) => boolean {
89
92
  let bracketedPasteBuffer: string | null = null
90
93
 
94
+ function isFocusReportSequence(sequence: string): boolean {
95
+ return sequence === FOCUS_IN_SEQUENCE || sequence === FOCUS_OUT_SEQUENCE
96
+ }
97
+
91
98
  function flushPaste(tabId: string, payload: string): void {
92
99
  logInputDebug('raw.flushPaste', {
93
100
  bracketedPasteModeEnabled: deps.getBracketedPasteModeEnabled(),
@@ -95,7 +102,9 @@ export function createRawInputHandler(deps: {
95
102
  payloadPreview: payload.slice(0, 120),
96
103
  tabId,
97
104
  })
98
- deps.writeToPty(tabId, buildPtyPastePayload(payload, deps.getBracketedPasteModeEnabled()))
105
+ deps.writeToPty(tabId, buildPtyPastePayload(payload, deps.getBracketedPasteModeEnabled()), {
106
+ autoBottom: true,
107
+ })
99
108
  }
100
109
 
101
110
  function handleTerminalShortcut(sequence: string): boolean {
@@ -162,7 +171,7 @@ export function createRawInputHandler(deps: {
162
171
  return true
163
172
  }
164
173
 
165
- deps.writeToPty(tabId, normalized)
174
+ deps.writeToPty(tabId, normalized, { autoBottom: !isFocusReportSequence(normalized) })
166
175
  return true
167
176
  }
168
177
 
@@ -113,15 +113,15 @@ function buildLine(
113
113
  let bg = getColorHex(current.getBgColor(), bgMode)
114
114
 
115
115
  if (current.isInverse()) {
116
- const resolvedFg = fg ?? getCurrentTheme().colors['editor.foreground']
117
- const resolvedBg = bg ?? getCurrentTheme().colors['editor.background']
116
+ const resolvedFg = fg ?? getCurrentTheme().palette.ink
117
+ const resolvedBg = bg ?? getCurrentTheme().palette.neutral
118
118
  ;[fg, bg] = [resolvedBg, resolvedFg]
119
119
  }
120
120
 
121
121
  const isCursorCell = cursorVisible && cursorColumn === column
122
122
  if (isCursorCell) {
123
- const resolvedFg = fg ?? getCurrentTheme().colors['editor.foreground']
124
- const resolvedBg = bg ?? getCurrentTheme().colors['editor.background']
123
+ const resolvedFg = fg ?? getCurrentTheme().palette.ink
124
+ const resolvedBg = bg ?? getCurrentTheme().palette.neutral
125
125
  ;[fg, bg] = [resolvedBg, resolvedFg]
126
126
  }
127
127
 
@@ -74,10 +74,10 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
74
74
  case 'open-split-picker':
75
75
  return {
76
76
  ...state,
77
- focusMode: 'modal',
77
+ focusMode: 'command-edit',
78
78
  modal: {
79
79
  cursorPos: 0,
80
- editBuffer: null,
80
+ editBuffer: '',
81
81
  selectedIndex: 0,
82
82
  sessionTargetId: null,
83
83
  splitDirection: action.direction,
@@ -104,6 +104,7 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
104
104
  modal: {
105
105
  cursorPos: initialName.length,
106
106
  editBuffer: initialName,
107
+ returnToSessionPicker: action.returnToSessionPicker ?? true,
107
108
  selectedIndex: 0,
108
109
  sessionTargetId: action.sessionTargetId ?? null,
109
110
  type: 'session-name',
@@ -46,13 +46,6 @@ export function reduceSessionState(state: AppState, action: AppAction): AppState
46
46
  case 'rename-session-record':
47
47
  return {
48
48
  ...state,
49
- focusMode: 'modal',
50
- modal: {
51
- editBuffer: null,
52
- selectedIndex: state.modal.selectedIndex,
53
- sessionTargetId: null,
54
- type: 'session-picker',
55
- },
56
49
  sessions: state.sessions.map((session) =>
57
50
  session.id === action.sessionId
58
51
  ? { ...session, name: action.name, updatedAt: new Date().toISOString() }
@@ -291,6 +291,7 @@ export interface ModalSessionPicker extends ModalBase {
291
291
 
292
292
  export interface ModalSessionName extends ModalBase {
293
293
  type: 'session-name'
294
+ returnToSessionPicker: boolean
294
295
  }
295
296
 
296
297
  export interface ModalRenameTab extends ModalBase {
@@ -407,7 +408,12 @@ export type ModalAction =
407
408
  | { type: 'open-help-modal'; scope?: ModeId }
408
409
  | { type: 'open-split-picker'; direction: import('./layout-tree').SplitDirection }
409
410
  | { type: 'open-session-picker' }
410
- | { type: 'open-session-name-modal'; sessionTargetId?: string; initialName?: string }
411
+ | {
412
+ type: 'open-session-name-modal'
413
+ sessionTargetId?: string
414
+ initialName?: string
415
+ returnToSessionPicker?: boolean
416
+ }
411
417
  | { type: 'close-modal' }
412
418
  | { type: 'move-modal-selection'; delta: number }
413
419
  | { type: 'update-command-edit'; char: string }
@@ -1,4 +1,4 @@
1
- import { useTheme } from '../theme'
1
+ import { useTokens } from '../theme'
2
2
 
3
3
  interface BareInputProps {
4
4
  value: string
@@ -7,10 +7,10 @@ interface BareInputProps {
7
7
  }
8
8
 
9
9
  export function BareInput({ cursorPos, placeholder = '', value }: BareInputProps) {
10
- const theme = useTheme()
11
- const fg = theme.colors['editor.foreground']
12
- const bg = theme.colors['editor.background']
13
- const placeholderFg = theme.colors['editorLineNumber.foreground']
10
+ const t = useTokens()
11
+ const fg = t.palette.ink
12
+ const bg = t.palette.neutral
13
+ const placeholderFg = t.faint
14
14
 
15
15
  if (!value) {
16
16
  const firstChar = placeholder.charAt(0) || ' '
@@ -0,0 +1,21 @@
1
+ import type { BoxRenderable, MouseEvent as OtuiMouseEvent } from '@opentui/core'
2
+ import type { BoxProps } from '@opentui/react'
3
+
4
+ import { type ContextMenuItem, openContextMenu } from '../context-menu/controller'
5
+
6
+ interface ContextMenuBoxProps extends BoxProps {
7
+ rightClickMenu?: ContextMenuItem[]
8
+ }
9
+
10
+ export function ContextMenuBox({ onMouseDown, rightClickMenu, ...boxProps }: ContextMenuBoxProps) {
11
+ function handleMouseDown(this: BoxRenderable, event: OtuiMouseEvent): void {
12
+ if (event.button === 2 && rightClickMenu && rightClickMenu.length > 0) {
13
+ event.preventDefault()
14
+ event.stopPropagation()
15
+ openContextMenu(event.x, event.y, rightClickMenu)
16
+ return
17
+ }
18
+ onMouseDown?.call(this, event)
19
+ }
20
+ return <box {...boxProps} onMouseDown={handleMouseDown} />
21
+ }
@@ -0,0 +1,114 @@
1
+ import { useKeyboard } from '@opentui/react'
2
+ import { useEffect, useState } from 'react'
3
+
4
+ import { useAppStore } from '../../state/app-store'
5
+ import {
6
+ closeContextMenu,
7
+ type ContextMenuState,
8
+ subscribeContextMenu,
9
+ } from '../context-menu/controller'
10
+ import { useTokens } from '../theme'
11
+
12
+ export function ContextMenuOverlay() {
13
+ const [menu, setMenu] = useState<ContextMenuState | null>(null)
14
+ const [selected, setSelected] = useState(0)
15
+ const t = useTokens()
16
+ const terminalCols = useAppStore((s) => s.layout.terminalCols)
17
+ const terminalRows = useAppStore((s) => s.layout.terminalRows)
18
+
19
+ useEffect(() => {
20
+ return subscribeContextMenu((state) => {
21
+ setMenu(state)
22
+ setSelected(0)
23
+ })
24
+ }, [])
25
+
26
+ useKeyboard((key) => {
27
+ if (!menu) return
28
+ if (key.name === 'escape') {
29
+ key.preventDefault()
30
+ closeContextMenu()
31
+ return
32
+ }
33
+ if (key.name === 'up') {
34
+ key.preventDefault()
35
+ setSelected((i) => (i - 1 + menu.items.length) % menu.items.length)
36
+ return
37
+ }
38
+ if (key.name === 'down') {
39
+ key.preventDefault()
40
+ setSelected((i) => (i + 1) % menu.items.length)
41
+ return
42
+ }
43
+ if (key.name === 'return') {
44
+ key.preventDefault()
45
+ const item = menu.items[selected]
46
+ closeContextMenu()
47
+ item?.[1]()
48
+ }
49
+ })
50
+
51
+ if (!menu) return null
52
+
53
+ const maxLabel = Math.max(...menu.items.map(([label]) => label.length))
54
+ const width = maxLabel + 4
55
+ const height = menu.items.length + 2
56
+ const left = Math.max(0, Math.min(menu.anchorX, terminalCols - width))
57
+ const top = Math.max(0, Math.min(menu.anchorY, terminalRows - height))
58
+
59
+ return (
60
+ <box position="absolute" top={0} left={0} width="100%" height="100%">
61
+ <box
62
+ position="absolute"
63
+ top={0}
64
+ left={0}
65
+ width="100%"
66
+ height="100%"
67
+ onMouseDown={(e) => {
68
+ e.preventDefault()
69
+ e.stopPropagation()
70
+ closeContextMenu()
71
+ }}
72
+ />
73
+ <box
74
+ position="absolute"
75
+ top={top}
76
+ left={left}
77
+ width={width}
78
+ flexDirection="column"
79
+ border
80
+ borderColor={t.palette.primary}
81
+ backgroundColor={t.elevated}
82
+ onMouseDown={(e) => {
83
+ e.stopPropagation()
84
+ }}
85
+ >
86
+ {menu.items.map(([label, onSelect], index) => {
87
+ const active = index === selected
88
+ return (
89
+ <box
90
+ key={`${label}-${index}`}
91
+ width={width - 2}
92
+ flexShrink={0}
93
+ paddingLeft={1}
94
+ paddingRight={1}
95
+ backgroundColor={active ? t.selected : undefined}
96
+ onMouseOver={() => setSelected(index)}
97
+ onMouseDown={(e) => {
98
+ e.preventDefault()
99
+ e.stopPropagation()
100
+ if (e.button !== 0) return
101
+ closeContextMenu()
102
+ onSelect()
103
+ }}
104
+ >
105
+ <text fg={active ? t.palette.ink : t.muted} selectable={false}>
106
+ {label}
107
+ </text>
108
+ </box>
109
+ )
110
+ })}
111
+ </box>
112
+ </box>
113
+ )
114
+ }
@@ -1,7 +1,7 @@
1
1
  import type { DirectoryResult } from '../../state/types'
2
2
 
3
3
  import { abbreviatePath } from '../path-format'
4
- import { getCurrentTheme, useTheme } from '../theme'
4
+ import { getCurrentTokens, useTokens } from '../theme'
5
5
  import { uiTokens } from '../ui-tokens'
6
6
  import { InputField } from './input-field'
7
7
  import { ListItem } from './list-item'
@@ -10,27 +10,16 @@ import { ModalShell } from './modal-shell'
10
10
  const VISIBLE_ROWS = 8
11
11
 
12
12
  function getDirectoryResultIcon(result: DirectoryResult): string {
13
- if (result.type === 'worktree') {
14
- return '\u{e728}'
15
- }
16
-
17
- if (result.type === 'workspace') {
18
- return '\u{f07c}'
19
- }
20
-
13
+ if (result.type === 'worktree') return '\u{e728}'
14
+ if (result.type === 'workspace') return '\u{f07c}'
21
15
  return '\u{e702}'
22
16
  }
23
17
 
24
18
  function getDirectoryResultColor(result: DirectoryResult): string {
25
- if (result.type === 'worktree') {
26
- return getCurrentTheme().colors['editorWarning.foreground']
27
- }
28
-
29
- if (result.type === 'workspace') {
30
- return getCurrentTheme().colors['terminal.ansiMagenta']
31
- }
32
-
33
- return getCurrentTheme().colors['textLink.foreground']
19
+ const t = getCurrentTokens()
20
+ if (result.type === 'worktree') return t.palette.warning
21
+ if (result.type === 'workspace') return t.accent
22
+ return t.palette.primary
34
23
  }
35
24
 
36
25
  interface CreateSessionModalProps {
@@ -50,7 +39,7 @@ export function CreateSessionModal({
50
39
  selectedIndex,
51
40
  sessionName,
52
41
  }: CreateSessionModalProps) {
53
- const theme = useTheme()
42
+ const t = useTokens()
54
43
  const dirActive = activeField === 'directory'
55
44
  const nameActive = activeField === 'name'
56
45
 
@@ -64,11 +53,7 @@ export function CreateSessionModal({
64
53
  width={uiTokens.modalWidth.xl}
65
54
  >
66
55
  <box flexDirection="column">
67
- <text
68
- fg={dirActive ? theme.colors['editor.foreground'] : theme.colors['descriptionForeground']}
69
- >
70
- Search projects
71
- </text>
56
+ <text fg={dirActive ? t.palette.ink : t.muted}>Search projects</text>
72
57
  <InputField
73
58
  active={dirActive}
74
59
  placeholder="Type a project name..."
@@ -80,7 +65,7 @@ export function CreateSessionModal({
80
65
 
81
66
  <box flexDirection="column" height={VISIBLE_ROWS}>
82
67
  {results.length === 0 ? (
83
- <text fg={theme.colors['descriptionForeground']}>
68
+ <text fg={t.muted}>
84
69
  {directoryQuery.length > 0 ? 'No matches' : 'Type a project name to search...'}
85
70
  </text>
86
71
  ) : (
@@ -94,15 +79,7 @@ export function CreateSessionModal({
94
79
  <text fg={getDirectoryResultColor(result)}>{getDirectoryResultIcon(result)}</text>
95
80
  }
96
81
  title={
97
- <text
98
- fg={
99
- active
100
- ? theme.colors['editor.foreground']
101
- : theme.colors['descriptionForeground']
102
- }
103
- >
104
- {abbreviatePath(result.path)}
105
- </text>
82
+ <text fg={active ? t.palette.ink : t.muted}>{abbreviatePath(result.path)}</text>
106
83
  }
107
84
  />
108
85
  )
@@ -111,13 +88,7 @@ export function CreateSessionModal({
111
88
  </box>
112
89
 
113
90
  <box flexDirection="column">
114
- <text
115
- fg={
116
- nameActive ? theme.colors['editor.foreground'] : theme.colors['descriptionForeground']
117
- }
118
- >
119
- Session name
120
- </text>
91
+ <text fg={nameActive ? t.palette.ink : t.muted}>Session name</text>
121
92
  <InputField active={nameActive} value={sessionName} />
122
93
  </box>
123
94
  </ModalShell>
@@ -1,4 +1,4 @@
1
- import { useBg, useTheme } from '../../theme'
1
+ import { useBg, useTokens } from '../../theme'
2
2
  import { FOLD_STEP, type FoldInfo } from './build-rows'
3
3
  import { type FoldDispatch } from './pierre-diff'
4
4
 
@@ -8,11 +8,11 @@ interface Props {
8
8
  }
9
9
 
10
10
  function Button({ label, onPress }: { label: string; onPress: () => void }) {
11
- const theme = useTheme()
12
- const bg = useBg('sideBar.background')
11
+ const t = useTokens()
12
+ const bg = useBg('elevated')
13
13
  return (
14
14
  <box paddingLeft={1} paddingRight={1} backgroundColor={bg} onMouseDown={onPress}>
15
- <text fg={theme.colors['textLink.foreground']}>{label}</text>
15
+ <text fg={t.palette.primary}>{label}</text>
16
16
  </box>
17
17
  )
18
18
  }
@@ -22,8 +22,8 @@ function Spacer() {
22
22
  }
23
23
 
24
24
  export function FoldStrip({ dispatch, fold }: Props) {
25
- const theme = useTheme()
26
- const headerBg = useBg('sideBarSectionHeader.background')
25
+ const t = useTokens()
26
+ const headerBg = useBg('elevated')
27
27
  const { bottomExpanded, foldId, hidden, topExpanded, total } = fold
28
28
  const stepUp = Math.min(FOLD_STEP, hidden)
29
29
  const stepDown = Math.min(FOLD_STEP, hidden)
@@ -71,7 +71,7 @@ export function FoldStrip({ dispatch, fold }: Props) {
71
71
 
72
72
  return (
73
73
  <box flexDirection="row" backgroundColor={headerBg} paddingLeft={1} paddingRight={1}>
74
- <text fg={theme.colors['descriptionForeground']}>{`⋯ ${hidden} hidden `}</text>
74
+ <text fg={t.muted}>{`⋯ ${hidden} hidden `}</text>
75
75
  {controls}
76
76
  </box>
77
77
  )
@@ -1,6 +1,6 @@
1
1
  import type { ThemedToken } from 'shiki'
2
2
 
3
- import { ensureShikiLang, ensureShikiTheme, getShikiHighlighter } from '../../shiki'
3
+ import { ensureActiveShikiTheme, ensureShikiLang, getShikiHighlighter } from '../../shiki'
4
4
 
5
5
  export interface HighlightSpan {
6
6
  bold?: boolean
@@ -22,24 +22,20 @@ export function tokenToSpan(token: ThemedToken): HighlightSpan {
22
22
  }
23
23
  }
24
24
 
25
- export async function tokenizeSide(
26
- lines: string[],
27
- lang: string,
28
- theme: string
29
- ): Promise<ThemedToken[][]> {
25
+ export async function tokenizeSide(lines: string[], lang: string): Promise<ThemedToken[][]> {
30
26
  if (lines.length === 0) return []
31
27
  const highlighter = await getShikiHighlighter()
32
- const [langOk, themeOk] = await Promise.all([
28
+ const [langOk, themeName] = await Promise.all([
33
29
  ensureShikiLang(highlighter, lang),
34
- ensureShikiTheme(highlighter, theme),
30
+ ensureActiveShikiTheme(highlighter),
35
31
  ])
36
- if (!langOk || !themeOk) return []
32
+ if (!langOk) return []
37
33
  try {
38
34
  const result = highlighter.codeToTokens(lines.join(''), {
39
35
  // eslint-disable-next-line typescript/no-explicit-any
40
36
  lang: lang as any,
41
37
  // eslint-disable-next-line typescript/no-explicit-any
42
- theme: theme as any,
38
+ theme: themeName as any,
43
39
  })
44
40
  return result.tokens
45
41
  } catch {