@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,133 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect, useMemo, useRef } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
import TextInput from 'ink-text-input';
|
|
5
|
+
import { Frame } from './Frame.js';
|
|
6
|
+
const OTHER_ID = '__other__';
|
|
7
|
+
function themeToAccent(mode) {
|
|
8
|
+
if (mode === 'light')
|
|
9
|
+
return '#A24E1F';
|
|
10
|
+
if (mode === 'mono')
|
|
11
|
+
return 'white';
|
|
12
|
+
if (mode === 'dark')
|
|
13
|
+
return '#CC9166';
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
export function Picker(props) {
|
|
17
|
+
const augmentedRows = useMemo(() => {
|
|
18
|
+
if (!props.allowOther)
|
|
19
|
+
return props.rows;
|
|
20
|
+
return [
|
|
21
|
+
...props.rows,
|
|
22
|
+
{
|
|
23
|
+
id: OTHER_ID,
|
|
24
|
+
label: props.otherLabel ?? 'Other',
|
|
25
|
+
description: props.otherDescription ?? 'Type a free-form answer',
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
}, [props.rows, props.allowOther, props.otherLabel, props.otherDescription]);
|
|
29
|
+
const [cursor, setCursor] = useState(() => Math.max(0, Math.min(props.initialCursor ?? 0, augmentedRows.length - 1)));
|
|
30
|
+
const [phase, setPhase] = useState(props.prefilledOther !== undefined ? 'other' : 'pick');
|
|
31
|
+
const [otherText, setOtherText] = useState(props.prefilledOther ?? '');
|
|
32
|
+
const [preview, setPreview] = useState(undefined);
|
|
33
|
+
// Picker is intentionally "exit-agnostic" — it just calls
|
|
34
|
+
// `props.onResolve(result)` and trusts the caller (runPicker or the
|
|
35
|
+
// chat overlay slot) to decide whether to unmount Ink. This matters
|
|
36
|
+
// because when Picker renders as an overlay INSIDE the chat Ink, an
|
|
37
|
+
// internal `useApp().exit()` would unmount the WHOLE chat instead of
|
|
38
|
+
// just hiding the picker. runPicker.tsx wraps Picker in a small
|
|
39
|
+
// `ExitWrapper` for the standalone mount that owns the exit.
|
|
40
|
+
const finish = (result) => {
|
|
41
|
+
props.onResolve(result);
|
|
42
|
+
};
|
|
43
|
+
// Recompute preview when cursor moves OR on first mount.
|
|
44
|
+
//
|
|
45
|
+
// CRITICAL: store onCursorChange in a ref instead of including it in
|
|
46
|
+
// the deps array. Callers pass an inline lambda; React would see a
|
|
47
|
+
// new function reference every render, fire this effect, which calls
|
|
48
|
+
// setPreview(), which re-renders, which makes a new lambda, which
|
|
49
|
+
// fires the effect again — infinite loop that swallows every
|
|
50
|
+
// keystroke. The latest-callback ref pattern is the canonical fix.
|
|
51
|
+
const onCursorChangeRef = useRef(props.onCursorChange);
|
|
52
|
+
useEffect(() => { onCursorChangeRef.current = props.onCursorChange; });
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (phase !== 'pick') {
|
|
55
|
+
setPreview(undefined);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const cb = onCursorChangeRef.current;
|
|
59
|
+
if (!cb) {
|
|
60
|
+
setPreview(undefined);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const row = augmentedRows[cursor];
|
|
64
|
+
if (!row || row.id === OTHER_ID) {
|
|
65
|
+
setPreview(undefined);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
setPreview(cb(row.id, cursor));
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
setPreview(undefined);
|
|
73
|
+
}
|
|
74
|
+
// Intentionally omit augmentedRows + onCursorChangeRef from deps:
|
|
75
|
+
// augmentedRows is derived from props and stable across renders
|
|
76
|
+
// for a given mount; the callback is read through the ref.
|
|
77
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
78
|
+
}, [cursor, phase]);
|
|
79
|
+
useInput((input, key) => {
|
|
80
|
+
if (key.ctrl && input === 'c') {
|
|
81
|
+
finish({ kind: 'cancelled' });
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (phase === 'other') {
|
|
85
|
+
// TextInput owns Enter/Backspace/character handling via onChange.
|
|
86
|
+
// We only handle Esc here to bail back to pick phase.
|
|
87
|
+
if (key.escape) {
|
|
88
|
+
setPhase('pick');
|
|
89
|
+
setOtherText('');
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (key.upArrow) {
|
|
94
|
+
setCursor((c) => (c - 1 + augmentedRows.length) % augmentedRows.length);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (key.downArrow) {
|
|
98
|
+
setCursor((c) => (c + 1) % augmentedRows.length);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (key.return) {
|
|
102
|
+
const row = augmentedRows[cursor];
|
|
103
|
+
if (row.id === OTHER_ID) {
|
|
104
|
+
setPhase('other');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
finish({ kind: 'pick', id: row.id });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (key.escape || input === 'q') {
|
|
111
|
+
finish({ kind: 'cancelled' });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
const footer = props.footer ?? (phase === 'other'
|
|
116
|
+
? '↵ accept · esc back · ⌫ erase'
|
|
117
|
+
: '↑/↓ navigate · ↵ confirm · esc / q cancel');
|
|
118
|
+
const accent = props.accentColor ?? themeToAccent(props.theme?.mode) ?? '#CC9166';
|
|
119
|
+
return (_jsxs(Frame, { title: props.title, subtitle: props.subtitle, badge: props.badge, footer: footer, accentColor: accent, children: [phase === 'pick' ? (_jsx(PickerRows, { rows: augmentedRows, cursor: cursor, accentColor: accent })) : (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, color: accent, children: "\u203A Type your answer" }), _jsx(Text, { color: "gray", dimColor: true, children: props.otherDescription ?? 'Press ENTER to accept' }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "cyan", children: "\u203A " }), _jsx(TextInput, { value: otherText, onChange: setOtherText, onSubmit: (value) => {
|
|
120
|
+
const trimmed = value.trim();
|
|
121
|
+
if (!trimmed)
|
|
122
|
+
return;
|
|
123
|
+
finish({ kind: 'other', text: trimmed });
|
|
124
|
+
} })] })] })), preview && preview.length > 0 ? (_jsx(Box, { flexDirection: "column", marginTop: 1, borderStyle: "single", borderColor: "gray", borderTop: true, borderLeft: false, borderRight: false, borderBottom: false, children: preview.map((line, i) => _jsx(Text, { children: line }, i)) })) : null] }));
|
|
125
|
+
}
|
|
126
|
+
function PickerRows({ rows, cursor, accentColor }) {
|
|
127
|
+
return (_jsx(Box, { flexDirection: "column", children: rows.map((row, i) => (_jsx(PickerRowView, { row: row, selected: i === cursor, accentColor: accentColor }, row.id))) }));
|
|
128
|
+
}
|
|
129
|
+
function PickerRowView({ row, selected, accentColor }) {
|
|
130
|
+
// Selected glyph + bold label + right-aligned value, lifted from
|
|
131
|
+
// openSrc/grok-cli/src/ui/components/SuggestionOverlay.tsx
|
|
132
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: accentColor, children: selected ? ' › ' : ' ' }), _jsx(Box, { flexGrow: 1, children: _jsx(Text, { bold: selected, color: selected ? accentColor : undefined, children: row.label }) }), row.value ? _jsx(Text, { color: "gray", children: row.value }) : null] }), row.description ? (_jsx(Box, { paddingLeft: 5, children: _jsx(Text, { color: "gray", dimColor: true, children: row.description }) })) : null] }));
|
|
133
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude-code-style slash command palette.
|
|
3
|
+
*
|
|
4
|
+
* Opens when the user types `/` at an empty prompt. Renders:
|
|
5
|
+
*
|
|
6
|
+
* ─────────────────────────────────────────────────────────
|
|
7
|
+
* ❯ /loop
|
|
8
|
+
* ─────────────────────────────────────────────────────────
|
|
9
|
+
* › /loop Run a prompt or slash command on a cadence
|
|
10
|
+
* /login In-REPL MCP profile editor
|
|
11
|
+
* /logout Clear API keys from the active profile
|
|
12
|
+
* ↑/↓ select · ↵ confirm · tab autocomplete · esc cancel
|
|
13
|
+
*
|
|
14
|
+
* Filter ranking (lifted from openSrc/grok-cli/src/ui/slash-menu.ts +
|
|
15
|
+
* openSrc/codex/codex-rs/tui/src/bottom_pane/command_popup.rs:143):
|
|
16
|
+
*
|
|
17
|
+
* 0 command body starts with query (best — same-prefix match)
|
|
18
|
+
* 1 command body contains query
|
|
19
|
+
* 2 description contains query
|
|
20
|
+
* 3 no match — filtered out
|
|
21
|
+
*
|
|
22
|
+
* Stable secondary sort by original index so commands with the same
|
|
23
|
+
* score render in their canonical order each keystroke. Max visible
|
|
24
|
+
* rows capped at 8 (claude-code CHANGELOG line 378 — popup should
|
|
25
|
+
* NOT scale with terminal height; it stays compact).
|
|
26
|
+
*/
|
|
27
|
+
export interface SlashCommandDef {
|
|
28
|
+
/** "/help", "/config", etc. — the literal token. */
|
|
29
|
+
cmd: string;
|
|
30
|
+
/** One-line description shown after the command. */
|
|
31
|
+
description: string;
|
|
32
|
+
}
|
|
33
|
+
export interface SlashPaletteProps {
|
|
34
|
+
/** Initial input buffer (typically just "/" — the keystroke that opened the palette). */
|
|
35
|
+
initialQuery: string;
|
|
36
|
+
/** All registered slash commands. */
|
|
37
|
+
commands: SlashCommandDef[];
|
|
38
|
+
/** Theme accent for highlights / borders. Defaults to brand orange. */
|
|
39
|
+
accentColor?: string;
|
|
40
|
+
/** Called when the user accepts a line (Enter) — text is the full submitted line. */
|
|
41
|
+
onResolve: (result: SlashPaletteResult) => void;
|
|
42
|
+
}
|
|
43
|
+
export type SlashPaletteResult = {
|
|
44
|
+
kind: 'submit';
|
|
45
|
+
text: string;
|
|
46
|
+
} | {
|
|
47
|
+
kind: 'cancelled';
|
|
48
|
+
};
|
|
49
|
+
export declare function scoreSlashCommand(cmd: SlashCommandDef, query: string): number;
|
|
50
|
+
export declare function filterCommands(commands: SlashCommandDef[], query: string): SlashCommandDef[];
|
|
51
|
+
export declare function SlashPalette({ initialQuery, commands, accentColor, onResolve }: SlashPaletteProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useMemo, useEffect, useRef } from 'react';
|
|
3
|
+
import { Box, Text, useApp, useInput } from 'ink';
|
|
4
|
+
import TextInput from 'ink-text-input';
|
|
5
|
+
import { useTerminalSize } from './useTerminalSize.js';
|
|
6
|
+
const MAX_VISIBLE = 8;
|
|
7
|
+
export function scoreSlashCommand(cmd, query) {
|
|
8
|
+
if (!query)
|
|
9
|
+
return 0;
|
|
10
|
+
const q = query.toLowerCase();
|
|
11
|
+
const body = cmd.cmd.slice(1).toLowerCase();
|
|
12
|
+
if (body.startsWith(q))
|
|
13
|
+
return 0;
|
|
14
|
+
if (body.includes(q))
|
|
15
|
+
return 1;
|
|
16
|
+
if (cmd.description.toLowerCase().includes(q))
|
|
17
|
+
return 2;
|
|
18
|
+
return 3;
|
|
19
|
+
}
|
|
20
|
+
export function filterCommands(commands, query) {
|
|
21
|
+
if (!query)
|
|
22
|
+
return commands.slice(0, MAX_VISIBLE);
|
|
23
|
+
const scored = commands
|
|
24
|
+
.map((c, i) => ({ c, i, s: scoreSlashCommand(c, query) }))
|
|
25
|
+
.filter((x) => x.s < 3);
|
|
26
|
+
scored.sort((a, b) => (a.s - b.s) || (a.i - b.i));
|
|
27
|
+
return scored.slice(0, MAX_VISIBLE).map((x) => x.c);
|
|
28
|
+
}
|
|
29
|
+
export function SlashPalette({ initialQuery, commands, accentColor = '#CC9166', onResolve }) {
|
|
30
|
+
const [value, setValue] = useState(initialQuery);
|
|
31
|
+
const [cursor, setCursor] = useState(0);
|
|
32
|
+
const { exit } = useApp();
|
|
33
|
+
// Compute the query portion (everything after the leading `/`, up to
|
|
34
|
+
// the first space — so `/spawn researcher` filters by `spawn`).
|
|
35
|
+
const query = useMemo(() => {
|
|
36
|
+
if (!value.startsWith('/'))
|
|
37
|
+
return '';
|
|
38
|
+
const tail = value.slice(1);
|
|
39
|
+
const space = tail.indexOf(' ');
|
|
40
|
+
return space < 0 ? tail : tail.slice(0, space);
|
|
41
|
+
}, [value]);
|
|
42
|
+
const matches = useMemo(() => filterCommands(commands, query), [commands, query]);
|
|
43
|
+
// Clamp cursor when matches shrink.
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (cursor >= matches.length)
|
|
46
|
+
setCursor(Math.max(0, matches.length - 1));
|
|
47
|
+
}, [matches.length, cursor]);
|
|
48
|
+
const onResolveRef = useRef(onResolve);
|
|
49
|
+
useEffect(() => { onResolveRef.current = onResolve; });
|
|
50
|
+
useInput((input, key) => {
|
|
51
|
+
if (key.ctrl && input === 'c') {
|
|
52
|
+
onResolveRef.current({ kind: 'cancelled' });
|
|
53
|
+
exit();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (key.escape) {
|
|
57
|
+
onResolveRef.current({ kind: 'cancelled' });
|
|
58
|
+
exit();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (key.upArrow) {
|
|
62
|
+
if (matches.length > 0) {
|
|
63
|
+
setCursor((c) => (c - 1 + matches.length) % matches.length);
|
|
64
|
+
}
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (key.downArrow) {
|
|
68
|
+
if (matches.length > 0) {
|
|
69
|
+
setCursor((c) => (c + 1) % matches.length);
|
|
70
|
+
}
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (key.tab) {
|
|
74
|
+
// Tab autocompletes to the currently-highlighted command + space.
|
|
75
|
+
// User can continue typing args, then hit Enter to submit.
|
|
76
|
+
if (matches.length > 0) {
|
|
77
|
+
const picked = matches[cursor] ?? matches[0];
|
|
78
|
+
setValue(picked.cmd + ' ');
|
|
79
|
+
setCursor(0);
|
|
80
|
+
}
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
// When user presses Enter — TextInput's onSubmit fires. Resolve with
|
|
85
|
+
// the highlighted match IF the buffer is JUST `/<query>` (no args
|
|
86
|
+
// typed yet); otherwise submit the buffer as-is so /spawn role prompt
|
|
87
|
+
// works without forcing the user to tab-complete first.
|
|
88
|
+
const onSubmit = (text) => {
|
|
89
|
+
const trimmed = text.trim();
|
|
90
|
+
if (!trimmed) {
|
|
91
|
+
onResolveRef.current({ kind: 'cancelled' });
|
|
92
|
+
exit();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
// If the user typed JUST the slash + query (no args yet), AND the
|
|
96
|
+
// highlighted match is different from what they typed, expand to
|
|
97
|
+
// the match. Otherwise submit verbatim.
|
|
98
|
+
const tail = trimmed.slice(1);
|
|
99
|
+
const hasSpace = tail.includes(' ');
|
|
100
|
+
if (!hasSpace && matches.length > 0) {
|
|
101
|
+
const picked = matches[cursor] ?? matches[0];
|
|
102
|
+
if (picked.cmd !== trimmed) {
|
|
103
|
+
onResolveRef.current({ kind: 'submit', text: picked.cmd });
|
|
104
|
+
exit();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
onResolveRef.current({ kind: 'submit', text: trimmed });
|
|
109
|
+
exit();
|
|
110
|
+
};
|
|
111
|
+
// If the user types more than just `/`, AND it no longer starts with
|
|
112
|
+
// `/` (e.g. backspaced past the slash), cancel the palette so the
|
|
113
|
+
// user returns to the normal readline prompt.
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
if (value.length > 0 && !value.startsWith('/')) {
|
|
116
|
+
onResolveRef.current({ kind: 'cancelled' });
|
|
117
|
+
exit();
|
|
118
|
+
}
|
|
119
|
+
}, [value, exit]);
|
|
120
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Divider, { color: accentColor }), _jsxs(Box, { paddingX: 1, children: [_jsx(Text, { color: accentColor, children: "\u276F " }), _jsx(TextInput, { value: value, onChange: setValue, onSubmit: onSubmit })] }), _jsx(Divider, { color: accentColor }), matches.length === 0 ? (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: "(no matching commands)" }) })) : (_jsx(_Fragment, { children: matches.map((cmd, i) => (_jsx(SlashRow, { cmd: cmd, selected: i === cursor, accentColor: accentColor }, cmd.cmd))) })), _jsx(Box, { paddingX: 1, marginTop: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: "\u2191/\u2193 select \u00B7 \u21B5 confirm \u00B7 tab autocomplete \u00B7 esc cancel" }) })] }));
|
|
121
|
+
}
|
|
122
|
+
function SlashRow({ cmd, selected, accentColor }) {
|
|
123
|
+
// Width hint: pad the cmd column so descriptions align across rows.
|
|
124
|
+
// The widest known slash command is around 28 chars (e.g. `/implement-plan`
|
|
125
|
+
// is 15; longest help cmd lines are longer); 24 is a sensible target.
|
|
126
|
+
const cmdCol = 26;
|
|
127
|
+
const padded = cmd.cmd.length >= cmdCol ? cmd.cmd : cmd.cmd + ' '.repeat(cmdCol - cmd.cmd.length);
|
|
128
|
+
return (_jsxs(Box, { paddingX: 1, children: [_jsx(Text, { color: accentColor, children: selected ? '› ' : ' ' }), _jsx(Text, { bold: selected, color: selected ? accentColor : undefined, children: padded }), _jsx(Text, { color: "gray", children: cmd.description })] }));
|
|
129
|
+
}
|
|
130
|
+
function Divider({ color }) {
|
|
131
|
+
// Full-width horizontal rule — matches claude-code's chrome. Uses
|
|
132
|
+
// useTerminalSize so the divider auto-reflows on terminal resize
|
|
133
|
+
// instead of being frozen at its initial width.
|
|
134
|
+
const { columns } = useTerminalSize();
|
|
135
|
+
return (_jsx(Box, { children: _jsx(Text, { color: color, dimColor: true, children: '─'.repeat(Math.max(20, columns - 1)) }) }));
|
|
136
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framed free-text input. Used by the wizard's API-key step, the
|
|
3
|
+
* remote-URL prompt, and the /config / /login sub-prompts.
|
|
4
|
+
*
|
|
5
|
+
* The `validate` callback runs on submit. Return undefined to accept;
|
|
6
|
+
* return a string to render it as an inline error and keep the field
|
|
7
|
+
* open for the user to fix.
|
|
8
|
+
*/
|
|
9
|
+
export interface TextFieldProps {
|
|
10
|
+
title: string;
|
|
11
|
+
subtitle?: string;
|
|
12
|
+
badge?: string;
|
|
13
|
+
prefilled?: string;
|
|
14
|
+
placeholder?: string;
|
|
15
|
+
mask?: boolean;
|
|
16
|
+
validate?: (value: string) => string | undefined;
|
|
17
|
+
accentColor?: string;
|
|
18
|
+
/** Back-compat: pulls accentColor from theme.mode if accentColor not set. */
|
|
19
|
+
theme?: {
|
|
20
|
+
mode: string;
|
|
21
|
+
};
|
|
22
|
+
/** Ignored — kept for back-compat with old picker shape. */
|
|
23
|
+
eraseOnClose?: boolean;
|
|
24
|
+
/** Ignored — kept for back-compat. */
|
|
25
|
+
footer?: string;
|
|
26
|
+
onResolve: (result: TextFieldResult) => void;
|
|
27
|
+
}
|
|
28
|
+
export type TextFieldResult = {
|
|
29
|
+
kind: 'accept';
|
|
30
|
+
text: string;
|
|
31
|
+
} | {
|
|
32
|
+
kind: 'cancelled';
|
|
33
|
+
};
|
|
34
|
+
export declare function TextField(props: TextFieldProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
import TextInput from 'ink-text-input';
|
|
5
|
+
import { Frame } from './Frame.js';
|
|
6
|
+
function themeToAccent(mode) {
|
|
7
|
+
if (mode === 'light')
|
|
8
|
+
return '#A24E1F';
|
|
9
|
+
if (mode === 'mono')
|
|
10
|
+
return 'white';
|
|
11
|
+
if (mode === 'dark')
|
|
12
|
+
return '#CC9166';
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
export function TextField(props) {
|
|
16
|
+
const [value, setValue] = useState(props.prefilled ?? '');
|
|
17
|
+
const [error, setError] = useState(undefined);
|
|
18
|
+
// Exit-agnostic: just resolves. The caller (runTextField for the
|
|
19
|
+
// standalone mount, or the chat overlay slot when invoked from inside
|
|
20
|
+
// the Ink chat REPL) decides what to do. See Picker.tsx comment for
|
|
21
|
+
// why a built-in `useApp().exit()` here would break the overlay path.
|
|
22
|
+
const finish = (result) => {
|
|
23
|
+
props.onResolve(result);
|
|
24
|
+
};
|
|
25
|
+
useInput((input, key) => {
|
|
26
|
+
if (key.escape || (key.ctrl && input === 'c')) {
|
|
27
|
+
finish({ kind: 'cancelled' });
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
const onSubmit = (next) => {
|
|
31
|
+
if (props.validate) {
|
|
32
|
+
const verdict = props.validate(next);
|
|
33
|
+
if (verdict !== undefined) {
|
|
34
|
+
setError(verdict);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
finish({ kind: 'accept', text: next });
|
|
39
|
+
};
|
|
40
|
+
const onChange = (next) => {
|
|
41
|
+
setValue(next);
|
|
42
|
+
if (error)
|
|
43
|
+
setError(undefined);
|
|
44
|
+
};
|
|
45
|
+
const accent = props.accentColor ?? themeToAccent(props.theme?.mode) ?? '#CC9166';
|
|
46
|
+
return (_jsxs(Frame, { title: props.title, subtitle: props.subtitle, badge: props.badge, footer: props.footer ?? '↵ accept · esc cancel · ⌫ erase', accentColor: accent, children: [_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: "\u203A " }), _jsx(TextInput, { value: value, onChange: onChange, onSubmit: onSubmit, placeholder: props.placeholder, mask: props.mask ? '·' : undefined })] }), error ? (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "red", children: ["\u2717 ", error] }) })) : null] }));
|
|
47
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type WizardState } from '../wizard/types.js';
|
|
2
|
+
export interface WizardAppProps {
|
|
3
|
+
workspaceRoot: string;
|
|
4
|
+
/** Fires once the wizard reaches a terminal state. */
|
|
5
|
+
onFinish: (state: WizardState) => void;
|
|
6
|
+
}
|
|
7
|
+
export declare function WizardApp({ workspaceRoot, onFinish }: WizardAppProps): import("react/jsx-runtime").JSX.Element;
|