@brimveyn/aimux 1.19.0 → 1.19.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brimveyn/aimux",
3
- "version": "1.19.0",
3
+ "version": "1.19.1",
4
4
  "description": "A terminal multiplexer for AI CLIs. Run Claude, Codex, OpenCode side-by-side with tabbed navigation, split panes, and persistent sessions.",
5
5
  "keywords": [
6
6
  "ai",
@@ -15,7 +15,7 @@ import { formatDivergence } from '../../../../state/session-worktrees'
15
15
  import { IDLE_SESSION_STATUS } from '../../../../state/types'
16
16
  import { useBusySpinner } from '../../../hooks/use-busy-spinner'
17
17
  import { moveIdToIdPosition, orderSessionsForDisplay } from '../../../session-ordering'
18
- import { useTheme } from '../../../theme'
18
+ import { useBaseTheme, useTheme } from '../../../theme'
19
19
  import { FlashLabelBadge } from '../../flash/flash-label-badge'
20
20
  import { ContextMenuBox } from '../../overlays/context-menu/context-menu-box'
21
21
  import { useSidebarAutoScroll } from './use-sidebar-auto-scroll'
@@ -298,14 +298,17 @@ const WorkspaceRow = memo(function WorkspaceRow({
298
298
  status,
299
299
  }: WorkspaceRowProps) {
300
300
  const t = useTheme()
301
+ // Selection highlight must stay opaque in transparent mode — otherwise the
302
+ // cursor row visually disappears against the see-through chrome.
303
+ const base = useBaseTheme()
301
304
  const showSpinner = status.working
302
305
  const showWaiting = status.waiting
303
306
  const spinner = useBusySpinner(showSpinner)
304
307
  let bgColor: string | undefined
305
308
  if (dragging || isActiveItem) {
306
- bgColor = t.backgroundElement
309
+ bgColor = base.backgroundElement
307
310
  } else if (inCurrentGroup) {
308
- bgColor = t.backgroundPanel
311
+ bgColor = base.backgroundPanel
309
312
  }
310
313
  const workingColor = t.primary
311
314
  const waitingColor = t.warning
@@ -7,7 +7,7 @@ import type { SessionRecord, WorktreeRecord } from '../../../../state/types'
7
7
  import { useAppStore } from '../../../../state/app-store'
8
8
  import { dispatchGlobal, runSideEffectGlobal } from '../../../../state/dispatch-ref'
9
9
  import { formatDivergence } from '../../../../state/session-worktrees'
10
- import { useTheme } from '../../../theme'
10
+ import { useBaseTheme, useTheme } from '../../../theme'
11
11
  import { FlashLabelBadge } from '../../flash/flash-label-badge'
12
12
  import { ContextMenuBox } from '../../overlays/context-menu/context-menu-box'
13
13
 
@@ -33,6 +33,8 @@ export const WorktreeRow = memo(function WorktreeRow({
33
33
  worktree,
34
34
  }: WorktreeRowProps) {
35
35
  const t = useTheme()
36
+ // Selection highlight must stay opaque in transparent mode.
37
+ const base = useBaseTheme()
36
38
  const currentSessionId = useAppStore((s) => s.currentSessionId)
37
39
  const isCurrentSession = session.id === currentSessionId
38
40
  const divergence = useAppStore((s) => s.worktreeDivergence[worktree.id])
@@ -102,9 +104,9 @@ export const WorktreeRow = memo(function WorktreeRow({
102
104
 
103
105
  let bgColor: string | undefined
104
106
  if (isActiveItem) {
105
- bgColor = t.backgroundElement
107
+ bgColor = base.backgroundElement
106
108
  } else if (inCurrentGroup) {
107
- bgColor = t.backgroundPanel
109
+ bgColor = base.backgroundPanel
108
110
  }
109
111
 
110
112
  const connector = isLast ? '└─' : '├─'
@@ -11,7 +11,7 @@ import { useAIUsageStore } from '../../../state/ai-usage-store'
11
11
  import { useAppStore } from '../../../state/app-store'
12
12
  import { useKeymap } from '../../keymap-context'
13
13
  import { getStatusBarModel, type IdentitySegment } from '../../status-bar-model'
14
- import { useTheme } from '../../theme'
14
+ import { useBaseTheme, useTheme } from '../../theme'
15
15
  import { AIUsageIndicator } from '../overlays/ai-usage/ai-usage-indicator'
16
16
 
17
17
  // Powerline-style separator glyph pairs.
@@ -110,6 +110,10 @@ function Separator({ bg, fg, glyph }: { bg: string; fg: string; glyph: string })
110
110
 
111
111
  export function StatusBar() {
112
112
  const t = useTheme()
113
+ // Badge foregrounds (mode label, version) sit on colored tiles and would
114
+ // render invisible with a transparent chrome color — always use the base
115
+ // (opaque) background token for contrast against those tiles.
116
+ const base = useBaseTheme()
113
117
  const state = useAppStore((s) => s)
114
118
  const config = useKeymap()
115
119
  const model = getStatusBarModel(state, config)
@@ -139,7 +143,7 @@ export function StatusBar() {
139
143
  <box height={1} flexShrink={0} flexDirection="row" overflow="hidden">
140
144
  {/* A: mode */}
141
145
  <box backgroundColor={modeColor} paddingLeft={1} paddingRight={1}>
142
- <text fg={t.background} selectable={false}>
146
+ <text fg={base.background} selectable={false}>
143
147
  {getModeBadge(state.focusMode)}
144
148
  </text>
145
149
  </box>
@@ -186,7 +190,7 @@ export function StatusBar() {
186
190
 
187
191
  {/* Y: version */}
188
192
  <box backgroundColor={tileY} paddingLeft={1} paddingRight={1} flexShrink={0}>
189
- <text fg={t.background} selectable={false}>
193
+ <text fg={base.background} selectable={false}>
190
194
  v{APP_VERSION}
191
195
  </text>
192
196
  </box>
@@ -26,8 +26,7 @@ const themeStore = createStore<ThemeStore>(() => ({
26
26
  let cachedId: ThemeId | null = null
27
27
  let cachedMode: ThemeMode | null = null
28
28
  let cachedBase: ResolvedTuiTheme | null = null
29
- let cachedTransparent: boolean | null = null
30
- let cachedFinal: ResolvedTuiTheme | null = null
29
+ let cachedOverlay: ResolvedTuiTheme | null = null
31
30
 
32
31
  // Chrome surface tokens: when transparent mode is on we replace these with
33
32
  // 'transparent' so opentui's BoxRenderable skips its fill (alpha=0 early-returns
@@ -41,27 +40,27 @@ const CHROME_BG_TOKENS = [
41
40
  'backgroundMenu',
42
41
  ] as const
43
42
 
43
+ // Both the opaque base and the transparent overlay are cached side-by-side
44
+ // (both keyed by id+mode). A single-slot cache would thrash when useTheme() and
45
+ // useBaseTheme() are both mounted with different transparent values — each call
46
+ // would invalidate the other's cache, hand React a fresh object reference every
47
+ // render, and drive an infinite update loop.
44
48
  function derive(id: ThemeId, mode: ThemeMode, transparent: boolean): ResolvedTuiTheme {
45
- if (cachedFinal && cachedId === id && cachedMode === mode && cachedTransparent === transparent)
46
- return cachedFinal
47
-
48
49
  if (!cachedBase || cachedId !== id || cachedMode !== mode) {
49
50
  const json = TUI_THEMES[id] ?? TUI_THEMES.aimux
50
51
  if (!json) throw new Error(`No theme JSON for ${id}`)
51
52
  cachedBase = resolveTuiTheme(json, mode)
53
+ cachedOverlay = null
54
+ cachedId = id
55
+ cachedMode = mode
52
56
  }
53
- cachedId = id
54
- cachedMode = mode
55
- cachedTransparent = transparent
56
-
57
- if (transparent) {
57
+ if (!transparent) return cachedBase
58
+ if (!cachedOverlay) {
58
59
  const overlay: ResolvedTuiTheme = { ...cachedBase }
59
60
  for (const key of CHROME_BG_TOKENS) overlay[key] = 'transparent'
60
- cachedFinal = overlay
61
- } else {
62
- cachedFinal = cachedBase
61
+ cachedOverlay = overlay
63
62
  }
64
- return cachedFinal
63
+ return cachedOverlay
65
64
  }
66
65
 
67
66
  /** Subscribe to the resolved TUI theme for the active id+mode (+transparent overlay). */
@@ -69,6 +68,16 @@ export function useTheme(): ResolvedTuiTheme {
69
68
  return useStore(themeStore, (s) => derive(s.id, s.mode, s.transparent))
70
69
  }
71
70
 
71
+ /**
72
+ * Resolved TUI theme WITHOUT the transparent chrome overlay. Use for the rare
73
+ * chrome sites that must stay opaque even in transparent mode — e.g. the
74
+ * sidebar selection highlight (would otherwise vanish) and the status-bar
75
+ * badge foregrounds (would otherwise render as transparent-on-color).
76
+ */
77
+ export function useBaseTheme(): ResolvedTuiTheme {
78
+ return useStore(themeStore, (s) => derive(s.id, s.mode, false))
79
+ }
80
+
72
81
  /** Synchronous snapshot of the resolved theme for non-React callers. */
73
82
  export function getCurrentTheme(): ResolvedTuiTheme {
74
83
  const s = themeStore.getState()
package/src/ui/theme.ts CHANGED
@@ -12,6 +12,7 @@ export {
12
12
  setMode,
13
13
  setTransparent,
14
14
  subscribeThemeChanges,
15
+ useBaseTheme,
15
16
  useMode,
16
17
  useTheme,
17
18
  useTransparent,