@brimveyn/aimux 1.6.2 → 1.7.0
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 +3 -0
- package/package.json +2 -2
- package/src/app-runtime/backend-attach-runtime.ts +6 -0
- package/src/app-runtime/backend-runtime-events.ts +21 -21
- package/src/app-runtime/side-effects.ts +24 -12
- package/src/app-runtime/use-backend-runtime.ts +2 -20
- package/src/app-runtime/use-directory-search.ts +6 -1
- package/src/app.tsx +2 -1
- package/src/config.ts +9 -0
- package/src/daemon/daemon.ts +197 -15
- package/src/daemon/session-manager.ts +9 -0
- package/src/daemon/session-registry.ts +5 -5
- package/src/index.tsx +10 -2
- package/src/input/keymap/help-entries.ts +5 -5
- package/src/input/modes/bridge.ts +5 -8
- package/src/input/modes/transitions.ts +15 -23
- package/src/input/modes/types.ts +2 -5
- package/src/ipc/protocol.ts +49 -5
- package/src/platform/project-search.ts +45 -12
- package/src/pty/assistant-status-detection-loop.ts +192 -0
- package/src/pty/assistant-status-detector.ts +226 -0
- package/src/pty/pty-manager.ts +3 -37
- package/src/session-backend/bootstrap.ts +4 -1
- package/src/session-backend/local-session-backend.ts +43 -56
- package/src/session-backend/remote-session-backend.ts +15 -0
- package/src/session-backend/types.ts +10 -1
- package/src/state/reducers/modal-state.ts +91 -134
- package/src/state/reducers/session-state.ts +13 -6
- package/src/state/reducers/tab-state.ts +0 -10
- package/src/state/selectors.ts +12 -0
- package/src/state/session-persistence.ts +20 -15
- package/src/state/store.ts +11 -3
- package/src/state/types.ts +29 -13
- package/src/ui/breaking-update-screen.tsx +31 -0
- package/src/ui/components/bare-input.tsx +44 -0
- package/src/ui/components/create-session-modal.tsx +16 -8
- package/src/ui/components/diff-renderer/fold-strip.tsx +5 -13
- package/src/ui/components/diff-renderer/split-view.tsx +11 -17
- package/src/ui/components/diff-renderer/stacked-view.tsx +7 -14
- package/src/ui/components/git-panel.tsx +10 -3
- package/src/ui/components/git-view.tsx +4 -8
- package/src/ui/components/help-modal.tsx +45 -160
- package/src/ui/components/input-field.tsx +6 -2
- package/src/ui/components/list-item.tsx +36 -28
- package/src/ui/components/modal-shell.tsx +51 -13
- package/src/ui/components/new-tab-modal.tsx +78 -45
- package/src/ui/components/picker.tsx +179 -0
- package/src/ui/components/session-bar.tsx +47 -21
- package/src/ui/components/session-picker-modal.tsx +58 -56
- package/src/ui/components/sidebar.tsx +49 -22
- package/src/ui/components/snippet-picker-modal.tsx +43 -34
- package/src/ui/components/status-bar.tsx +3 -2
- package/src/ui/components/surface.tsx +11 -8
- package/src/ui/components/tab-item.tsx +38 -22
- package/src/ui/components/terminal-pane.tsx +8 -8
- package/src/ui/components/theme-picker-modal.tsx +51 -91
- package/src/ui/root.tsx +14 -23
- package/src/ui/status-bar-model.ts +5 -5
- package/src/ui/theme-store.ts +26 -2
- package/src/ui/theme.ts +9 -1
- package/src/ui/components/modal-filter-bar.tsx +0 -19
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useTheme } from '../theme'
|
|
2
|
+
|
|
3
|
+
interface BareInputProps {
|
|
4
|
+
value: string
|
|
5
|
+
cursorPos?: number
|
|
6
|
+
placeholder?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function BareInput({ cursorPos, placeholder = '', value }: BareInputProps) {
|
|
10
|
+
const theme = useTheme()
|
|
11
|
+
const fg = theme.colors['editor.foreground']
|
|
12
|
+
const bg = theme.colors['editor.background']
|
|
13
|
+
const placeholderFg = theme.colors['editorLineNumber.foreground']
|
|
14
|
+
|
|
15
|
+
if (!value) {
|
|
16
|
+
const firstChar = placeholder.charAt(0) || ' '
|
|
17
|
+
const rest = placeholder.slice(1)
|
|
18
|
+
return (
|
|
19
|
+
<text>
|
|
20
|
+
<span bg={fg} fg={bg}>
|
|
21
|
+
{firstChar}
|
|
22
|
+
</span>
|
|
23
|
+
<span fg={placeholderFg}>{rest}</span>
|
|
24
|
+
</text>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const safePos =
|
|
29
|
+
cursorPos === undefined ? value.length : Math.max(0, Math.min(value.length, cursorPos))
|
|
30
|
+
const before = value.slice(0, safePos)
|
|
31
|
+
const cursorOnEnd = safePos >= value.length
|
|
32
|
+
const cursorChar = cursorOnEnd ? ' ' : value.charAt(safePos)
|
|
33
|
+
const after = cursorOnEnd ? '' : value.slice(safePos + 1)
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<text fg={fg}>
|
|
37
|
+
{before}
|
|
38
|
+
<span bg={fg} fg={bg}>
|
|
39
|
+
{cursorChar}
|
|
40
|
+
</span>
|
|
41
|
+
{after}
|
|
42
|
+
</text>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
@@ -7,6 +7,8 @@ import { InputField } from './input-field'
|
|
|
7
7
|
import { ListItem } from './list-item'
|
|
8
8
|
import { ModalShell } from './modal-shell'
|
|
9
9
|
|
|
10
|
+
const VISIBLE_ROWS = 8
|
|
11
|
+
|
|
10
12
|
function getDirectoryResultIcon(result: DirectoryResult): string {
|
|
11
13
|
if (result.type === 'worktree') {
|
|
12
14
|
return '\u{e728}'
|
|
@@ -52,6 +54,9 @@ export function CreateSessionModal({
|
|
|
52
54
|
const dirActive = activeField === 'directory'
|
|
53
55
|
const nameActive = activeField === 'name'
|
|
54
56
|
|
|
57
|
+
const scrollOffset = Math.max(0, selectedIndex - VISIBLE_ROWS + 1)
|
|
58
|
+
const visibleResults = results.slice(scrollOffset, scrollOffset + VISIBLE_ROWS)
|
|
59
|
+
|
|
55
60
|
return (
|
|
56
61
|
<ModalShell
|
|
57
62
|
title="Create session"
|
|
@@ -66,19 +71,21 @@ export function CreateSessionModal({
|
|
|
66
71
|
</text>
|
|
67
72
|
<InputField
|
|
68
73
|
active={dirActive}
|
|
74
|
+
placeholder="Type a project name..."
|
|
69
75
|
value={
|
|
70
76
|
pendingProjectPath && !dirActive ? abbreviatePath(pendingProjectPath) : directoryQuery
|
|
71
77
|
}
|
|
72
78
|
/>
|
|
73
79
|
</box>
|
|
74
80
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
81
|
+
<box flexDirection="column" height={VISIBLE_ROWS}>
|
|
82
|
+
{results.length === 0 ? (
|
|
83
|
+
<text fg={theme.colors['descriptionForeground']}>
|
|
84
|
+
{directoryQuery.length > 0 ? 'No matches' : 'Type a project name to search...'}
|
|
85
|
+
</text>
|
|
86
|
+
) : (
|
|
87
|
+
visibleResults.map((result, index) => {
|
|
88
|
+
const active = dirActive && scrollOffset + index === selectedIndex
|
|
82
89
|
return (
|
|
83
90
|
<ListItem
|
|
84
91
|
key={result.path}
|
|
@@ -100,7 +107,8 @@ export function CreateSessionModal({
|
|
|
100
107
|
/>
|
|
101
108
|
)
|
|
102
109
|
})
|
|
103
|
-
|
|
110
|
+
)}
|
|
111
|
+
</box>
|
|
104
112
|
|
|
105
113
|
<box flexDirection="column">
|
|
106
114
|
<text
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useTheme } from '../../theme'
|
|
1
|
+
import { useBg, useTheme } from '../../theme'
|
|
2
2
|
import { FOLD_STEP, type FoldInfo } from './build-rows'
|
|
3
3
|
import { type FoldDispatch } from './pierre-diff'
|
|
4
4
|
|
|
@@ -9,13 +9,9 @@ interface Props {
|
|
|
9
9
|
|
|
10
10
|
function Button({ label, onPress }: { label: string; onPress: () => void }) {
|
|
11
11
|
const theme = useTheme()
|
|
12
|
+
const bg = useBg('sideBar.background')
|
|
12
13
|
return (
|
|
13
|
-
<box
|
|
14
|
-
paddingLeft={1}
|
|
15
|
-
paddingRight={1}
|
|
16
|
-
backgroundColor={theme.colors['sideBar.background']}
|
|
17
|
-
onMouseDown={onPress}
|
|
18
|
-
>
|
|
14
|
+
<box paddingLeft={1} paddingRight={1} backgroundColor={bg} onMouseDown={onPress}>
|
|
19
15
|
<text fg={theme.colors['textLink.foreground']}>{label}</text>
|
|
20
16
|
</box>
|
|
21
17
|
)
|
|
@@ -27,6 +23,7 @@ function Spacer() {
|
|
|
27
23
|
|
|
28
24
|
export function FoldStrip({ dispatch, fold }: Props) {
|
|
29
25
|
const theme = useTheme()
|
|
26
|
+
const headerBg = useBg('sideBarSectionHeader.background')
|
|
30
27
|
const { bottomExpanded, foldId, hidden, topExpanded, total } = fold
|
|
31
28
|
const stepUp = Math.min(FOLD_STEP, hidden)
|
|
32
29
|
const stepDown = Math.min(FOLD_STEP, hidden)
|
|
@@ -73,12 +70,7 @@ export function FoldStrip({ dispatch, fold }: Props) {
|
|
|
73
70
|
}
|
|
74
71
|
|
|
75
72
|
return (
|
|
76
|
-
<box
|
|
77
|
-
flexDirection="row"
|
|
78
|
-
backgroundColor={theme.colors['sideBarSectionHeader.background']}
|
|
79
|
-
paddingLeft={1}
|
|
80
|
-
paddingRight={1}
|
|
81
|
-
>
|
|
73
|
+
<box flexDirection="row" backgroundColor={headerBg} paddingLeft={1} paddingRight={1}>
|
|
82
74
|
<text fg={theme.colors['descriptionForeground']}>{`⋯ ${hidden} hidden `}</text>
|
|
83
75
|
{controls}
|
|
84
76
|
</box>
|
|
@@ -13,7 +13,7 @@ import type { DiffHighlights, FoldDispatch } from './pierre-diff'
|
|
|
13
13
|
|
|
14
14
|
import { getScrollViewportDelta } from '../../../app-runtime/terminal-mouse-adapter'
|
|
15
15
|
import { scrollGitDiff } from '../../git-view-controls'
|
|
16
|
-
import { useTheme } from '../../theme'
|
|
16
|
+
import { useBg, useTheme, useTransparent } from '../../theme'
|
|
17
17
|
import { buildSplitRows, gutterWidth, type SplitCell, type SplitRowOrHeader } from './build-rows'
|
|
18
18
|
import { FoldStrip } from './fold-strip'
|
|
19
19
|
import { tokenToSpan } from './highlight'
|
|
@@ -48,7 +48,7 @@ export const SplitView = forwardRef<SplitViewHandle, Props>(function SplitView(
|
|
|
48
48
|
{ contentWidth, file, foldDispatch, folds, highlights },
|
|
49
49
|
ref
|
|
50
50
|
) {
|
|
51
|
-
const
|
|
51
|
+
const separatorBg = useBg('editor.background')
|
|
52
52
|
const leftRef = useRef<ScrollBoxRenderable | null>(null)
|
|
53
53
|
const rightRef = useRef<ScrollBoxRenderable | null>(null)
|
|
54
54
|
|
|
@@ -94,7 +94,7 @@ export const SplitView = forwardRef<SplitViewHandle, Props>(function SplitView(
|
|
|
94
94
|
))}
|
|
95
95
|
{truncated ? <TruncationNotice hidden={rows.length - displayRows.length} /> : null}
|
|
96
96
|
</scrollbox>
|
|
97
|
-
<box width={1} backgroundColor={
|
|
97
|
+
<box width={1} backgroundColor={separatorBg} />
|
|
98
98
|
<scrollbox
|
|
99
99
|
ref={rightRef}
|
|
100
100
|
flexGrow={1}
|
|
@@ -122,13 +122,9 @@ export const SplitView = forwardRef<SplitViewHandle, Props>(function SplitView(
|
|
|
122
122
|
|
|
123
123
|
function TruncationNotice({ hidden }: { hidden: number }) {
|
|
124
124
|
const theme = useTheme()
|
|
125
|
+
const headerBg = useBg('sideBarSectionHeader.background')
|
|
125
126
|
return (
|
|
126
|
-
<box
|
|
127
|
-
flexDirection="row"
|
|
128
|
-
backgroundColor={theme.colors['sideBarSectionHeader.background']}
|
|
129
|
-
paddingLeft={1}
|
|
130
|
-
paddingRight={1}
|
|
131
|
-
>
|
|
127
|
+
<box flexDirection="row" backgroundColor={headerBg} paddingLeft={1} paddingRight={1}>
|
|
132
128
|
<text fg={theme.colors['editorWarning.foreground']}>
|
|
133
129
|
…diff truncated — {hidden} more rows hidden
|
|
134
130
|
</text>
|
|
@@ -159,13 +155,9 @@ function SideRow({
|
|
|
159
155
|
|
|
160
156
|
function HunkHeaderRow({ row }: { row: Extract<SplitRowOrHeader, { type: 'hunk-header' }> }) {
|
|
161
157
|
const theme = useTheme()
|
|
158
|
+
const headerBg = useBg('sideBarSectionHeader.background')
|
|
162
159
|
return (
|
|
163
|
-
<box
|
|
164
|
-
flexDirection="row"
|
|
165
|
-
backgroundColor={theme.colors['sideBarSectionHeader.background']}
|
|
166
|
-
paddingLeft={1}
|
|
167
|
-
paddingRight={1}
|
|
168
|
-
>
|
|
160
|
+
<box flexDirection="row" backgroundColor={headerBg} paddingLeft={1} paddingRight={1}>
|
|
169
161
|
<text fg={theme.colors['descriptionForeground']}>{row.spec}</text>
|
|
170
162
|
{row.context ? (
|
|
171
163
|
<text fg={theme.colors['editor.lineHighlightBackground']}> {row.context}</text>
|
|
@@ -186,8 +178,10 @@ function HalfRow({
|
|
|
186
178
|
tokens: ThemedToken[][]
|
|
187
179
|
}) {
|
|
188
180
|
const theme = useTheme()
|
|
181
|
+
const headerBg = useBg('sideBarSectionHeader.background')
|
|
182
|
+
const transparent = useTransparent()
|
|
189
183
|
if (cell.type === 'filler') {
|
|
190
|
-
return <box backgroundColor={
|
|
184
|
+
return <box backgroundColor={headerBg} height={height} />
|
|
191
185
|
}
|
|
192
186
|
let bg: string | undefined
|
|
193
187
|
let sign = ' '
|
|
@@ -204,7 +198,7 @@ function HalfRow({
|
|
|
204
198
|
const num = String(cell.lineNumber).padStart(gw, ' ')
|
|
205
199
|
const lineTokens = tokens[cell.lineIdx]
|
|
206
200
|
return (
|
|
207
|
-
<box flexDirection="row" backgroundColor={bg} height={height}>
|
|
201
|
+
<box flexDirection="row" backgroundColor={transparent ? undefined : bg} height={height}>
|
|
208
202
|
<text fg={theme.colors['descriptionForeground']}>{` ${num} `}</text>
|
|
209
203
|
<text fg={signColor}>{`${sign} `}</text>
|
|
210
204
|
<LineContent content={cell.content} tokens={lineTokens} />
|
|
@@ -13,7 +13,7 @@ import type { DiffHighlights, FoldDispatch } from './pierre-diff'
|
|
|
13
13
|
|
|
14
14
|
import { getScrollViewportDelta } from '../../../app-runtime/terminal-mouse-adapter'
|
|
15
15
|
import { scrollGitDiff } from '../../git-view-controls'
|
|
16
|
-
import { useTheme } from '../../theme'
|
|
16
|
+
import { useBg, useTheme, useTransparent } from '../../theme'
|
|
17
17
|
import { buildUnifiedRows, gutterWidth, type UnifiedRowOrHeader } from './build-rows'
|
|
18
18
|
import { FoldStrip } from './fold-strip'
|
|
19
19
|
import { tokenToSpan } from './highlight'
|
|
@@ -90,13 +90,9 @@ export const StackedView = forwardRef<StackedViewHandle, Props>(function Stacked
|
|
|
90
90
|
|
|
91
91
|
function TruncationNotice({ hidden }: { hidden: number }) {
|
|
92
92
|
const theme = useTheme()
|
|
93
|
+
const headerBg = useBg('sideBarSectionHeader.background')
|
|
93
94
|
return (
|
|
94
|
-
<box
|
|
95
|
-
flexDirection="row"
|
|
96
|
-
backgroundColor={theme.colors['sideBarSectionHeader.background']}
|
|
97
|
-
paddingLeft={1}
|
|
98
|
-
paddingRight={1}
|
|
99
|
-
>
|
|
95
|
+
<box flexDirection="row" backgroundColor={headerBg} paddingLeft={1} paddingRight={1}>
|
|
100
96
|
<text fg={theme.colors['editorWarning.foreground']}>
|
|
101
97
|
…diff truncated — {hidden} more rows hidden
|
|
102
98
|
</text>
|
|
@@ -116,14 +112,11 @@ function UnifiedRowRender({
|
|
|
116
112
|
row: UnifiedRowOrHeader
|
|
117
113
|
}) {
|
|
118
114
|
const theme = useTheme()
|
|
115
|
+
const headerBg = useBg('sideBarSectionHeader.background')
|
|
116
|
+
const transparent = useTransparent()
|
|
119
117
|
if (row.type === 'hunk-header') {
|
|
120
118
|
return (
|
|
121
|
-
<box
|
|
122
|
-
flexDirection="row"
|
|
123
|
-
backgroundColor={theme.colors['sideBarSectionHeader.background']}
|
|
124
|
-
paddingLeft={1}
|
|
125
|
-
paddingRight={1}
|
|
126
|
-
>
|
|
119
|
+
<box flexDirection="row" backgroundColor={headerBg} paddingLeft={1} paddingRight={1}>
|
|
127
120
|
<text fg={theme.colors['descriptionForeground']}>{row.spec}</text>
|
|
128
121
|
{row.context ? (
|
|
129
122
|
<text fg={theme.colors['editor.lineHighlightBackground']}> {row.context}</text>
|
|
@@ -161,7 +154,7 @@ function UnifiedRowRender({
|
|
|
161
154
|
const addNum = row.type === 'addition' ? row.lineNumber : undefined
|
|
162
155
|
const tokens = row.type === 'addition' ? highlights.add[row.lineIdx] : highlights.del[row.lineIdx]
|
|
163
156
|
return (
|
|
164
|
-
<box flexDirection="row" backgroundColor={bg} height={row.height}>
|
|
157
|
+
<box flexDirection="row" backgroundColor={transparent ? undefined : bg} height={row.height}>
|
|
165
158
|
<text fg={theme.colors['descriptionForeground']}>{` ${pad(delNum)} ${pad(addNum)} `}</text>
|
|
166
159
|
<text fg={signColor}>{`${sign} `}</text>
|
|
167
160
|
<LineContent content={row.content} tokens={tokens} />
|
|
@@ -13,7 +13,7 @@ import type {
|
|
|
13
13
|
|
|
14
14
|
import { dispatchGlobal, runSideEffectGlobal } from '../../state/dispatch-ref'
|
|
15
15
|
import { buildGitTreeRows, type GitTreeFileRow, type GitTreeFolderRow } from '../../state/git-tree'
|
|
16
|
-
import { getCurrentTheme, useTheme } from '../theme'
|
|
16
|
+
import { getCurrentTheme, getTransparent, useTheme, useTransparent } from '../theme'
|
|
17
17
|
|
|
18
18
|
interface GitPanelProps {
|
|
19
19
|
collapsedFolders?: Record<string, true>
|
|
@@ -159,7 +159,10 @@ function renderDiffCount(
|
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
function renderFolderRow(row: GitTreeFolderRow, isSelected: boolean): ReactNode {
|
|
162
|
-
const bg =
|
|
162
|
+
const bg =
|
|
163
|
+
isSelected && !getTransparent()
|
|
164
|
+
? getCurrentTheme().colors['list.activeSelectionBackground']
|
|
165
|
+
: undefined
|
|
163
166
|
const onSelect = (): void => {
|
|
164
167
|
dispatchGlobal({ key: row.key, type: 'git-mode-select-entry-by-key' })
|
|
165
168
|
}
|
|
@@ -196,7 +199,10 @@ function renderFileRow(
|
|
|
196
199
|
): ReactNode {
|
|
197
200
|
const file = row.file
|
|
198
201
|
const hasNumstat = file.added !== null || file.removed !== null
|
|
199
|
-
const bg =
|
|
202
|
+
const bg =
|
|
203
|
+
isSelected && !getTransparent()
|
|
204
|
+
? getCurrentTheme().colors['list.activeSelectionBackground']
|
|
205
|
+
: undefined
|
|
200
206
|
const onSelect = (): void => {
|
|
201
207
|
dispatchGlobal({ key: row.key, type: 'git-mode-select-entry-by-key' })
|
|
202
208
|
}
|
|
@@ -342,6 +348,7 @@ export const GitPanel = memo(function GitPanel({
|
|
|
342
348
|
selectedEntryKey,
|
|
343
349
|
}: GitPanelProps) {
|
|
344
350
|
const theme = useTheme()
|
|
351
|
+
useTransparent()
|
|
345
352
|
const sectionOrder = headOffset > 0 ? HISTORICAL_SECTION_ORDER : BASE_SECTION_ORDER
|
|
346
353
|
const scrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
347
354
|
const tree = useMemo(
|
|
@@ -11,7 +11,7 @@ import { useAppStore } from '../../state/app-store'
|
|
|
11
11
|
import { dispatchGlobal } from '../../state/dispatch-ref'
|
|
12
12
|
import { getSelectedGitFile, gitFileKey } from '../../state/git-tree'
|
|
13
13
|
import { setGitDiffScroller } from '../git-view-controls'
|
|
14
|
-
import { useTheme } from '../theme'
|
|
14
|
+
import { useBg, useTheme } from '../theme'
|
|
15
15
|
import { PierreDiff, type PierreDiffHandle } from './diff-renderer'
|
|
16
16
|
import { useDiffPrefetch } from './diff-renderer/use-diff-prefetch'
|
|
17
17
|
import { GitPanel } from './git-panel'
|
|
@@ -106,6 +106,7 @@ interface GitViewProps {
|
|
|
106
106
|
|
|
107
107
|
export const GitView = memo(function GitView({ themeId }: GitViewProps) {
|
|
108
108
|
const theme = useTheme()
|
|
109
|
+
const sidebarBg = useBg('sideBar.background')
|
|
109
110
|
const dimensions = useTerminalDimensions()
|
|
110
111
|
const gitPane = useAppStore((s) => s.gitPane)
|
|
111
112
|
const gitPanel = useAppStore((s) => s.gitPanel)
|
|
@@ -210,7 +211,7 @@ export const GitView = memo(function GitView({ themeId }: GitViewProps) {
|
|
|
210
211
|
<box
|
|
211
212
|
width={fileBarWidth}
|
|
212
213
|
flexDirection="column"
|
|
213
|
-
backgroundColor={
|
|
214
|
+
backgroundColor={sidebarBg}
|
|
214
215
|
padding={0}
|
|
215
216
|
gap={0}
|
|
216
217
|
>
|
|
@@ -254,12 +255,7 @@ export const GitView = memo(function GitView({ themeId }: GitViewProps) {
|
|
|
254
255
|
/>
|
|
255
256
|
</box>
|
|
256
257
|
{footerNode ? (
|
|
257
|
-
<box
|
|
258
|
-
paddingLeft={1}
|
|
259
|
-
paddingRight={1}
|
|
260
|
-
backgroundColor={theme.colors['sideBar.background']}
|
|
261
|
-
flexDirection="column"
|
|
262
|
-
>
|
|
258
|
+
<box paddingLeft={1} paddingRight={1} backgroundColor={sidebarBg} flexDirection="column">
|
|
263
259
|
{footerNode}
|
|
264
260
|
</box>
|
|
265
261
|
) : null}
|
|
@@ -1,207 +1,92 @@
|
|
|
1
1
|
import type { ModeId } from '@brimveyn/aimux-config'
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { useLayoutEffect, useMemo, useRef } from 'react'
|
|
3
|
+
import { useLayoutEffect, useMemo } from 'react'
|
|
5
4
|
|
|
6
|
-
import {
|
|
7
|
-
collectHelpEntries,
|
|
8
|
-
HELP_MODE_LABELS,
|
|
9
|
-
type HelpEntry,
|
|
10
|
-
} from '../../input/keymap/help-entries'
|
|
5
|
+
import { collectHelpEntries, HELP_MODE_LABELS } from '../../input/keymap/help-entries'
|
|
11
6
|
import { dispatchGlobal } from '../../state/dispatch-ref'
|
|
12
7
|
import { useKeymap } from '../keymap-context'
|
|
13
8
|
import { useTheme } from '../theme'
|
|
14
9
|
import { uiTokens } from '../ui-tokens'
|
|
15
|
-
import {
|
|
16
|
-
import { ModalShell } from './modal-shell'
|
|
10
|
+
import { Picker, type PickerItem } from './picker'
|
|
17
11
|
|
|
18
12
|
interface HelpModalProps {
|
|
19
13
|
filter: string | null
|
|
20
14
|
selectedIndex: number
|
|
21
15
|
scope: ModeId | null
|
|
16
|
+
cursorPos?: number
|
|
22
17
|
}
|
|
23
18
|
|
|
24
|
-
function matchesFilter(
|
|
19
|
+
function matchesFilter(needle: string, ...fields: (string | undefined)[]): boolean {
|
|
25
20
|
if (!needle) return true
|
|
26
21
|
const lower = needle.toLowerCase()
|
|
27
|
-
return (
|
|
28
|
-
(entry.description?.toLowerCase().includes(lower) ?? false) ||
|
|
29
|
-
entry.modeLabel.toLowerCase().includes(lower) ||
|
|
30
|
-
entry.keysDisplay.toLowerCase().includes(lower) ||
|
|
31
|
-
(entry.group?.toLowerCase().includes(lower) ?? false)
|
|
32
|
-
)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
type Row =
|
|
36
|
-
| { kind: 'header'; label: string }
|
|
37
|
-
| { kind: 'entry'; entry: HelpEntry; entryIndex: number }
|
|
38
|
-
|
|
39
|
-
function buildRows(entries: HelpEntry[]): Row[] {
|
|
40
|
-
const rows: Row[] = []
|
|
41
|
-
let lastLabel: string | null = null
|
|
42
|
-
for (let entryIndex = 0; entryIndex < entries.length; entryIndex++) {
|
|
43
|
-
const entry = entries[entryIndex]
|
|
44
|
-
if (!entry) continue
|
|
45
|
-
if (entry.modeLabel !== lastLabel) {
|
|
46
|
-
rows.push({ kind: 'header', label: entry.modeLabel })
|
|
47
|
-
lastLabel = entry.modeLabel
|
|
48
|
-
}
|
|
49
|
-
rows.push({ entry, entryIndex, kind: 'entry' })
|
|
50
|
-
}
|
|
51
|
-
return rows
|
|
22
|
+
return fields.some((f) => f?.toLowerCase().includes(lower))
|
|
52
23
|
}
|
|
53
24
|
|
|
54
|
-
|
|
55
|
-
const VIEWPORT_HEIGHT_RATIO = 0.6
|
|
56
|
-
const MODAL_CHROME_ROWS = 6
|
|
57
|
-
|
|
58
|
-
function clampSelection(index: number, count: number): number {
|
|
59
|
-
if (count === 0) return 0
|
|
60
|
-
return Math.max(0, Math.min(count - 1, index))
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Pick a window start that (a) keeps the selected row on screen and
|
|
65
|
-
* (b) only scrolls when the selection leaves the margin — avoids jumpy
|
|
66
|
-
* recentering on every keypress.
|
|
67
|
-
*/
|
|
68
|
-
function computeWindowStart(
|
|
69
|
-
prevStart: number,
|
|
70
|
-
selectedRowIndex: number,
|
|
71
|
-
total: number,
|
|
72
|
-
windowSize: number
|
|
73
|
-
): number {
|
|
74
|
-
if (total <= windowSize) return 0
|
|
75
|
-
const margin = 1
|
|
76
|
-
const maxStart = total - windowSize
|
|
77
|
-
let start = Math.max(0, Math.min(maxStart, prevStart))
|
|
78
|
-
const topThreshold = start + margin
|
|
79
|
-
const bottomThreshold = start + windowSize - 1 - margin
|
|
80
|
-
if (selectedRowIndex < topThreshold) {
|
|
81
|
-
start = Math.max(0, selectedRowIndex - margin)
|
|
82
|
-
} else if (selectedRowIndex > bottomThreshold) {
|
|
83
|
-
start = Math.min(maxStart, selectedRowIndex - windowSize + 1 + margin)
|
|
84
|
-
}
|
|
85
|
-
return start
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function HelpModal({ filter, scope, selectedIndex }: HelpModalProps) {
|
|
25
|
+
export function HelpModal({ cursorPos, filter, scope, selectedIndex }: HelpModalProps) {
|
|
89
26
|
const theme = useTheme()
|
|
90
27
|
const config = useKeymap()
|
|
91
|
-
const dimensions = useTerminalDimensions()
|
|
92
28
|
const allEntries = useMemo(() => collectHelpEntries(config), [config])
|
|
93
29
|
const scoped = useMemo(
|
|
94
30
|
() => (scope ? allEntries.filter((e) => e.mode === scope) : allEntries),
|
|
95
31
|
[allEntries, scope]
|
|
96
32
|
)
|
|
97
33
|
const filtered = useMemo(
|
|
98
|
-
() =>
|
|
34
|
+
() =>
|
|
35
|
+
scoped.filter((e) =>
|
|
36
|
+
matchesFilter(filter ?? '', e.description, e.modeLabel, e.keysDisplay, e.group)
|
|
37
|
+
),
|
|
99
38
|
[scoped, filter]
|
|
100
39
|
)
|
|
40
|
+
|
|
101
41
|
const title = useMemo(() => {
|
|
102
42
|
if (!scope) return 'Keybindings'
|
|
103
43
|
const label = HELP_MODE_LABELS.find((m) => m.modeId === scope)?.label ?? scope
|
|
104
44
|
return `${label} — keybindings`
|
|
105
45
|
}, [scope])
|
|
106
|
-
const rows = useMemo(() => buildRows(filtered), [filtered])
|
|
107
46
|
|
|
108
47
|
useLayoutEffect(() => {
|
|
109
48
|
dispatchGlobal({ count: filtered.length, type: 'set-help-entry-count' })
|
|
110
49
|
}, [filtered.length])
|
|
111
50
|
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
51
|
+
const items: PickerItem[] = filtered.map((entry, index) => {
|
|
52
|
+
const active = index === selectedIndex
|
|
53
|
+
return {
|
|
54
|
+
group: entry.modeLabel,
|
|
55
|
+
key: `${entry.mode}::${entry.keysDisplay}::${entry.description ?? ''}::${index}`,
|
|
56
|
+
title: (
|
|
57
|
+
<text
|
|
58
|
+
fg={active ? theme.colors['editor.foreground'] : theme.colors['descriptionForeground']}
|
|
59
|
+
wrapMode="none"
|
|
60
|
+
>
|
|
61
|
+
{entry.description ?? ''}
|
|
62
|
+
</text>
|
|
63
|
+
),
|
|
64
|
+
trailing: (
|
|
65
|
+
<text
|
|
66
|
+
fg={active ? theme.colors['textLink.foreground'] : theme.colors['terminal.ansiMagenta']}
|
|
67
|
+
wrapMode="none"
|
|
68
|
+
>
|
|
69
|
+
{entry.keysDisplay}
|
|
70
|
+
</text>
|
|
71
|
+
),
|
|
72
|
+
}
|
|
73
|
+
})
|
|
130
74
|
|
|
131
75
|
return (
|
|
132
|
-
<
|
|
76
|
+
<Picker
|
|
133
77
|
title={title}
|
|
134
|
-
keybindsModeId="modal.help"
|
|
78
|
+
keybindsModeId="modal.help.filtering"
|
|
135
79
|
width={uiTokens.modalWidth.lg}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
: ` ${effectiveIndex + 1} / ${filtered.length}${filter ? '' : ' — type / to filter'}`}
|
|
142
|
-
</text>
|
|
143
|
-
<ModalFilterBar filter={filter} />
|
|
144
|
-
</box>
|
|
145
|
-
}
|
|
146
|
-
>
|
|
147
|
-
{filtered.length === 0 ? (
|
|
80
|
+
filter={filter}
|
|
81
|
+
cursorPos={cursorPos}
|
|
82
|
+
items={items}
|
|
83
|
+
selectedIndex={selectedIndex}
|
|
84
|
+
emptyState={
|
|
148
85
|
<text fg={theme.colors['descriptionForeground']}>
|
|
149
86
|
{filter ? 'No matching bindings.' : 'No bindings registered.'}
|
|
150
87
|
</text>
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const rowIndex = start + i
|
|
155
|
-
if (row.kind === 'header') {
|
|
156
|
-
return (
|
|
157
|
-
<box key={`h-${rowIndex}`} paddingLeft={1} paddingTop={i === 0 ? 0 : 1}>
|
|
158
|
-
<text fg={theme.colors['editorWarning.foreground']} wrapMode="none">
|
|
159
|
-
<strong>{row.label}</strong>
|
|
160
|
-
</text>
|
|
161
|
-
</box>
|
|
162
|
-
)
|
|
163
|
-
}
|
|
164
|
-
const active = row.entryIndex === effectiveIndex
|
|
165
|
-
const bg = active ? theme.colors['list.activeSelectionBackground'] : undefined
|
|
166
|
-
return (
|
|
167
|
-
<box
|
|
168
|
-
key={`e-${rowIndex}`}
|
|
169
|
-
flexDirection="row"
|
|
170
|
-
paddingLeft={2}
|
|
171
|
-
paddingRight={1}
|
|
172
|
-
backgroundColor={bg}
|
|
173
|
-
>
|
|
174
|
-
<box width={KEYS_COLUMN_WIDTH} flexShrink={0}>
|
|
175
|
-
<text
|
|
176
|
-
fg={
|
|
177
|
-
active
|
|
178
|
-
? theme.colors['textLink.foreground']
|
|
179
|
-
: theme.colors['terminal.ansiMagenta']
|
|
180
|
-
}
|
|
181
|
-
bg={bg}
|
|
182
|
-
wrapMode="none"
|
|
183
|
-
>
|
|
184
|
-
{row.entry.keysDisplay}
|
|
185
|
-
</text>
|
|
186
|
-
</box>
|
|
187
|
-
<box flexGrow={1} overflow="hidden">
|
|
188
|
-
<text
|
|
189
|
-
fg={
|
|
190
|
-
active
|
|
191
|
-
? theme.colors['editor.foreground']
|
|
192
|
-
: theme.colors['descriptionForeground']
|
|
193
|
-
}
|
|
194
|
-
bg={bg}
|
|
195
|
-
wrapMode="none"
|
|
196
|
-
>
|
|
197
|
-
{row.entry.description ?? ''}
|
|
198
|
-
</text>
|
|
199
|
-
</box>
|
|
200
|
-
</box>
|
|
201
|
-
)
|
|
202
|
-
})}
|
|
203
|
-
</box>
|
|
204
|
-
)}
|
|
205
|
-
</ModalShell>
|
|
88
|
+
}
|
|
89
|
+
onHover={(index) => dispatchGlobal({ index, type: 'set-modal-selection-index' })}
|
|
90
|
+
/>
|
|
206
91
|
)
|
|
207
92
|
}
|
|
@@ -5,15 +5,19 @@ interface InputFieldProps {
|
|
|
5
5
|
active: boolean
|
|
6
6
|
value: string
|
|
7
7
|
cursorPos?: number
|
|
8
|
+
placeholder?: string
|
|
8
9
|
}
|
|
9
10
|
|
|
10
|
-
export function InputField({ active, cursorPos, value }: InputFieldProps) {
|
|
11
|
+
export function InputField({ active, cursorPos, placeholder, value }: InputFieldProps) {
|
|
11
12
|
const theme = useTheme()
|
|
12
13
|
const fg = active ? theme.colors['editor.foreground'] : theme.colors['descriptionForeground']
|
|
13
14
|
if (!active) {
|
|
15
|
+
const showPlaceholder = !value && !!placeholder
|
|
14
16
|
return (
|
|
15
17
|
<Surface tone="input" padding={1}>
|
|
16
|
-
<text fg={fg}>
|
|
18
|
+
<text fg={showPlaceholder ? theme.colors['editorLineNumber.foreground'] : fg}>
|
|
19
|
+
{showPlaceholder ? placeholder : value}
|
|
20
|
+
</text>
|
|
17
21
|
</Surface>
|
|
18
22
|
)
|
|
19
23
|
}
|