@brimveyn/aimux 1.7.0 → 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 (57) 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/ipc/manager-protocol.ts +2 -2
  11. package/src/ipc/protocol.ts +2 -2
  12. package/src/pty/terminal-snapshot.ts +4 -4
  13. package/src/session-backend/bootstrap.ts +21 -1
  14. package/src/state/reducers/modal-state.ts +3 -2
  15. package/src/state/reducers/session-state.ts +0 -7
  16. package/src/state/types.ts +7 -1
  17. package/src/ui/components/bare-input.tsx +5 -5
  18. package/src/ui/components/context-menu-box.tsx +21 -0
  19. package/src/ui/components/context-menu-overlay.tsx +114 -0
  20. package/src/ui/components/create-session-modal.tsx +12 -41
  21. package/src/ui/components/diff-renderer/fold-strip.tsx +7 -7
  22. package/src/ui/components/diff-renderer/highlight.ts +6 -10
  23. package/src/ui/components/diff-renderer/pierre-diff.tsx +3 -3
  24. package/src/ui/components/diff-renderer/prepare-diff.ts +2 -3
  25. package/src/ui/components/diff-renderer/split-view.tsx +22 -29
  26. package/src/ui/components/diff-renderer/stacked-view.tsx +18 -31
  27. package/src/ui/components/diff-renderer/use-diff-prefetch.ts +0 -1
  28. package/src/ui/components/diff-renderer/use-diff-preparation.ts +1 -1
  29. package/src/ui/components/git-commit-modal.tsx +4 -16
  30. package/src/ui/components/git-panel.tsx +37 -73
  31. package/src/ui/components/git-view.tsx +17 -19
  32. package/src/ui/components/help-modal.tsx +5 -13
  33. package/src/ui/components/input-field.tsx +5 -7
  34. package/src/ui/components/list-item.tsx +3 -11
  35. package/src/ui/components/modal-keybinds-overlay.tsx +4 -4
  36. package/src/ui/components/modal-shell.tsx +6 -11
  37. package/src/ui/components/new-tab-modal.tsx +7 -15
  38. package/src/ui/components/pending-chord-overlay.tsx +5 -5
  39. package/src/ui/components/picker.tsx +6 -6
  40. package/src/ui/components/session-bar.tsx +34 -20
  41. package/src/ui/components/session-picker-modal.tsx +7 -20
  42. package/src/ui/components/sidebar.tsx +36 -35
  43. package/src/ui/components/snippet-editor-modal.tsx +4 -18
  44. package/src/ui/components/snippet-picker-modal.tsx +5 -9
  45. package/src/ui/components/split-layout.tsx +3 -3
  46. package/src/ui/components/status-bar.tsx +12 -13
  47. package/src/ui/components/surface.tsx +8 -11
  48. package/src/ui/components/tab-item.tsx +54 -29
  49. package/src/ui/components/terminal-pane.tsx +59 -25
  50. package/src/ui/components/theme-picker-modal.tsx +7 -21
  51. package/src/ui/components/update-available-modal.tsx +3 -13
  52. package/src/ui/context-menu/controller.ts +34 -0
  53. package/src/ui/root.tsx +5 -2
  54. package/src/ui/shiki.ts +29 -21
  55. package/src/ui/theme-store.ts +110 -21
  56. package/src/ui/theme.ts +6 -3
  57. package/src/ui/themes.ts +18 -13
@@ -4,7 +4,7 @@ import type { ScrollBoxRenderable } from '@opentui/core'
4
4
  import { useTerminalDimensions } from '@opentui/react'
5
5
  import { type ReactNode, useLayoutEffect, useRef } from 'react'
6
6
 
7
- import { useTheme } from '../theme'
7
+ import { useTokens } from '../theme'
8
8
  import { BareInput } from './bare-input'
9
9
  import { ListItem } from './list-item'
10
10
  import { ModalShell } from './modal-shell'
@@ -39,7 +39,7 @@ const VIEWPORT_HEIGHT_RATIO = 0.6
39
39
  const MODAL_CHROME_ROWS = 6
40
40
 
41
41
  function PickerItemCtas({ onDelete, onEdit }: { onEdit?: () => void; onDelete?: () => void }) {
42
- const theme = useTheme()
42
+ const t = useTokens()
43
43
  return (
44
44
  <box flexDirection="row" gap={1}>
45
45
  {onEdit ? (
@@ -49,7 +49,7 @@ function PickerItemCtas({ onDelete, onEdit }: { onEdit?: () => void; onDelete?:
49
49
  onEdit()
50
50
  }}
51
51
  >
52
- <text fg={theme.colors['textLink.foreground']}>[edit]</text>
52
+ <text fg={t.palette.primary}>[edit]</text>
53
53
  </box>
54
54
  ) : null}
55
55
  {onDelete ? (
@@ -59,7 +59,7 @@ function PickerItemCtas({ onDelete, onEdit }: { onEdit?: () => void; onDelete?:
59
59
  onDelete()
60
60
  }}
61
61
  >
62
- <text fg={theme.colors['editorError.foreground']}>[del]</text>
62
+ <text fg={t.palette.error}>[del]</text>
63
63
  </box>
64
64
  ) : null}
65
65
  </box>
@@ -80,7 +80,7 @@ export function Picker({
80
80
  title,
81
81
  width,
82
82
  }: PickerProps) {
83
- const theme = useTheme()
83
+ const t = useTokens()
84
84
  const dimensions = useTerminalDimensions()
85
85
  const maxHeight = Math.max(6, Math.floor(dimensions.height * VIEWPORT_HEIGHT_RATIO))
86
86
  const listHeight = Math.max(1, maxHeight - MODAL_CHROME_ROWS)
@@ -140,7 +140,7 @@ export function Picker({
140
140
  if (item.group && item.group !== prevGroup) {
141
141
  nodes.push(
142
142
  <box key={`group::${item.group}`} paddingLeft={1} paddingTop={index === 0 ? 0 : 1}>
143
- <text fg={theme.colors['editorWarning.foreground']} wrapMode="none">
143
+ <text fg={t.palette.warning} wrapMode="none">
144
144
  {item.group}
145
145
  </text>
146
146
  </box>
@@ -10,11 +10,12 @@ import { dispatchGlobal, runSideEffectGlobal } from '../../state/dispatch-ref'
10
10
  import { IDLE_SESSION_STATUS } from '../../state/types'
11
11
  import { useBusySpinner } from '../hooks/use-busy-spinner'
12
12
  import { moveIdToIdPosition, orderSessionsForDisplay } from '../session-ordering'
13
- import { useBg, useTheme } from '../theme'
13
+ import { useBg, useTokens } from '../theme'
14
+ import { ContextMenuBox } from './context-menu-box'
14
15
 
15
16
  export function SessionBar() {
16
- const theme = useTheme()
17
- const headerBg = useBg('sideBarSectionHeader.background')
17
+ const t = useTokens()
18
+ const headerBg = useBg('elevated')
18
19
  const sessions = useAppStore((s) => s.sessions)
19
20
  const currentId = useAppStore((s) => s.currentSessionId)
20
21
  const bar = useAppStore((s) => s.sessionBar)
@@ -22,11 +23,7 @@ export function SessionBar() {
22
23
 
23
24
  const [draggingId, setDraggingId] = useState<string | null>(null)
24
25
  const [dragOrder, setDragOrder] = useState<string[] | null>(null)
25
- // Hysteresis: the id of the chip we most recently swapped with. While the
26
- // cursor remains over that chip we refuse to swap back (prevents oscillation
27
- // when a long chip's new bounds still cover the cursor after a swap).
28
26
  const lastSwapWithRef = useRef<string | null>(null)
29
- // Live bounds of each chip after render, keyed by session id.
30
27
  const chipRefs = useRef(new Map<string, BoxRenderable>())
31
28
 
32
29
  const ordered = useMemo(() => orderSessionsForDisplay(sessions), [sessions])
@@ -125,6 +122,22 @@ export function SessionBar() {
125
122
  onMouseDrag={handleMouseDrag}
126
123
  onMouseUp={commitDrop}
127
124
  onMouseDragEnd={cancelDrag}
125
+ rightClickMenu={[
126
+ [
127
+ 'Rename',
128
+ () =>
129
+ dispatchGlobal({
130
+ initialName: session.name,
131
+ returnToSessionPicker: false,
132
+ sessionTargetId: session.id,
133
+ type: 'open-session-name-modal',
134
+ }),
135
+ ],
136
+ [
137
+ 'Close',
138
+ () => runSideEffectGlobal({ sessionId: session.id, type: 'delete-session' }),
139
+ ],
140
+ ]}
128
141
  />
129
142
  )
130
143
  })}
@@ -133,13 +146,13 @@ export function SessionBar() {
133
146
  flexDirection="row"
134
147
  paddingLeft={1}
135
148
  paddingRight={1}
136
- backgroundColor={theme.colors['list.activeSelectionBackground']}
149
+ backgroundColor={t.selected}
137
150
  onMouseDown={(e) => {
138
151
  e.stopPropagation()
139
152
  dispatchGlobal({ returnToSessionPicker: false, type: 'open-create-session-modal' })
140
153
  }}
141
154
  >
142
- <text fg={theme.colors['editor.foreground']} selectable={false}>
155
+ <text fg={t.palette.ink} selectable={false}>
143
156
  + New
144
157
  </text>
145
158
  </box>
@@ -166,6 +179,7 @@ interface SessionChipProps {
166
179
  onMouseDrag: (event: OtuiMouseEvent) => void
167
180
  onMouseUp: (event: OtuiMouseEvent) => void
168
181
  onMouseDragEnd: (event: OtuiMouseEvent) => void
182
+ rightClickMenu: Array<[string, () => void]>
169
183
  }
170
184
 
171
185
  function SessionChip({
@@ -177,29 +191,29 @@ function SessionChip({
177
191
  onMouseDragEnd,
178
192
  onMouseUp,
179
193
  onRef,
194
+ rightClickMenu,
180
195
  session,
181
196
  status,
182
197
  }: SessionChipProps) {
183
- const theme = useTheme()
184
- const selectionBg = useBg('list.activeSelectionBackground')
198
+ const t = useTokens()
199
+ const selectionBg = useBg('selected')
185
200
  const showSpinner = status.working
186
201
  const showWaiting = status.waiting
187
202
  const spinner = useBusySpinner(showSpinner)
188
- const labelColor = active
189
- ? theme.colors['editor.foreground']
190
- : theme.colors['descriptionForeground']
203
+ const labelColor = active ? t.palette.ink : t.muted
191
204
  const bgColor = dragging || active ? selectionBg : undefined
192
- const idleColor = theme.colors['gitDecoration.addedResourceForeground'] ?? ''
193
- const workingColor = theme.colors['textLink.foreground'] ?? ''
194
- const waitingColor = theme.colors['editorWarning.foreground'] ?? ''
205
+ const idleColor = t.palette.success
206
+ const workingColor = t.palette.primary
207
+ const waitingColor = t.palette.warning
195
208
 
196
209
  return (
197
- <box
210
+ <ContextMenuBox
198
211
  ref={onRef}
199
212
  flexDirection="row"
200
213
  paddingLeft={1}
201
214
  paddingRight={1}
202
215
  backgroundColor={bgColor}
216
+ rightClickMenu={rightClickMenu}
203
217
  onMouseDown={(e) => {
204
218
  e.preventDefault()
205
219
  onMouseDown(e)
@@ -233,9 +247,9 @@ function SessionChip({
233
247
  <text fg={labelColor} selectable={false}>
234
248
  [{index}] {session.name}
235
249
  </text>
236
- <text fg={theme.colors['editor.lineHighlightBackground']} selectable={false}>
250
+ <text fg={t.hover} selectable={false}>
237
251
  {' '}
238
252
  </text>
239
- </box>
253
+ </ContextMenuBox>
240
254
  )
241
255
  }
@@ -4,7 +4,7 @@ import { dispatchGlobal, runSideEffectGlobal } from '../../state/dispatch-ref'
4
4
  import { filterSessions } from '../../state/selectors'
5
5
  import { abbreviatePath } from '../path-format'
6
6
  import { orderSessionsForDisplay } from '../session-ordering'
7
- import { useTheme } from '../theme'
7
+ import { useTokens } from '../theme'
8
8
  import { uiTokens } from '../ui-tokens'
9
9
  import { Picker, type PickerItem } from './picker'
10
10
 
@@ -31,10 +31,7 @@ function formatSessionLine(
31
31
  }
32
32
 
33
33
  function getEmptyStateMessage(hasFilter: boolean): string {
34
- if (hasFilter) {
35
- return 'No matching sessions.'
36
- }
37
-
34
+ if (hasFilter) return 'No matching sessions.'
38
35
  return 'No sessions yet. Press Enter or n to create your first session.'
39
36
  }
40
37
 
@@ -46,7 +43,7 @@ export function SessionPickerModal({
46
43
  selectedIndex,
47
44
  sessions,
48
45
  }: SessionPickerModalProps) {
49
- const theme = useTheme()
46
+ const t = useTokens()
50
47
  const ordered = orderSessionsForDisplay(sessions)
51
48
  const baselineOrder = ordered.map((s) => s.id)
52
49
  const filtered = filterSessions(ordered, filter)
@@ -60,14 +57,10 @@ export function SessionPickerModal({
60
57
  onClick: () => runSideEffectGlobal({ type: 'confirm-selected-session' }),
61
58
  onDelete: () => runSideEffectGlobal({ type: 'delete-selected-session' }),
62
59
  subtitle: session.projectPath ? (
63
- <text fg={theme.colors['descriptionForeground']}>
64
- {abbreviatePath(session.projectPath)}
65
- </text>
60
+ <text fg={t.muted}>{abbreviatePath(session.projectPath)}</text>
66
61
  ) : undefined,
67
62
  title: (
68
- <text
69
- fg={active ? theme.colors['editor.foreground'] : theme.colors['descriptionForeground']}
70
- >
63
+ <text fg={active ? t.palette.ink : t.muted}>
71
64
  {formatSessionLine(session, currentSessionId, currentTabCount, displayIndex)}
72
65
  </text>
73
66
  ),
@@ -78,13 +71,7 @@ export function SessionPickerModal({
78
71
  key: '__create-new__',
79
72
  onClick: () => runSideEffectGlobal({ type: 'confirm-selected-session' }),
80
73
  title: (
81
- <text
82
- fg={
83
- selectedIndex === filtered.length
84
- ? theme.colors['editor.foreground']
85
- : theme.colors['descriptionForeground']
86
- }
87
- >
74
+ <text fg={selectedIndex === filtered.length ? t.palette.ink : t.muted}>
88
75
  Create new session
89
76
  </text>
90
77
  ),
@@ -104,7 +91,7 @@ export function SessionPickerModal({
104
91
  selectedIndex={selectedIndex}
105
92
  emptyState={
106
93
  filtered.length === 0 ? (
107
- <text fg={theme.colors['descriptionForeground']}>{getEmptyStateMessage(hasFilter)}</text>
94
+ <text fg={t.muted}>{getEmptyStateMessage(hasFilter)}</text>
108
95
  ) : undefined
109
96
  }
110
97
  onHover={(index) => dispatchGlobal({ index, type: 'set-modal-selection-index' })}
@@ -1,11 +1,10 @@
1
- import type { Theme } from '@brimveyn/aimux-config'
2
-
3
1
  import { type ScrollBoxRenderable } from '@opentui/core'
4
2
  import { memo, useMemo, useRef } from 'react'
5
3
 
6
4
  import { useAppStore } from '../../state/app-store'
7
5
  import { dispatchGlobal } from '../../state/dispatch-ref'
8
- import { getCurrentTheme, useBg, useTheme } from '../theme'
6
+ import { getCurrentTokens, type ThemeTokens, useBg, useTokens } from '../theme'
7
+ import { ContextMenuBox } from './context-menu-box'
9
8
  import { GitPaneWidget } from './git-pane-widget'
10
9
  import { buildTabGroupInfo } from './sidebar-group-metadata'
11
10
  import { TabItem } from './tab-item'
@@ -24,19 +23,19 @@ const GUTTER_PAD = '│'
24
23
  function getRowBackground({
25
24
  alternate,
26
25
  isActive,
27
- theme,
26
+ tokens,
28
27
  }: {
29
28
  isActive: boolean
30
29
  alternate: boolean
31
- theme: Theme
30
+ tokens: ThemeTokens
32
31
  }): string | undefined {
33
- if (isActive) return theme.colors['list.activeSelectionBackground']
34
- if (alternate) return theme.colors['editor.lineHighlightBackground']
32
+ if (isActive) return tokens.selected
33
+ if (alternate) return tokens.hover
35
34
  return undefined
36
35
  }
37
36
 
38
37
  const SidebarTop = memo(function SidebarTop() {
39
- const theme = useTheme()
38
+ const tokens = useTokens()
40
39
  const sidebarWidth = useAppStore((s) => s.sidebar.width)
41
40
  const currentSessionId = useAppStore((s) => s.currentSessionId)
42
41
  const sessions = useAppStore((s) => s.sessions)
@@ -48,22 +47,20 @@ const SidebarTop = memo(function SidebarTop() {
48
47
 
49
48
  return (
50
49
  <box flexDirection="column" flexShrink={0} gap={0}>
51
- <text fg={theme.colors['textLink.foreground']}>
50
+ <text fg={tokens.palette.primary}>
52
51
  <strong>aimux</strong>
53
52
  </text>
54
- <text fg={theme.colors['terminal.ansiMagenta']}>
55
- {currentSession ? currentSession.name : 'No session selected'}
56
- </text>
53
+ <text fg={tokens.accent}>{currentSession ? currentSession.name : 'No session selected'}</text>
57
54
  {branch ? (
58
55
  <box flexDirection="row">
59
- <text fg={theme.colors['textLink.foreground']}>{'\u{e702}'} </text>
60
- <text fg={theme.colors['descriptionForeground']}>{branch}</text>
56
+ <text fg={tokens.palette.primary}>{'\u{e702}'} </text>
57
+ <text fg={tokens.muted}>{branch}</text>
61
58
  </box>
62
59
  ) : null}
63
60
  <box
64
61
  flexDirection="row"
65
62
  paddingY={1}
66
- backgroundColor={theme.colors['list.activeSelectionBackground']}
63
+ backgroundColor={tokens.selected}
67
64
  justifyContent="center"
68
65
  marginTop={1}
69
66
  onMouseDown={(e) => {
@@ -71,25 +68,22 @@ const SidebarTop = memo(function SidebarTop() {
71
68
  dispatchGlobal({ type: 'open-new-tab-modal' })
72
69
  }}
73
70
  >
74
- <text fg={theme.colors['editor.foreground']}>+ New assistant</text>
71
+ <text fg={tokens.palette.ink}>+ New assistant</text>
75
72
  </box>
76
- <text fg={theme.colors['editor.lineHighlightBackground']}>
77
- {'·'.repeat(Math.max(0, sidebarWidth - 2))}
78
- </text>
73
+ <text fg={tokens.hover}>{'·'.repeat(Math.max(0, sidebarWidth - 2))}</text>
79
74
  </box>
80
75
  )
81
76
  })
82
77
 
83
78
  function renderGroupGutter(isGroupStart: boolean, isGroupMiddle: boolean, isGroupEnd: boolean) {
79
+ const t = getCurrentTokens()
84
80
  return (
85
81
  <box flexDirection="column" width={1} overflow="hidden">
86
- <text fg={getCurrentTheme().colors['terminal.ansiMagenta']}>
82
+ <text fg={t.accent}>
87
83
  {/* oxlint-disable-next-line no-nested-ternary */}
88
84
  {isGroupStart ? GUTTER_START : isGroupMiddle ? GUTTER_MIDDLE : GUTTER_PAD}
89
85
  </text>
90
- <text fg={getCurrentTheme().colors['terminal.ansiMagenta']}>
91
- {isGroupEnd ? GUTTER_END : GUTTER_PAD}
92
- </text>
86
+ <text fg={t.accent}>{isGroupEnd ? GUTTER_END : GUTTER_PAD}</text>
93
87
  </box>
94
88
  )
95
89
  }
@@ -99,7 +93,7 @@ interface TabsBodyProps {
99
93
  }
100
94
 
101
95
  const TabsBody = memo(function TabsBody({ onTabActivate }: TabsBodyProps) {
102
- const theme = useTheme()
96
+ const tokens = useTokens()
103
97
  const tabs = useAppStore((s) => s.tabs)
104
98
  const activeTabId = useAppStore((s) => s.activeTabId)
105
99
  const focusMode = useAppStore((s) => s.focusMode)
@@ -129,7 +123,7 @@ const TabsBody = memo(function TabsBody({ onTabActivate }: TabsBodyProps) {
129
123
  >
130
124
  {tabs.length === 0 ? (
131
125
  <box paddingTop={1}>
132
- <text fg={theme.colors['descriptionForeground']}>No tabs yet. Press Ctrl+n.</text>
126
+ <text fg={tokens.muted}>No tabs yet. Press Ctrl+n.</text>
133
127
  </box>
134
128
  ) : (
135
129
  tabs.map((tab, index) => {
@@ -145,7 +139,7 @@ const TabsBody = memo(function TabsBody({ onTabActivate }: TabsBodyProps) {
145
139
  return (
146
140
  <box
147
141
  key={tab.id}
148
- backgroundColor={getRowBackground({ alternate, isActive, theme })}
142
+ backgroundColor={getRowBackground({ alternate, isActive, tokens })}
149
143
  flexDirection="row"
150
144
  onMouseDown={(event) => {
151
145
  event.stopPropagation()
@@ -171,8 +165,8 @@ const TabsBody = memo(function TabsBody({ onTabActivate }: TabsBodyProps) {
171
165
  })
172
166
 
173
167
  export function Sidebar({ onTabActivate }: SidebarProps) {
174
- const theme = useTheme()
175
- const sidebarBg = useBg('sideBar.background')
168
+ const tokens = useTokens()
169
+ const sidebarBg = useBg('elevated')
176
170
  const sidebarVisible = useAppStore((s) => s.sidebar.visible)
177
171
  const sidebarWidth = useAppStore((s) => s.sidebar.width)
178
172
  const gitPane = useAppStore((s) => s.gitPane)
@@ -190,11 +184,7 @@ export function Sidebar({ onTabActivate }: SidebarProps) {
190
184
  const tabsGrow = gitEmbedded ? Math.max(1, Math.round((1 - gitPane.ratio) * 100)) : 1
191
185
  const gitGrow = gitEmbedded ? Math.max(1, Math.round(gitPane.ratio * 100)) : 0
192
186
 
193
- const separator = (
194
- <text fg={theme.colors['editor.lineHighlightBackground']}>
195
- {'·'.repeat(Math.max(0, sidebarWidth - 2))}
196
- </text>
197
- )
187
+ const separator = <text fg={tokens.hover}>{'·'.repeat(Math.max(0, sidebarWidth - 2))}</text>
198
188
  const gitBody = gitEmbedded ? (
199
189
  <box flexDirection="column" flexGrow={gitGrow} flexShrink={1} flexBasis={0} overflow="hidden">
200
190
  <GitPaneWidget pollingEnabled={gitPane.visible} />
@@ -202,12 +192,23 @@ export function Sidebar({ onTabActivate }: SidebarProps) {
202
192
  ) : null
203
193
 
204
194
  return (
205
- <box
195
+ <ContextMenuBox
206
196
  width={sidebarWidth}
207
197
  padding={0}
208
198
  flexDirection="column"
209
199
  backgroundColor={sidebarBg}
210
200
  gap={0}
201
+ rightClickMenu={[
202
+ ['Hide sidebar', () => dispatchGlobal({ type: 'toggle-sidebar' })],
203
+ ['Toggle git pane', () => dispatchGlobal({ type: 'toggle-git-pane' })],
204
+ [
205
+ 'Toggle diff mode',
206
+ () => {
207
+ dispatchGlobal({ type: 'enter-git-mode' })
208
+ dispatchGlobal({ type: 'git-mode-toggle-diff-view' })
209
+ },
210
+ ],
211
+ ]}
211
212
  onMouseDown={() => {
212
213
  if (focusMode === 'terminal-input') {
213
214
  dispatchGlobal({ focusMode: 'navigation', type: 'set-focus-mode' })
@@ -236,6 +237,6 @@ export function Sidebar({ onTabActivate }: SidebarProps) {
236
237
  {gitBody}
237
238
  </>
238
239
  ) : null}
239
- </box>
240
+ </ContextMenuBox>
240
241
  )
241
242
  }
@@ -1,4 +1,4 @@
1
- import { useTheme } from '../theme'
1
+ import { useTokens } from '../theme'
2
2
  import { uiTokens } from '../ui-tokens'
3
3
  import { InputField } from './input-field'
4
4
  import { ModalShell } from './modal-shell'
@@ -16,7 +16,7 @@ export function SnippetEditorModal({
16
16
  snippetContent,
17
17
  snippetName,
18
18
  }: SnippetEditorModalProps) {
19
- const theme = useTheme()
19
+ const t = useTokens()
20
20
  const nameActive = activeField === 'name'
21
21
  const contentActive = activeField === 'content'
22
22
 
@@ -27,26 +27,12 @@ export function SnippetEditorModal({
27
27
  width={uiTokens.modalWidth.xl}
28
28
  >
29
29
  <box flexDirection="column">
30
- <text
31
- fg={
32
- nameActive ? theme.colors['editor.foreground'] : theme.colors['descriptionForeground']
33
- }
34
- >
35
- Name
36
- </text>
30
+ <text fg={nameActive ? t.palette.ink : t.muted}>Name</text>
37
31
  <InputField active={nameActive} value={snippetName} />
38
32
  </box>
39
33
 
40
34
  <box flexDirection="column">
41
- <text
42
- fg={
43
- contentActive
44
- ? theme.colors['editor.foreground']
45
- : theme.colors['descriptionForeground']
46
- }
47
- >
48
- Content
49
- </text>
35
+ <text fg={contentActive ? t.palette.ink : t.muted}>Content</text>
50
36
  <InputField active={contentActive} value={snippetContent} />
51
37
  </box>
52
38
  </ModalShell>
@@ -2,7 +2,7 @@ import type { SnippetRecord } from '../../state/types'
2
2
 
3
3
  import { dispatchGlobal, runSideEffectGlobal } from '../../state/dispatch-ref'
4
4
  import { filterSnippets } from '../../state/selectors'
5
- import { useTheme } from '../theme'
5
+ import { useTokens } from '../theme'
6
6
  import { uiTokens } from '../ui-tokens'
7
7
  import { Picker, type PickerItem } from './picker'
8
8
 
@@ -27,7 +27,7 @@ export function SnippetPickerModal({
27
27
  selectedIndex,
28
28
  snippets,
29
29
  }: SnippetPickerModalProps) {
30
- const theme = useTheme()
30
+ const t = useTokens()
31
31
  const filtered = filterSnippets(snippets, filter)
32
32
 
33
33
  const items: PickerItem[] = filtered.map((snippet, index) => {
@@ -40,13 +40,9 @@ export function SnippetPickerModal({
40
40
  },
41
41
  onDelete: () => runSideEffectGlobal({ type: 'delete-selected-snippet' }),
42
42
  onEdit: () => runSideEffectGlobal({ type: 'edit-selected-snippet' }),
43
- subtitle: (
44
- <text fg={theme.colors['descriptionForeground']}>{truncateContent(snippet.content)}</text>
45
- ),
43
+ subtitle: <text fg={t.muted}>{truncateContent(snippet.content)}</text>,
46
44
  title: (
47
- <text
48
- fg={active ? theme.colors['editor.foreground'] : theme.colors['descriptionForeground']}
49
- >
45
+ <text fg={active ? t.palette.ink : t.muted}>
50
46
  <strong>{snippet.name}</strong>
51
47
  </text>
52
48
  ),
@@ -64,7 +60,7 @@ export function SnippetPickerModal({
64
60
  items={items}
65
61
  selectedIndex={selectedIndex}
66
62
  emptyState={
67
- <text fg={theme.colors['descriptionForeground']}>
63
+ <text fg={t.muted}>
68
64
  {filter ? 'No matching snippets.' : 'No snippets yet. Press n to create one.'}
69
65
  </text>
70
66
  }
@@ -11,7 +11,7 @@ import {
11
11
  type PaneRect,
12
12
  type SplitDirection,
13
13
  } from '../../state/layout-tree'
14
- import { useTheme } from '../theme'
14
+ import { useTokens } from '../theme'
15
15
  import { TerminalPane } from './terminal-pane'
16
16
 
17
17
  const PANE_CHROME = PANE_BORDER
@@ -58,7 +58,7 @@ export function SplitLayout({
58
58
  onTerminalScrollEvent,
59
59
  tabs,
60
60
  }: SplitLayoutProps) {
61
- const theme = useTheme()
61
+ const t = useTokens()
62
62
  if (node.type === 'leaf') {
63
63
  const tab = tabs.find((t) => t.id === node.tabId)
64
64
  const isActive = node.tabId === activeTabId
@@ -135,7 +135,7 @@ export function SplitLayout({
135
135
  <box
136
136
  minWidth={node.direction === 'vertical' ? 1 : undefined}
137
137
  minHeight={node.direction === 'horizontal' ? 1 : undefined}
138
- backgroundColor={theme.colors['editorGroup.border']}
138
+ backgroundColor={t.border}
139
139
  onMouseDown={(e: OtuiMouseEvent) => {
140
140
  e.preventDefault()
141
141
  if (!onSeparatorDragStart) return
@@ -4,22 +4,21 @@ 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 { getCurrentTheme, useBg, useTheme } from '../theme'
7
+ import { getCurrentTokens, useBg, useTokens } from '../theme'
8
8
 
9
9
  function getModeColor(focusMode: AppState['focusMode']): string {
10
- const t = getCurrentTheme()
10
+ const t = getCurrentTokens()
11
11
  switch (focusMode) {
12
12
  case 'terminal-input':
13
- return t.colors['textLink.foreground']
13
+ return t.palette.primary
14
14
  case 'modal':
15
- return t.colors['editorWarning.foreground']
16
15
  case 'command-edit':
17
- return t.colors['editorWarning.foreground']
16
+ return t.palette.warning
18
17
  case 'git':
19
- return t.colors['gitDecoration.addedResourceForeground']
18
+ return t.palette.success
20
19
  case 'navigation':
21
20
  default:
22
- return t.colors['terminal.ansiMagenta']
21
+ return t.accent
23
22
  }
24
23
  }
25
24
 
@@ -40,8 +39,8 @@ function getModeLabel(focusMode: AppState['focusMode']): string {
40
39
  }
41
40
 
42
41
  export function StatusBar() {
43
- const theme = useTheme()
44
- const headerBg = useBg('sideBarSectionHeader.background')
42
+ const t = useTokens()
43
+ const headerBg = useBg('elevated')
45
44
  const state = useAppStore((s) => s)
46
45
  const activeTab = state.tabs.find((tab) => tab.id === state.activeTabId)
47
46
  const config = useKeymap()
@@ -60,13 +59,13 @@ export function StatusBar() {
60
59
  <box width="100%" flexDirection="row">
61
60
  <text fg={getModeColor(state.focusMode)}>[{getModeLabel(state.focusMode)}]</text>
62
61
  <text> </text>
63
- <text fg={theme.colors['editor.foreground']}>{model.left}</text>
62
+ <text fg={t.palette.ink}>{model.left}</text>
64
63
  </box>
65
64
  <box width="100%" flexDirection="row" justifyContent="space-between">
66
- <text fg={theme.colors['descriptionForeground']}>{model.right}</text>
65
+ <text fg={t.muted}>{model.right}</text>
67
66
  <box flexDirection="row" gap={2}>
68
- {model.help ? <text fg={theme.colors['descriptionForeground']}>{model.help}</text> : null}
69
- <text fg={theme.colors['editor.lineHighlightBackground']}>v{APP_VERSION}</text>
67
+ {model.help ? <text fg={t.muted}>{model.help}</text> : null}
68
+ <text fg={t.hover}>v{APP_VERSION}</text>
70
69
  </box>
71
70
  </box>
72
71
  </box>
@@ -1,24 +1,21 @@
1
1
  import type { ReactNode } from 'react'
2
2
 
3
- import type { ThemeColorMap } from '../themes'
4
-
5
- import { useBg } from '../theme'
3
+ import { type SurfaceToken, useBg } from '../theme'
6
4
 
7
5
  type SurfaceTone = 'muted' | 'elevated' | 'selected' | 'input' | 'inputActive'
8
6
 
9
- function toneToColorKey(tone: SurfaceTone): keyof ThemeColorMap {
7
+ function toneToToken(tone: SurfaceTone): SurfaceToken {
10
8
  switch (tone) {
11
9
  case 'elevated':
12
- return 'sideBar.background'
10
+ return 'elevated'
13
11
  case 'selected':
14
- return 'list.activeSelectionBackground'
15
- case 'input':
16
- return 'editor.background'
17
12
  case 'inputActive':
18
- return 'list.activeSelectionBackground'
13
+ return 'selected'
14
+ case 'input':
15
+ return 'base'
19
16
  case 'muted':
20
17
  default:
21
- return 'sideBarSectionHeader.background'
18
+ return 'elevated'
22
19
  }
23
20
  }
24
21
 
@@ -47,7 +44,7 @@ export function Surface({
47
44
  tone = 'muted',
48
45
  width,
49
46
  }: SurfaceProps) {
50
- const bg = useBg(toneToColorKey(tone))
47
+ const bg = useBg(toneToToken(tone))
51
48
  return (
52
49
  <box
53
50
  backgroundColor={bg}