@brimveyn/aimux 1.14.2 → 1.14.4

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.
@@ -0,0 +1,127 @@
1
+ import { memo, useCallback } from 'react'
2
+
3
+ import type { GitPanelState } from '../../../../state/types'
4
+
5
+ import { useAppStore } from '../../../../state/app-store'
6
+ import { dispatchGlobal, runSideEffectGlobal } from '../../../../state/dispatch-ref'
7
+ import { useTheme } from '../../../theme'
8
+
9
+ interface GitPaneHeaderProps {
10
+ gitPanel: GitPanelState
11
+ projectPath: string | undefined
12
+ headOffset?: number
13
+ baseLabel?: string
14
+ }
15
+
16
+ export const GitPaneHeader = memo(function GitPaneHeader({
17
+ baseLabel,
18
+ gitPanel,
19
+ headOffset = 0,
20
+ projectPath,
21
+ }: GitPaneHeaderProps) {
22
+ const t = useTheme()
23
+ const fileListMode = useAppStore((s) => s.gitPane.fileListMode)
24
+ const nextFileListMode = fileListMode === 'tree' ? 'flat' : 'tree'
25
+ const toggleListMode = useCallback(() => {
26
+ dispatchGlobal({ type: 'git-mode-toggle-file-list-mode' })
27
+ runSideEffectGlobal({ mode: nextFileListMode, type: 'persist-git-file-list-mode' })
28
+ }, [nextFileListMode])
29
+
30
+ const hasProject = projectPath != null && projectPath !== ''
31
+ if (!hasProject) return null
32
+ if (gitPanel.error !== null) return null
33
+
34
+ const branch = gitPanel.branch
35
+ const branchLabel = branch != null && branch !== '' ? branch : 'detached'
36
+ const branchIsResolved = branch != null && branch !== ''
37
+
38
+ const ahead = gitPanel.ahead
39
+ const behind = gitPanel.behind
40
+ const showAhead = ahead > 0
41
+ const showBehind = behind > 0
42
+ const showTracking = showAhead || showBehind
43
+
44
+ const showToggle = gitPanel.files.length > 0
45
+ const showHistorical = headOffset > 0
46
+ const showReviewBase = baseLabel != null && baseLabel !== ''
47
+ const showScope = showHistorical || showReviewBase
48
+
49
+ return (
50
+ <box flexDirection="column" flexShrink={0} paddingBottom={1}>
51
+ <box flexDirection="row" justifyContent="space-between">
52
+ <box flexDirection="row" flexShrink={1} overflow="hidden">
53
+ <text selectable={false} fg={t.textMuted} wrapMode="none">
54
+ {'\u{e702}'}
55
+ </text>
56
+ <text selectable={false} fg={t.textMuted} wrapMode="none">
57
+ {' '}
58
+ </text>
59
+ <text selectable={false} fg={branchIsResolved ? t.text : t.textMuted} wrapMode="none">
60
+ {branchIsResolved ? <strong>{branchLabel}</strong> : branchLabel}
61
+ </text>
62
+ </box>
63
+ <box flexDirection="row" flexShrink={0} gap={2}>
64
+ {showTracking ? (
65
+ <box flexDirection="row" gap={1}>
66
+ {showAhead ? (
67
+ <text selectable={false} fg={t.textMuted} wrapMode="none">
68
+ {`↑${ahead}`}
69
+ </text>
70
+ ) : null}
71
+ {showBehind ? (
72
+ <text selectable={false} fg={t.textMuted} wrapMode="none">
73
+ {`↓${behind}`}
74
+ </text>
75
+ ) : null}
76
+ </box>
77
+ ) : null}
78
+ {showToggle ? (
79
+ <box flexDirection="row" gap={1} onMouseDown={toggleListMode}>
80
+ <text
81
+ selectable={false}
82
+ fg={fileListMode === 'tree' ? t.primary : t.textMuted}
83
+ wrapMode="none"
84
+ >
85
+ tree
86
+ </text>
87
+ <text selectable={false} fg={t.textMuted} wrapMode="none">
88
+ |
89
+ </text>
90
+ <text
91
+ selectable={false}
92
+ fg={fileListMode === 'flat' ? t.primary : t.textMuted}
93
+ wrapMode="none"
94
+ >
95
+ flat
96
+ </text>
97
+ </box>
98
+ ) : null}
99
+ </box>
100
+ </box>
101
+ {showScope ? (
102
+ <box flexDirection="row" gap={2}>
103
+ {showHistorical ? (
104
+ <text selectable={false} fg={t.warning} wrapMode="none">
105
+ <strong>HEAD~{headOffset}</strong>
106
+ </text>
107
+ ) : null}
108
+ {showHistorical ? (
109
+ <text selectable={false} fg={t.textMuted} wrapMode="none">
110
+ [ newer · ] older
111
+ </text>
112
+ ) : null}
113
+ {showReviewBase ? (
114
+ <text selectable={false} fg={t.primary} wrapMode="none">
115
+ <strong>{baseLabel}</strong>
116
+ </text>
117
+ ) : null}
118
+ {showReviewBase ? (
119
+ <text selectable={false} fg={t.textMuted} wrapMode="none">
120
+ b: back
121
+ </text>
122
+ ) : null}
123
+ </box>
124
+ ) : null}
125
+ </box>
126
+ )
127
+ })
@@ -7,6 +7,7 @@ import { useRepoDiscovery } from '../../../../git/use-repo-discovery'
7
7
  import { useAppStore } from '../../../../state/app-store'
8
8
  import { getSessionProjectPath } from '../../../../state/session-worktrees'
9
9
  import { GitPanel } from '../git-panel'
10
+ import { GitPaneHeader } from './git-pane-header'
10
11
 
11
12
  interface GitPaneWidgetProps {
12
13
  pollingEnabled: boolean
@@ -43,15 +44,20 @@ export const GitPaneWidget = memo(function GitPaneWidget({ pollingEnabled }: Git
43
44
  const display = lastGoodRef.current ?? gitPanel
44
45
 
45
46
  return (
46
- <GitPanel
47
- collapsedFolders={gitMode.collapsedFolders}
48
- compact={treeCompaction}
49
- diffCountConfig={diffCountConfig}
50
- fileListMode={gitFileListMode}
51
- gitPanel={display}
52
- pathConfig={pathConfig}
53
- projectPath={projectPath}
54
- selectedEntryKey={gitMode.selectedEntryKey}
55
- />
47
+ <box flexDirection="column" flexGrow={1} flexShrink={1} flexBasis={0} overflow="hidden">
48
+ <GitPaneHeader gitPanel={display} projectPath={projectPath} />
49
+ <GitPanel
50
+ collapsedFolders={gitMode.collapsedFolders}
51
+ compact={treeCompaction}
52
+ diffCountConfig={diffCountConfig}
53
+ fileListMode={gitFileListMode}
54
+ gitPanel={display}
55
+ pathConfig={pathConfig}
56
+ projectPath={projectPath}
57
+ selectedEntryKey={gitMode.selectedEntryKey}
58
+ showFileListToggle={false}
59
+ showRemoteTracking={false}
60
+ />
61
+ </box>
56
62
  )
57
63
  })
@@ -10,6 +10,7 @@ import { type MeasuredPaneRect, usePaneSizeReport } from '../../../app-runtime/u
10
10
  import { logInputDebug } from '../../../debug/input-log'
11
11
  import { dispatchGlobal, runSideEffectGlobal } from '../../../state/dispatch-ref'
12
12
  import { type ContextMenuItem, openContextMenu } from '../../context-menu/controller'
13
+ import { resolvePaletteIndex } from '../../host-palette'
13
14
  import { getCurrentTheme, useTheme } from '../../theme'
14
15
  import { ContextMenuBox } from '../overlays/context-menu/context-menu-box'
15
16
 
@@ -76,8 +77,16 @@ function renderSpan(span: TerminalSpan, key: string): ReactNode {
76
77
  node = <strong>{node}</strong>
77
78
  }
78
79
 
80
+ // Palette indices are resolved here (not in the daemon) so they pick up
81
+ // the host terminal's actual ANSI palette queried at startup.
82
+ const fg =
83
+ span.fgPalette !== undefined
84
+ ? resolvePaletteIndex(span.fgPalette)
85
+ : (span.fg ?? getCurrentTheme().text)
86
+ const bg = span.bgPalette !== undefined ? resolvePaletteIndex(span.bgPalette) : span.bg
87
+
79
88
  return (
80
- <span key={key} fg={span.fg ?? getCurrentTheme().text} bg={span.bg}>
89
+ <span key={key} fg={fg} bg={bg}>
81
90
  {node}
82
91
  </span>
83
92
  )
@@ -0,0 +1,75 @@
1
+ // Resolves ANSI palette indices (0-255) to hex strings using the host
2
+ // terminal's actual palette, queried via OSC 4 at startup (see index.tsx).
3
+ //
4
+ // Before detection completes — or when the host terminal doesn't respond —
5
+ // FALLBACK_PALETTE (xterm defaults) is used. Indices ≥16 fall back to the
6
+ // universal 6×6×6 cube + grayscale ramp, which every terminal agrees on.
7
+
8
+ const FALLBACK_PALETTE: readonly string[] = [
9
+ '#000000',
10
+ '#cd0000',
11
+ '#00cd00',
12
+ '#cdcd00',
13
+ '#0000ee',
14
+ '#cd00cd',
15
+ '#00cdcd',
16
+ '#e5e5e5',
17
+ '#7f7f7f',
18
+ '#ff0000',
19
+ '#00ff00',
20
+ '#ffff00',
21
+ '#5c5cff',
22
+ '#ff00ff',
23
+ '#00ffff',
24
+ '#ffffff',
25
+ ]
26
+
27
+ const CUBE_CHANNELS = [0, 95, 135, 175, 215, 255] as const
28
+
29
+ const hostPalette: string[] = [...FALLBACK_PALETTE]
30
+
31
+ function toHex(value: number): string {
32
+ return `#${value.toString(16).padStart(6, '0')}`
33
+ }
34
+
35
+ function paletteFromFormula(index: number): string {
36
+ if (index >= 232) {
37
+ const shade = 8 + (index - 232) * 10
38
+ return toHex((shade << 16) | (shade << 8) | shade)
39
+ }
40
+
41
+ const normalized = index - 16
42
+ const r = Math.floor(normalized / 36)
43
+ const g = Math.floor((normalized % 36) / 6)
44
+ const b = normalized % 6
45
+ return toHex(
46
+ ((CUBE_CHANNELS[r] ?? 0) << 16) | ((CUBE_CHANNELS[g] ?? 0) << 8) | (CUBE_CHANNELS[b] ?? 0)
47
+ )
48
+ }
49
+
50
+ /** Replace entries 0..N-1 of the resolver with values queried from the host
51
+ * terminal. Null entries keep the existing fallback (terminal didn't respond
52
+ * for that index). Safe to call before any snapshots have been rendered. */
53
+ export function setHostPalette(palette: readonly (string | null)[]): void {
54
+ for (let index = 0; index < palette.length; index += 1) {
55
+ const value = palette[index]
56
+ if (typeof value === 'string' && value.length > 0) {
57
+ hostPalette[index] = value
58
+ }
59
+ }
60
+ }
61
+
62
+ /** Resolve an ANSI palette index (0-255) to a hex color. Indices 0-15 use
63
+ * the host-queried palette (or xterm fallback); 16-255 use the universal
64
+ * cube/grayscale formula unless the host explicitly customized them. */
65
+ const BLACK = '#000000'
66
+ const WHITE = '#ffffff'
67
+
68
+ export function resolvePaletteIndex(index: number): string {
69
+ if (index < 0) return BLACK
70
+ const override = hostPalette[index]
71
+ if (typeof override === 'string' && override.length > 0) return override
72
+ if (index < 16) return FALLBACK_PALETTE.at(index) ?? BLACK
73
+ if (index > 255) return WHITE
74
+ return paletteFromFormula(index)
75
+ }