@brimveyn/aimux 1.8.0 → 1.9.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 (80) hide show
  1. package/package.json +2 -2
  2. package/src/app-runtime/side-effects.ts +6 -1
  3. package/src/app.tsx +20 -16
  4. package/src/input/keymap/describe-bindings.ts +63 -2
  5. package/src/input/modes/bridge.ts +1 -0
  6. package/src/input/modes/transitions.ts +3 -1
  7. package/src/input/modes/types.ts +1 -0
  8. package/src/pty/terminal-snapshot.ts +7 -5
  9. package/src/services/ai-usage/adapters/claude.ts +102 -26
  10. package/src/services/ai-usage/adapters/codex.ts +83 -32
  11. package/src/services/ai-usage/cache.ts +60 -0
  12. package/src/services/ai-usage/pace.ts +79 -0
  13. package/src/services/ai-usage/provider.ts +43 -20
  14. package/src/services/ai-usage/types.ts +31 -0
  15. package/src/state/ai-usage-store.ts +10 -3
  16. package/src/state/reducers/git-mode-state.ts +58 -0
  17. package/src/state/reducers/modal-state.ts +13 -0
  18. package/src/state/types.ts +18 -6
  19. package/src/ui/breaking-update-screen.tsx +2 -2
  20. package/src/ui/components/git/diff-renderer/build-rows.ts +467 -0
  21. package/src/ui/components/{diff-renderer → git/diff-renderer}/fold-strip.tsx +1 -1
  22. package/src/ui/components/git/diff-renderer/highlight.ts +102 -0
  23. package/src/ui/components/{diff-renderer → git/diff-renderer}/pierre-diff.tsx +23 -15
  24. package/src/ui/components/git/diff-renderer/prepare-diff.ts +62 -0
  25. package/src/ui/components/{diff-renderer → git/diff-renderer}/split-view.tsx +122 -47
  26. package/src/ui/components/{diff-renderer → git/diff-renderer}/stacked-view.tsx +106 -35
  27. package/src/ui/components/{diff-renderer → git/diff-renderer}/use-diff-prefetch.ts +8 -17
  28. package/src/ui/components/git/diff-renderer/use-diff-preparation.ts +240 -0
  29. package/src/ui/components/git/diff-renderer/use-segment-virtualization.ts +95 -0
  30. package/src/ui/components/{git-panel.tsx → git/git-panel.tsx} +9 -5
  31. package/src/ui/components/{git-view.tsx → git/git-view.tsx} +70 -37
  32. package/src/ui/components/{git-pane-context-menu.ts → git/pane/git-pane-context-menu.ts} +2 -2
  33. package/src/ui/components/{git-pane-widget.tsx → git/pane/git-pane-widget.tsx} +4 -4
  34. package/src/ui/components/{session-bar.tsx → layout/session-bar.tsx} +36 -13
  35. package/src/ui/components/{sidebar-group-metadata.ts → layout/sidebar/sidebar-group-metadata.ts} +2 -2
  36. package/src/ui/components/{sidebar.tsx → layout/sidebar/sidebar.tsx} +6 -6
  37. package/src/ui/components/{tab-item.tsx → layout/sidebar/tab-item.tsx} +5 -5
  38. package/src/ui/components/{use-sidebar-branch.ts → layout/sidebar/use-sidebar-branch.ts} +1 -1
  39. package/src/ui/components/{split-layout.tsx → layout/split-layout.tsx} +5 -5
  40. package/src/ui/components/{status-bar.tsx → layout/status-bar.tsx} +7 -7
  41. package/src/ui/components/{terminal-pane.tsx → layout/terminal-pane.tsx} +8 -8
  42. package/src/ui/components/modals/app/ai-usage-modal.tsx +190 -0
  43. package/src/ui/components/{help-modal.tsx → modals/app/help-modal.tsx} +6 -6
  44. package/src/ui/components/{update-available-modal.tsx → modals/app/update-available-modal.tsx} +4 -4
  45. package/src/ui/components/{git-commit-modal.tsx → modals/git/git-commit-modal.tsx} +8 -8
  46. package/src/ui/components/modals/sessions/create-session-modal.tsx +79 -0
  47. package/src/ui/components/modals/sessions/session-name-modal.tsx +10 -0
  48. package/src/ui/components/{session-picker-modal.tsx → modals/sessions/session-picker-modal.tsx} +8 -8
  49. package/src/ui/components/modals/shared/form.tsx +164 -0
  50. package/src/ui/components/{modal-keybinds-overlay.tsx → modals/shared/modal-keybinds-overlay.tsx} +7 -7
  51. package/src/ui/components/{modal-shell.tsx → modals/shared/modal-shell.tsx} +1 -1
  52. package/src/ui/components/{picker.tsx → modals/shared/picker.tsx} +3 -3
  53. package/src/ui/components/modals/snippets/snippet-editor-modal.tsx +30 -0
  54. package/src/ui/components/{snippet-picker-modal.tsx → modals/snippets/snippet-picker-modal.tsx} +6 -6
  55. package/src/ui/components/{new-tab-modal.tsx → modals/tabs/new-tab-modal.tsx} +17 -20
  56. package/src/ui/components/{theme-picker-modal.tsx → modals/themes/theme-picker-modal.tsx} +6 -6
  57. package/src/ui/components/{ai-usage-indicator.tsx → overlays/ai-usage/ai-usage-indicator.tsx} +56 -24
  58. package/src/ui/components/{context-menu-box.tsx → overlays/context-menu/context-menu-box.tsx} +1 -1
  59. package/src/ui/components/{context-menu-overlay.tsx → overlays/context-menu/context-menu-overlay.tsx} +3 -3
  60. package/src/ui/components/{pending-chord-overlay.tsx → overlays/pending-chord-overlay.tsx} +6 -6
  61. package/src/ui/components/{bare-input.tsx → primitives/bare-input.tsx} +2 -2
  62. package/src/ui/components/{input-field.tsx → primitives/input-field.tsx} +2 -2
  63. package/src/ui/components/{list-item.tsx → primitives/list-item.tsx} +1 -1
  64. package/src/ui/components/{surface.tsx → primitives/surface.tsx} +1 -1
  65. package/src/ui/root.tsx +26 -23
  66. package/src/ui/theme-store.ts +30 -26
  67. package/src/ui/themes.ts +1 -0
  68. package/src/ui/ai-usage/controller.ts +0 -35
  69. package/src/ui/components/ai-usage-popover.tsx +0 -152
  70. package/src/ui/components/create-session-modal.tsx +0 -96
  71. package/src/ui/components/diff-renderer/build-rows.ts +0 -349
  72. package/src/ui/components/diff-renderer/highlight.ts +0 -44
  73. package/src/ui/components/diff-renderer/prepare-diff.ts +0 -65
  74. package/src/ui/components/diff-renderer/use-diff-preparation.ts +0 -106
  75. package/src/ui/components/session-name-modal.tsx +0 -11
  76. package/src/ui/components/snippet-editor-modal.tsx +0 -40
  77. /package/src/ui/components/{diff-renderer → git/diff-renderer}/filetype.ts +0 -0
  78. /package/src/ui/components/{diff-renderer → git/diff-renderer}/index.ts +0 -0
  79. /package/src/ui/components/{sidebar-scroll.ts → layout/sidebar/sidebar-scroll.ts} +0 -0
  80. /package/src/ui/components/{use-sidebar-auto-scroll.ts → layout/sidebar/use-sidebar-auto-scroll.ts} +0 -0
@@ -0,0 +1,30 @@
1
+ import { uiTokens } from '../../../ui-tokens'
2
+ import { Form, TextField } from '../shared/form'
3
+
4
+ interface SnippetEditorModalProps {
5
+ activeField: 'name' | 'content'
6
+ snippetName: string
7
+ snippetContent: string
8
+ isEditing: boolean
9
+ }
10
+
11
+ export function SnippetEditorModal({
12
+ activeField,
13
+ isEditing,
14
+ snippetContent,
15
+ snippetName,
16
+ }: SnippetEditorModalProps) {
17
+ const nameActive = activeField === 'name'
18
+ const contentActive = activeField === 'content'
19
+
20
+ return (
21
+ <Form
22
+ title={isEditing ? 'Edit snippet' : 'Create snippet'}
23
+ keybindsModeId="modal.snippet-editor"
24
+ width={uiTokens.modalWidth.xl}
25
+ >
26
+ <TextField active={nameActive} label="Name" value={snippetName} />
27
+ <TextField active={contentActive} label="Content" value={snippetContent} />
28
+ </Form>
29
+ )
30
+ }
@@ -1,10 +1,10 @@
1
- import type { SnippetRecord } from '../../state/types'
1
+ import type { SnippetRecord } from '../../../../state/types'
2
2
 
3
- import { dispatchGlobal, runSideEffectGlobal } from '../../state/dispatch-ref'
4
- import { filterSnippets } from '../../state/selectors'
5
- import { useTokens } from '../theme'
6
- import { uiTokens } from '../ui-tokens'
7
- import { Picker, type PickerItem } from './picker'
3
+ import { dispatchGlobal, runSideEffectGlobal } from '../../../../state/dispatch-ref'
4
+ import { filterSnippets } from '../../../../state/selectors'
5
+ import { useTokens } from '../../../theme'
6
+ import { uiTokens } from '../../../ui-tokens'
7
+ import { Picker, type PickerItem } from '../shared/picker'
8
8
 
9
9
  interface SnippetPickerModalProps {
10
10
  snippets: SnippetRecord[]
@@ -1,13 +1,12 @@
1
- import type { AssistantId } from '../../state/types'
1
+ import type { AssistantId } from '../../../../state/types'
2
2
 
3
- import { getAllAssistantOptions, getAssistantOption } from '../../pty/command-registry'
4
- import { dispatchGlobal, runSideEffectGlobal } from '../../state/dispatch-ref'
5
- import { filterAssistants } from '../../state/selectors'
6
- import { useTokens } from '../theme'
7
- import { uiTokens } from '../ui-tokens'
8
- import { InputField } from './input-field'
9
- import { ModalShell } from './modal-shell'
10
- import { Picker, type PickerItem } from './picker'
3
+ import { getAllAssistantOptions, getAssistantOption } from '../../../../pty/command-registry'
4
+ import { dispatchGlobal, runSideEffectGlobal } from '../../../../state/dispatch-ref'
5
+ import { filterAssistants } from '../../../../state/selectors'
6
+ import { useTokens } from '../../../theme'
7
+ import { uiTokens } from '../../../ui-tokens'
8
+ import { Form, TextField } from '../shared/form'
9
+ import { Picker, type PickerItem } from '../shared/picker'
11
10
 
12
11
  interface NewTabModalProps {
13
12
  selectedIndex: number
@@ -33,21 +32,19 @@ export function NewTabModal({
33
32
  getAllAssistantOptions(customCommands).find((o) => o.id === editingCommand) ??
34
33
  getAssistantOption(0)
35
34
  return (
36
- <ModalShell
35
+ <Form
37
36
  title={`Edit command — ${option.label}`}
38
37
  keybindsModeId="modal.new-tab.editing-command"
39
38
  width={uiTokens.modalWidth.md}
40
39
  >
41
- <box flexDirection="column">
42
- <text fg={t.muted}>Custom command (blank to reset to default: {option.command})</text>
43
- <InputField
44
- active
45
- value={editBuffer}
46
- cursorPos={cursorPos}
47
- placeholder={option.command}
48
- />
49
- </box>
50
- </ModalShell>
40
+ <TextField
41
+ active
42
+ description={<>blank to reset to default: {option.command}</>}
43
+ value={editBuffer}
44
+ cursorPos={cursorPos}
45
+ placeholder={option.command}
46
+ />
47
+ </Form>
51
48
  )
52
49
  }
53
50
 
@@ -1,11 +1,11 @@
1
1
  import { useLayoutEffect, useMemo } from 'react'
2
2
 
3
- import { dispatchGlobal, runSideEffectGlobal } from '../../state/dispatch-ref'
4
- import { filterThemeIds } from '../filter-themes'
5
- import { useTokens, useTransparent } from '../theme'
6
- import { type ThemeId, THEMES } from '../themes'
7
- import { uiTokens } from '../ui-tokens'
8
- import { Picker, type PickerItem } from './picker'
3
+ import { dispatchGlobal, runSideEffectGlobal } from '../../../../state/dispatch-ref'
4
+ import { filterThemeIds } from '../../../filter-themes'
5
+ import { useTokens, useTransparent } from '../../../theme'
6
+ import { type ThemeId, THEMES } from '../../../themes'
7
+ import { uiTokens } from '../../../ui-tokens'
8
+ import { Picker, type PickerItem } from '../shared/picker'
9
9
 
10
10
  interface ThemePickerModalProps {
11
11
  currentThemeId: ThemeId
@@ -1,8 +1,8 @@
1
1
  import type { AIUsageTool } from '@brimveyn/aimux-config'
2
2
 
3
- import { useAIUsageStore } from '../../state/ai-usage-store'
4
- import { toggleAIUsagePopover } from '../ai-usage/controller'
5
- import { useTokens } from '../theme'
3
+ import { useAIUsageStore } from '../../../../state/ai-usage-store'
4
+ import { dispatchGlobal } from '../../../../state/dispatch-ref'
5
+ import { useBg, useTokens } from '../../../theme'
6
6
 
7
7
  const TOOL_ICON: Record<AIUsageTool, string> = {
8
8
  claude: 'CC',
@@ -19,7 +19,7 @@ function formatTokens(total: number): string {
19
19
  return String(total)
20
20
  }
21
21
 
22
- function buildBar(percent: number): { filled: string; empty: string } {
22
+ function buildBar(percent: number): { empty: string; filled: string } {
23
23
  let filledCount = 0
24
24
  for (let i = 0; i < BAR_SEGMENTS; i++) {
25
25
  if (percent > i * (100 / BAR_SEGMENTS)) filledCount++
@@ -48,6 +48,7 @@ function formatResetIn(snap: {
48
48
 
49
49
  export function AIUsageIndicator() {
50
50
  const t = useTokens()
51
+ const bg = useBg('elevated')
51
52
  const enabled = useAIUsageStore((s) => s.enabled)
52
53
  const snapshots = useAIUsageStore((s) => s.snapshots)
53
54
 
@@ -58,35 +59,49 @@ export function AIUsageIndicator() {
58
59
  .map((tool) => ({ snap: snapshots[tool], tool }))
59
60
  .filter((entry) => entry.snap !== undefined)
60
61
 
62
+ const openModal = (e: { preventDefault: () => void; stopPropagation: () => void }) => {
63
+ e.preventDefault()
64
+ e.stopPropagation()
65
+ dispatchGlobal({ type: 'open-ai-usage-modal' })
66
+ }
67
+
61
68
  if (entries.length === 0) {
62
69
  return (
63
- <box flexDirection="row" gap={1}>
70
+ <box
71
+ flexDirection="row"
72
+ paddingLeft={1}
73
+ paddingRight={1}
74
+ backgroundColor={bg}
75
+ onMouseDown={openModal}
76
+ >
64
77
  <text fg={t.muted}>…</text>
65
78
  </box>
66
79
  )
67
80
  }
68
81
 
69
82
  return (
70
- <box
71
- flexDirection="row"
72
- gap={2}
73
- onMouseDown={(e) => {
74
- e.preventDefault()
75
- e.stopPropagation()
76
- if (e.button !== 0) return
77
- toggleAIUsagePopover(e.x, e.y)
78
- }}
79
- >
83
+ <box flexDirection="row" gap={1}>
80
84
  {entries.map(({ snap, tool }) => {
81
85
  if (!snap) return null
82
86
  const icon = TOOL_ICON[tool]
83
- if (snap.error) {
87
+
88
+ if (snap.error && !snap.stale) {
84
89
  return (
85
- <text key={tool} fg={t.palette.error} selectable={false}>
86
- {icon}
87
- </text>
90
+ <box
91
+ key={tool}
92
+ flexDirection="row"
93
+ paddingLeft={1}
94
+ paddingRight={1}
95
+ backgroundColor={bg}
96
+ onMouseDown={openModal}
97
+ >
98
+ <text fg={t.palette.error} selectable={false}>
99
+ {`${icon} —`}
100
+ </text>
101
+ </box>
88
102
  )
89
103
  }
104
+
90
105
  if (snap.percent !== null) {
91
106
  const p = Math.round(snap.percent)
92
107
  let color = t.palette.success
@@ -99,9 +114,16 @@ export function AIUsageIndicator() {
99
114
  const reset = formatResetIn(snap)
100
115
  const pctText = `${String(p).padStart(2, ' ')}%`
101
116
  return (
102
- <box key={tool} flexDirection="row">
117
+ <box
118
+ key={tool}
119
+ flexDirection="row"
120
+ paddingLeft={1}
121
+ paddingRight={1}
122
+ backgroundColor={bg}
123
+ onMouseDown={openModal}
124
+ >
103
125
  <text fg={color} selectable={false}>
104
- {icon}{' '}
126
+ {`${icon} `}
105
127
  </text>
106
128
  <text fg={color} selectable={false}>
107
129
  {filled}
@@ -120,10 +142,20 @@ export function AIUsageIndicator() {
120
142
  </box>
121
143
  )
122
144
  }
145
+
123
146
  return (
124
- <text key={tool} fg={t.muted} selectable={false}>
125
- {icon} {formatTokens(snap.tokens.total)}
126
- </text>
147
+ <box
148
+ key={tool}
149
+ flexDirection="row"
150
+ paddingLeft={1}
151
+ paddingRight={1}
152
+ backgroundColor={bg}
153
+ onMouseDown={openModal}
154
+ >
155
+ <text fg={t.muted} selectable={false}>
156
+ {`${icon} ${formatTokens(snap.tokens.total)}`}
157
+ </text>
158
+ </box>
127
159
  )
128
160
  })}
129
161
  </box>
@@ -1,7 +1,7 @@
1
1
  import type { BoxRenderable, MouseEvent as OtuiMouseEvent } from '@opentui/core'
2
2
  import type { BoxProps } from '@opentui/react'
3
3
 
4
- import { type ContextMenuItem, openContextMenu } from '../context-menu/controller'
4
+ import { type ContextMenuItem, openContextMenu } from '../../../context-menu/controller'
5
5
 
6
6
  interface ContextMenuBoxProps extends BoxProps {
7
7
  rightClickMenu?: ContextMenuItem[]
@@ -1,13 +1,13 @@
1
1
  import { useKeyboard } from '@opentui/react'
2
2
  import { useEffect, useState } from 'react'
3
3
 
4
- import { useAppStore } from '../../state/app-store'
4
+ import { useAppStore } from '../../../../state/app-store'
5
5
  import {
6
6
  closeContextMenu,
7
7
  type ContextMenuState,
8
8
  subscribeContextMenu,
9
- } from '../context-menu/controller'
10
- import { useTokens } from '../theme'
9
+ } from '../../../context-menu/controller'
10
+ import { useTokens } from '../../../theme'
11
11
 
12
12
  export function ContextMenuOverlay() {
13
13
  const [menu, setMenu] = useState<ContextMenuState | null>(null)
@@ -1,9 +1,9 @@
1
- import { parseKeyNotation } from '../../input/keymap/key-chord'
2
- import { formatChord } from '../../input/keymap/key-format'
3
- import { useAppStore } from '../../state/app-store'
4
- import { useKeymap } from '../keymap-context'
5
- import { useTokens } from '../theme'
6
- import { Surface } from './surface'
1
+ import { parseKeyNotation } from '../../../input/keymap/key-chord'
2
+ import { formatChord } from '../../../input/keymap/key-format'
3
+ import { useAppStore } from '../../../state/app-store'
4
+ import { useKeymap } from '../../keymap-context'
5
+ import { useTokens } from '../../theme'
6
+ import { Surface } from '../primitives/surface'
7
7
 
8
8
  export function PendingChordOverlay() {
9
9
  const t = useTokens()
@@ -1,4 +1,4 @@
1
- import { useTokens } from '../theme'
1
+ import { useTokens } from '../../theme'
2
2
 
3
3
  interface BareInputProps {
4
4
  value: string
@@ -9,7 +9,7 @@ interface BareInputProps {
9
9
  export function BareInput({ cursorPos, placeholder = '', value }: BareInputProps) {
10
10
  const t = useTokens()
11
11
  const fg = t.palette.ink
12
- const bg = t.palette.neutral
12
+ const bg = t.bg
13
13
  const placeholderFg = t.faint
14
14
 
15
15
  if (!value) {
@@ -1,4 +1,4 @@
1
- import { useTokens } from '../theme'
1
+ import { useTokens } from '../../theme'
2
2
  import { Surface } from './surface'
3
3
 
4
4
  interface InputFieldProps {
@@ -32,7 +32,7 @@ export function InputField({ active, cursorPos, placeholder, value }: InputField
32
32
  <Surface tone="inputActive" padding={1}>
33
33
  <text fg={fg}>
34
34
  {before}
35
- <span bg={t.palette.ink} fg={t.palette.neutral}>
35
+ <span bg={t.palette.ink} fg={t.bg}>
36
36
  {cursorDisplay}
37
37
  </span>
38
38
  {trailing}
@@ -1,6 +1,6 @@
1
1
  import type { ReactNode } from 'react'
2
2
 
3
- import { useTokens } from '../theme'
3
+ import { useTokens } from '../../theme'
4
4
  import { Surface } from './surface'
5
5
 
6
6
  export type ListItemDirection = 'row' | 'column'
@@ -1,6 +1,6 @@
1
1
  import type { ReactNode } from 'react'
2
2
 
3
- import { type SurfaceToken, useBg } from '../theme'
3
+ import { type SurfaceToken, useBg } from '../../theme'
4
4
 
5
5
  type SurfaceTone = 'muted' | 'elevated' | 'selected' | 'input' | 'inputActive'
6
6
 
package/src/ui/root.tsx CHANGED
@@ -8,28 +8,28 @@ import { useAppStore } from '../state/app-store'
8
8
  import { dispatchGlobal } from '../state/dispatch-ref'
9
9
  import { getGitPaneWidthFromRatio } from '../state/git-pane-sizing'
10
10
  import { getTreeForTab, PANE_BORDER, type SplitDirection } from '../state/layout-tree'
11
- import { AIUsagePopover } from './components/ai-usage-popover'
12
- import { ContextMenuBox } from './components/context-menu-box'
13
- import { ContextMenuOverlay } from './components/context-menu-overlay'
14
- import { CreateSessionModal } from './components/create-session-modal'
15
- import { GitCommitModal } from './components/git-commit-modal'
16
- import { buildGitPaneContextMenu } from './components/git-pane-context-menu'
17
- import { GitPaneWidget } from './components/git-pane-widget'
18
- import { GitView } from './components/git-view'
19
- import { HelpModal } from './components/help-modal'
20
- import { NewTabModal } from './components/new-tab-modal'
21
- import { PendingChordOverlay } from './components/pending-chord-overlay'
22
- import { SessionBar } from './components/session-bar'
23
- import { SessionNameModal } from './components/session-name-modal'
24
- import { SessionPickerModal } from './components/session-picker-modal'
25
- import { Sidebar } from './components/sidebar'
26
- import { SnippetEditorModal } from './components/snippet-editor-modal'
27
- import { SnippetPickerModal } from './components/snippet-picker-modal'
28
- import { SplitLayout } from './components/split-layout'
29
- import { StatusBar } from './components/status-bar'
30
- import { TerminalPane } from './components/terminal-pane'
31
- import { ThemePickerModal } from './components/theme-picker-modal'
32
- import { UpdateAvailableModal } from './components/update-available-modal'
11
+ import { GitView } from './components/git/git-view'
12
+ import { buildGitPaneContextMenu } from './components/git/pane/git-pane-context-menu'
13
+ import { GitPaneWidget } from './components/git/pane/git-pane-widget'
14
+ import { SessionBar } from './components/layout/session-bar'
15
+ import { Sidebar } from './components/layout/sidebar/sidebar'
16
+ import { SplitLayout } from './components/layout/split-layout'
17
+ import { StatusBar } from './components/layout/status-bar'
18
+ import { TerminalPane } from './components/layout/terminal-pane'
19
+ import { AIUsageModal } from './components/modals/app/ai-usage-modal'
20
+ import { HelpModal } from './components/modals/app/help-modal'
21
+ import { UpdateAvailableModal } from './components/modals/app/update-available-modal'
22
+ import { GitCommitModal } from './components/modals/git/git-commit-modal'
23
+ import { CreateSessionModal } from './components/modals/sessions/create-session-modal'
24
+ import { SessionNameModal } from './components/modals/sessions/session-name-modal'
25
+ import { SessionPickerModal } from './components/modals/sessions/session-picker-modal'
26
+ import { SnippetEditorModal } from './components/modals/snippets/snippet-editor-modal'
27
+ import { SnippetPickerModal } from './components/modals/snippets/snippet-picker-modal'
28
+ import { NewTabModal } from './components/modals/tabs/new-tab-modal'
29
+ import { ThemePickerModal } from './components/modals/themes/theme-picker-modal'
30
+ import { ContextMenuBox } from './components/overlays/context-menu/context-menu-box'
31
+ import { ContextMenuOverlay } from './components/overlays/context-menu/context-menu-overlay'
32
+ import { PendingChordOverlay } from './components/overlays/pending-chord-overlay'
33
33
  import { useBg, useTokens } from './theme'
34
34
 
35
35
  function getCreateSessionFields(modal: ModalState) {
@@ -172,6 +172,8 @@ function renderModal(
172
172
  cursorPos={modal.cursorPos}
173
173
  />
174
174
  )
175
+ case 'ai-usage':
176
+ return <AIUsageModal />
175
177
  case 'git-commit': {
176
178
  const titleText =
177
179
  modal.activeField === 'title' ? (modal.editBuffer ?? '') : modal.contentBuffer
@@ -275,11 +277,12 @@ export function RootView({
275
277
  if (inGitMode) {
276
278
  return (
277
279
  <box flexDirection="column" width="100%" height="100%" backgroundColor={editorBg}>
280
+ {sessionBarPosition === 'top' && <SessionBar forceVisible />}
278
281
  <GitView themeId={themeId} />
282
+ {sessionBarPosition === 'bottom' && <SessionBar forceVisible />}
279
283
  <StatusBar />
280
284
  <PendingChordOverlay />
281
285
  <ContextMenuOverlay />
282
- <AIUsagePopover />
283
286
  {renderModal(modal, {
284
287
  activeAssistant: activeTab?.assistant,
285
288
  createSessionFields,
@@ -5,22 +5,20 @@ import {
5
5
  accent,
6
6
  type AimuxPalette,
7
7
  type AimuxTheme,
8
- border,
8
+ computeSurfaces,
9
9
  diffAddBg,
10
10
  diffDeleteBg,
11
- elevated,
12
11
  extendPalette,
13
- faint,
14
- hover,
15
- muted,
16
- selected,
17
12
  type ThemeId,
13
+ type ThemeMode,
18
14
  THEMES,
19
15
  } from './themes'
20
16
 
21
17
  export interface ThemeTokens {
22
18
  /** `accent` with `primary` fallback. */
23
19
  accent: string
20
+ /** App-level background surface (derived from palette.neutral seed via OKLCH). */
21
+ bg: string
24
22
  /** Subtle border between panels. */
25
23
  border: string
26
24
  /** Background tint for inserted diff lines. */
@@ -40,29 +38,33 @@ export interface ThemeTokens {
40
38
  selected: string
41
39
  }
42
40
 
43
- function computeTokens(palette: AimuxPalette): ThemeTokens {
41
+ function computeTokens(palette: AimuxPalette, mode: ThemeMode): ThemeTokens {
42
+ const surfaces = computeSurfaces(palette, mode)
44
43
  return {
45
44
  accent: accent(palette),
46
- border: border(palette),
45
+ bg: surfaces.bg,
46
+ border: surfaces.border,
47
47
  diffAddBg: diffAddBg(palette),
48
48
  diffDeleteBg: diffDeleteBg(palette),
49
- elevated: elevated(palette),
50
- faint: faint(palette),
51
- hover: hover(palette),
52
- muted: muted(palette),
49
+ elevated: surfaces.elevated,
50
+ faint: surfaces.faint,
51
+ hover: surfaces.hover,
52
+ muted: surfaces.muted,
53
53
  palette,
54
- selected: selected(palette),
54
+ selected: surfaces.selected,
55
55
  }
56
56
  }
57
57
 
58
- // Memoize by palette reference so `useStore` selectors return stable objects.
58
+ // Memoize by (palette, mode) so `useStore` selectors return stable objects.
59
59
  let cachedPalette: AimuxPalette | null = null
60
+ let cachedMode: ThemeMode | null = null
60
61
  let cachedTokens: ThemeTokens | null = null
61
62
 
62
- function deriveTokens(palette: AimuxPalette): ThemeTokens {
63
- if (cachedPalette === palette && cachedTokens) return cachedTokens
63
+ function deriveTokens(palette: AimuxPalette, mode: ThemeMode): ThemeTokens {
64
+ if (cachedPalette === palette && cachedMode === mode && cachedTokens) return cachedTokens
64
65
  cachedPalette = palette
65
- cachedTokens = computeTokens(palette)
66
+ cachedMode = mode
67
+ cachedTokens = computeTokens(palette, mode)
66
68
  return cachedTokens
67
69
  }
68
70
 
@@ -78,12 +80,13 @@ const themeStore = createStore<ThemeStore>(() => ({ theme: DEFAULT, transparent:
78
80
 
79
81
  /** Subscribe to the active palette plus precomputed derived shades. */
80
82
  export function useTokens(): ThemeTokens {
81
- return useStore(themeStore, (s) => deriveTokens(s.theme.palette))
83
+ return useStore(themeStore, (s) => deriveTokens(s.theme.palette, s.theme.mode))
82
84
  }
83
85
 
84
86
  /** Synchronous tokens for non-React callers. */
85
87
  export function getCurrentTokens(): ThemeTokens {
86
- return deriveTokens(themeStore.getState().theme.palette)
88
+ const s = themeStore.getState()
89
+ return deriveTokens(s.theme.palette, s.theme.mode)
87
90
  }
88
91
 
89
92
  /** Synchronous snapshot for non-React callers (reducers, side effects). */
@@ -121,18 +124,19 @@ export function setTransparent(value: boolean): void {
121
124
 
122
125
  export type SurfaceToken = 'base' | 'elevated' | 'hover' | 'selected' | 'border'
123
126
 
124
- function resolveSurface(palette: AimuxPalette, token: SurfaceToken): string {
127
+ function resolveSurface(palette: AimuxPalette, mode: ThemeMode, token: SurfaceToken): string {
128
+ const surfaces = computeSurfaces(palette, mode)
125
129
  switch (token) {
126
130
  case 'base':
127
- return palette.neutral
131
+ return surfaces.bg
128
132
  case 'elevated':
129
- return elevated(palette)
133
+ return surfaces.elevated
130
134
  case 'hover':
131
- return hover(palette)
135
+ return surfaces.hover
132
136
  case 'selected':
133
- return selected(palette)
137
+ return surfaces.selected
134
138
  case 'border':
135
- return border(palette)
139
+ return surfaces.border
136
140
  }
137
141
  }
138
142
 
@@ -144,6 +148,6 @@ function resolveSurface(palette: AimuxPalette, token: SurfaceToken): string {
144
148
  export function useBg(token: SurfaceToken): string | undefined {
145
149
  return useStore(themeStore, (s) => {
146
150
  if (s.transparent && token === 'base') return undefined
147
- return resolveSurface(s.theme.palette, token)
151
+ return resolveSurface(s.theme.palette, s.theme.mode, token)
148
152
  })
149
153
  }
package/src/ui/themes.ts CHANGED
@@ -13,6 +13,7 @@ export type {
13
13
  export {
14
14
  accent,
15
15
  border,
16
+ computeSurfaces,
16
17
  diffAddBg,
17
18
  diffDeleteBg,
18
19
  elevated,
@@ -1,35 +0,0 @@
1
- export interface AIUsagePopoverState {
2
- anchorX: number
3
- anchorY: number
4
- }
5
-
6
- type Listener = (state: AIUsagePopoverState | null) => void
7
-
8
- let current: AIUsagePopoverState | null = null
9
- const listeners = new Set<Listener>()
10
-
11
- export function openAIUsagePopover(anchorX: number, anchorY: number): void {
12
- current = { anchorX, anchorY }
13
- for (const l of listeners) l(current)
14
- }
15
-
16
- export function closeAIUsagePopover(): void {
17
- if (current === null) return
18
- current = null
19
- for (const l of listeners) l(null)
20
- }
21
-
22
- export function toggleAIUsagePopover(anchorX: number, anchorY: number): void {
23
- if (current) {
24
- closeAIUsagePopover()
25
- } else {
26
- openAIUsagePopover(anchorX, anchorY)
27
- }
28
- }
29
-
30
- export function subscribeAIUsagePopover(listener: Listener): () => void {
31
- listeners.add(listener)
32
- return () => {
33
- listeners.delete(listener)
34
- }
35
- }