@brimveyn/aimux 1.7.1 → 1.7.3

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 (55) hide show
  1. package/package.json +3 -4
  2. package/src/app-runtime/pty-write.ts +15 -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/daemon/daemon.ts +7 -2
  8. package/src/input/modes/bridge.ts +1 -1
  9. package/src/input/modes/types.ts +6 -1
  10. package/src/input/raw-input-handler.ts +12 -3
  11. package/src/pty/terminal-snapshot.ts +4 -4
  12. package/src/state/reducers/modal-state.ts +3 -2
  13. package/src/state/reducers/session-state.ts +0 -7
  14. package/src/state/types.ts +7 -1
  15. package/src/ui/components/bare-input.tsx +5 -5
  16. package/src/ui/components/context-menu-box.tsx +21 -0
  17. package/src/ui/components/context-menu-overlay.tsx +114 -0
  18. package/src/ui/components/create-session-modal.tsx +12 -41
  19. package/src/ui/components/diff-renderer/fold-strip.tsx +7 -7
  20. package/src/ui/components/diff-renderer/highlight.ts +6 -10
  21. package/src/ui/components/diff-renderer/pierre-diff.tsx +3 -3
  22. package/src/ui/components/diff-renderer/prepare-diff.ts +2 -3
  23. package/src/ui/components/diff-renderer/split-view.tsx +22 -29
  24. package/src/ui/components/diff-renderer/stacked-view.tsx +18 -31
  25. package/src/ui/components/diff-renderer/use-diff-prefetch.ts +0 -1
  26. package/src/ui/components/diff-renderer/use-diff-preparation.ts +1 -1
  27. package/src/ui/components/git-commit-modal.tsx +4 -16
  28. package/src/ui/components/git-panel.tsx +37 -73
  29. package/src/ui/components/git-view.tsx +17 -19
  30. package/src/ui/components/help-modal.tsx +5 -13
  31. package/src/ui/components/input-field.tsx +5 -7
  32. package/src/ui/components/list-item.tsx +3 -11
  33. package/src/ui/components/modal-keybinds-overlay.tsx +4 -4
  34. package/src/ui/components/modal-shell.tsx +6 -11
  35. package/src/ui/components/new-tab-modal.tsx +7 -15
  36. package/src/ui/components/pending-chord-overlay.tsx +5 -5
  37. package/src/ui/components/picker.tsx +6 -6
  38. package/src/ui/components/session-bar.tsx +34 -20
  39. package/src/ui/components/session-picker-modal.tsx +7 -20
  40. package/src/ui/components/sidebar.tsx +36 -35
  41. package/src/ui/components/snippet-editor-modal.tsx +4 -18
  42. package/src/ui/components/snippet-picker-modal.tsx +5 -9
  43. package/src/ui/components/split-layout.tsx +3 -3
  44. package/src/ui/components/status-bar.tsx +12 -13
  45. package/src/ui/components/surface.tsx +8 -11
  46. package/src/ui/components/tab-item.tsx +54 -29
  47. package/src/ui/components/terminal-pane.tsx +59 -25
  48. package/src/ui/components/theme-picker-modal.tsx +7 -21
  49. package/src/ui/components/update-available-modal.tsx +3 -13
  50. package/src/ui/context-menu/controller.ts +34 -0
  51. package/src/ui/root.tsx +5 -2
  52. package/src/ui/shiki.ts +29 -21
  53. package/src/ui/theme-store.ts +110 -21
  54. package/src/ui/theme.ts +6 -3
  55. package/src/ui/themes.ts +18 -13
@@ -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}
@@ -4,7 +4,8 @@ import type { TabSession } from '../../state/types'
4
4
 
5
5
  import { dispatchGlobal, runSideEffectGlobal } from '../../state/dispatch-ref'
6
6
  import { useBusySpinner } from '../hooks/use-busy-spinner'
7
- import { getCurrentTheme, useTheme } from '../theme'
7
+ import { getCurrentTokens, useTokens } from '../theme'
8
+ import { ContextMenuBox } from './context-menu-box'
8
9
 
9
10
  interface TabItemProps {
10
11
  id?: string
@@ -15,15 +16,16 @@ interface TabItemProps {
15
16
  }
16
17
 
17
18
  function getStatusColor(status: TabSession['status']): string {
19
+ const t = getCurrentTokens()
18
20
  switch (status) {
19
21
  case 'running':
20
- return getCurrentTheme().colors['gitDecoration.addedResourceForeground']
22
+ return t.palette.success
21
23
  case 'disconnected':
22
- return getCurrentTheme().colors['editorWarning.foreground']
24
+ return t.palette.warning
23
25
  case 'error':
24
- return getCurrentTheme().colors['editorError.foreground']
26
+ return t.palette.error
25
27
  default:
26
- return getCurrentTheme().colors['descriptionForeground']
28
+ return t.muted
27
29
  }
28
30
  }
29
31
 
@@ -36,36 +38,33 @@ function getIndicator(active: boolean, focused: boolean, inLayout: boolean): str
36
38
  }
37
39
 
38
40
  function getIndicatorColor(active: boolean, focused: boolean, inLayout: boolean): string {
41
+ const t = getCurrentTokens()
39
42
  if (active) {
40
- return focused
41
- ? getCurrentTheme().colors['textLink.foreground']
42
- : getCurrentTheme().colors['terminal.ansiMagenta']
43
+ return focused ? t.palette.primary : t.accent
43
44
  }
44
45
 
45
- return inLayout
46
- ? getCurrentTheme().colors['descriptionForeground']
47
- : getCurrentTheme().colors['editor.lineHighlightBackground']
46
+ return inLayout ? t.muted : t.hover
48
47
  }
49
48
 
50
49
  function BusyIndicator() {
51
- const theme = useTheme()
50
+ const t = useTokens()
52
51
  const frame = useBusySpinner()
53
- return <text fg={theme.colors['textLink.foreground']}>{frame} working</text>
52
+ return <text fg={t.palette.primary}>{frame} working</text>
54
53
  }
55
54
 
56
55
  function WaitingIndicator() {
57
- const theme = useTheme()
58
- return <text fg={theme.colors['editorWarning.foreground']}>? waiting</text>
56
+ const t = useTokens()
57
+ return <text fg={t.palette.warning}>? waiting</text>
59
58
  }
60
59
 
61
60
  function ActivityIndicator({ tab }: { tab: TabSession }) {
62
- const theme = useTheme()
61
+ const t = useTokens()
63
62
  if (tab.status === 'error') {
64
- return <text fg={theme.colors['editorError.foreground']}>✗ error</text>
63
+ return <text fg={t.palette.error}>✗ error</text>
65
64
  }
66
65
 
67
66
  if (tab.status === 'disconnected') {
68
- return <text fg={theme.colors['editorWarning.foreground']}>⏸ restore</text>
67
+ return <text fg={t.palette.warning}>⏸ restore</text>
69
68
  }
70
69
 
71
70
  if (tab.activity === 'working') {
@@ -77,14 +76,14 @@ function ActivityIndicator({ tab }: { tab: TabSession }) {
77
76
  }
78
77
 
79
78
  if (tab.activity === 'idle') {
80
- return <text fg={theme.colors['gitDecoration.addedResourceForeground']}>● idle</text>
79
+ return <text fg={t.palette.success}>● idle</text>
81
80
  }
82
81
 
83
82
  return <text fg={getStatusColor(tab.status)}>{tab.status}</text>
84
83
  }
85
84
 
86
85
  export function TabItem({ active, focused, id, inLayout, tab }: TabItemProps) {
87
- const theme = useTheme()
86
+ const t = useTokens()
88
87
  const label = tab.command.split(' ')[0]
89
88
  const isInLayout = inLayout ?? false
90
89
  const indicator = getIndicator(active, focused, isInLayout)
@@ -92,7 +91,7 @@ export function TabItem({ active, focused, id, inLayout, tab }: TabItemProps) {
92
91
  const [hovered, setHovered] = useState(false)
93
92
 
94
93
  return (
95
- <box
94
+ <ContextMenuBox
96
95
  id={id}
97
96
  paddingLeft={1}
98
97
  paddingRight={1}
@@ -100,17 +99,43 @@ export function TabItem({ active, focused, id, inLayout, tab }: TabItemProps) {
100
99
  paddingBottom={0}
101
100
  flexDirection="column"
102
101
  gap={0}
102
+ rightClickMenu={[
103
+ [
104
+ 'Rename',
105
+ () => {
106
+ dispatchGlobal({ tabId: tab.id, type: 'set-active-tab' })
107
+ dispatchGlobal({ type: 'open-rename-tab-modal' })
108
+ },
109
+ ],
110
+ [
111
+ 'Close',
112
+ () => {
113
+ dispatchGlobal({ tabId: tab.id, type: 'close-tab' })
114
+ runSideEffectGlobal({ tabId: tab.id, type: 'close-tab' })
115
+ },
116
+ ],
117
+ [
118
+ 'Move up',
119
+ () => {
120
+ dispatchGlobal({ tabId: tab.id, type: 'set-active-tab' })
121
+ dispatchGlobal({ delta: -1, type: 'reorder-active-tab' })
122
+ },
123
+ ],
124
+ [
125
+ 'Move down',
126
+ () => {
127
+ dispatchGlobal({ tabId: tab.id, type: 'set-active-tab' })
128
+ dispatchGlobal({ delta: 1, type: 'reorder-active-tab' })
129
+ },
130
+ ],
131
+ ]}
103
132
  onMouseOver={() => setHovered(true)}
104
133
  onMouseOut={() => setHovered(false)}
105
134
  >
106
135
  <box flexDirection="row" alignItems="center">
107
136
  <text fg={indicatorColor}>{indicator} </text>
108
137
  <box flexGrow={1}>
109
- <text
110
- fg={active ? theme.colors['editor.foreground'] : theme.colors['descriptionForeground']}
111
- >
112
- {tab.title}
113
- </text>
138
+ <text fg={active ? t.palette.ink : t.muted}>{tab.title}</text>
114
139
  </box>
115
140
  {hovered ? (
116
141
  <box
@@ -120,14 +145,14 @@ export function TabItem({ active, focused, id, inLayout, tab }: TabItemProps) {
120
145
  runSideEffectGlobal({ tabId: tab.id, type: 'close-tab' })
121
146
  }}
122
147
  >
123
- <text fg={theme.colors['descriptionForeground']}>×</text>
148
+ <text fg={t.muted}>×</text>
124
149
  </box>
125
150
  ) : null}
126
151
  </box>
127
152
  <box flexDirection="row">
128
- <text fg={theme.colors['descriptionForeground']}> {label} </text>
153
+ <text fg={t.muted}> {label} </text>
129
154
  <ActivityIndicator tab={tab} />
130
155
  </box>
131
- </box>
156
+ </ContextMenuBox>
132
157
  )
133
158
  }