@brimveyn/aimux 1.6.0 → 1.6.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 (46) hide show
  1. package/package.json +2 -2
  2. package/src/git/git-diff.ts +24 -12
  3. package/src/git/git-poller.ts +11 -3
  4. package/src/git/git-status.ts +98 -17
  5. package/src/pty/terminal-snapshot.ts +5 -5
  6. package/src/state/git-tree.ts +1 -1
  7. package/src/state/reducers/git-mode-state.ts +44 -1
  8. package/src/state/reducers/git-panel-state.ts +6 -1
  9. package/src/state/types.ts +5 -1
  10. package/src/ui/components/create-session-modal.tsx +5 -4
  11. package/src/ui/components/diff-renderer/fold-strip.tsx +3 -1
  12. package/src/ui/components/diff-renderer/pierre-diff.tsx +2 -1
  13. package/src/ui/components/diff-renderer/split-view.tsx +5 -1
  14. package/src/ui/components/diff-renderer/stacked-view.tsx +3 -1
  15. package/src/ui/components/git-commit-modal.tsx +2 -1
  16. package/src/ui/components/git-pane-widget.tsx +1 -1
  17. package/src/ui/components/git-panel.tsx +77 -45
  18. package/src/ui/components/git-view.tsx +15 -4
  19. package/src/ui/components/help-modal.tsx +2 -1
  20. package/src/ui/components/input-field.tsx +2 -1
  21. package/src/ui/components/list-item.tsx +2 -1
  22. package/src/ui/components/modal-filter-bar.tsx +2 -1
  23. package/src/ui/components/modal-keybinds-overlay.tsx +2 -1
  24. package/src/ui/components/modal-shell.tsx +2 -1
  25. package/src/ui/components/new-tab-modal.tsx +2 -1
  26. package/src/ui/components/pending-chord-overlay.tsx +2 -1
  27. package/src/ui/components/session-bar.tsx +3 -1
  28. package/src/ui/components/session-picker-modal.tsx +2 -1
  29. package/src/ui/components/sidebar.tsx +8 -5
  30. package/src/ui/components/snippet-editor-modal.tsx +2 -1
  31. package/src/ui/components/snippet-picker-modal.tsx +2 -1
  32. package/src/ui/components/split-layout.tsx +2 -1
  33. package/src/ui/components/status-bar.tsx +8 -6
  34. package/src/ui/components/surface.tsx +6 -6
  35. package/src/ui/components/tab-item.tsx +14 -9
  36. package/src/ui/components/terminal-pane.tsx +7 -5
  37. package/src/ui/components/theme-picker-modal.tsx +2 -1
  38. package/src/ui/components/update-available-modal.tsx +2 -1
  39. package/src/ui/filter-themes.ts +7 -2
  40. package/src/ui/root.tsx +3 -1
  41. package/src/ui/status-bar-model.ts +13 -3
  42. package/src/ui/theme-store.ts +36 -0
  43. package/src/ui/theme.ts +4 -29
  44. package/src/ui/themes.ts +15 -63
  45. package/src/ui/house-themes.ts +0 -313
  46. package/src/ui/themes.generated.ts +0 -59794
@@ -1,22 +1,22 @@
1
1
  import type { ReactNode } from 'react'
2
2
 
3
- import { theme } from '../theme'
3
+ import { getCurrentTheme } from '../theme'
4
4
 
5
5
  type SurfaceTone = 'muted' | 'elevated' | 'selected' | 'input' | 'inputActive'
6
6
 
7
7
  function getSurfaceColor(tone: SurfaceTone): string {
8
8
  switch (tone) {
9
9
  case 'elevated':
10
- return theme.colors['sideBar.background']
10
+ return getCurrentTheme().colors['sideBar.background']
11
11
  case 'selected':
12
- return theme.colors['list.activeSelectionBackground']
12
+ return getCurrentTheme().colors['list.activeSelectionBackground']
13
13
  case 'input':
14
- return theme.colors['editor.background']
14
+ return getCurrentTheme().colors['editor.background']
15
15
  case 'inputActive':
16
- return theme.colors['list.activeSelectionBackground']
16
+ return getCurrentTheme().colors['list.activeSelectionBackground']
17
17
  case 'muted':
18
18
  default:
19
- return theme.colors['sideBarSectionHeader.background']
19
+ return getCurrentTheme().colors['sideBarSectionHeader.background']
20
20
  }
21
21
  }
22
22
 
@@ -1,7 +1,7 @@
1
1
  import type { TabSession } from '../../state/types'
2
2
 
3
3
  import { useBusySpinner } from '../hooks/use-busy-spinner'
4
- import { theme } from '../theme'
4
+ import { getCurrentTheme, useTheme } from '../theme'
5
5
 
6
6
  interface TabItemProps {
7
7
  id?: string
@@ -15,15 +15,15 @@ interface TabItemProps {
15
15
  function getStatusColor(status: TabSession['status']): string {
16
16
  switch (status) {
17
17
  case 'running':
18
- return theme.colors['gitDecoration.addedResourceForeground']
18
+ return getCurrentTheme().colors['gitDecoration.addedResourceForeground']
19
19
  case 'disconnected':
20
- return theme.colors['editorWarning.foreground']
20
+ return getCurrentTheme().colors['editorWarning.foreground']
21
21
  case 'error':
22
- return theme.colors['editorError.foreground']
22
+ return getCurrentTheme().colors['editorError.foreground']
23
23
  case 'exited':
24
- return theme.colors['editorWarning.foreground']
24
+ return getCurrentTheme().colors['editorWarning.foreground']
25
25
  default:
26
- return theme.colors['descriptionForeground']
26
+ return getCurrentTheme().colors['descriptionForeground']
27
27
  }
28
28
  }
29
29
 
@@ -37,20 +37,24 @@ function getIndicator(active: boolean, focused: boolean, inLayout: boolean): str
37
37
 
38
38
  function getIndicatorColor(active: boolean, focused: boolean, inLayout: boolean): string {
39
39
  if (active) {
40
- return focused ? theme.colors['textLink.foreground'] : theme.colors['terminal.ansiMagenta']
40
+ return focused
41
+ ? getCurrentTheme().colors['textLink.foreground']
42
+ : getCurrentTheme().colors['terminal.ansiMagenta']
41
43
  }
42
44
 
43
45
  return inLayout
44
- ? theme.colors['descriptionForeground']
45
- : theme.colors['editor.lineHighlightBackground']
46
+ ? getCurrentTheme().colors['descriptionForeground']
47
+ : getCurrentTheme().colors['editor.lineHighlightBackground']
46
48
  }
47
49
 
48
50
  function BusyIndicator() {
51
+ const theme = useTheme()
49
52
  const frame = useBusySpinner()
50
53
  return <text fg={theme.colors['textLink.foreground']}>{frame} busy</text>
51
54
  }
52
55
 
53
56
  function ActivityIndicator({ isFocusedInput, tab }: { tab: TabSession; isFocusedInput: boolean }) {
57
+ const theme = useTheme()
54
58
  if (tab.status === 'error') {
55
59
  return <text fg={theme.colors['editorError.foreground']}>✗ error</text>
56
60
  }
@@ -79,6 +83,7 @@ function ActivityIndicator({ isFocusedInput, tab }: { tab: TabSession; isFocused
79
83
  }
80
84
 
81
85
  export function TabItem({ active, focused, id, inLayout, isFocusedInput, tab }: TabItemProps) {
86
+ const theme = useTheme()
82
87
  const label = tab.command.split(' ')[0]
83
88
  const isInLayout = inLayout ?? false
84
89
  const indicator = getIndicator(active, focused, isInLayout)
@@ -6,7 +6,7 @@ import type { TerminalContentOrigin } from '../../input/raw-input-handler'
6
6
  import type { TabSession, TerminalSnapshot, TerminalSpan } from '../../state/types'
7
7
 
8
8
  import { logInputDebug } from '../../debug/input-log'
9
- import { theme } from '../theme'
9
+ import { getCurrentTheme, useTheme } from '../theme'
10
10
 
11
11
  interface TerminalPaneProps {
12
12
  tab?: TabSession
@@ -46,14 +46,14 @@ function getTitle(
46
46
 
47
47
  function getBorderColor(isActive: boolean, focusMode: TerminalPaneProps['focusMode']): string {
48
48
  if (isActive && focusMode === 'terminal-input') {
49
- return theme.colors['focusBorder']
49
+ return getCurrentTheme().colors['focusBorder']
50
50
  }
51
51
 
52
52
  if (isActive) {
53
- return theme.colors['terminal.ansiMagenta']
53
+ return getCurrentTheme().colors['terminal.ansiMagenta']
54
54
  }
55
55
 
56
- return theme.colors['editor.lineHighlightBackground']
56
+ return getCurrentTheme().colors['editor.lineHighlightBackground']
57
57
  }
58
58
 
59
59
  function renderSpan(span: TerminalSpan, key: string): ReactNode {
@@ -72,7 +72,7 @@ function renderSpan(span: TerminalSpan, key: string): ReactNode {
72
72
  }
73
73
 
74
74
  return (
75
- <span key={key} fg={span.fg ?? theme.colors['editor.foreground']} bg={span.bg}>
75
+ <span key={key} fg={span.fg ?? getCurrentTheme().colors['editor.foreground']} bg={span.bg}>
76
76
  {node}
77
77
  </span>
78
78
  )
@@ -87,6 +87,7 @@ const TerminalViewport = memo(function TerminalViewport({
87
87
  buffer,
88
88
  viewport,
89
89
  }: TerminalViewportProps) {
90
+ const theme = useTheme()
90
91
  if (viewport && viewport.lines.length > 0) {
91
92
  const lines = viewport.lines
92
93
  return (
@@ -123,6 +124,7 @@ export function TerminalPane({
123
124
  tab,
124
125
  tabId,
125
126
  }: TerminalPaneProps) {
127
+ const theme = useTheme()
126
128
  const paneIsActive = isActive ?? true
127
129
  const canForwardMouse = focusMode === 'terminal-input' && !!tab && mouseForwardingEnabled
128
130
  const canUseLocalScrollback = focusMode === 'terminal-input' && !!tab && localScrollbackEnabled
@@ -3,7 +3,7 @@ import { useLayoutEffect, useMemo, useRef } from 'react'
3
3
 
4
4
  import { dispatchGlobal } from '../../state/dispatch-ref'
5
5
  import { filterThemeIds } from '../filter-themes'
6
- import { theme } from '../theme'
6
+ import { useTheme } from '../theme'
7
7
  import { type ThemeId, THEMES } from '../themes'
8
8
  import { uiTokens } from '../ui-tokens'
9
9
  import { ListItem } from './list-item'
@@ -45,6 +45,7 @@ function computeWindowStart(
45
45
  }
46
46
 
47
47
  export function ThemePickerModal({ currentThemeId, filter, selectedIndex }: ThemePickerModalProps) {
48
+ const theme = useTheme()
48
49
  const dimensions = useTerminalDimensions()
49
50
  const filtered = useMemo(() => filterThemeIds(filter), [filter])
50
51
 
@@ -1,4 +1,4 @@
1
- import { theme } from '../theme'
1
+ import { useTheme } from '../theme'
2
2
  import { uiTokens } from '../ui-tokens'
3
3
  import { ListItem } from './list-item'
4
4
  import { ModalShell } from './modal-shell'
@@ -19,6 +19,7 @@ export function UpdateAvailableModal({
19
19
  latestVersion,
20
20
  selectedIndex,
21
21
  }: UpdateAvailableModalProps) {
22
+ const theme = useTheme()
22
23
  return (
23
24
  <ModalShell
24
25
  title="Update available"
@@ -4,7 +4,12 @@ export function filterThemeIds(filter: string | null): ThemeId[] {
4
4
  if (!filter) return THEME_IDS
5
5
  const needle = filter.toLowerCase()
6
6
  return THEME_IDS.filter((id) => {
7
- const name = THEMES[id]?.name.toLowerCase() ?? ''
8
- return id.toLowerCase().includes(needle) || name.includes(needle)
7
+ const entry = THEMES[id]
8
+ if (!entry) return false
9
+ return (
10
+ id.toLowerCase().includes(needle) ||
11
+ entry.displayName.toLowerCase().includes(needle) ||
12
+ entry.name.toLowerCase().includes(needle)
13
+ )
9
14
  })
10
15
  }
package/src/ui/root.tsx CHANGED
@@ -24,7 +24,7 @@ import { StatusBar } from './components/status-bar'
24
24
  import { TerminalPane } from './components/terminal-pane'
25
25
  import { ThemePickerModal } from './components/theme-picker-modal'
26
26
  import { UpdateAvailableModal } from './components/update-available-modal'
27
- import { theme } from './theme'
27
+ import { useTheme } from './theme'
28
28
 
29
29
  function getCreateSessionFields(modal: ModalState) {
30
30
  if (modal.type !== 'create-session') {
@@ -215,6 +215,7 @@ export function RootView({
215
215
  terminalRows,
216
216
  themeId,
217
217
  }: RootViewProps) {
218
+ const theme = useTheme()
218
219
  const tabs = useAppStore((s) => s.tabs)
219
220
  const activeTabId = useAppStore((s) => s.activeTabId)
220
221
  const layoutTrees = useAppStore((s) => s.layoutTrees)
@@ -342,6 +343,7 @@ export function RootView({
342
343
  }
343
344
 
344
345
  function GitPaneInPaneMode({ ratio }: { ratio: number }) {
346
+ const theme = useTheme()
345
347
  // Ratio maps to a fixed column count (20..80), mirroring the reservation in
346
348
  // use-terminal-resize so the terminal-content area stays in sync.
347
349
  const width = Math.max(20, Math.min(80, Math.round(ratio * 80)))
@@ -32,10 +32,17 @@ function getActiveTabLabel(tab?: TabSession): string {
32
32
  return `${truncateLabel(tab.title)} (${tab.status})`
33
33
  }
34
34
 
35
+ const STAGING_DESCRIPTIONS = ['Stage', 'Unstage/delete', 'Commit', 'Push']
36
+
35
37
  function hintForMode(config: ResolvedKeymapConfig, modeId: ModeId): string {
36
38
  return buildHintText(config, modeId, HINT_LIMIT, { excludeDescriptions: [HELP_DESCRIPTION] })
37
39
  }
38
40
 
41
+ function hintForGitMode(config: ResolvedKeymapConfig, headOffset: number): string {
42
+ const exclude = headOffset > 0 ? [HELP_DESCRIPTION, ...STAGING_DESCRIPTIONS] : [HELP_DESCRIPTION]
43
+ return buildHintText(config, 'git-mode', HINT_LIMIT, { excludeDescriptions: exclude })
44
+ }
45
+
39
46
  function helpHintForMode(config: ResolvedKeymapConfig, modeId: ModeId): string {
40
47
  const bindings = describeBindings(config, modeId, {
41
48
  dedupeByDescription: true,
@@ -77,12 +84,15 @@ export function getStatusBarModel(
77
84
  right: modalMode ? hintForMode(config, modalMode) : '',
78
85
  }
79
86
  }
80
- case 'git':
87
+ case 'git': {
88
+ const headOffset = state.gitMode.headOffset
89
+ const offsetTag = headOffset > 0 ? ` HEAD~${headOffset}` : ''
81
90
  return {
82
91
  help: helpHintForMode(config, 'git-mode'),
83
- left: `${sessionIcon} ${sessionLabel}`,
84
- right: hintForMode(config, 'git-mode'),
92
+ left: `${sessionIcon} ${sessionLabel}${offsetTag}`,
93
+ right: hintForGitMode(config, headOffset),
85
94
  }
95
+ }
86
96
  case 'command-edit': {
87
97
  const commandEditMode = deriveCommandEditModeId(state.modal.type)
88
98
  return {
@@ -0,0 +1,36 @@
1
+ import { useStore } from 'zustand'
2
+ import { createStore } from 'zustand/vanilla'
3
+
4
+ import { type Theme, type ThemeId, THEMES } from './themes'
5
+
6
+ interface ThemeStore {
7
+ theme: Theme
8
+ }
9
+
10
+ const DEFAULT = THEMES['aimux']
11
+ if (!DEFAULT) throw new Error('default theme missing')
12
+
13
+ const themeStore = createStore<ThemeStore>(() => ({ theme: DEFAULT }))
14
+
15
+ /**
16
+ * Subscribe to the active theme. Pass a selector to subscribe to a narrower
17
+ * slice (`useTheme(t => t.colors['editor.foreground'])`) or call with no
18
+ * arguments to get the whole theme.
19
+ */
20
+ export function useTheme(): Theme
21
+ export function useTheme<T>(selector: (theme: Theme) => T): T
22
+ export function useTheme<T>(selector?: (theme: Theme) => T): T | Theme {
23
+ return useStore(themeStore, (s) => (selector ? selector(s.theme) : s.theme))
24
+ }
25
+
26
+ /** Synchronous snapshot for non-React callers (reducers, side effects). */
27
+ export function getCurrentTheme(): Theme {
28
+ return themeStore.getState().theme
29
+ }
30
+
31
+ /** Swap the active theme. Triggers re-renders on every `useTheme` subscriber. */
32
+ export function applyTheme(id: ThemeId): void {
33
+ const entry = THEMES[id]
34
+ if (!entry) return
35
+ themeStore.setState({ theme: entry })
36
+ }
package/src/ui/theme.ts CHANGED
@@ -1,30 +1,5 @@
1
- import { type Theme, type ThemeId, THEMES } from './themes'
1
+ // Back-compat shim: the theme singleton has been replaced with a Zustand store.
2
+ // React components should import `useTheme` from `./theme-store`; non-React
3
+ // callers use `getCurrentTheme()`. `applyTheme` moved to the store module.
2
4
 
3
- const DEFAULT = THEMES['aimux']
4
- if (!DEFAULT) throw new Error('default theme missing')
5
-
6
- // Mutable theme singleton. Components access `theme.colors['editor.foreground']`
7
- // directly. On `applyTheme(id)` the colors map is replaced in place so live
8
- // imports keep pointing at the current palette.
9
- export const theme: Theme = {
10
- bg: DEFAULT.bg,
11
- colors: { ...DEFAULT.colors },
12
- displayName: DEFAULT.displayName,
13
- fg: DEFAULT.fg,
14
- name: DEFAULT.name,
15
- settings: DEFAULT.settings,
16
- type: DEFAULT.type,
17
- }
18
-
19
- export function applyTheme(id: ThemeId): void {
20
- const entry = THEMES[id]
21
- if (!entry) return
22
- for (const key of Object.keys(theme.colors)) delete theme.colors[key]
23
- Object.assign(theme.colors, entry.colors)
24
- theme.name = entry.name
25
- theme.displayName = entry.displayName
26
- theme.type = entry.type
27
- theme.fg = entry.fg
28
- theme.bg = entry.bg
29
- theme.settings = entry.settings
30
- }
5
+ export { applyTheme, getCurrentTheme, useTheme } from './theme-store'
package/src/ui/themes.ts CHANGED
@@ -1,18 +1,7 @@
1
- import type { BundledTheme } from 'shiki'
2
-
3
- import {
4
- AIMUX_COLOR_KEYS,
5
- type AimuxColorKey,
6
- type NamedTheme,
7
- type NamedThemeDefinition,
8
- type Theme,
9
- type ThemeColorMap,
10
- type ThemeSettings,
11
- type ThemeTokenRule,
12
- } from '@brimveyn/aimux-config'
13
-
14
- import { HOUSE_THEME_IDS, HOUSE_THEMES } from './house-themes'
15
- import { GENERATED_THEME_IDS, GENERATED_THEMES } from './themes.generated'
1
+ // Thin runtime re-export. Theme data, types, normalization, and registration
2
+ // all live in `@brimveyn/aimux-config` so the dependency-free config package is
3
+ // the single source of truth. The mutable theme singleton + `applyTheme` stay
4
+ // in `./theme.ts` since they're UI-layer concerns.
16
5
 
17
6
  export type {
18
7
  AimuxColorKey,
@@ -20,54 +9,17 @@ export type {
20
9
  NamedThemeDefinition,
21
10
  Theme,
22
11
  ThemeColorMap,
12
+ ThemeId,
23
13
  ThemeSettings,
24
14
  ThemeTokenRule,
25
- }
26
- export { AIMUX_COLOR_KEYS }
27
-
28
- export type ThemeId = BundledTheme | (string & {})
29
-
30
- export const THEMES: Record<string, Theme> = { ...GENERATED_THEMES, ...HOUSE_THEMES }
31
- export const THEME_IDS: ThemeId[] = [...GENERATED_THEME_IDS, ...HOUSE_THEME_IDS]
32
-
33
- const DEFAULT_THEME_ID: ThemeId = 'aimux'
34
-
35
- /**
36
- * Merge user-declared themes into the registry. Themes declared via
37
- * `themes.full(...)` arrive as complete `NamedTheme` objects; themes declared
38
- * via `themes.define(...)` arrive as `NamedThemeDefinition` (partial overrides
39
- * over a base) and are expanded here.
40
- */
41
- export function registerUserThemes(
42
- defs: Record<string, NamedTheme | NamedThemeDefinition> | undefined
43
- ): void {
44
- if (!defs) return
45
- const fallback = THEMES[DEFAULT_THEME_ID]
46
- if (!fallback) return
47
- for (const [id, def] of Object.entries(defs)) {
48
- THEMES[id] = isFullTheme(def) ? def : expandDefinition(id, def, fallback)
49
- if (!THEME_IDS.includes(id)) THEME_IDS.push(id)
50
- }
51
- }
52
-
53
- function isFullTheme(def: NamedTheme | NamedThemeDefinition): def is NamedTheme {
54
- return 'settings' in def && Array.isArray((def as Theme).settings)
55
- }
56
-
57
- function expandDefinition(id: string, def: NamedThemeDefinition, fallback: Theme): Theme {
58
- const base = def.base ? (THEMES[def.base] ?? fallback) : fallback
59
- const colors: ThemeColorMap = { ...base.colors }
60
- for (const [k, v] of Object.entries(def.colors ?? {})) {
61
- if (typeof v === 'string') colors[k] = v
62
- }
63
- return {
64
- ...base,
65
- colors,
66
- displayName: def.name,
67
- name: id,
68
- }
69
- }
15
+ } from '@brimveyn/aimux-config'
70
16
 
71
- export function isKnownThemeId(id: string): boolean {
72
- return id in THEMES
73
- }
17
+ export {
18
+ AIMUX_COLOR_KEYS,
19
+ isKnownThemeId,
20
+ migrateThemeId,
21
+ normalizeTheme,
22
+ registerUserThemes,
23
+ THEME_IDS,
24
+ THEMES,
25
+ } from '@brimveyn/aimux-config'