@brimveyn/aimux 1.5.4 → 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.
- package/README.md +14 -2
- package/package.json +5 -3
- package/src/app-runtime/side-effects.ts +41 -6
- package/src/app.tsx +17 -5
- package/src/config.ts +29 -4
- package/src/diff-parser/clean-last-newline.ts +5 -0
- package/src/diff-parser/constants.ts +13 -0
- package/src/diff-parser/index.ts +12 -0
- package/src/diff-parser/parse-line-type.ts +29 -0
- package/src/diff-parser/parse-patch-files.ts +369 -0
- package/src/diff-parser/types.ts +75 -0
- package/src/git/git-diff.ts +24 -12
- package/src/git/git-poller.ts +11 -3
- package/src/git/git-status.ts +98 -17
- package/src/input/modes/bridge.ts +8 -0
- package/src/input/modes/transitions.ts +2 -1
- package/src/input/modes/types.ts +4 -1
- package/src/pty/terminal-snapshot.ts +5 -5
- package/src/state/git-tree.ts +220 -0
- package/src/state/reducers/git-mode-state.ts +276 -36
- package/src/state/reducers/git-panel-state.ts +35 -4
- package/src/state/reducers/modal-state.ts +43 -10
- package/src/state/store.ts +5 -1
- package/src/state/types.ts +46 -6
- package/src/state/workspace-save.ts +2 -0
- package/src/ui/components/create-session-modal.tsx +25 -8
- package/src/ui/components/diff-renderer/build-rows.ts +349 -0
- package/src/ui/components/diff-renderer/filetype.ts +29 -0
- package/src/ui/components/diff-renderer/fold-strip.tsx +86 -0
- package/src/ui/components/diff-renderer/highlight.ts +48 -0
- package/src/ui/components/diff-renderer/index.ts +1 -0
- package/src/ui/components/diff-renderer/pierre-diff.tsx +171 -0
- package/src/ui/components/diff-renderer/split-view.tsx +211 -0
- package/src/ui/components/diff-renderer/stacked-view.tsx +168 -0
- package/src/ui/components/git-commit-modal.tsx +16 -3
- package/src/ui/components/git-pane-widget.tsx +6 -1
- package/src/ui/components/git-panel.tsx +215 -86
- package/src/ui/components/git-view.tsx +103 -90
- package/src/ui/components/help-modal.tsx +45 -12
- package/src/ui/components/input-field.tsx +4 -3
- package/src/ui/components/list-item.tsx +11 -2
- package/src/ui/components/modal-filter-bar.tsx +3 -2
- package/src/ui/components/modal-keybinds-overlay.tsx +4 -3
- package/src/ui/components/modal-shell.tsx +5 -4
- package/src/ui/components/new-tab-modal.tsx +17 -4
- package/src/ui/components/pending-chord-overlay.tsx +5 -4
- package/src/ui/components/session-bar.tsx +13 -6
- package/src/ui/components/session-picker-modal.tsx +28 -10
- package/src/ui/components/sidebar.tsx +26 -11
- package/src/ui/components/snippet-editor-modal.tsx +18 -3
- package/src/ui/components/snippet-picker-modal.tsx +13 -4
- package/src/ui/components/split-layout.tsx +3 -2
- package/src/ui/components/status-bar.tsx +15 -10
- package/src/ui/components/surface.tsx +6 -6
- package/src/ui/components/tab-item.tsx +28 -17
- package/src/ui/components/terminal-pane.tsx +28 -16
- package/src/ui/components/theme-picker-modal.tsx +113 -17
- package/src/ui/components/update-available-modal.tsx +13 -2
- package/src/ui/filter-themes.ts +15 -0
- package/src/ui/keymap-context.ts +10 -2
- package/src/ui/root.tsx +29 -7
- package/src/ui/shiki.ts +49 -0
- package/src/ui/status-bar-model.ts +32 -4
- package/src/ui/theme-store.ts +36 -0
- package/src/ui/theme.ts +4 -17
- package/src/ui/themes.ts +23 -277
- package/src/ui/syntax.ts +0 -102
|
@@ -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 {
|
|
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
|
|
49
|
+
return getCurrentTheme().colors['focusBorder']
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
if (isActive) {
|
|
53
|
-
return
|
|
53
|
+
return getCurrentTheme().colors['terminal.ansiMagenta']
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
return
|
|
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 ??
|
|
75
|
+
<span key={key} fg={span.fg ?? getCurrentTheme().colors['editor.foreground']} bg={span.bg}>
|
|
76
76
|
{node}
|
|
77
77
|
</span>
|
|
78
78
|
)
|
|
@@ -87,10 +87,11 @@ 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 (
|
|
93
|
-
<text fg={theme.
|
|
94
|
+
<text fg={theme.colors['editor.foreground']}>
|
|
94
95
|
{lines.map((line, lineIndex) => (
|
|
95
96
|
<span key={`line-${lineIndex}`}>
|
|
96
97
|
{line.spans.map((span, spanIndex) => renderSpan(span, `s-${spanIndex}`))}
|
|
@@ -101,7 +102,11 @@ const TerminalViewport = memo(function TerminalViewport({
|
|
|
101
102
|
)
|
|
102
103
|
}
|
|
103
104
|
|
|
104
|
-
return
|
|
105
|
+
return (
|
|
106
|
+
<text fg={theme.colors['editor.foreground']}>
|
|
107
|
+
{buffer.length > 0 ? buffer : 'Waiting for session output...'}
|
|
108
|
+
</text>
|
|
109
|
+
)
|
|
105
110
|
})
|
|
106
111
|
|
|
107
112
|
export function TerminalPane({
|
|
@@ -119,6 +124,7 @@ export function TerminalPane({
|
|
|
119
124
|
tab,
|
|
120
125
|
tabId,
|
|
121
126
|
}: TerminalPaneProps) {
|
|
127
|
+
const theme = useTheme()
|
|
122
128
|
const paneIsActive = isActive ?? true
|
|
123
129
|
const canForwardMouse = focusMode === 'terminal-input' && !!tab && mouseForwardingEnabled
|
|
124
130
|
const canUseLocalScrollback = focusMode === 'terminal-input' && !!tab && localScrollbackEnabled
|
|
@@ -179,19 +185,19 @@ export function TerminalPane({
|
|
|
179
185
|
padding={0}
|
|
180
186
|
flexDirection="column"
|
|
181
187
|
flexGrow={1}
|
|
182
|
-
backgroundColor={theme.background}
|
|
188
|
+
backgroundColor={theme.colors['editor.background']}
|
|
183
189
|
onMouseDrag={forwardMouseEvent}
|
|
184
190
|
onMouseScroll={forwardScrollEvent}
|
|
185
191
|
onMouseUp={forwardMouseEvent}
|
|
186
192
|
>
|
|
187
193
|
{!tab ? (
|
|
188
194
|
<box flexGrow={1} justifyContent="center" alignItems="center" flexDirection="column">
|
|
189
|
-
<text fg={theme.
|
|
190
|
-
<text fg={theme.
|
|
195
|
+
<text fg={theme.colors['editor.lineHighlightBackground']}>· · ·</text>
|
|
196
|
+
<text fg={theme.colors['descriptionForeground']}> </text>
|
|
191
197
|
<box flexDirection="row">
|
|
192
|
-
<text fg={theme.
|
|
193
|
-
<text fg={theme.
|
|
194
|
-
<text fg={theme.
|
|
198
|
+
<text fg={theme.colors['descriptionForeground']}>Press </text>
|
|
199
|
+
<text fg={theme.colors['textLink.foreground']}>Ctrl+n</text>
|
|
200
|
+
<text fg={theme.colors['descriptionForeground']}> to launch an assistant</text>
|
|
195
201
|
</box>
|
|
196
202
|
</box>
|
|
197
203
|
) : (
|
|
@@ -209,12 +215,18 @@ export function TerminalPane({
|
|
|
209
215
|
)}
|
|
210
216
|
</box>
|
|
211
217
|
{tab?.status === 'exited' && tab.exitCode !== undefined ? (
|
|
212
|
-
<text fg={theme.
|
|
218
|
+
<text fg={theme.colors['editorWarning.foreground']}>
|
|
219
|
+
Process exited with code {tab.exitCode}
|
|
220
|
+
</text>
|
|
213
221
|
) : null}
|
|
214
222
|
{tab?.status === 'disconnected' ? (
|
|
215
|
-
<text fg={theme.
|
|
223
|
+
<text fg={theme.colors['editorWarning.foreground']}>
|
|
224
|
+
Restored snapshot. Press Ctrl+r to restart this session.
|
|
225
|
+
</text>
|
|
226
|
+
) : null}
|
|
227
|
+
{tab?.errorMessage ? (
|
|
228
|
+
<text fg={theme.colors['editorError.foreground']}>{tab.errorMessage}</text>
|
|
216
229
|
) : null}
|
|
217
|
-
{tab?.errorMessage ? <text fg={theme.danger}>{tab.errorMessage}</text> : null}
|
|
218
230
|
</box>
|
|
219
231
|
)
|
|
220
232
|
}
|
|
@@ -1,35 +1,131 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { useTerminalDimensions } from '@opentui/react'
|
|
2
|
+
import { useLayoutEffect, useMemo, useRef } from 'react'
|
|
3
|
+
|
|
4
|
+
import { dispatchGlobal } from '../../state/dispatch-ref'
|
|
5
|
+
import { filterThemeIds } from '../filter-themes'
|
|
6
|
+
import { useTheme } from '../theme'
|
|
7
|
+
import { type ThemeId, THEMES } from '../themes'
|
|
3
8
|
import { uiTokens } from '../ui-tokens'
|
|
4
9
|
import { ListItem } from './list-item'
|
|
10
|
+
import { ModalFilterBar } from './modal-filter-bar'
|
|
5
11
|
import { ModalShell } from './modal-shell'
|
|
6
12
|
|
|
7
13
|
interface ThemePickerModalProps {
|
|
8
|
-
selectedIndex: number
|
|
9
14
|
currentThemeId: ThemeId
|
|
15
|
+
filter: string | null
|
|
16
|
+
selectedIndex: number
|
|
10
17
|
}
|
|
11
18
|
|
|
12
|
-
|
|
19
|
+
const VIEWPORT_HEIGHT_RATIO = 0.6
|
|
20
|
+
const MODAL_CHROME_ROWS = 6
|
|
21
|
+
|
|
22
|
+
function clampSelection(index: number, count: number): number {
|
|
23
|
+
if (count === 0) return 0
|
|
24
|
+
return Math.max(0, Math.min(count - 1, index))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function computeWindowStart(
|
|
28
|
+
prevStart: number,
|
|
29
|
+
selectedRowIndex: number,
|
|
30
|
+
total: number,
|
|
31
|
+
windowSize: number
|
|
32
|
+
): number {
|
|
33
|
+
if (total <= windowSize) return 0
|
|
34
|
+
const margin = 1
|
|
35
|
+
const maxStart = total - windowSize
|
|
36
|
+
let start = Math.max(0, Math.min(maxStart, prevStart))
|
|
37
|
+
const topThreshold = start + margin
|
|
38
|
+
const bottomThreshold = start + windowSize - 1 - margin
|
|
39
|
+
if (selectedRowIndex < topThreshold) {
|
|
40
|
+
start = Math.max(0, selectedRowIndex - margin)
|
|
41
|
+
} else if (selectedRowIndex > bottomThreshold) {
|
|
42
|
+
start = Math.min(maxStart, selectedRowIndex - windowSize + 1 + margin)
|
|
43
|
+
}
|
|
44
|
+
return start
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function ThemePickerModal({ currentThemeId, filter, selectedIndex }: ThemePickerModalProps) {
|
|
48
|
+
const theme = useTheme()
|
|
49
|
+
const dimensions = useTerminalDimensions()
|
|
50
|
+
const filtered = useMemo(() => filterThemeIds(filter), [filter])
|
|
51
|
+
|
|
52
|
+
useLayoutEffect(() => {
|
|
53
|
+
dispatchGlobal({ count: filtered.length, type: 'set-theme-entry-count' })
|
|
54
|
+
}, [filtered.length])
|
|
55
|
+
|
|
56
|
+
const effectiveIndex = clampSelection(selectedIndex, filtered.length)
|
|
57
|
+
const maxHeight = Math.max(6, Math.floor(dimensions.height * VIEWPORT_HEIGHT_RATIO))
|
|
58
|
+
const listHeight = Math.max(1, maxHeight - MODAL_CHROME_ROWS)
|
|
59
|
+
|
|
60
|
+
const prevStartRef = useRef(0)
|
|
61
|
+
const prevFilterRef = useRef<string | null>(filter)
|
|
62
|
+
if (prevFilterRef.current !== filter) {
|
|
63
|
+
prevFilterRef.current = filter
|
|
64
|
+
prevStartRef.current = 0
|
|
65
|
+
}
|
|
66
|
+
const start = computeWindowStart(
|
|
67
|
+
prevStartRef.current,
|
|
68
|
+
effectiveIndex,
|
|
69
|
+
filtered.length,
|
|
70
|
+
listHeight
|
|
71
|
+
)
|
|
72
|
+
prevStartRef.current = start
|
|
73
|
+
const visible = filtered.slice(start, start + listHeight)
|
|
74
|
+
|
|
13
75
|
return (
|
|
14
76
|
<ModalShell
|
|
15
77
|
title="Select theme"
|
|
16
78
|
keybindsModeId="modal.theme-picker"
|
|
17
79
|
width={uiTokens.modalWidth.md}
|
|
18
80
|
listGap={0}
|
|
81
|
+
footer={
|
|
82
|
+
<box flexDirection="column" gap={0}>
|
|
83
|
+
<text fg={theme.colors['editor.lineHighlightBackground']}>
|
|
84
|
+
{filtered.length === 0
|
|
85
|
+
? ''
|
|
86
|
+
: ` ${effectiveIndex + 1} / ${filtered.length}${filter ? '' : ' — type / to filter'}`}
|
|
87
|
+
</text>
|
|
88
|
+
<ModalFilterBar filter={filter} />
|
|
89
|
+
</box>
|
|
90
|
+
}
|
|
19
91
|
>
|
|
20
|
-
{
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
92
|
+
{filtered.length === 0 ? (
|
|
93
|
+
<text fg={theme.colors['descriptionForeground']}>
|
|
94
|
+
{filter ? 'No matching themes.' : 'No themes available.'}
|
|
95
|
+
</text>
|
|
96
|
+
) : (
|
|
97
|
+
<box height={listHeight} flexDirection="column" overflow="hidden">
|
|
98
|
+
{visible.map((id, i) => {
|
|
99
|
+
const rowIndex = start + i
|
|
100
|
+
const entry = THEMES[id]
|
|
101
|
+
if (!entry) return null
|
|
102
|
+
const active = rowIndex === effectiveIndex
|
|
103
|
+
const isCurrent = id === currentThemeId
|
|
104
|
+
return (
|
|
105
|
+
<ListItem
|
|
106
|
+
key={id}
|
|
107
|
+
active={active}
|
|
108
|
+
title={
|
|
109
|
+
<text
|
|
110
|
+
fg={
|
|
111
|
+
active
|
|
112
|
+
? theme.colors['editor.foreground']
|
|
113
|
+
: theme.colors['descriptionForeground']
|
|
114
|
+
}
|
|
115
|
+
>
|
|
116
|
+
{entry.name}
|
|
117
|
+
</text>
|
|
118
|
+
}
|
|
119
|
+
trailing={
|
|
120
|
+
isCurrent ? (
|
|
121
|
+
<text fg={theme.colors['textLink.foreground']}>current</text>
|
|
122
|
+
) : undefined
|
|
123
|
+
}
|
|
124
|
+
/>
|
|
125
|
+
)
|
|
126
|
+
})}
|
|
127
|
+
</box>
|
|
128
|
+
)}
|
|
33
129
|
</ModalShell>
|
|
34
130
|
)
|
|
35
131
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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"
|
|
@@ -35,7 +36,17 @@ export function UpdateAvailableModal({
|
|
|
35
36
|
key={option.label}
|
|
36
37
|
active={active}
|
|
37
38
|
direction="row"
|
|
38
|
-
title={
|
|
39
|
+
title={
|
|
40
|
+
<text
|
|
41
|
+
fg={
|
|
42
|
+
active
|
|
43
|
+
? theme.colors['editor.foreground']
|
|
44
|
+
: theme.colors['descriptionForeground']
|
|
45
|
+
}
|
|
46
|
+
>
|
|
47
|
+
{option.label}
|
|
48
|
+
</text>
|
|
49
|
+
}
|
|
39
50
|
/>
|
|
40
51
|
)
|
|
41
52
|
})}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { THEME_IDS, type ThemeId, THEMES } from './themes'
|
|
2
|
+
|
|
3
|
+
export function filterThemeIds(filter: string | null): ThemeId[] {
|
|
4
|
+
if (!filter) return THEME_IDS
|
|
5
|
+
const needle = filter.toLowerCase()
|
|
6
|
+
return THEME_IDS.filter((id) => {
|
|
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
|
+
)
|
|
14
|
+
})
|
|
15
|
+
}
|
package/src/ui/keymap-context.ts
CHANGED
|
@@ -18,17 +18,25 @@ export function useKeymap(): ResolvedKeymapConfig {
|
|
|
18
18
|
const HELP_JOINER = ' · '
|
|
19
19
|
const MAX_HELP_ENTRIES = 5
|
|
20
20
|
|
|
21
|
+
interface BuildHintOptions {
|
|
22
|
+
excludeDescriptions?: readonly string[]
|
|
23
|
+
}
|
|
24
|
+
|
|
21
25
|
/** Build a single-line help string for a mode (used in modal headers and status bar). */
|
|
22
26
|
export function buildHintText(
|
|
23
27
|
config: ResolvedKeymapConfig,
|
|
24
28
|
modeId: ModeId,
|
|
25
|
-
limit: number = MAX_HELP_ENTRIES
|
|
29
|
+
limit: number = MAX_HELP_ENTRIES,
|
|
30
|
+
options: BuildHintOptions = {}
|
|
26
31
|
): string {
|
|
27
32
|
const bindings = describeBindings(config, modeId, {
|
|
28
33
|
dedupeByDescription: true,
|
|
29
34
|
withDescriptionOnly: true,
|
|
30
35
|
})
|
|
31
36
|
if (bindings.length === 0) return ''
|
|
32
|
-
const
|
|
37
|
+
const excluded = new Set(options.excludeDescriptions ?? [])
|
|
38
|
+
const kept =
|
|
39
|
+
excluded.size > 0 ? bindings.filter((b) => !excluded.has(b.description ?? '')) : bindings
|
|
40
|
+
const trimmed = kept.slice(0, limit)
|
|
33
41
|
return trimmed.map((b) => `${b.keysDisplay} ${b.description ?? ''}`.trim()).join(HELP_JOINER)
|
|
34
42
|
}
|
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 {
|
|
27
|
+
import { useTheme } from './theme'
|
|
28
28
|
|
|
29
29
|
function getCreateSessionFields(modal: ModalState) {
|
|
30
30
|
if (modal.type !== 'create-session') {
|
|
@@ -134,7 +134,11 @@ function renderModal(
|
|
|
134
134
|
)
|
|
135
135
|
case 'theme-picker':
|
|
136
136
|
return (
|
|
137
|
-
<ThemePickerModal
|
|
137
|
+
<ThemePickerModal
|
|
138
|
+
selectedIndex={modal.selectedIndex}
|
|
139
|
+
currentThemeId={options.themeId}
|
|
140
|
+
filter={modal.editBuffer}
|
|
141
|
+
/>
|
|
138
142
|
)
|
|
139
143
|
case 'update-available':
|
|
140
144
|
return (
|
|
@@ -145,7 +149,13 @@ function renderModal(
|
|
|
145
149
|
/>
|
|
146
150
|
)
|
|
147
151
|
case 'help':
|
|
148
|
-
return
|
|
152
|
+
return (
|
|
153
|
+
<HelpModal
|
|
154
|
+
filter={modal.editBuffer}
|
|
155
|
+
selectedIndex={modal.selectedIndex}
|
|
156
|
+
scope={modal.scope}
|
|
157
|
+
/>
|
|
158
|
+
)
|
|
149
159
|
case 'git-commit': {
|
|
150
160
|
const titleText =
|
|
151
161
|
modal.activeField === 'title' ? (modal.editBuffer ?? '') : modal.contentBuffer
|
|
@@ -205,6 +215,7 @@ export function RootView({
|
|
|
205
215
|
terminalRows,
|
|
206
216
|
themeId,
|
|
207
217
|
}: RootViewProps) {
|
|
218
|
+
const theme = useTheme()
|
|
208
219
|
const tabs = useAppStore((s) => s.tabs)
|
|
209
220
|
const activeTabId = useAppStore((s) => s.activeTabId)
|
|
210
221
|
const layoutTrees = useAppStore((s) => s.layoutTrees)
|
|
@@ -230,8 +241,13 @@ export function RootView({
|
|
|
230
241
|
const inGitMode = focusMode === 'git' || modal.type === 'git-commit'
|
|
231
242
|
if (inGitMode) {
|
|
232
243
|
return (
|
|
233
|
-
<box
|
|
234
|
-
|
|
244
|
+
<box
|
|
245
|
+
flexDirection="column"
|
|
246
|
+
width="100%"
|
|
247
|
+
height="100%"
|
|
248
|
+
backgroundColor={theme.colors['editor.background']}
|
|
249
|
+
>
|
|
250
|
+
<GitView themeId={themeId} />
|
|
235
251
|
<StatusBar />
|
|
236
252
|
<PendingChordOverlay />
|
|
237
253
|
{renderModal(modal, {
|
|
@@ -249,7 +265,12 @@ export function RootView({
|
|
|
249
265
|
}
|
|
250
266
|
|
|
251
267
|
return (
|
|
252
|
-
<box
|
|
268
|
+
<box
|
|
269
|
+
flexDirection="column"
|
|
270
|
+
width="100%"
|
|
271
|
+
height="100%"
|
|
272
|
+
backgroundColor={theme.colors['editor.background']}
|
|
273
|
+
>
|
|
253
274
|
{sessionBarPosition === 'top' && <SessionBar />}
|
|
254
275
|
<box flexDirection="row" gap={0} padding={0} flexGrow={1}>
|
|
255
276
|
<Sidebar onTabActivate={onPaneActivate} />
|
|
@@ -322,6 +343,7 @@ export function RootView({
|
|
|
322
343
|
}
|
|
323
344
|
|
|
324
345
|
function GitPaneInPaneMode({ ratio }: { ratio: number }) {
|
|
346
|
+
const theme = useTheme()
|
|
325
347
|
// Ratio maps to a fixed column count (20..80), mirroring the reservation in
|
|
326
348
|
// use-terminal-resize so the terminal-content area stays in sync.
|
|
327
349
|
const width = Math.max(20, Math.min(80, Math.round(ratio * 80)))
|
|
@@ -330,7 +352,7 @@ function GitPaneInPaneMode({ ratio }: { ratio: number }) {
|
|
|
330
352
|
flexDirection="column"
|
|
331
353
|
width={width}
|
|
332
354
|
flexShrink={0}
|
|
333
|
-
backgroundColor={theme.
|
|
355
|
+
backgroundColor={theme.colors['sideBar.background']}
|
|
334
356
|
overflow="hidden"
|
|
335
357
|
>
|
|
336
358
|
<GitPaneWidget pollingEnabled />
|
package/src/ui/shiki.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type BundledLanguage, type BundledTheme, createHighlighter, type Highlighter } from 'shiki'
|
|
2
|
+
|
|
3
|
+
import { THEMES } from './themes'
|
|
4
|
+
|
|
5
|
+
// Shiki's highlighter is created with at least one real shiki-bundled theme so
|
|
6
|
+
// its worker can warm up. House and user themes load on demand.
|
|
7
|
+
const WARM_THEME: BundledTheme = 'catppuccin-mocha'
|
|
8
|
+
|
|
9
|
+
let highlighterPromise: Promise<Highlighter> | null = null
|
|
10
|
+
|
|
11
|
+
export async function getShikiHighlighter(): Promise<Highlighter> {
|
|
12
|
+
if (!highlighterPromise) {
|
|
13
|
+
highlighterPromise = createHighlighter({ langs: [], themes: [WARM_THEME] })
|
|
14
|
+
}
|
|
15
|
+
return highlighterPromise
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const loadedLangs = new Set<string>()
|
|
19
|
+
|
|
20
|
+
export async function ensureShikiLang(h: Highlighter, lang: string): Promise<boolean> {
|
|
21
|
+
if (loadedLangs.has(lang)) return true
|
|
22
|
+
try {
|
|
23
|
+
await h.loadLanguage(lang as BundledLanguage)
|
|
24
|
+
loadedLangs.add(lang)
|
|
25
|
+
return true
|
|
26
|
+
} catch {
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const loadedThemes = new Set<string>([WARM_THEME])
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Load the theme keyed by `id` into the shiki highlighter. The same object
|
|
35
|
+
* powers the UI palette and the code highlighter — no synthesis, no mapping.
|
|
36
|
+
*/
|
|
37
|
+
export async function ensureShikiTheme(h: Highlighter, id: string): Promise<boolean> {
|
|
38
|
+
if (loadedThemes.has(id)) return true
|
|
39
|
+
const entry = THEMES[id]
|
|
40
|
+
if (!entry) return false
|
|
41
|
+
try {
|
|
42
|
+
// eslint-disable-next-line typescript/no-explicit-any
|
|
43
|
+
await h.loadTheme(entry as any)
|
|
44
|
+
loadedThemes.add(id)
|
|
45
|
+
return true
|
|
46
|
+
} catch {
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -2,16 +2,19 @@ import type { ModeId, ResolvedKeymapConfig } from '@brimveyn/aimux-config'
|
|
|
2
2
|
|
|
3
3
|
import type { AppState, TabSession } from '../state/types'
|
|
4
4
|
|
|
5
|
+
import { describeBindings } from '../input/keymap/describe-bindings'
|
|
5
6
|
import { buildHintText } from './keymap-context'
|
|
6
7
|
import { abbreviatePath } from './path-format'
|
|
7
8
|
|
|
8
9
|
export interface StatusBarModel {
|
|
9
10
|
left: string
|
|
10
11
|
right: string
|
|
12
|
+
help: string
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
const MAX_TAB_LABEL_LENGTH = 24
|
|
14
16
|
const HINT_LIMIT = 6
|
|
17
|
+
const HELP_DESCRIPTION = 'Help'
|
|
15
18
|
|
|
16
19
|
function truncateLabel(label: string): string {
|
|
17
20
|
if (label.length <= MAX_TAB_LABEL_LENGTH) {
|
|
@@ -29,8 +32,25 @@ function getActiveTabLabel(tab?: TabSession): string {
|
|
|
29
32
|
return `${truncateLabel(tab.title)} (${tab.status})`
|
|
30
33
|
}
|
|
31
34
|
|
|
35
|
+
const STAGING_DESCRIPTIONS = ['Stage', 'Unstage/delete', 'Commit', 'Push']
|
|
36
|
+
|
|
32
37
|
function hintForMode(config: ResolvedKeymapConfig, modeId: ModeId): string {
|
|
33
|
-
return buildHintText(config, modeId, HINT_LIMIT)
|
|
38
|
+
return buildHintText(config, modeId, HINT_LIMIT, { excludeDescriptions: [HELP_DESCRIPTION] })
|
|
39
|
+
}
|
|
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
|
+
|
|
46
|
+
function helpHintForMode(config: ResolvedKeymapConfig, modeId: ModeId): string {
|
|
47
|
+
const bindings = describeBindings(config, modeId, {
|
|
48
|
+
dedupeByDescription: true,
|
|
49
|
+
withDescriptionOnly: true,
|
|
50
|
+
})
|
|
51
|
+
const helpBinding = bindings.find((b) => b.description === HELP_DESCRIPTION)
|
|
52
|
+
if (!helpBinding) return ''
|
|
53
|
+
return `${helpBinding.keysDisplay} ${helpBinding.description ?? ''}`.trim()
|
|
34
54
|
}
|
|
35
55
|
|
|
36
56
|
export function getStatusBarModel(
|
|
@@ -52,24 +72,31 @@ export function getStatusBarModel(
|
|
|
52
72
|
switch (state.focusMode) {
|
|
53
73
|
case 'terminal-input':
|
|
54
74
|
return {
|
|
75
|
+
help: '',
|
|
55
76
|
left: `${getActiveTabLabel(activeTab)} ${sessionIcon} ${sessionLabel}`,
|
|
56
77
|
right: hintForMode(config, 'terminal-input'),
|
|
57
78
|
}
|
|
58
79
|
case 'modal': {
|
|
59
80
|
const modalMode = deriveModalModeId(state.modal.type)
|
|
60
81
|
return {
|
|
82
|
+
help: '',
|
|
61
83
|
left: `${sessionIcon} ${sessionLabel}`,
|
|
62
84
|
right: modalMode ? hintForMode(config, modalMode) : '',
|
|
63
85
|
}
|
|
64
86
|
}
|
|
65
|
-
case 'git':
|
|
87
|
+
case 'git': {
|
|
88
|
+
const headOffset = state.gitMode.headOffset
|
|
89
|
+
const offsetTag = headOffset > 0 ? ` HEAD~${headOffset}` : ''
|
|
66
90
|
return {
|
|
67
|
-
|
|
68
|
-
|
|
91
|
+
help: helpHintForMode(config, 'git-mode'),
|
|
92
|
+
left: `${sessionIcon} ${sessionLabel}${offsetTag}`,
|
|
93
|
+
right: hintForGitMode(config, headOffset),
|
|
69
94
|
}
|
|
95
|
+
}
|
|
70
96
|
case 'command-edit': {
|
|
71
97
|
const commandEditMode = deriveCommandEditModeId(state.modal.type)
|
|
72
98
|
return {
|
|
99
|
+
help: '',
|
|
73
100
|
left: `${sessionIcon} ${sessionLabel}`,
|
|
74
101
|
right: commandEditMode ? hintForMode(config, commandEditMode) : '',
|
|
75
102
|
}
|
|
@@ -77,6 +104,7 @@ export function getStatusBarModel(
|
|
|
77
104
|
case 'navigation':
|
|
78
105
|
default:
|
|
79
106
|
return {
|
|
107
|
+
help: helpHintForMode(config, 'navigation'),
|
|
80
108
|
left: `${sessionIcon} ${sessionLabel} ${getActiveTabLabel(activeTab)}`,
|
|
81
109
|
right: hintForMode(config, 'navigation'),
|
|
82
110
|
}
|
|
@@ -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,18 +1,5 @@
|
|
|
1
|
-
|
|
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
|
-
export
|
|
4
|
-
|
|
5
|
-
type ThemeListener = () => void
|
|
6
|
-
const themeListeners = new Set<ThemeListener>()
|
|
7
|
-
|
|
8
|
-
export function onThemeChange(listener: ThemeListener): () => void {
|
|
9
|
-
themeListeners.add(listener)
|
|
10
|
-
return () => themeListeners.delete(listener)
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function applyTheme(id: ThemeId): void {
|
|
14
|
-
const entry = THEMES[id]
|
|
15
|
-
if (!entry) return
|
|
16
|
-
Object.assign(theme, entry.colors)
|
|
17
|
-
for (const listener of themeListeners) listener()
|
|
18
|
-
}
|
|
5
|
+
export { applyTheme, getCurrentTheme, useTheme } from './theme-store'
|