@kinqs/brainrouter-cli 0.3.5 → 0.3.7
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 +29 -52
- package/agents/architect.json +18 -0
- package/agents/explorer.json +18 -0
- package/agents/reviewer.json +18 -0
- package/agents/verifier.json +18 -0
- package/agents/worker.json +18 -0
- package/bin/cli.cjs +71 -0
- package/dist/agent/agent.d.ts +224 -3
- package/dist/agent/agent.js +561 -55
- package/dist/cli/banner.d.ts +80 -0
- package/dist/cli/banner.js +232 -0
- package/dist/cli/cliPrompt.d.ts +106 -0
- package/dist/cli/cliPrompt.js +314 -0
- package/dist/cli/commands/_context.d.ts +3 -1
- package/dist/cli/commands/_helpers.d.ts +1 -1
- package/dist/cli/commands/_helpers.js +6 -6
- package/dist/cli/commands/config.d.ts +46 -0
- package/dist/cli/commands/config.js +1042 -0
- package/dist/cli/commands/guard.js +75 -10
- package/dist/cli/commands/init.d.ts +20 -0
- package/dist/cli/commands/init.js +64 -0
- package/dist/cli/commands/login.d.ts +13 -0
- package/dist/cli/commands/login.js +179 -0
- package/dist/cli/commands/mcp.d.ts +19 -0
- package/dist/cli/commands/mcp.js +286 -0
- package/dist/cli/commands/memory.js +2 -2
- package/dist/cli/commands/obs.js +22 -22
- package/dist/cli/commands/orchestration.js +18 -0
- package/dist/cli/commands/session.js +13 -5
- package/dist/cli/commands/ui.js +202 -91
- package/dist/cli/commands/workflow.d.ts +20 -0
- package/dist/cli/commands/workflow.js +368 -51
- package/dist/cli/ink/ChatApp.d.ts +206 -0
- package/dist/cli/ink/ChatApp.js +493 -0
- package/dist/cli/ink/Frame.d.ts +26 -0
- package/dist/cli/ink/Frame.js +5 -0
- package/dist/cli/ink/Picker.d.ts +65 -0
- package/dist/cli/ink/Picker.js +133 -0
- package/dist/cli/ink/SlashPalette.d.ts +51 -0
- package/dist/cli/ink/SlashPalette.js +136 -0
- package/dist/cli/ink/TextField.d.ts +34 -0
- package/dist/cli/ink/TextField.js +47 -0
- package/dist/cli/ink/WizardApp.d.ts +7 -0
- package/dist/cli/ink/WizardApp.js +422 -0
- package/dist/cli/ink/ambientChat.d.ts +34 -0
- package/dist/cli/ink/ambientChat.js +7 -0
- package/dist/cli/ink/consoleCapture.d.ts +11 -0
- package/dist/cli/ink/consoleCapture.js +33 -0
- package/dist/cli/ink/markdownRender.d.ts +41 -0
- package/dist/cli/ink/markdownRender.js +278 -0
- package/dist/cli/ink/renderWithResizeClear.d.ts +14 -0
- package/dist/cli/ink/renderWithResizeClear.js +33 -0
- package/dist/cli/ink/runChat.d.ts +34 -0
- package/dist/cli/ink/runChat.js +571 -0
- package/dist/cli/ink/runPicker.d.ts +31 -0
- package/dist/cli/ink/runPicker.js +139 -0
- package/dist/cli/ink/runSlashPalette.d.ts +23 -0
- package/dist/cli/ink/runSlashPalette.js +33 -0
- package/dist/cli/ink/runWizard.d.ts +22 -0
- package/dist/cli/ink/runWizard.js +133 -0
- package/dist/cli/ink/stdinHandoff.d.ts +51 -0
- package/dist/cli/ink/stdinHandoff.js +78 -0
- package/dist/cli/ink/toolFormat.d.ts +73 -0
- package/dist/cli/ink/toolFormat.js +180 -0
- package/dist/cli/ink/useTerminalSize.d.ts +35 -0
- package/dist/cli/ink/useTerminalSize.js +26 -0
- package/dist/cli/repl.d.ts +25 -3
- package/dist/cli/repl.js +64 -646
- package/dist/cli/slashSuggest.d.ts +32 -0
- package/dist/cli/slashSuggest.js +146 -0
- package/dist/cli/spinner.d.ts +34 -0
- package/dist/cli/spinner.js +36 -0
- package/dist/cli/statusline.d.ts +67 -0
- package/dist/cli/statusline.js +204 -0
- package/dist/cli/theme.d.ts +79 -0
- package/dist/cli/theme.js +106 -0
- package/dist/cli/whereView.d.ts +81 -0
- package/dist/cli/whereView.js +245 -0
- package/dist/cli/wizard/modelsApi.d.ts +72 -0
- package/dist/cli/wizard/modelsApi.js +166 -0
- package/dist/cli/wizard/picker.d.ts +202 -0
- package/dist/cli/wizard/picker.js +547 -0
- package/dist/cli/wizard/providers.d.ts +86 -0
- package/dist/cli/wizard/providers.js +190 -0
- package/dist/cli/wizard/runner.d.ts +13 -0
- package/dist/cli/wizard/runner.js +488 -0
- package/dist/cli/wizard/types.d.ts +122 -0
- package/dist/cli/wizard/types.js +109 -0
- package/dist/config/config.d.ts +52 -0
- package/dist/config/config.js +89 -75
- package/dist/index.js +215 -206
- package/dist/memory/briefing.d.ts +11 -1
- package/dist/memory/briefing.js +69 -1
- package/dist/memory/consolidation.d.ts +1 -1
- package/dist/orchestration/agentRegistry.d.ts +36 -0
- package/dist/orchestration/agentRegistry.js +64 -0
- package/dist/orchestration/orchestrator.d.ts +7 -0
- package/dist/orchestration/orchestrator.js +2 -0
- package/dist/orchestration/tools.d.ts +10 -1
- package/dist/orchestration/tools.js +48 -4
- package/dist/prompt/breadthHint.d.ts +5 -0
- package/dist/prompt/breadthHint.js +44 -0
- package/dist/prompt/skillCatalog.d.ts +11 -0
- package/dist/prompt/skillCatalog.js +134 -0
- package/dist/prompt/skillRunner.d.ts +2 -2
- package/dist/prompt/skillRunner.js +2 -31
- package/dist/prompt/systemPrompt.d.ts +34 -0
- package/dist/prompt/systemPrompt.js +128 -108
- package/dist/runtime/dangerousCommand.d.ts +53 -0
- package/dist/runtime/dangerousCommand.js +105 -0
- package/dist/runtime/mcpClient.d.ts +38 -1
- package/dist/runtime/mcpClient.js +104 -13
- package/dist/runtime/mcpPool.d.ts +162 -0
- package/dist/runtime/mcpPool.js +423 -0
- package/dist/runtime/mcpUtils.d.ts +3 -1
- package/dist/state/goalStore.d.ts +98 -17
- package/dist/state/goalStore.js +132 -42
- package/dist/state/preferencesStore.d.ts +67 -3
- package/dist/state/preferencesStore.js +84 -1
- package/dist/state/workflowArtifacts.d.ts +63 -2
- package/dist/state/workflowArtifacts.js +120 -8
- package/dist/tests/_helpers.d.ts +31 -0
- package/dist/tests/_helpers.js +91 -0
- package/package.json +12 -5
- package/.env.example +0 -109
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
|
3
|
+
import { Box, Text, useApp, useInput } from 'ink';
|
|
4
|
+
import TextInput from 'ink-text-input';
|
|
5
|
+
import Spinner from 'ink-spinner';
|
|
6
|
+
import { classifyDiffLine, looksLikeDiff } from './toolFormat.js';
|
|
7
|
+
import { renderMarkdown } from './markdownRender.js';
|
|
8
|
+
import { useTerminalSize } from './useTerminalSize.js';
|
|
9
|
+
// --- Main app ---------------------------------------------------------
|
|
10
|
+
export function ChatApp({ initialBanner, initialOfflineWarning, initialHint, slashCommands, promptLabel, accentColor = '#CC9166', onSubmit, onReady, onAccessModeCycle, initialAccessMode = 'read', initialFooter = {}, }) {
|
|
11
|
+
const { exit } = useApp();
|
|
12
|
+
// useTerminalSize subscribes to stdout 'resize' and pushes the new
|
|
13
|
+
// width into React state, forcing a re-render. Reading
|
|
14
|
+
// useStdout().stdout.columns inline LOOKS like it would work (it's a
|
|
15
|
+
// live getter and Ink claims to re-render on resize), but in practice
|
|
16
|
+
// the dividers + footer + slash palette were left at the OLD width
|
|
17
|
+
// until the next unrelated state change — which is what causes the
|
|
18
|
+
// duplicated/growing dash residue when dragging the window. See
|
|
19
|
+
// useTerminalSize.ts for the full rationale.
|
|
20
|
+
const { columns: cols } = useTerminalSize();
|
|
21
|
+
const [scrollback, setScrollback] = useState(() => seedScrollback(initialBanner, initialOfflineWarning, initialHint));
|
|
22
|
+
const nextIdRef = useRef(scrollback.length);
|
|
23
|
+
const [composerValue, setComposerValue] = useState('');
|
|
24
|
+
const [phase, setPhase] = useState('idle');
|
|
25
|
+
const [spinnerLabel, setSpinnerLabel] = useState('');
|
|
26
|
+
const [accessMode, setAccessMode] = useState(initialAccessMode);
|
|
27
|
+
const [footer, setFooter] = useState(initialFooter);
|
|
28
|
+
/**
|
|
29
|
+
* Per-turn elapsed time, ticked once a second while phase === 'turn-running'.
|
|
30
|
+
* Drives the amber spinner-color transition at 10s — claude-code's
|
|
31
|
+
* "Claude is still working" cue (CHANGELOG v2.1.130 entry 154).
|
|
32
|
+
*/
|
|
33
|
+
const [turnElapsedMs, setTurnElapsedMs] = useState(0);
|
|
34
|
+
/**
|
|
35
|
+
* Overlay slot — when set, hides the composer + palette and renders
|
|
36
|
+
* the overlay node instead. Set via controller.showOverlay; cleared
|
|
37
|
+
* via controller.clearOverlay. Used by runPicker/runTextField so
|
|
38
|
+
* /config /login /init render inside the chat Ink (not as a second
|
|
39
|
+
* Ink mount that would fight for stdin).
|
|
40
|
+
*/
|
|
41
|
+
const [overlay, setOverlay] = useState(null);
|
|
42
|
+
const overlayResolveRef = useRef(null);
|
|
43
|
+
/**
|
|
44
|
+
* Slash palette cursor — lifted out of SlashPalettePanel so this
|
|
45
|
+
* component owns both the highlight + the keystroke handlers.
|
|
46
|
+
* (useInput at the panel level would race with TextInput for arrow
|
|
47
|
+
* keys; centralizing here makes the precedence explicit.)
|
|
48
|
+
*/
|
|
49
|
+
const [paletteCursor, setPaletteCursor] = useState(0);
|
|
50
|
+
const pushFns = useMemo(() => {
|
|
51
|
+
const push = (entry) => {
|
|
52
|
+
setScrollback((s) => {
|
|
53
|
+
const id = ++nextIdRef.current;
|
|
54
|
+
return [...s, { id, ...entry }];
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
return {
|
|
58
|
+
raw: (text, opts) => push({ kind: 'raw', text, noWrap: opts?.noWrap }),
|
|
59
|
+
user: (text) => push({ kind: 'user', text }),
|
|
60
|
+
assistant: (text, meta) => push({ kind: 'assistant', text, ...meta }),
|
|
61
|
+
tool: (header, ok, opts) => push({ kind: 'tool', header, ok, ...opts }),
|
|
62
|
+
memory: (level, text) => push({ kind: 'memory', level, text }),
|
|
63
|
+
plan: (items, explanation) => push({ kind: 'plan', items, explanation }),
|
|
64
|
+
notice: (text, level) => push({ kind: 'notice', text, level: level ?? 'info' }),
|
|
65
|
+
setStatus: (label) => setSpinnerLabel(label),
|
|
66
|
+
setPhase: (p) => setPhase(p),
|
|
67
|
+
};
|
|
68
|
+
}, []);
|
|
69
|
+
// Tick the per-turn elapsed time while a turn is running. Resets to 0
|
|
70
|
+
// on each phase change. Spinner color blends from green → amber when
|
|
71
|
+
// this crosses 10s, matching claude-code's "still working" cue
|
|
72
|
+
// (CHANGELOG v2.1.130 entry 154).
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (phase !== 'turn-running') {
|
|
75
|
+
setTurnElapsedMs(0);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const startedAt = Date.now();
|
|
79
|
+
setTurnElapsedMs(0);
|
|
80
|
+
const interval = setInterval(() => {
|
|
81
|
+
setTurnElapsedMs(Date.now() - startedAt);
|
|
82
|
+
}, 1000);
|
|
83
|
+
return () => clearInterval(interval);
|
|
84
|
+
}, [phase]);
|
|
85
|
+
// Imperative controller — exposed once on mount via onReady so the
|
|
86
|
+
// orchestrator can push from outside the React tree (child agent
|
|
87
|
+
// callbacks fire long after `await agent.runTurn()` resolves and need
|
|
88
|
+
// a way to inject into scrollback without re-entering React state).
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (!onReady)
|
|
91
|
+
return;
|
|
92
|
+
onReady({
|
|
93
|
+
push: pushFns,
|
|
94
|
+
replaceBanner: (text) => {
|
|
95
|
+
setScrollback((rows) => {
|
|
96
|
+
const idx = rows.findIndex((entry) => entry.kind === 'raw');
|
|
97
|
+
if (idx < 0)
|
|
98
|
+
return [{ id: ++nextIdRef.current, kind: 'raw', text, noWrap: true }, ...rows];
|
|
99
|
+
return rows.map((entry, i) => i === idx ? { ...entry, text } : entry);
|
|
100
|
+
});
|
|
101
|
+
},
|
|
102
|
+
setFooter: (patch) => {
|
|
103
|
+
if (patch.accessMode)
|
|
104
|
+
setAccessMode(patch.accessMode);
|
|
105
|
+
setFooter((prev) => ({ ...prev, ...patch }));
|
|
106
|
+
},
|
|
107
|
+
setComposer: (text) => setComposerValue(text),
|
|
108
|
+
showOverlay: (node) => new Promise((resolve) => {
|
|
109
|
+
// Save the resolver; clearOverlay() will fire it. Setting the
|
|
110
|
+
// overlay React state hides the composer next render so the
|
|
111
|
+
// overlay's own useInput hooks own keystrokes uncontested.
|
|
112
|
+
overlayResolveRef.current = resolve;
|
|
113
|
+
setOverlay(node);
|
|
114
|
+
}),
|
|
115
|
+
clearOverlay: () => {
|
|
116
|
+
setOverlay(null);
|
|
117
|
+
const r = overlayResolveRef.current;
|
|
118
|
+
overlayResolveRef.current = null;
|
|
119
|
+
if (r)
|
|
120
|
+
r();
|
|
121
|
+
},
|
|
122
|
+
exit,
|
|
123
|
+
});
|
|
124
|
+
// Run exactly once — the controller's identity is stable across renders.
|
|
125
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
126
|
+
}, []);
|
|
127
|
+
// Slash palette visibility — open when input is just `/<query>`
|
|
128
|
+
// with no whitespace yet (so the user is still composing the
|
|
129
|
+
// command name, not args).
|
|
130
|
+
const slashQuery = useMemo(() => {
|
|
131
|
+
if (!composerValue.startsWith('/'))
|
|
132
|
+
return null;
|
|
133
|
+
const tail = composerValue.slice(1);
|
|
134
|
+
if (tail.includes(' '))
|
|
135
|
+
return null;
|
|
136
|
+
return tail;
|
|
137
|
+
}, [composerValue]);
|
|
138
|
+
// All matches for the current query, in filter rank order. Computed
|
|
139
|
+
// once per keystroke so the panel and the Enter/Tab handlers all
|
|
140
|
+
// share the same view of "what's highlighted".
|
|
141
|
+
const paletteMatches = useMemo(() => (slashQuery !== null ? filterPaletteCommands(slashCommands, slashQuery) : []), [slashCommands, slashQuery]);
|
|
142
|
+
// Reset the cursor whenever the filter changes (matches array shrinks
|
|
143
|
+
// or shifts), and snap to 0 when the palette closes so a fresh `/`
|
|
144
|
+
// doesn't land on a stale row index.
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
if (slashQuery === null) {
|
|
147
|
+
setPaletteCursor(0);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
setPaletteCursor((c) => (paletteMatches.length === 0 ? 0 : Math.min(c, paletteMatches.length - 1)));
|
|
151
|
+
}, [slashQuery, paletteMatches.length]);
|
|
152
|
+
const onComposerSubmit = useCallback(async (text) => {
|
|
153
|
+
let trimmed = text.trim();
|
|
154
|
+
// Palette substitution: if the user pressed Enter while a slash
|
|
155
|
+
// palette match is highlighted AND the buffer is still in palette
|
|
156
|
+
// mode (just `/<query>`, no args yet), submit the highlighted
|
|
157
|
+
// command instead of the literal typed text. Matches the standalone
|
|
158
|
+
// SlashPalette in cli/ink/SlashPalette.tsx:onSubmit.
|
|
159
|
+
if (trimmed.startsWith('/') && !trimmed.includes(' ') && paletteMatches.length > 0) {
|
|
160
|
+
const picked = paletteMatches[paletteCursor] ?? paletteMatches[0];
|
|
161
|
+
if (picked.cmd !== trimmed) {
|
|
162
|
+
trimmed = picked.cmd;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (!trimmed)
|
|
166
|
+
return;
|
|
167
|
+
pushFns.user(trimmed);
|
|
168
|
+
setComposerValue('');
|
|
169
|
+
setPhase('turn-running');
|
|
170
|
+
setSpinnerLabel('thinking');
|
|
171
|
+
try {
|
|
172
|
+
await onSubmit(trimmed, pushFns);
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
pushFns.notice(`✗ ${err?.message ?? err}`);
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
setPhase('idle');
|
|
179
|
+
setSpinnerLabel('');
|
|
180
|
+
}
|
|
181
|
+
}, [onSubmit, pushFns, paletteMatches, paletteCursor]);
|
|
182
|
+
// Ctrl+D / Ctrl+C exit; Shift+Tab cycles access mode; while the
|
|
183
|
+
// slash palette is open, arrow keys navigate it and Tab autocompletes
|
|
184
|
+
// the highlighted command into the composer.
|
|
185
|
+
//
|
|
186
|
+
// CRITICAL: when an overlay is active (e.g. /config picker), this
|
|
187
|
+
// handler returns immediately so the overlay's useInput owns every
|
|
188
|
+
// keystroke uncontested. Without this, Ctrl+C / Shift+Tab would
|
|
189
|
+
// still fire from chat-level and cause double-handling — the kind
|
|
190
|
+
// of bug that makes "/config exits to bash" symptoms.
|
|
191
|
+
useInput((input, key) => {
|
|
192
|
+
if (overlay !== null)
|
|
193
|
+
return;
|
|
194
|
+
if (key.ctrl && (input === 'c' || input === 'd')) {
|
|
195
|
+
exit();
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (key.shift && key.tab && onAccessModeCycle) {
|
|
199
|
+
const next = onAccessModeCycle();
|
|
200
|
+
if (next === 'read' || next === 'write' || next === 'shell') {
|
|
201
|
+
setAccessMode(next);
|
|
202
|
+
pushFns.notice(`Access mode → ${next}`);
|
|
203
|
+
}
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
// Palette navigation — only when palette is open AND there's at
|
|
207
|
+
// least one match. We DON'T use `key.return` here because Enter is
|
|
208
|
+
// handled by TextInput's onSubmit (which calls onComposerSubmit
|
|
209
|
+
// above, which performs the highlight-substitution).
|
|
210
|
+
if (slashQuery !== null && paletteMatches.length > 0) {
|
|
211
|
+
if (key.upArrow) {
|
|
212
|
+
setPaletteCursor((c) => (c - 1 + paletteMatches.length) % paletteMatches.length);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (key.downArrow) {
|
|
216
|
+
setPaletteCursor((c) => (c + 1) % paletteMatches.length);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (key.tab && !key.shift) {
|
|
220
|
+
// Tab autocompletes the highlighted command into the composer
|
|
221
|
+
// (with a trailing space so the user can keep typing args).
|
|
222
|
+
const picked = paletteMatches[paletteCursor] ?? paletteMatches[0];
|
|
223
|
+
setComposerValue(picked.cmd + ' ');
|
|
224
|
+
setPaletteCursor(0);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { flexDirection: "column", children: scrollback.map((entry) => (_jsx(ScrollbackRow, { entry: entry, accentColor: accentColor }, entry.id))) }), overlay !== null ? (_jsxs(_Fragment, { children: [overlay, _jsx(FooterStatus, { promptLabel: promptLabel, phase: phase, accentColor: accentColor, accessMode: accessMode, footer: footer, cols: cols })] })) : (_jsxs(_Fragment, { children: [phase === 'turn-running' ? (_jsxs(Box, { children: [_jsx(Text, { color: turnElapsedMs >= 10_000 ? 'yellow' : 'green', children: React.createElement(Spinner, { type: 'dots' }) }), _jsxs(Text, { color: "gray", wrap: "truncate", children: [" ", spinnerLabel] })] })) : null, _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: accentColor, dimColor: true, children: '─'.repeat(Math.max(10, cols - 2)) }), _jsxs(Box, { children: [_jsx(Text, { color: accentColor, children: ' ❯ ' }), _jsx(TextInput, { value: composerValue, onChange: setComposerValue, onSubmit: onComposerSubmit, placeholder: phase === 'turn-running' ? '' : 'type a prompt or / for commands' })] }), _jsx(Text, { color: accentColor, dimColor: true, children: '─'.repeat(Math.max(10, cols - 2)) })] }), slashQuery !== null ? (_jsx(SlashPalettePanel, { matches: paletteMatches, cursor: paletteCursor, accentColor: accentColor, cols: cols })) : null, _jsx(FooterStatus, { promptLabel: promptLabel, phase: phase, accentColor: accentColor, accessMode: accessMode, footer: footer, cols: cols })] }))] }));
|
|
230
|
+
}
|
|
231
|
+
// --- Sub-components ---------------------------------------------------
|
|
232
|
+
/**
|
|
233
|
+
* Per-entry renderer — every scrollback kind has its own claude-code-style
|
|
234
|
+
* layout. Glyph conventions:
|
|
235
|
+
*
|
|
236
|
+
* ⏺ assistant turn (first line) — green dot
|
|
237
|
+
* ❯ user prompt (left margin) — accent (orange)
|
|
238
|
+
* ⏺ tool call header — green dot when ok, red when failed
|
|
239
|
+
* ⎿ tool result connector — first line of preview
|
|
240
|
+
* ✓ / ✗ status mark — final status line of tool block
|
|
241
|
+
* ↳ plan / memory dim-italic explanation
|
|
242
|
+
*/
|
|
243
|
+
function ScrollbackRow({ entry, accentColor }) {
|
|
244
|
+
switch (entry.kind) {
|
|
245
|
+
case 'raw':
|
|
246
|
+
return _jsx(Text, { wrap: entry.noWrap ? 'truncate' : 'wrap', children: entry.text });
|
|
247
|
+
case 'user':
|
|
248
|
+
// Flex layout: ❯ on the left, prompt body in an inner column that
|
|
249
|
+
// takes the remaining width. Continuation lines (when the user
|
|
250
|
+
// pastes a multi-line prompt) align under the body column, not
|
|
251
|
+
// under the caret.
|
|
252
|
+
return (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: accentColor, children: "\u276F " }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: _jsx(Text, { children: entry.text }) })] }));
|
|
253
|
+
case 'assistant': {
|
|
254
|
+
// Pass the WHOLE rendered markdown to a single <Text> instead of
|
|
255
|
+
// splitting on \n and re-rendering each line. The old line-split
|
|
256
|
+
// approach broke ANSI styling that spans newlines — e.g. a
|
|
257
|
+
// multi-line blockquote whose `gray italic` open code sat on line
|
|
258
|
+
// 1 but whose close code sat on line 3 lost its style on lines
|
|
259
|
+
// 2-3. `renderMarkdown` re-scopes the styling per line so the
|
|
260
|
+
// single <Text> reads cleanly.
|
|
261
|
+
//
|
|
262
|
+
// The `⏺` lives in its own Text to the left of the body. The body
|
|
263
|
+
// Box has flexGrow=1 so it takes the remaining terminal width and
|
|
264
|
+
// Ink's wrap-ansi handles reflow inside it. Continuation lines
|
|
265
|
+
// (both from wrap and from explicit \n in the rendered output)
|
|
266
|
+
// align under the body column.
|
|
267
|
+
//
|
|
268
|
+
// `entry.raw === true` (user's rawScrollback preference) skips
|
|
269
|
+
// marked entirely — useful when the user wants to see the LLM's
|
|
270
|
+
// literal markdown source.
|
|
271
|
+
const rendered = (entry.raw ? entry.text : renderMarkdown(entry.text)).trimEnd();
|
|
272
|
+
const meta = entry.durationMs !== undefined
|
|
273
|
+
? ` ${Math.floor(entry.durationMs / 1000)}s${entry.tokensIn !== undefined ? ` · ${entry.tokensIn.toLocaleString()} in / ${entry.tokensOut?.toLocaleString() ?? 0} out` : ''}`
|
|
274
|
+
: '';
|
|
275
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: "green", children: "\u23FA " }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: _jsx(Text, { children: rendered }) })] }), meta ? (_jsx(Box, { paddingLeft: 2, children: _jsx(Text, { color: "gray", dimColor: true, children: meta }) })) : null] }));
|
|
276
|
+
}
|
|
277
|
+
case 'tool': {
|
|
278
|
+
// Claude-code layout:
|
|
279
|
+
// ⏺ Read(src/foo.ts)
|
|
280
|
+
// ⎿ <line 1 of preview>
|
|
281
|
+
// <line 2 of preview>
|
|
282
|
+
// (+N more lines hidden)
|
|
283
|
+
// The header DOT is green on success and red on failure so the user
|
|
284
|
+
// can scan a long turn at a glance. Duration appended in dim if set.
|
|
285
|
+
const dotColor = entry.ok ? 'green' : 'red';
|
|
286
|
+
const previewLines = entry.preview ? splitForPreview(entry.preview) : null;
|
|
287
|
+
const isDiff = entry.preview ? looksLikeDiff(entry.preview) : false;
|
|
288
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: dotColor, children: "\u23FA " }), _jsx(Text, { wrap: "truncate", children: entry.header }), entry.durationMs !== undefined ? (_jsx(Text, { color: "gray", dimColor: true, children: ` · ${formatDuration(entry.durationMs)}` })) : null, !entry.ok ? (_jsx(Text, { color: "red", dimColor: true, children: ' · failed' })) : null] }), previewLines ? previewLines.visible.map((line, i) => (_jsx(ToolPreviewLine, { line: line, isFirst: i === 0, isDiff: isDiff }, i))) : null, previewLines && previewLines.hidden > 0 ? (_jsx(Box, { children: _jsx(Text, { color: "gray", dimColor: true, children: ` (+${previewLines.hidden} more line${previewLines.hidden === 1 ? '' : 's'} hidden)` }) })) : null] }));
|
|
289
|
+
}
|
|
290
|
+
case 'memory':
|
|
291
|
+
// Memory pipeline events — briefing / capture / citation / contradiction.
|
|
292
|
+
// Warnings (contradictions, extraction failures) stand out; info events
|
|
293
|
+
// stay dim so the chat doesn't drown in capture chatter.
|
|
294
|
+
return (_jsx(Box, { children: _jsxs(Text, { color: entry.level === 'warn' ? 'yellow' : 'gray', bold: entry.level === 'warn', dimColor: entry.level === 'info', wrap: "truncate", children: [entry.level === 'warn' ? '⚠ ' : '· ', entry.text] }) }));
|
|
295
|
+
case 'plan':
|
|
296
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: "gray", bold: true, children: "\uD83D\uDCCB Plan" }), entry.explanation ? (_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: "gray", dimColor: true, italic: true, children: [" \u21B3 ", entry.explanation] }) })) : null, entry.items.map((item, i) => {
|
|
297
|
+
const mark = item.status === 'completed' ? '✓' : item.status === 'in_progress' ? '⏳' : '☐';
|
|
298
|
+
const color = item.status === 'completed' ? 'green' : item.status === 'in_progress' ? 'yellow' : 'gray';
|
|
299
|
+
// Multi-line steps indent under the first line so the checkbox
|
|
300
|
+
// anchor stays visually attached to the whole step.
|
|
301
|
+
const stepLines = String(item.step).split('\n');
|
|
302
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsxs(Text, { color: color, children: [" ", mark, " "] }), _jsx(Text, { color: item.status === 'completed' ? 'gray' : undefined, children: stepLines[0] })] }), stepLines.slice(1).map((line, j) => (_jsxs(Box, { children: [_jsx(Text, { children: ' ' }), _jsx(Text, { color: item.status === 'completed' ? 'gray' : undefined, dimColor: true, children: line })] }, j)))] }, i));
|
|
303
|
+
})] }));
|
|
304
|
+
case 'notice': {
|
|
305
|
+
// info → gray dim
|
|
306
|
+
// warn → yellow
|
|
307
|
+
// error → red bold
|
|
308
|
+
const level = entry.level ?? 'info';
|
|
309
|
+
const color = level === 'error' ? 'red' : level === 'warn' ? 'yellow' : 'gray';
|
|
310
|
+
return (_jsx(Box, { children: _jsx(Text, { color: color, bold: level === 'error', dimColor: level === 'info', wrap: "truncate", children: entry.text }) }));
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Render one line of a tool-result preview. Diff lines get red/green
|
|
316
|
+
* coloring (see classifyDiffLine). The first line of the preview is
|
|
317
|
+
* prefixed with `⎿` connector under the tool header; continuation lines
|
|
318
|
+
* just indent to align with the connector body.
|
|
319
|
+
*/
|
|
320
|
+
function ToolPreviewLine({ line, isFirst, isDiff }) {
|
|
321
|
+
const indent = isFirst ? ' ⎿ ' : ' ';
|
|
322
|
+
let textColor = 'gray';
|
|
323
|
+
let dim = true;
|
|
324
|
+
if (isDiff) {
|
|
325
|
+
const kind = classifyDiffLine(line);
|
|
326
|
+
if (kind === 'add') {
|
|
327
|
+
textColor = 'green';
|
|
328
|
+
dim = false;
|
|
329
|
+
}
|
|
330
|
+
else if (kind === 'del') {
|
|
331
|
+
textColor = 'red';
|
|
332
|
+
dim = false;
|
|
333
|
+
}
|
|
334
|
+
else if (kind === 'hunk') {
|
|
335
|
+
textColor = 'cyan';
|
|
336
|
+
dim = true;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: "gray", dimColor: true, children: indent }), _jsx(Text, { color: textColor, dimColor: dim, wrap: "truncate", children: line })] }));
|
|
340
|
+
}
|
|
341
|
+
const TOOL_PREVIEW_MAX_LINES = 8;
|
|
342
|
+
/** Split preview into the visible head + the count of hidden tail lines. */
|
|
343
|
+
function splitForPreview(preview) {
|
|
344
|
+
const lines = preview.split('\n');
|
|
345
|
+
if (lines.length <= TOOL_PREVIEW_MAX_LINES)
|
|
346
|
+
return { visible: lines, hidden: 0 };
|
|
347
|
+
return { visible: lines.slice(0, TOOL_PREVIEW_MAX_LINES), hidden: lines.length - TOOL_PREVIEW_MAX_LINES };
|
|
348
|
+
}
|
|
349
|
+
/** Human-readable duration: 950ms, 1.2s, 12s, 1m 23s. */
|
|
350
|
+
function formatDuration(ms) {
|
|
351
|
+
if (ms < 1000)
|
|
352
|
+
return `${ms}ms`;
|
|
353
|
+
if (ms < 10_000)
|
|
354
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
355
|
+
if (ms < 60_000)
|
|
356
|
+
return `${Math.round(ms / 1000)}s`;
|
|
357
|
+
const m = Math.floor(ms / 60_000);
|
|
358
|
+
const s = Math.round((ms % 60_000) / 1000);
|
|
359
|
+
return `${m}m ${s}s`;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Slash command palette — scrollable, navigable, full-list view.
|
|
363
|
+
*
|
|
364
|
+
* Sized to a fixed `MAX_VISIBLE` window; when the match count exceeds
|
|
365
|
+
* the window, the viewport scrolls to keep the highlighted cursor in
|
|
366
|
+
* range. "↑ N more" / "↓ N more" hints render at the edges so the user
|
|
367
|
+
* knows there's more list to see.
|
|
368
|
+
*
|
|
369
|
+
* The command column has a fixed width so descriptions align across
|
|
370
|
+
* rows; descriptions use Ink's `wrap="truncate"` so a long line is
|
|
371
|
+
* cut with an ellipsis at the terminal edge instead of wrapping to
|
|
372
|
+
* the next row (which would break the per-row layout).
|
|
373
|
+
*/
|
|
374
|
+
const PALETTE_MAX_VISIBLE = 10;
|
|
375
|
+
const PALETTE_CMD_COL_WIDTH = 24;
|
|
376
|
+
function SlashPalettePanel({ matches, cursor, accentColor, cols, }) {
|
|
377
|
+
if (matches.length === 0) {
|
|
378
|
+
return (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: "(no matching commands)" }) }));
|
|
379
|
+
}
|
|
380
|
+
// Compute a sliding viewport so the cursor stays comfortably inside.
|
|
381
|
+
// Prefer centering when possible; clamp at the ends so we never show
|
|
382
|
+
// an empty row at top or bottom.
|
|
383
|
+
const total = matches.length;
|
|
384
|
+
const safeCursor = Math.max(0, Math.min(cursor, total - 1));
|
|
385
|
+
const windowSize = Math.min(PALETTE_MAX_VISIBLE, total);
|
|
386
|
+
let viewportStart = safeCursor - Math.floor(windowSize / 2);
|
|
387
|
+
if (viewportStart < 0)
|
|
388
|
+
viewportStart = 0;
|
|
389
|
+
if (viewportStart + windowSize > total)
|
|
390
|
+
viewportStart = Math.max(0, total - windowSize);
|
|
391
|
+
const visible = matches.slice(viewportStart, viewportStart + windowSize);
|
|
392
|
+
const hiddenAbove = viewportStart;
|
|
393
|
+
const hiddenBelow = total - (viewportStart + windowSize);
|
|
394
|
+
// Progressive collapse for narrow terminals:
|
|
395
|
+
// - Below 50 cols: drop the description column entirely; the cmd
|
|
396
|
+
// column expands to fill the remaining width.
|
|
397
|
+
// - At normal widths: fixed 24-col cmd column, description takes
|
|
398
|
+
// the rest with `wrap="truncate"`.
|
|
399
|
+
const showDescription = cols >= 50;
|
|
400
|
+
const cmdColWidth = showDescription
|
|
401
|
+
? PALETTE_CMD_COL_WIDTH
|
|
402
|
+
: Math.max(12, cols - 6);
|
|
403
|
+
const descBudget = showDescription ? Math.max(12, cols - cmdColWidth - 5) : 0;
|
|
404
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [hiddenAbove > 0 ? (_jsx(Box, { children: _jsx(Text, { color: "gray", dimColor: true, children: ` ↑ ${hiddenAbove} more above` }) })) : null, visible.map((cmd, i) => {
|
|
405
|
+
const actualIdx = viewportStart + i;
|
|
406
|
+
const isSelected = actualIdx === safeCursor;
|
|
407
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: accentColor, children: isSelected ? ' › ' : ' ' }), _jsx(Box, { width: cmdColWidth, children: _jsx(Text, { bold: isSelected, color: isSelected ? accentColor : undefined, wrap: "truncate", children: cmd.cmd }) }), showDescription ? (_jsx(Box, { width: descBudget, children: _jsx(Text, { color: "gray", wrap: "truncate", children: cmd.description }) })) : null] }, cmd.cmd));
|
|
408
|
+
}), hiddenBelow > 0 ? (_jsx(Box, { children: _jsx(Text, { color: "gray", dimColor: true, children: ` ↓ ${hiddenBelow} more below` }) })) : null, _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", dimColor: true, wrap: "truncate", children: cols >= 90
|
|
409
|
+
? '↑/↓ navigate · tab autocomplete · ↵ submit · type to filter · esc / backspace past / to cancel'
|
|
410
|
+
: cols >= 60
|
|
411
|
+
? '↑/↓ · tab autocomplete · ↵ submit · esc to cancel'
|
|
412
|
+
: '↑/↓ · tab · ↵ · esc' }) })] }));
|
|
413
|
+
}
|
|
414
|
+
function FooterStatus({ promptLabel, phase, accentColor, accessMode, footer, cols, }) {
|
|
415
|
+
// Pill background mirrors the readline REPL's mode-to-token mapping:
|
|
416
|
+
// read → green (safe)
|
|
417
|
+
// write → accent (default brand)
|
|
418
|
+
// shell → red (escalated)
|
|
419
|
+
// See cli/repl.ts:refreshPromptForMode for the rationale.
|
|
420
|
+
const pillBg = accessMode === 'shell' ? 'red' : accessMode === 'write' ? accentColor : 'green';
|
|
421
|
+
const pillFg = 'black';
|
|
422
|
+
// Effort glyphs — claude-code v2.1.147 convention:
|
|
423
|
+
// low → ○ (open circle, light)
|
|
424
|
+
// medium → ◐ (half circle)
|
|
425
|
+
// high → ● (filled circle, heavy)
|
|
426
|
+
// Rendered inline next to the pill, not as a separate boxed pill, so
|
|
427
|
+
// the footer stays compact on narrow terminals.
|
|
428
|
+
const effortGlyph = footer.effort === 'high' ? '●' : footer.effort === 'medium' ? '◐' : footer.effort === 'low' ? '○' : '';
|
|
429
|
+
const effortColor = footer.effort === 'high' ? 'magenta' : footer.effort === 'medium' ? 'yellow' : 'gray';
|
|
430
|
+
// Left side: model · session · branch. Right side: ? for shortcuts.
|
|
431
|
+
// Spreads out so the footer feels like claude-code's bottom bar.
|
|
432
|
+
const leftSegs = [];
|
|
433
|
+
if (footer.model)
|
|
434
|
+
leftSegs.push(footer.model);
|
|
435
|
+
if (footer.session)
|
|
436
|
+
leftSegs.push(footer.session.slice(0, 16));
|
|
437
|
+
if (footer.branch)
|
|
438
|
+
leftSegs.push(footer.branch);
|
|
439
|
+
if (footer.rightExtra)
|
|
440
|
+
leftSegs.push(footer.rightExtra);
|
|
441
|
+
// Progressive collapse — borrowed from codex's footer.rs:58–86 pattern.
|
|
442
|
+
// Below 80 cols, drop the auxiliary left segments (model · session ·
|
|
443
|
+
// branch). Below 60 cols, drop the right-side hint. Below 40 cols,
|
|
444
|
+
// even the effort glyph collapses to just the access pill — that's
|
|
445
|
+
// the smallest viable status row.
|
|
446
|
+
const showLeftSegs = cols >= 80 && leftSegs.length > 0;
|
|
447
|
+
const showEffortLabel = cols >= 50;
|
|
448
|
+
const showEffortGlyph = !!effortGlyph && cols >= 40;
|
|
449
|
+
const showRightHint = cols >= 60;
|
|
450
|
+
const rightText = showRightHint
|
|
451
|
+
? (cols >= 80 ? '? for shortcuts · / for commands' : '? · /')
|
|
452
|
+
: '';
|
|
453
|
+
// Render the WHOLE footer as a single Text with nested Text children
|
|
454
|
+
// for the colored pill and glyphs. wrap="truncate" on the outer Text
|
|
455
|
+
// ensures it NEVER visually wraps (which is what causes Ink's diff
|
|
456
|
+
// to leave residue on resize — each wrap-overflow row that Ink
|
|
457
|
+
// thinks is 1 logical row but is 2+ visual rows accumulates as
|
|
458
|
+
// duplicated composer/footer blocks on every resize).
|
|
459
|
+
//
|
|
460
|
+
// Ink supports nested Text — children inherit parent props (like
|
|
461
|
+
// wrap) but their own color/backgroundColor/bold etc. apply locally.
|
|
462
|
+
return (_jsxs(Box, { justifyContent: "space-between", paddingX: 1, children: [_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { backgroundColor: pillBg, color: pillFg, children: ` ◉ ${accessMode} ` }), showEffortGlyph ? (_jsxs(Text, { children: [' ', _jsx(Text, { color: effortColor, children: effortGlyph }), showEffortLabel ? _jsx(Text, { color: "gray", dimColor: true, children: ` ${footer.effort}` }) : null] })) : null, showLeftSegs ? (_jsx(Text, { color: "gray", dimColor: true, children: ' ' + leftSegs.join(' · ') })) : null, phase === 'turn-running' && cols >= 50 ? (_jsx(Text, { color: "gray", dimColor: true, children: ' · running' })) : null] }), showRightHint ? (_jsx(Text, { color: "gray", dimColor: true, wrap: "truncate", children: rightText })) : null] }));
|
|
463
|
+
}
|
|
464
|
+
// --- Helpers ----------------------------------------------------------
|
|
465
|
+
function seedScrollback(banner, offline, hint) {
|
|
466
|
+
let id = 0;
|
|
467
|
+
const next = () => ++id;
|
|
468
|
+
const out = [{ id: next(), kind: 'raw', text: banner, noWrap: true }];
|
|
469
|
+
if (offline)
|
|
470
|
+
out.push({ id: next(), kind: 'raw', text: offline, noWrap: true });
|
|
471
|
+
out.push({ id: next(), kind: 'raw', text: hint, noWrap: true });
|
|
472
|
+
return out;
|
|
473
|
+
}
|
|
474
|
+
export function filterPaletteCommands(commands, query) {
|
|
475
|
+
if (!query)
|
|
476
|
+
return commands;
|
|
477
|
+
const q = query.toLowerCase();
|
|
478
|
+
const scored = commands
|
|
479
|
+
.map((c, i) => {
|
|
480
|
+
const body = c.cmd.slice(1).toLowerCase();
|
|
481
|
+
let s = 3;
|
|
482
|
+
if (body.startsWith(q))
|
|
483
|
+
s = 0;
|
|
484
|
+
else if (body.includes(q))
|
|
485
|
+
s = 1;
|
|
486
|
+
else if (c.description.toLowerCase().includes(q))
|
|
487
|
+
s = 2;
|
|
488
|
+
return { c, i, s };
|
|
489
|
+
})
|
|
490
|
+
.filter((x) => x.s < 3);
|
|
491
|
+
scored.sort((a, b) => (a.s - b.s) || (a.i - b.i));
|
|
492
|
+
return scored.map((x) => x.c);
|
|
493
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Bordered panel chrome shared by every Ink picker / prompt in the
|
|
4
|
+
* 0.3.7 redesign.
|
|
5
|
+
*
|
|
6
|
+
* Why a Frame component? Ink owns the render loop and diffs the cell
|
|
7
|
+
* grid between frames — so we don't have to track cursor positions or
|
|
8
|
+
* worry about lines stacking. Every picker / wizard step / config
|
|
9
|
+
* panel is a `<Frame>` with a title, optional badge, and a body. Ink
|
|
10
|
+
* handles all the redraw mechanics that the previous raw-stdout
|
|
11
|
+
* approach got wrong.
|
|
12
|
+
*
|
|
13
|
+
* Visual reference: matches the previous renderFrame() chrome —
|
|
14
|
+
* theme-colored border, bold title left, muted badge right, muted
|
|
15
|
+
* footer hint at the bottom.
|
|
16
|
+
*/
|
|
17
|
+
export interface FrameProps {
|
|
18
|
+
title: string;
|
|
19
|
+
badge?: string;
|
|
20
|
+
subtitle?: string;
|
|
21
|
+
footer?: string;
|
|
22
|
+
/** Border color — defaults to brand orange. */
|
|
23
|
+
accentColor?: string;
|
|
24
|
+
children?: React.ReactNode;
|
|
25
|
+
}
|
|
26
|
+
export declare function Frame({ title, badge, subtitle, footer, accentColor, children }: FrameProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
export function Frame({ title, badge, subtitle, footer, accentColor = '#CC9166', children }) {
|
|
4
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: accentColor, paddingX: 1, marginY: 0, children: [_jsxs(Box, { justifyContent: "space-between", marginBottom: subtitle ? 0 : 0, children: [_jsx(Text, { bold: true, color: accentColor, children: title }), badge ? _jsx(Text, { color: "gray", children: badge }) : null] }), subtitle ? (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "gray", children: subtitle }) })) : null, _jsx(Box, { flexDirection: "column", children: children }), footer ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: footer }) })) : null] }));
|
|
5
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Arrow-key picker built on Ink.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the raw-stdout `pickFromList` primitive that was creeping
|
|
5
|
+
* upward on every cursor move + stacking frames on step transitions.
|
|
6
|
+
* Ink's render loop handles all the redraw / diff / cursor mechanics
|
|
7
|
+
* correctly because it owns the screen.
|
|
8
|
+
*
|
|
9
|
+
* Supports:
|
|
10
|
+
* - N rows (no 2-4 cap)
|
|
11
|
+
* - Optional "Other" free-text fallback
|
|
12
|
+
* - Pre-filled "Other" mode for env-var-derived defaults
|
|
13
|
+
* - Live preview rows (returned from `onCursorChange`, rendered
|
|
14
|
+
* INSIDE the frame above the footer)
|
|
15
|
+
* - Value column right-aligned per row
|
|
16
|
+
* - `›` selected glyph + theme-colored highlight
|
|
17
|
+
*
|
|
18
|
+
* Pattern lineage: state machine + reducer split lifted from grok-cli's
|
|
19
|
+
* SuggestionOverlay; live-preview-from-state contract from codex's
|
|
20
|
+
* theme_picker.rs.
|
|
21
|
+
*/
|
|
22
|
+
export interface PickerRow {
|
|
23
|
+
id: string;
|
|
24
|
+
label: string;
|
|
25
|
+
value?: string;
|
|
26
|
+
description?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface PickerProps {
|
|
29
|
+
title: string;
|
|
30
|
+
subtitle?: string;
|
|
31
|
+
badge?: string;
|
|
32
|
+
/** Optional footer override; defaults to "↑/↓ navigate · ↵ confirm · esc / q cancel". */
|
|
33
|
+
footer?: string;
|
|
34
|
+
rows: PickerRow[];
|
|
35
|
+
initialCursor?: number;
|
|
36
|
+
allowOther?: boolean;
|
|
37
|
+
otherLabel?: string;
|
|
38
|
+
otherDescription?: string;
|
|
39
|
+
prefilledOther?: string;
|
|
40
|
+
onCursorChange?: (id: string, index: number) => string[] | undefined;
|
|
41
|
+
/** Hex / named color for the panel border + title. Defaults to brand orange. */
|
|
42
|
+
accentColor?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Back-compat: callers from `commands/config.ts` etc. pass a Theme
|
|
45
|
+
* object (chalk-based). When present, we pull `accentColor` from
|
|
46
|
+
* the theme's primary hex. Ignored if accentColor is also set.
|
|
47
|
+
*/
|
|
48
|
+
theme?: {
|
|
49
|
+
mode: string;
|
|
50
|
+
};
|
|
51
|
+
/** Ignored (kept for back-compat with the raw-stdout picker shape). */
|
|
52
|
+
eraseOnClose?: boolean;
|
|
53
|
+
/** Resolves with the picker outcome. The component unmounts itself after the callback. */
|
|
54
|
+
onResolve: (result: PickerResult) => void;
|
|
55
|
+
}
|
|
56
|
+
export type PickerResult = {
|
|
57
|
+
kind: 'pick';
|
|
58
|
+
id: string;
|
|
59
|
+
} | {
|
|
60
|
+
kind: 'other';
|
|
61
|
+
text: string;
|
|
62
|
+
} | {
|
|
63
|
+
kind: 'cancelled';
|
|
64
|
+
};
|
|
65
|
+
export declare function Picker(props: PickerProps): import("react/jsx-runtime").JSX.Element;
|