@brimveyn/aimux 1.4.1 → 1.5.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.
- package/README.md +142 -176
- package/package.json +6 -3
- package/src/app-runtime/use-renderer-bindings.ts +1 -1
- package/src/app-runtime/use-terminal-resize.ts +12 -2
- package/src/app.tsx +20 -3
- package/src/config.ts +53 -17
- package/src/index.tsx +2 -2
- package/src/input/keymap/build-handlers.ts +1 -0
- package/src/input/keymap/help-entries.ts +44 -0
- package/src/input/keymap/keymap-ref.ts +11 -0
- package/src/input/keymap/sequence-resolver.ts +35 -2
- package/src/input/keymap/trie.ts +1 -0
- package/src/input/modes/bridge.ts +1 -1
- package/src/input/modes/handlers/shared.ts +0 -16
- package/src/input/modes/transitions.ts +4 -4
- package/src/input/modes/types.ts +1 -1
- package/src/platform/daemon-control.ts +0 -8
- package/src/state/reducers/git-panel-state.ts +35 -8
- package/src/state/reducers/modal-state.ts +48 -3
- package/src/state/selectors.ts +1 -5
- package/src/state/session-persistence.ts +1 -20
- package/src/state/store.ts +38 -5
- package/src/state/types.ts +27 -13
- package/src/state/validation.ts +0 -2
- package/src/state/workspace-save.ts +6 -2
- package/src/ui/components/create-session-modal.tsx +5 -3
- package/src/ui/components/git-commit-modal.tsx +1 -3
- package/src/ui/components/git-pane-widget.tsx +46 -0
- package/src/ui/components/git-panel.tsx +72 -25
- package/src/ui/components/help-modal.tsx +156 -42
- package/src/ui/components/modal-keybinds-overlay.tsx +39 -0
- package/src/ui/components/modal-shell.tsx +15 -3
- package/src/ui/components/new-tab-modal.tsx +9 -10
- package/src/ui/components/pending-chord-overlay.tsx +2 -1
- package/src/ui/components/session-name-modal.tsx +1 -3
- package/src/ui/components/session-picker-modal.tsx +1 -3
- package/src/ui/components/sidebar.tsx +24 -50
- package/src/ui/components/snippet-editor-modal.tsx +1 -3
- package/src/ui/components/snippet-picker-modal.tsx +1 -3
- package/src/ui/components/status-bar.tsx +0 -4
- package/src/ui/components/theme-picker-modal.tsx +6 -3
- package/src/ui/components/update-available-modal.tsx +7 -4
- package/src/ui/keymap-context.ts +1 -6
- package/src/ui/root.tsx +29 -1
- package/src/ui/status-bar-model.ts +0 -5
- package/src/ui/directory-search.ts +0 -1
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { DirectoryResult } from '../../state/types'
|
|
2
2
|
|
|
3
|
-
import { useModalHelp } from '../keymap-context'
|
|
4
3
|
import { abbreviatePath } from '../path-format'
|
|
5
4
|
import { theme } from '../theme'
|
|
6
5
|
import { uiTokens } from '../ui-tokens'
|
|
@@ -51,10 +50,13 @@ export function CreateSessionModal({
|
|
|
51
50
|
}: CreateSessionModalProps) {
|
|
52
51
|
const dirActive = activeField === 'directory'
|
|
53
52
|
const nameActive = activeField === 'name'
|
|
54
|
-
const help = useModalHelp('modal.create-session')
|
|
55
53
|
|
|
56
54
|
return (
|
|
57
|
-
<ModalShell
|
|
55
|
+
<ModalShell
|
|
56
|
+
title="Create session"
|
|
57
|
+
keybindsModeId="modal.create-session"
|
|
58
|
+
width={uiTokens.modalWidth.xl}
|
|
59
|
+
>
|
|
58
60
|
<box flexDirection="column">
|
|
59
61
|
<text fg={dirActive ? theme.text : theme.textMuted}>Search projects</text>
|
|
60
62
|
<InputField
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { useModalHelp } from '../keymap-context'
|
|
2
1
|
import { theme } from '../theme'
|
|
3
2
|
import { uiTokens } from '../ui-tokens'
|
|
4
3
|
import { InputField } from './input-field'
|
|
@@ -14,10 +13,9 @@ interface GitCommitModalProps {
|
|
|
14
13
|
export function GitCommitModal({ activeField, body, cursorPos, title }: GitCommitModalProps) {
|
|
15
14
|
const titleActive = activeField === 'title'
|
|
16
15
|
const bodyActive = activeField === 'body'
|
|
17
|
-
const help = useModalHelp('modal.git-commit')
|
|
18
16
|
|
|
19
17
|
return (
|
|
20
|
-
<ModalShell title="Commit"
|
|
18
|
+
<ModalShell title="Commit" keybindsModeId="modal.git-commit" width={uiTokens.modalWidth.xl}>
|
|
21
19
|
<box flexDirection="column">
|
|
22
20
|
<text fg={titleActive ? theme.text : theme.textMuted}>Title</text>
|
|
23
21
|
<InputField
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { memo, useRef } from 'react'
|
|
2
|
+
|
|
3
|
+
import type { GitPanelState } from '../../state/types'
|
|
4
|
+
|
|
5
|
+
import { useGitPanelPolling } from '../../git/git-poller'
|
|
6
|
+
import { useAppStore } from '../../state/app-store'
|
|
7
|
+
import { GitPanel } from './git-panel'
|
|
8
|
+
|
|
9
|
+
interface GitPaneWidgetProps {
|
|
10
|
+
pollingEnabled: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const GitPaneWidget = memo(function GitPaneWidget({ pollingEnabled }: GitPaneWidgetProps) {
|
|
14
|
+
const gitPanel = useAppStore((s) => s.gitPanel)
|
|
15
|
+
const pathConfig = useAppStore((s) => s.gitPane.path)
|
|
16
|
+
const diffCountConfig = useAppStore((s) => s.gitPane.diffCount)
|
|
17
|
+
const currentSessionId = useAppStore((s) => s.currentSessionId)
|
|
18
|
+
const sessions = useAppStore((s) => s.sessions)
|
|
19
|
+
const currentSession = currentSessionId
|
|
20
|
+
? sessions.find((s) => s.id === currentSessionId)
|
|
21
|
+
: undefined
|
|
22
|
+
const projectPath = currentSession?.projectPath
|
|
23
|
+
|
|
24
|
+
useGitPanelPolling({ enabled: pollingEnabled, projectPath })
|
|
25
|
+
|
|
26
|
+
const lastGoodRef = useRef<GitPanelState | null>(null)
|
|
27
|
+
const prevProjectPathRef = useRef(projectPath)
|
|
28
|
+
if (prevProjectPathRef.current !== projectPath) {
|
|
29
|
+
prevProjectPathRef.current = projectPath
|
|
30
|
+
lastGoodRef.current = null
|
|
31
|
+
}
|
|
32
|
+
const isGood = gitPanel.error === null && gitPanel.branch !== null
|
|
33
|
+
if (isGood) {
|
|
34
|
+
lastGoodRef.current = gitPanel
|
|
35
|
+
}
|
|
36
|
+
const display = lastGoodRef.current ?? gitPanel
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<GitPanel
|
|
40
|
+
diffCountConfig={diffCountConfig}
|
|
41
|
+
gitPanel={display}
|
|
42
|
+
pathConfig={pathConfig}
|
|
43
|
+
projectPath={projectPath}
|
|
44
|
+
/>
|
|
45
|
+
)
|
|
46
|
+
})
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { memo, type ReactNode, useMemo } from 'react'
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
GitFileEntry,
|
|
5
|
+
GitFileSection,
|
|
6
|
+
GitPaneDiffCountConfig,
|
|
7
|
+
GitPanelState,
|
|
8
|
+
GitPanePathConfig,
|
|
9
|
+
} from '../../state/types'
|
|
4
10
|
|
|
5
11
|
import { theme } from '../theme'
|
|
6
12
|
|
|
@@ -8,6 +14,8 @@ interface GitPanelProps {
|
|
|
8
14
|
gitPanel: GitPanelState
|
|
9
15
|
projectPath: string | undefined
|
|
10
16
|
selectedFileKey?: string | null
|
|
17
|
+
pathConfig?: GitPanePathConfig
|
|
18
|
+
diffCountConfig?: GitPaneDiffCountConfig
|
|
11
19
|
}
|
|
12
20
|
|
|
13
21
|
export function fileKey(file: Pick<GitFileEntry, 'path' | 'section'>): string {
|
|
@@ -69,36 +77,69 @@ function stripTrailingSlash(prefix: string): string {
|
|
|
69
77
|
return prefix.endsWith('/') ? prefix.slice(0, -1) : prefix
|
|
70
78
|
}
|
|
71
79
|
|
|
72
|
-
function renderPath(file: GitFileEntry): ReactNode {
|
|
73
|
-
const
|
|
80
|
+
function renderPath(file: GitFileEntry, pathConfig: GitPanePathConfig): ReactNode {
|
|
81
|
+
const showDir = pathConfig.enabled
|
|
82
|
+
const transform = pathConfig.enabled ? pathConfig.pathFn : undefined
|
|
83
|
+
const displayPath = transform ? transform(file.path) : file.path
|
|
84
|
+
const { basename, prefix } = splitPath(displayPath)
|
|
74
85
|
const dir = stripTrailingSlash(prefix)
|
|
75
86
|
if (file.renamedFrom) {
|
|
76
|
-
const
|
|
87
|
+
const renamedDisplay = transform ? transform(file.renamedFrom) : file.renamedFrom
|
|
88
|
+
const renamed = splitPath(renamedDisplay)
|
|
77
89
|
const renamedDir = stripTrailingSlash(renamed.prefix)
|
|
78
90
|
return (
|
|
79
91
|
<text wrapMode="none">
|
|
80
92
|
<span fg={theme.text}>{renamed.basename}</span>
|
|
81
|
-
{renamedDir ? <span fg={theme.textMuted}> {renamedDir}</span> : null}
|
|
93
|
+
{showDir && renamedDir ? <span fg={theme.textMuted}> {renamedDir}</span> : null}
|
|
82
94
|
<span fg={theme.textMuted}> → </span>
|
|
83
95
|
<span fg={theme.text}>{basename}</span>
|
|
84
|
-
{dir ? <span fg={theme.textMuted}> {dir}</span> : null}
|
|
96
|
+
{showDir && dir ? <span fg={theme.textMuted}> {dir}</span> : null}
|
|
85
97
|
</text>
|
|
86
98
|
)
|
|
87
99
|
}
|
|
88
100
|
return (
|
|
89
101
|
<text wrapMode="none">
|
|
90
102
|
<span fg={theme.text}>{basename}</span>
|
|
91
|
-
{dir ? <span fg={theme.textMuted}> {dir}</span> : null}
|
|
103
|
+
{showDir && dir ? <span fg={theme.textMuted}> {dir}</span> : null}
|
|
92
104
|
</text>
|
|
93
105
|
)
|
|
94
106
|
}
|
|
95
107
|
|
|
108
|
+
function renderDiffCount(
|
|
109
|
+
file: GitFileEntry,
|
|
110
|
+
addedW: number,
|
|
111
|
+
removedW: number,
|
|
112
|
+
bg: string | undefined,
|
|
113
|
+
diffCountConfig: GitPaneDiffCountConfig,
|
|
114
|
+
hasNumstat: boolean
|
|
115
|
+
): ReactNode {
|
|
116
|
+
if (!diffCountConfig.enabled) return null
|
|
117
|
+
if (!hasNumstat) {
|
|
118
|
+
return (
|
|
119
|
+
<text fg={theme.textMuted} bg={bg} flexShrink={0}>
|
|
120
|
+
—
|
|
121
|
+
</text>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
return (
|
|
125
|
+
<box flexDirection="row" flexShrink={0}>
|
|
126
|
+
<text fg={theme.success} bg={bg}>{`+${padRight(file.added, addedW)}`}</text>
|
|
127
|
+
<text fg={theme.dim} bg={bg}>
|
|
128
|
+
{' '}
|
|
129
|
+
</text>
|
|
130
|
+
<text fg={theme.danger} bg={bg}>{`−${padRight(file.removed, removedW)}`}</text>
|
|
131
|
+
</box>
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
96
135
|
function renderFileRow(
|
|
97
136
|
file: GitFileEntry,
|
|
98
137
|
key: string,
|
|
99
138
|
addedW: number,
|
|
100
139
|
removedW: number,
|
|
101
|
-
isSelected: boolean
|
|
140
|
+
isSelected: boolean,
|
|
141
|
+
pathConfig: GitPanePathConfig,
|
|
142
|
+
diffCountConfig: GitPaneDiffCountConfig
|
|
102
143
|
): ReactNode {
|
|
103
144
|
const hasNumstat = file.added !== null || file.removed !== null
|
|
104
145
|
const bg = isSelected ? theme.panelHighlight : undefined
|
|
@@ -108,21 +149,9 @@ function renderFileRow(
|
|
|
108
149
|
<strong>{displayStatus(file)}</strong>
|
|
109
150
|
</text>
|
|
110
151
|
<box flexGrow={1} overflow="hidden">
|
|
111
|
-
{renderPath(file)}
|
|
152
|
+
{renderPath(file, pathConfig)}
|
|
112
153
|
</box>
|
|
113
|
-
{
|
|
114
|
-
<box flexDirection="row" flexShrink={0}>
|
|
115
|
-
<text fg={theme.success} bg={bg}>{`+${padRight(file.added, addedW)}`}</text>
|
|
116
|
-
<text fg={theme.dim} bg={bg}>
|
|
117
|
-
{' '}
|
|
118
|
-
</text>
|
|
119
|
-
<text fg={theme.danger} bg={bg}>{`−${padRight(file.removed, removedW)}`}</text>
|
|
120
|
-
</box>
|
|
121
|
-
) : (
|
|
122
|
-
<text fg={theme.textMuted} bg={bg} flexShrink={0}>
|
|
123
|
-
—
|
|
124
|
-
</text>
|
|
125
|
-
)}
|
|
154
|
+
{renderDiffCount(file, addedW, removedW, bg, diffCountConfig, hasNumstat)}
|
|
126
155
|
</box>
|
|
127
156
|
)
|
|
128
157
|
}
|
|
@@ -133,7 +162,9 @@ function renderSection(
|
|
|
133
162
|
files: GitFileEntry[],
|
|
134
163
|
addedW: number,
|
|
135
164
|
removedW: number,
|
|
136
|
-
selectedFileKey: string | null | undefined
|
|
165
|
+
selectedFileKey: string | null | undefined,
|
|
166
|
+
pathConfig: GitPanePathConfig,
|
|
167
|
+
diffCountConfig: GitPaneDiffCountConfig
|
|
137
168
|
): ReactNode {
|
|
138
169
|
if (files.length === 0) return null
|
|
139
170
|
return (
|
|
@@ -149,7 +180,9 @@ function renderSection(
|
|
|
149
180
|
`${section}-${i}`,
|
|
150
181
|
addedW,
|
|
151
182
|
removedW,
|
|
152
|
-
!!selectedFileKey && fileKey(file) === selectedFileKey
|
|
183
|
+
!!selectedFileKey && fileKey(file) === selectedFileKey,
|
|
184
|
+
pathConfig,
|
|
185
|
+
diffCountConfig
|
|
153
186
|
)
|
|
154
187
|
)}
|
|
155
188
|
</box>
|
|
@@ -190,8 +223,13 @@ function computeStatusPlaceholder(
|
|
|
190
223
|
return null
|
|
191
224
|
}
|
|
192
225
|
|
|
226
|
+
const DEFAULT_PATH_CONFIG: GitPanePathConfig = { enabled: true }
|
|
227
|
+
const DEFAULT_DIFF_COUNT_CONFIG: GitPaneDiffCountConfig = { enabled: true }
|
|
228
|
+
|
|
193
229
|
export const GitPanel = memo(function GitPanel({
|
|
230
|
+
diffCountConfig = DEFAULT_DIFF_COUNT_CONFIG,
|
|
194
231
|
gitPanel,
|
|
232
|
+
pathConfig = DEFAULT_PATH_CONFIG,
|
|
195
233
|
projectPath,
|
|
196
234
|
selectedFileKey,
|
|
197
235
|
}: GitPanelProps) {
|
|
@@ -220,7 +258,16 @@ export const GitPanel = memo(function GitPanel({
|
|
|
220
258
|
contentOptions={{ flexDirection: 'column', gap: 0 }}
|
|
221
259
|
>
|
|
222
260
|
{SECTION_ORDER.map((s) =>
|
|
223
|
-
renderSection(
|
|
261
|
+
renderSection(
|
|
262
|
+
s.key,
|
|
263
|
+
s.title,
|
|
264
|
+
groups[s.key],
|
|
265
|
+
addedW,
|
|
266
|
+
removedW,
|
|
267
|
+
selectedFileKey,
|
|
268
|
+
pathConfig,
|
|
269
|
+
diffCountConfig
|
|
270
|
+
)
|
|
224
271
|
)}
|
|
225
272
|
</scrollbox>
|
|
226
273
|
)}
|
|
@@ -1,60 +1,174 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { useTerminalDimensions } from '@opentui/react'
|
|
2
|
+
import { useLayoutEffect, useMemo, useRef } from 'react'
|
|
2
3
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
4
|
+
import { collectHelpEntries, type HelpEntry } from '../../input/keymap/help-entries'
|
|
5
|
+
import { dispatchGlobal } from '../../state/dispatch-ref'
|
|
6
|
+
import { useKeymap } from '../keymap-context'
|
|
5
7
|
import { theme } from '../theme'
|
|
6
8
|
import { uiTokens } from '../ui-tokens'
|
|
9
|
+
import { ModalFilterBar } from './modal-filter-bar'
|
|
7
10
|
import { ModalShell } from './modal-shell'
|
|
8
11
|
|
|
9
|
-
interface
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
interface HelpModalProps {
|
|
13
|
+
filter: string | null
|
|
14
|
+
selectedIndex: number
|
|
12
15
|
}
|
|
13
16
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
function matchesFilter(entry: HelpEntry, needle: string): boolean {
|
|
18
|
+
if (!needle) return true
|
|
19
|
+
const lower = needle.toLowerCase()
|
|
20
|
+
return (
|
|
21
|
+
(entry.description?.toLowerCase().includes(lower) ?? false) ||
|
|
22
|
+
entry.modeLabel.toLowerCase().includes(lower) ||
|
|
23
|
+
entry.keysDisplay.toLowerCase().includes(lower) ||
|
|
24
|
+
(entry.group?.toLowerCase().includes(lower) ?? false)
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type Row =
|
|
29
|
+
| { kind: 'header'; label: string }
|
|
30
|
+
| { kind: 'entry'; entry: HelpEntry; entryIndex: number }
|
|
20
31
|
|
|
21
|
-
|
|
32
|
+
function buildRows(entries: HelpEntry[]): Row[] {
|
|
33
|
+
const rows: Row[] = []
|
|
34
|
+
let lastLabel: string | null = null
|
|
35
|
+
for (let entryIndex = 0; entryIndex < entries.length; entryIndex++) {
|
|
36
|
+
const entry = entries[entryIndex]
|
|
37
|
+
if (!entry) continue
|
|
38
|
+
if (entry.modeLabel !== lastLabel) {
|
|
39
|
+
rows.push({ kind: 'header', label: entry.modeLabel })
|
|
40
|
+
lastLabel = entry.modeLabel
|
|
41
|
+
}
|
|
42
|
+
rows.push({ entry, entryIndex, kind: 'entry' })
|
|
43
|
+
}
|
|
44
|
+
return rows
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const KEYS_COLUMN_WIDTH = 24
|
|
48
|
+
const VIEWPORT_HEIGHT_RATIO = 0.6
|
|
49
|
+
const MODAL_CHROME_ROWS = 6
|
|
50
|
+
|
|
51
|
+
function clampSelection(index: number, count: number): number {
|
|
52
|
+
if (count === 0) return 0
|
|
53
|
+
return Math.max(0, Math.min(count - 1, index))
|
|
54
|
+
}
|
|
22
55
|
|
|
23
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Pick a window start that (a) keeps the selected row on screen and
|
|
58
|
+
* (b) only scrolls when the selection leaves the margin — avoids jumpy
|
|
59
|
+
* recentering on every keypress.
|
|
60
|
+
*/
|
|
61
|
+
function computeWindowStart(
|
|
62
|
+
prevStart: number,
|
|
63
|
+
selectedRowIndex: number,
|
|
64
|
+
total: number,
|
|
65
|
+
windowSize: number
|
|
66
|
+
): number {
|
|
67
|
+
if (total <= windowSize) return 0
|
|
68
|
+
const margin = 1
|
|
69
|
+
const maxStart = total - windowSize
|
|
70
|
+
let start = Math.max(0, Math.min(maxStart, prevStart))
|
|
71
|
+
const topThreshold = start + margin
|
|
72
|
+
const bottomThreshold = start + windowSize - 1 - margin
|
|
73
|
+
if (selectedRowIndex < topThreshold) {
|
|
74
|
+
start = Math.max(0, selectedRowIndex - margin)
|
|
75
|
+
} else if (selectedRowIndex > bottomThreshold) {
|
|
76
|
+
start = Math.min(maxStart, selectedRowIndex - windowSize + 1 + margin)
|
|
77
|
+
}
|
|
78
|
+
return start
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function HelpModal({ filter, selectedIndex }: HelpModalProps) {
|
|
24
82
|
const config = useKeymap()
|
|
25
|
-
const
|
|
83
|
+
const dimensions = useTerminalDimensions()
|
|
84
|
+
const allEntries = useMemo(() => collectHelpEntries(config), [config])
|
|
85
|
+
const filtered = useMemo(
|
|
86
|
+
() => allEntries.filter((e) => matchesFilter(e, filter ?? '')),
|
|
87
|
+
[allEntries, filter]
|
|
88
|
+
)
|
|
89
|
+
const rows = useMemo(() => buildRows(filtered), [filtered])
|
|
90
|
+
|
|
91
|
+
useLayoutEffect(() => {
|
|
92
|
+
dispatchGlobal({ count: filtered.length, type: 'set-help-entry-count' })
|
|
93
|
+
}, [filtered.length])
|
|
94
|
+
|
|
95
|
+
const effectiveIndex = clampSelection(selectedIndex, filtered.length)
|
|
96
|
+
const selectedRowIndex = Math.max(
|
|
97
|
+
0,
|
|
98
|
+
rows.findIndex((r) => r.kind === 'entry' && r.entryIndex === effectiveIndex)
|
|
99
|
+
)
|
|
100
|
+
const maxHeight = Math.max(6, Math.floor(dimensions.height * VIEWPORT_HEIGHT_RATIO))
|
|
101
|
+
const listHeight = Math.max(1, maxHeight - MODAL_CHROME_ROWS)
|
|
102
|
+
|
|
103
|
+
const prevStartRef = useRef(0)
|
|
104
|
+
const prevFilterRef = useRef<string | null>(filter)
|
|
105
|
+
// Reset scroll when filter changes; selection goes back to index 0.
|
|
106
|
+
if (prevFilterRef.current !== filter) {
|
|
107
|
+
prevFilterRef.current = filter
|
|
108
|
+
prevStartRef.current = 0
|
|
109
|
+
}
|
|
110
|
+
const start = computeWindowStart(prevStartRef.current, selectedRowIndex, rows.length, listHeight)
|
|
111
|
+
prevStartRef.current = start
|
|
112
|
+
const visible = rows.slice(start, start + listHeight)
|
|
26
113
|
|
|
27
114
|
return (
|
|
28
|
-
<ModalShell
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
115
|
+
<ModalShell
|
|
116
|
+
title="Keybindings"
|
|
117
|
+
keybindsModeId="modal.help"
|
|
118
|
+
width={uiTokens.modalWidth.lg}
|
|
119
|
+
footer={
|
|
120
|
+
<box flexDirection="column" gap={0}>
|
|
121
|
+
<text fg={theme.dim}>
|
|
122
|
+
{filtered.length === 0
|
|
123
|
+
? ''
|
|
124
|
+
: ` ${effectiveIndex + 1} / ${filtered.length}${filter ? '' : ' — type / to filter'}`}
|
|
125
|
+
</text>
|
|
126
|
+
<ModalFilterBar filter={filter} />
|
|
127
|
+
</box>
|
|
128
|
+
}
|
|
129
|
+
>
|
|
130
|
+
{filtered.length === 0 ? (
|
|
131
|
+
<text fg={theme.textMuted}>
|
|
132
|
+
{filter ? 'No matching bindings.' : 'No bindings registered.'}
|
|
133
|
+
</text>
|
|
134
|
+
) : (
|
|
135
|
+
<box height={listHeight} flexDirection="column" overflow="hidden">
|
|
136
|
+
{visible.map((row, i) => {
|
|
137
|
+
const rowIndex = start + i
|
|
138
|
+
if (row.kind === 'header') {
|
|
139
|
+
return (
|
|
140
|
+
<box key={`h-${rowIndex}`} paddingLeft={1} paddingTop={i === 0 ? 0 : 1}>
|
|
141
|
+
<text fg={theme.warning} wrapMode="none">
|
|
142
|
+
<strong>{row.label}</strong>
|
|
143
|
+
</text>
|
|
144
|
+
</box>
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
const active = row.entryIndex === effectiveIndex
|
|
148
|
+
const bg = active ? theme.panelHighlight : undefined
|
|
149
|
+
return (
|
|
40
150
|
<box
|
|
41
|
-
key={
|
|
42
|
-
flexDirection="
|
|
151
|
+
key={`e-${rowIndex}`}
|
|
152
|
+
flexDirection="row"
|
|
153
|
+
paddingLeft={2}
|
|
154
|
+
paddingRight={1}
|
|
155
|
+
backgroundColor={bg}
|
|
43
156
|
>
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
157
|
+
<box width={KEYS_COLUMN_WIDTH} flexShrink={0}>
|
|
158
|
+
<text fg={active ? theme.accent : theme.accentAlt} bg={bg} wrapMode="none">
|
|
159
|
+
{row.entry.keysDisplay}
|
|
160
|
+
</text>
|
|
161
|
+
</box>
|
|
162
|
+
<box flexGrow={1} overflow="hidden">
|
|
163
|
+
<text fg={active ? theme.text : theme.textMuted} bg={bg} wrapMode="none">
|
|
164
|
+
{row.entry.description ?? ''}
|
|
165
|
+
</text>
|
|
166
|
+
</box>
|
|
53
167
|
</box>
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
168
|
+
)
|
|
169
|
+
})}
|
|
170
|
+
</box>
|
|
171
|
+
)}
|
|
58
172
|
</ModalShell>
|
|
59
173
|
)
|
|
60
174
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ModeId } from '@brimveyn/aimux-config'
|
|
2
|
+
|
|
3
|
+
import { describeBindings } from '../../input/keymap/describe-bindings'
|
|
4
|
+
import { useKeymap } from '../keymap-context'
|
|
5
|
+
import { theme } from '../theme'
|
|
6
|
+
import { Surface } from './surface'
|
|
7
|
+
|
|
8
|
+
const KEYS_COLUMN_WIDTH = 12
|
|
9
|
+
|
|
10
|
+
interface ModalKeybindsOverlayProps {
|
|
11
|
+
modeId: ModeId
|
|
12
|
+
limit?: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function ModalKeybindsOverlay({ limit, modeId }: ModalKeybindsOverlayProps) {
|
|
16
|
+
const config = useKeymap()
|
|
17
|
+
const bindings = describeBindings(config, modeId, {
|
|
18
|
+
dedupeByDescription: true,
|
|
19
|
+
withDescriptionOnly: true,
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
if (bindings.length === 0) return null
|
|
23
|
+
const entries = typeof limit === 'number' ? bindings.slice(0, limit) : bindings
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<box position="absolute" bottom={3} right={5}>
|
|
27
|
+
<Surface tone="elevated" paddingLeft={2} paddingRight={2} paddingTop={1} paddingBottom={1}>
|
|
28
|
+
{entries.map((binding) => (
|
|
29
|
+
<box key={`${binding.keys}-${binding.description}`} flexDirection="row">
|
|
30
|
+
<box width={KEYS_COLUMN_WIDTH}>
|
|
31
|
+
<text fg={theme.accentAlt}>{binding.keysDisplay}</text>
|
|
32
|
+
</box>
|
|
33
|
+
<text fg={theme.textMuted}>{binding.description ?? ''}</text>
|
|
34
|
+
</box>
|
|
35
|
+
))}
|
|
36
|
+
</Surface>
|
|
37
|
+
</box>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
@@ -1,18 +1,29 @@
|
|
|
1
|
+
import type { ModeId } from '@brimveyn/aimux-config'
|
|
1
2
|
import type { ReactNode } from 'react'
|
|
2
3
|
|
|
3
4
|
import { theme } from '../theme'
|
|
5
|
+
import { ModalKeybindsOverlay } from './modal-keybinds-overlay'
|
|
4
6
|
import { Surface } from './surface'
|
|
5
7
|
|
|
6
8
|
interface ModalShellProps {
|
|
7
9
|
children: ReactNode
|
|
8
10
|
footer?: ReactNode
|
|
9
11
|
listGap?: number
|
|
10
|
-
|
|
12
|
+
subtitle?: string
|
|
13
|
+
keybindsModeId?: ModeId
|
|
11
14
|
title: string
|
|
12
15
|
width: number | `${number}%`
|
|
13
16
|
}
|
|
14
17
|
|
|
15
|
-
export function ModalShell({
|
|
18
|
+
export function ModalShell({
|
|
19
|
+
children,
|
|
20
|
+
footer,
|
|
21
|
+
keybindsModeId,
|
|
22
|
+
listGap = 1,
|
|
23
|
+
subtitle,
|
|
24
|
+
title,
|
|
25
|
+
width,
|
|
26
|
+
}: ModalShellProps) {
|
|
16
27
|
return (
|
|
17
28
|
<box
|
|
18
29
|
position="absolute"
|
|
@@ -36,12 +47,13 @@ export function ModalShell({ children, footer, help, listGap = 1, title, width }
|
|
|
36
47
|
<box width="100%" flexDirection="column" gap={listGap}>
|
|
37
48
|
<box flexDirection="column">
|
|
38
49
|
<text fg={theme.accentAlt}>{title}</text>
|
|
39
|
-
{
|
|
50
|
+
{subtitle ? <text fg={theme.textMuted}>{subtitle}</text> : null}
|
|
40
51
|
</box>
|
|
41
52
|
{children}
|
|
42
53
|
{footer ? <box>{footer}</box> : null}
|
|
43
54
|
</box>
|
|
44
55
|
</Surface>
|
|
56
|
+
{keybindsModeId ? <ModalKeybindsOverlay modeId={keybindsModeId} /> : null}
|
|
45
57
|
</box>
|
|
46
58
|
)
|
|
47
59
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { getAllAssistantOptions } from '../../pty/command-registry'
|
|
2
|
-
import { useModalHelp } from '../keymap-context'
|
|
3
2
|
import { theme } from '../theme'
|
|
4
3
|
import { uiTokens } from '../ui-tokens'
|
|
5
4
|
import { InputField } from './input-field'
|
|
@@ -15,17 +14,17 @@ interface NewTabModalProps {
|
|
|
15
14
|
export function NewTabModal({ customCommands, editBuffer, selectedIndex }: NewTabModalProps) {
|
|
16
15
|
const options = getAllAssistantOptions(customCommands)
|
|
17
16
|
const selectedOption = options[selectedIndex]
|
|
18
|
-
const
|
|
19
|
-
const browsingHelp = useModalHelp('modal.new-tab')
|
|
20
|
-
const help =
|
|
21
|
-
editBuffer !== null
|
|
22
|
-
? `Editing command for ${selectedOption?.label}. ${editingHelp}`
|
|
23
|
-
: browsingHelp
|
|
17
|
+
const isEditing = editBuffer !== null
|
|
24
18
|
|
|
25
19
|
return (
|
|
26
|
-
<ModalShell
|
|
27
|
-
|
|
28
|
-
|
|
20
|
+
<ModalShell
|
|
21
|
+
title="New assistant tab"
|
|
22
|
+
subtitle={isEditing ? `Editing command for ${selectedOption?.label}` : undefined}
|
|
23
|
+
keybindsModeId={isEditing ? 'modal.new-tab.command-edit' : 'modal.new-tab'}
|
|
24
|
+
width={uiTokens.modalWidth.md}
|
|
25
|
+
>
|
|
26
|
+
{isEditing ? (
|
|
27
|
+
<InputField active value={editBuffer ?? ''} />
|
|
29
28
|
) : (
|
|
30
29
|
options.map((option, index) => {
|
|
31
30
|
const active = index === selectedIndex
|
|
@@ -7,6 +7,7 @@ import { Surface } from './surface'
|
|
|
7
7
|
|
|
8
8
|
export function PendingChordOverlay() {
|
|
9
9
|
const pendingChords = useAppStore((s) => s.pendingChords)
|
|
10
|
+
const modalOpen = useAppStore((s) => s.modal.type !== null)
|
|
10
11
|
const config = useKeymap()
|
|
11
12
|
|
|
12
13
|
if (!pendingChords || pendingChords.length === 0) return null
|
|
@@ -15,7 +16,7 @@ export function PendingChordOverlay() {
|
|
|
15
16
|
const display = pendingChords.map((c) => formatChord(c, leaderChord)).join(' ')
|
|
16
17
|
|
|
17
18
|
return (
|
|
18
|
-
<box position="absolute" bottom={2} right={1}>
|
|
19
|
+
<box position="absolute" bottom={modalOpen ? 10 : 2} right={1}>
|
|
19
20
|
<Surface tone="elevated" paddingLeft={1} paddingRight={1}>
|
|
20
21
|
<box flexDirection="row">
|
|
21
22
|
<text fg={theme.textMuted}>pending: </text>
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import { useModalHelp } from '../keymap-context'
|
|
2
1
|
import { uiTokens } from '../ui-tokens'
|
|
3
2
|
import { InputField } from './input-field'
|
|
4
3
|
import { ModalShell } from './modal-shell'
|
|
5
4
|
|
|
6
5
|
export function SessionNameModal({ title, value }: { title: string; value: string }) {
|
|
7
|
-
const help = useModalHelp('modal.session-name')
|
|
8
6
|
return (
|
|
9
|
-
<ModalShell title={title}
|
|
7
|
+
<ModalShell title={title} keybindsModeId="modal.session-name" width={uiTokens.modalWidth.md}>
|
|
10
8
|
<InputField active value={value} />
|
|
11
9
|
</ModalShell>
|
|
12
10
|
)
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { SessionRecord } from '../../state/types'
|
|
2
2
|
|
|
3
3
|
import { filterSessions } from '../../state/selectors'
|
|
4
|
-
import { useModalHelp } from '../keymap-context'
|
|
5
4
|
import { abbreviatePath } from '../path-format'
|
|
6
5
|
import { theme } from '../theme'
|
|
7
6
|
import { uiTokens } from '../ui-tokens'
|
|
@@ -48,12 +47,11 @@ export function SessionPickerModal({
|
|
|
48
47
|
const hasFilter = !!filter
|
|
49
48
|
const showFilteredEmptyState = filtered.length === 0 && sessions.length > 0
|
|
50
49
|
const showInitialEmptyState = filtered.length === 0 && sessions.length === 0
|
|
51
|
-
const help = useModalHelp('modal.session-picker')
|
|
52
50
|
|
|
53
51
|
return (
|
|
54
52
|
<ModalShell
|
|
55
53
|
title="Sessions"
|
|
56
|
-
|
|
54
|
+
keybindsModeId="modal.session-picker"
|
|
57
55
|
width={uiTokens.modalWidth.lg}
|
|
58
56
|
footer={<ModalFilterBar filter={filter} />}
|
|
59
57
|
>
|