gaunt-sloth 2.0.0-alpha.2 → 2.0.0-alpha.21
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 +55 -5
- package/cli.js +43 -10
- package/dist/cli.js +33 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/adkEvalRunner.d.ts +66 -0
- package/dist/commands/adkEvalRunner.js +69 -0
- package/dist/commands/adkEvalRunner.js.map +1 -0
- package/dist/commands/agUiEvalRunner.d.ts +73 -0
- package/dist/commands/agUiEvalRunner.js +216 -0
- package/dist/commands/agUiEvalRunner.js.map +1 -0
- package/dist/commands/askCommand.d.ts +3 -2
- package/dist/commands/askCommand.js +26 -7
- package/dist/commands/askCommand.js.map +1 -1
- package/dist/commands/batchCommand.d.ts +78 -0
- package/dist/commands/batchCommand.js +245 -0
- package/dist/commands/batchCommand.js.map +1 -0
- package/dist/commands/cliOptionParsers.d.ts +12 -0
- package/dist/commands/cliOptionParsers.js +18 -0
- package/dist/commands/cliOptionParsers.js.map +1 -0
- package/dist/commands/commandIntrospection.d.ts +6 -6
- package/dist/commands/commandIntrospection.js +12 -12
- package/dist/commands/commandIntrospection.js.map +1 -1
- package/dist/commands/commandUtils.d.ts +16 -16
- package/dist/commands/commandUtils.js +20 -20
- package/dist/commands/commandUtils.js.map +1 -1
- package/dist/commands/configCommand.d.ts +24 -0
- package/dist/commands/configCommand.js +117 -0
- package/dist/commands/configCommand.js.map +1 -0
- package/dist/commands/configSetup.d.ts +1 -1
- package/dist/commands/configSetup.js +17 -2
- package/dist/commands/configSetup.js.map +1 -1
- package/dist/commands/evalCommand.d.ts +41 -0
- package/dist/commands/evalCommand.js +483 -0
- package/dist/commands/evalCommand.js.map +1 -0
- package/dist/commands/execCommand.js +4 -1
- package/dist/commands/execCommand.js.map +1 -1
- package/dist/commands/firstRunDialog.d.ts +39 -5
- package/dist/commands/firstRunDialog.js +72 -10
- package/dist/commands/firstRunDialog.js.map +1 -1
- package/dist/commands/getCommand.js +7 -7
- package/dist/commands/getCommand.js.map +1 -1
- package/dist/commands/historyCommand.d.ts +17 -0
- package/dist/commands/historyCommand.js +102 -0
- package/dist/commands/historyCommand.js.map +1 -0
- package/dist/commands/initCommand.d.ts +3 -0
- package/dist/commands/initCommand.js +9 -4
- package/dist/commands/initCommand.js.map +1 -1
- package/dist/commands/insightsCommand.d.ts +8 -0
- package/dist/commands/insightsCommand.js +32 -0
- package/dist/commands/insightsCommand.js.map +1 -0
- package/dist/commands/modelsCommand.d.ts +25 -0
- package/dist/commands/modelsCommand.js +96 -0
- package/dist/commands/modelsCommand.js.map +1 -0
- package/dist/commands/prCommand.js +14 -15
- package/dist/commands/prCommand.js.map +1 -1
- package/dist/commands/prDiscovery.d.ts +2 -2
- package/dist/commands/prDiscovery.js +27 -29
- package/dist/commands/prDiscovery.js.map +1 -1
- package/dist/commands/reviewCommand.js +15 -10
- package/dist/commands/reviewCommand.js.map +1 -1
- package/dist/commands/workflowCommand.d.ts +19 -0
- package/dist/commands/workflowCommand.js +77 -0
- package/dist/commands/workflowCommand.js.map +1 -0
- package/dist/helpers/jira/jiraLogWork.js.map +1 -1
- package/dist/modules/startSession.js +9 -7
- package/dist/modules/startSession.js.map +1 -1
- package/dist/tools/gthJiraLogWorkTool.js +3 -3
- package/dist/tools/gthJiraLogWorkTool.js.map +1 -1
- package/dist/tui/components/App.js +321 -21
- package/dist/tui/components/App.js.map +1 -1
- package/dist/tui/components/ApprovalPrompt.d.ts +18 -0
- package/dist/tui/components/ApprovalPrompt.js +26 -0
- package/dist/tui/components/ApprovalPrompt.js.map +1 -0
- package/dist/tui/components/DebugPanel.d.ts +29 -9
- package/dist/tui/components/DebugPanel.js +46 -23
- package/dist/tui/components/DebugPanel.js.map +1 -1
- package/dist/tui/components/FetchProgress.d.ts +36 -0
- package/dist/tui/components/FetchProgress.js +39 -0
- package/dist/tui/components/FetchProgress.js.map +1 -0
- package/dist/tui/components/LiveTurn.d.ts +26 -4
- package/dist/tui/components/LiveTurn.js +104 -18
- package/dist/tui/components/LiveTurn.js.map +1 -1
- package/dist/tui/components/NoticeBar.d.ts +32 -0
- package/dist/tui/components/NoticeBar.js +39 -0
- package/dist/tui/components/NoticeBar.js.map +1 -0
- package/dist/tui/components/PromptInput.d.ts +26 -1
- package/dist/tui/components/PromptInput.js +93 -7
- package/dist/tui/components/PromptInput.js.map +1 -1
- package/dist/tui/components/Rule.js.map +1 -1
- package/dist/tui/components/SelectList.d.ts +98 -9
- package/dist/tui/components/SelectList.js +209 -26
- package/dist/tui/components/SelectList.js.map +1 -1
- package/dist/tui/components/SlashCommandMenu.d.ts +20 -0
- package/dist/tui/components/SlashCommandMenu.js +28 -0
- package/dist/tui/components/SlashCommandMenu.js.map +1 -0
- package/dist/tui/components/StatusBar.d.ts +7 -1
- package/dist/tui/components/StatusBar.js +7 -3
- package/dist/tui/components/StatusBar.js.map +1 -1
- package/dist/tui/components/Transcript.js +6 -1
- package/dist/tui/components/Transcript.js.map +1 -1
- package/dist/tui/debugRender.d.ts +41 -6
- package/dist/tui/debugRender.js +126 -12
- package/dist/tui/debugRender.js.map +1 -1
- package/dist/tui/debugSearch.d.ts +40 -0
- package/dist/tui/debugSearch.js +73 -0
- package/dist/tui/debugSearch.js.map +1 -0
- package/dist/tui/markdown.js +0 -0
- package/dist/tui/markdown.js.map +1 -1
- package/dist/tui/pasteParser.d.ts +21 -0
- package/dist/tui/pasteParser.js +24 -0
- package/dist/tui/pasteParser.js.map +1 -0
- package/dist/tui/selectCancelled.d.ts +13 -0
- package/dist/tui/selectCancelled.js +17 -0
- package/dist/tui/selectCancelled.js.map +1 -0
- package/dist/tui/slashCommands.d.ts +220 -1
- package/dist/tui/slashCommands.js +408 -1
- package/dist/tui/slashCommands.js.map +1 -1
- package/dist/tui/tuiSessionModule.js +249 -24
- package/dist/tui/tuiSessionModule.js.map +1 -1
- package/dist/tui/types.d.ts +96 -7
- package/dist/tui/viewModel.d.ts +34 -0
- package/dist/tui/viewModel.js +67 -0
- package/dist/tui/viewModel.js.map +1 -1
- package/dist/utils/stdinPolicy.d.ts +31 -0
- package/dist/utils/stdinPolicy.js +37 -0
- package/dist/utils/stdinPolicy.js.map +1 -0
- package/package.json +21 -17
|
@@ -1,16 +1,102 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState } from 'react';
|
|
3
|
-
import { Box, Text } from 'ink';
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { Box, Text, useInput, usePaste } from 'ink';
|
|
4
4
|
import TextInput from 'ink-text-input';
|
|
5
|
+
import { SlashCommandMenu } from '#src/tui/components/SlashCommandMenu.js';
|
|
6
|
+
import { filterSlashCommands, slashMenuQuery } from '#src/tui/slashCommands.js';
|
|
7
|
+
import { normalizePastedText } from '#src/tui/pasteParser.js';
|
|
5
8
|
/**
|
|
6
9
|
* The user prompt line. Mirrors the readline ` > ` prompt. Clears on submit; the parent
|
|
7
10
|
* hides it while a turn is running so Ink owns stdin uncontended during streaming.
|
|
11
|
+
*
|
|
12
|
+
* TUI-C10 — while the user is typing a bare slash command (input starts with `/`, no space yet),
|
|
13
|
+
* a discovery menu of the matching registered commands appears just above the prompt. The registry
|
|
14
|
+
* (`commands`) is the single source, so extension-registered commands show automatically. Arrow
|
|
15
|
+
* keys move the highlight, Tab completes the highlighted name, Enter dispatches it, Esc dismisses
|
|
16
|
+
* the menu. The keyboard split is clean: `ink-text-input` ignores Up/Down/Tab and only fires
|
|
17
|
+
* `onSubmit` on Enter, so this component's `useInput` owns the arrows/Tab/Esc while `onSubmit` owns
|
|
18
|
+
* Enter — no double-handling. A fully-typed command still dispatches exactly as before (Enter with
|
|
19
|
+
* the menu open just submits the highlighted command, which equals what was typed).
|
|
20
|
+
*
|
|
21
|
+
* TUI-C24 — multiline paste. `ink-text-input` is single-line: an embedded newline in a pasted
|
|
22
|
+
* burst would otherwise be read as Enter and submit the turn mid-paste (or jam the lines together).
|
|
23
|
+
* `usePaste` puts the terminal into bracketed-paste mode (`\x1b[?2004h`) while the prompt is mounted
|
|
24
|
+
* and disables it on unmount, and Ink delivers the pasted text on a channel separate from
|
|
25
|
+
* `useInput` — so the payload never reaches `TextInput`'s Enter handling. We append the normalized
|
|
26
|
+
* paste (CRLF/CR → `\n`) to the buffer without submitting; a subsequent explicit Enter submits the
|
|
27
|
+
* whole multi-line value through the existing `onSubmit` path. Rich multi-line cursor editing /
|
|
28
|
+
* continuation-line rendering is intentionally NOT handled here — that is node TUI-C25; a buffered
|
|
29
|
+
* multi-line value may render imperfectly, but the submitted value is correct and intact.
|
|
8
30
|
*/
|
|
9
|
-
export function PromptInput({ onSubmit, }) {
|
|
31
|
+
export function PromptInput({ onSubmit, commands = [], onMenuStateChange, }) {
|
|
10
32
|
const [value, setValue] = useState('');
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
33
|
+
// Highlighted row in the menu. Reset to the top whenever the query changes (the filtered list
|
|
34
|
+
// changes under it), so navigation always starts from the most-relevant match.
|
|
35
|
+
const [cursor, setCursor] = useState(0);
|
|
36
|
+
// Esc dismisses the menu without clearing the input; typing anything re-opens it (onChange
|
|
37
|
+
// clears this flag), matching how palette menus behave elsewhere.
|
|
38
|
+
const [dismissed, setDismissed] = useState(false);
|
|
39
|
+
const query = slashMenuQuery(value);
|
|
40
|
+
const matches = query !== null && !dismissed ? filterSlashCommands(commands, query) : [];
|
|
41
|
+
const menuActive = matches.length > 0;
|
|
42
|
+
// Clamp defensively in case the filtered list shrank below the cursor between renders.
|
|
43
|
+
const selectedIndex = menuActive ? Math.min(cursor, matches.length - 1) : 0;
|
|
44
|
+
// Let the parent suppress its competing Tab handler (debug-panel focus) while the menu is open.
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
onMenuStateChange?.(menuActive);
|
|
47
|
+
}, [menuActive, onMenuStateChange]);
|
|
48
|
+
// TUI-C24 — capture a bracketed paste as buffered text instead of keystrokes. Ink enables
|
|
49
|
+
// bracketed-paste mode while this hook is mounted and routes the pasted payload here (off the
|
|
50
|
+
// `useInput` channel), so its embedded newlines never reach TextInput's Enter handling. We append
|
|
51
|
+
// the normalized paste to the buffer and do NOT submit — a later explicit Enter submits it all.
|
|
52
|
+
// (Appending, rather than inserting at TextInput's internal cursor, is deliberate: multi-line
|
|
53
|
+
// cursor editing is TUI-C25's scope; the submitted value stays correct and intact.)
|
|
54
|
+
usePaste((text) => {
|
|
55
|
+
const pasted = normalizePastedText(text);
|
|
56
|
+
if (!pasted)
|
|
57
|
+
return;
|
|
58
|
+
setValue((current) => current + pasted);
|
|
59
|
+
// Mirror onChange's menu bookkeeping so a paste can't leave a stale/incorrectly-open menu.
|
|
60
|
+
setDismissed(false);
|
|
61
|
+
setCursor(0);
|
|
62
|
+
});
|
|
63
|
+
useInput((_input, key) => {
|
|
64
|
+
// Only claim keys while the menu is visible; otherwise TextInput handles everything.
|
|
65
|
+
if (!menuActive)
|
|
66
|
+
return;
|
|
67
|
+
if (key.upArrow) {
|
|
68
|
+
setCursor((c) => (Math.min(c, matches.length - 1) - 1 + matches.length) % matches.length);
|
|
69
|
+
}
|
|
70
|
+
else if (key.downArrow) {
|
|
71
|
+
setCursor((c) => (Math.min(c, matches.length - 1) + 1) % matches.length);
|
|
72
|
+
}
|
|
73
|
+
else if (key.tab) {
|
|
74
|
+
// Complete to the highlighted name plus a trailing space: the space closes the menu (the
|
|
75
|
+
// input now has whitespace) and leaves the cursor ready for args, without dispatching.
|
|
76
|
+
setValue(`/${matches[selectedIndex].name} `);
|
|
77
|
+
setDismissed(false);
|
|
78
|
+
setCursor(0);
|
|
79
|
+
}
|
|
80
|
+
else if (key.escape) {
|
|
81
|
+
setDismissed(true);
|
|
82
|
+
}
|
|
83
|
+
// Enter is intentionally NOT handled here — TextInput's onSubmit owns it (see onSubmit).
|
|
84
|
+
},
|
|
85
|
+
// Active only while the prompt is mounted; harmless when the menu is closed (early return).
|
|
86
|
+
{ isActive: true });
|
|
87
|
+
return (_jsxs(Box, { flexDirection: "column", children: [menuActive ? _jsx(SlashCommandMenu, { commands: matches, selectedIndex: selectedIndex }) : null, _jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: ' > ' }), _jsx(TextInput, { value: value, onChange: (next) => {
|
|
88
|
+
setValue(next);
|
|
89
|
+
setDismissed(false);
|
|
90
|
+
setCursor(0);
|
|
91
|
+
}, onSubmit: (submitted) => {
|
|
92
|
+
// With the menu open, Enter dispatches the HIGHLIGHTED command (so "/mo"+Enter runs
|
|
93
|
+
// "/mode"); otherwise submit exactly what was typed. Either way the parent's handler
|
|
94
|
+
// parses + dispatches through the registry, so fully-typed dispatch is unchanged.
|
|
95
|
+
const toSubmit = menuActive ? `/${matches[selectedIndex].name}` : submitted;
|
|
96
|
+
setValue('');
|
|
97
|
+
setDismissed(false);
|
|
98
|
+
setCursor(0);
|
|
99
|
+
onSubmit(toSubmit);
|
|
100
|
+
} })] })] }));
|
|
15
101
|
}
|
|
16
102
|
//# sourceMappingURL=PromptInput.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PromptInput.js","sourceRoot":"","sources":["../../../src/tui/components/PromptInput.tsx"],"names":[],"mappings":";AAAA,OAAc,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"PromptInput.js","sourceRoot":"","sources":["../../../src/tui/components/PromptInput.tsx"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Rule.js","sourceRoot":"","sources":["../../../src/tui/components/Rule.tsx"],"names":[],"mappings":";AAAA,OAAc,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,KAAK,CAAC;AAEtC,uEAAuE;AACvE,MAAM,eAAe,GAAG,EAAE,CAAC;AAC3B,oFAAoF;AACpF,MAAM,SAAS,GAAG,CAAC,CAAC;AAEpB;;;;;;GAMG;AACH,MAAM,UAAU,SAAS,CAAC,OAA2B;IACnD,MAAM,IAAI,GAAG,OAAO,OAAO,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC;IACjG,OAAO,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;AAC/C,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,IAAI,CAAC,EAAE,KAAK,
|
|
1
|
+
{"version":3,"file":"Rule.js","sourceRoot":"","sources":["../../../src/tui/components/Rule.tsx"],"names":[],"mappings":";AAAA,OAAc,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,KAAK,CAAC;AAEtC,uEAAuE;AACvE,MAAM,eAAe,GAAG,EAAE,CAAC;AAC3B,oFAAoF;AACpF,MAAM,SAAS,GAAG,CAAC,CAAC;AAEpB;;;;;;GAMG;AACH,MAAM,UAAU,SAAS,CAAC,OAA2B;IACnD,MAAM,IAAI,GAAG,OAAO,OAAO,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC;IACjG,OAAO,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;AAC/C,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,IAAI,CAAC,EAAE,KAAK,EAAE,GAAuB,EAAE;IACrD,MAAM,EAAE,MAAM,EAAE,GAAG,SAAS,EAAE,CAAC;IAC/B,mFAAmF;IACnF,oFAAoF;IACpF,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAqB,MAAM,EAAE,OAAO,CAAC,CAAC;IAE5E,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,MAAM;YAAE,OAAO;QACpB,MAAM,QAAQ,GAAG,GAAS,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QACxD,mFAAmF;QACnF,QAAQ,EAAE,CAAC;QACX,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;QAC9B,OAAO,GAAG,EAAE;YACV,MAAM,CAAC,GAAG,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;QACjC,CAAC,CAAC;IACJ,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;IAEb,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,IAAI,OAAO,CAAC,CAAC;IAC1C,OAAO,KAAC,IAAI,IAAC,QAAQ,kBAAE,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,GAAQ,CAAC;AACnD,CAAC"}
|
|
@@ -3,23 +3,112 @@ export interface SelectItem {
|
|
|
3
3
|
/** The text shown for this row. */
|
|
4
4
|
label: string;
|
|
5
5
|
}
|
|
6
|
+
/**
|
|
7
|
+
* CFG-20 — the pure "does this row match the typed filter?" predicate: a case-insensitive
|
|
8
|
+
* substring test on the row label (the model id, with its ⭐/padding prefix — a substring
|
|
9
|
+
* match on the id still hits). The raw substring test; query trimming is done once by the
|
|
10
|
+
* callers ({@link filteredItemIndices} / {@link filterSelectItems}), not here.
|
|
11
|
+
*/
|
|
12
|
+
export declare function matchesFilter(label: string, query: string): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* CFG-20 — **the single production filter path**: the absolute indices (into `items`) of the rows
|
|
15
|
+
* whose label matches `query`, in source order. The query is trimmed, so a leading/trailing space
|
|
16
|
+
* filters like the trimmed text and an all-whitespace query is treated as "no filter" (all rows).
|
|
17
|
+
* The widget's live filter calls exactly this (see the `filteredIndices` memo below), and
|
|
18
|
+
* {@link filterSelectItems} is a thin view over it — so a unit test can never validate a matcher
|
|
19
|
+
* the widget doesn't actually run. Indices (not items) so the widget can map a filtered-view
|
|
20
|
+
* selection back to the original absolute index even if two labels were to coincide.
|
|
21
|
+
*/
|
|
22
|
+
export declare function filteredItemIndices(items: SelectItem[], query: string): number[];
|
|
23
|
+
/**
|
|
24
|
+
* CFG-20 — the items (not indices) surviving the filter, in source order: a thin view over the
|
|
25
|
+
* production {@link filteredItemIndices}, so it can never diverge from the widget's live filter.
|
|
26
|
+
* Mirrors the shape of TUI-C10's `filterSlashCommands` (pure, case-insensitive, empty/whitespace
|
|
27
|
+
* query returns the full list) but deliberately does NOT re-rank prefix-before-substring: the
|
|
28
|
+
* model list arrives already ordered ⭐ preferred-first then alphabetical (CFG-14), and that order
|
|
29
|
+
* must be preserved within the filtered set, so the source order is kept rather than bucketed.
|
|
30
|
+
*/
|
|
31
|
+
export declare function filterSelectItems(items: SelectItem[], query: string): SelectItem[];
|
|
32
|
+
/**
|
|
33
|
+
* Derive how many option rows the scrolling window may show at once from the live terminal
|
|
34
|
+
* height. Ink/Node leaves `rows` `undefined` when not attached to a TTY (and in tests), in
|
|
35
|
+
* which case we fall back to {@link DEFAULT_WINDOW}. Always at least 1 so a tiny / mis-reported
|
|
36
|
+
* terminal can never collapse the window to zero.
|
|
37
|
+
*/
|
|
38
|
+
export declare function windowSize(rows: number | undefined): number;
|
|
39
|
+
/**
|
|
40
|
+
* Standard "keep the cursor in view" windowing. Given the previous window start, the current
|
|
41
|
+
* cursor index, the window `size` and total item `count`, return the window start that keeps
|
|
42
|
+
* `cursor` visible while moving as little as possible (sticky: the window only shifts when the
|
|
43
|
+
* cursor crosses an edge). Clamped to `[0, count - size]` so it never scrolls past either end,
|
|
44
|
+
* which also makes wraparound (cursor jumping to the far end) resolve to that end cleanly.
|
|
45
|
+
*/
|
|
46
|
+
export declare function clampWindowStart(prevStart: number, cursor: number, size: number, count: number): number;
|
|
6
47
|
/**
|
|
7
48
|
* CFG-11 — a minimal arrow-key + Enter selectable list for the first-run dialog. Up/Down
|
|
8
|
-
*
|
|
9
|
-
*
|
|
49
|
+
* move the highlight, Enter confirms.
|
|
50
|
+
*
|
|
51
|
+
* CFG-15 — the list renders within a bounded, scrolling viewport so a long option list (e.g.
|
|
52
|
+
* openrouter's discovered model list) never overflows the terminal. At most a window's worth of
|
|
53
|
+
* rows is shown; the window scrolls to keep the highlighted row in view, and dim `↑ N more` /
|
|
54
|
+
* `↓ N more` affordances signal hidden items above / below.
|
|
55
|
+
*
|
|
56
|
+
* CFG-20 — **type-to-filter**: typing letters incrementally filters the list (case-insensitive
|
|
57
|
+
* substring on the label — see {@link matchesFilter}), so a 200+-model catalog is reachable by
|
|
58
|
+
* typing a few chars instead of scrolling. Backspace edits the filter; Esc clears it (a second
|
|
59
|
+
* Esc, with the filter already empty, aborts — see `onCancel`). The filtered set keeps the source
|
|
60
|
+
* order (⭐ preferred-first then alphabetical). Because letters now feed the filter, the old j/k
|
|
61
|
+
* vim-nav shortcuts are gone — the arrow keys remain the movement keys.
|
|
62
|
+
*
|
|
63
|
+
* CFG-20 — **abort**: Ctrl+C (always) and Esc-on-empty-filter invoke `onCancel`, which the
|
|
64
|
+
* `runInkSelect` host turns into a {@link SelectCancelledError} so the dialog can abort without
|
|
65
|
+
* writing a config. Enter, Ctrl+C and Esc are handled as three DISTINCT keys (the previous
|
|
66
|
+
* default-`exitOnCtrlC` behaviour silently collapsed Ctrl+C into "selected the default").
|
|
10
67
|
*
|
|
11
|
-
* `onSelect` is called once with the chosen index
|
|
12
|
-
*
|
|
68
|
+
* `onSelect` is called once with the chosen **absolute** index into `items` (never the
|
|
69
|
+
* filtered-view position); the caller is responsible for unmounting (the default `runInkSelect`
|
|
70
|
+
* host calls `useApp().exit()` via `onSelect`/`onCancel`).
|
|
13
71
|
*/
|
|
14
|
-
export declare function SelectList({ title, items, initialIndex, onSelect, }: {
|
|
72
|
+
export declare function SelectList({ title, items, initialIndex, onSelect, onCancel, viewportRows, }: {
|
|
15
73
|
title: string;
|
|
16
74
|
items: SelectItem[];
|
|
17
75
|
initialIndex?: number;
|
|
18
76
|
onSelect: (index: number) => void;
|
|
77
|
+
/**
|
|
78
|
+
* CFG-20 — called when the user aborts the selection (Ctrl+C, or Esc with an empty filter).
|
|
79
|
+
* Optional so a bare `<SelectList>` (e.g. in a render test) can omit it; `runInkSelect` always
|
|
80
|
+
* supplies it.
|
|
81
|
+
*/
|
|
82
|
+
onCancel?: () => void;
|
|
83
|
+
/**
|
|
84
|
+
* Explicit window height override, mainly for tests. When omitted the window is derived from
|
|
85
|
+
* the live terminal height via {@link windowSize}.
|
|
86
|
+
*/
|
|
87
|
+
viewportRows?: number;
|
|
19
88
|
}): React.ReactElement;
|
|
20
89
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
90
|
+
* The subset of Ink's `render` that {@link runInkSelect} relies on: mount a node and expose a
|
|
91
|
+
* promise that settles when the app exits. Declared as a seam so a test can inject a fake-stream
|
|
92
|
+
* renderer and drive the real Ctrl+C→reject / Enter→resolve wire without a live TTY (the default
|
|
93
|
+
* dynamic-`import('ink')` path is otherwise un-unit-testable, since Ink's `useInput` needs raw mode).
|
|
94
|
+
*/
|
|
95
|
+
export type InkRenderFn = (node: React.ReactElement, options?: {
|
|
96
|
+
exitOnCtrlC?: boolean;
|
|
97
|
+
}) => {
|
|
98
|
+
waitUntilExit: () => Promise<unknown>;
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Renders {@link SelectList} with Ink and resolves with the chosen absolute index — or **rejects
|
|
102
|
+
* with a {@link SelectCancelledError}** when the user aborts (Ctrl+C, or Esc with an empty
|
|
103
|
+
* filter). Imports `ink` dynamically (unless a `render` is injected) so this module never pulls
|
|
104
|
+
* Ink into a readline-only run. Intended to be used only after the caller has confirmed an
|
|
105
|
+
* interactive TTY + Ink availability.
|
|
106
|
+
*
|
|
107
|
+
* `exitOnCtrlC: false` — Ink's default would unmount on Ctrl+C and let `waitUntilExit()` resolve
|
|
108
|
+
* normally, so the host would `resolve(chosen)` with the untouched default index (the CFG-20 bug:
|
|
109
|
+
* Ctrl+C read as "picked the default"). We own Ctrl+C in {@link SelectList} instead and route it
|
|
110
|
+
* to the reject path.
|
|
111
|
+
*
|
|
112
|
+
* @param render - injectable Ink renderer (defaults to Ink's `render`); for tests only.
|
|
24
113
|
*/
|
|
25
|
-
export declare function runInkSelect(title: string, items: SelectItem[], initialIndex?: number): Promise<number>;
|
|
114
|
+
export declare function runInkSelect(title: string, items: SelectItem[], initialIndex?: number, render?: InkRenderFn): Promise<number>;
|
|
@@ -1,50 +1,233 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState } from 'react';
|
|
3
|
-
import { Box, Text, useApp, useInput } from 'ink';
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
3
|
+
import { Box, Text, useApp, useInput, useStdout } from 'ink';
|
|
4
|
+
import { SelectCancelledError } from '#src/tui/selectCancelled.js';
|
|
5
|
+
/**
|
|
6
|
+
* CFG-20 — the pure "does this row match the typed filter?" predicate: a case-insensitive
|
|
7
|
+
* substring test on the row label (the model id, with its ⭐/padding prefix — a substring
|
|
8
|
+
* match on the id still hits). The raw substring test; query trimming is done once by the
|
|
9
|
+
* callers ({@link filteredItemIndices} / {@link filterSelectItems}), not here.
|
|
10
|
+
*/
|
|
11
|
+
export function matchesFilter(label, query) {
|
|
12
|
+
return label.toLowerCase().includes(query.toLowerCase());
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* CFG-20 — **the single production filter path**: the absolute indices (into `items`) of the rows
|
|
16
|
+
* whose label matches `query`, in source order. The query is trimmed, so a leading/trailing space
|
|
17
|
+
* filters like the trimmed text and an all-whitespace query is treated as "no filter" (all rows).
|
|
18
|
+
* The widget's live filter calls exactly this (see the `filteredIndices` memo below), and
|
|
19
|
+
* {@link filterSelectItems} is a thin view over it — so a unit test can never validate a matcher
|
|
20
|
+
* the widget doesn't actually run. Indices (not items) so the widget can map a filtered-view
|
|
21
|
+
* selection back to the original absolute index even if two labels were to coincide.
|
|
22
|
+
*/
|
|
23
|
+
export function filteredItemIndices(items, query) {
|
|
24
|
+
const q = query.trim();
|
|
25
|
+
const indices = [];
|
|
26
|
+
items.forEach((item, i) => {
|
|
27
|
+
if (!q || matchesFilter(item.label, q))
|
|
28
|
+
indices.push(i);
|
|
29
|
+
});
|
|
30
|
+
return indices;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* CFG-20 — the items (not indices) surviving the filter, in source order: a thin view over the
|
|
34
|
+
* production {@link filteredItemIndices}, so it can never diverge from the widget's live filter.
|
|
35
|
+
* Mirrors the shape of TUI-C10's `filterSlashCommands` (pure, case-insensitive, empty/whitespace
|
|
36
|
+
* query returns the full list) but deliberately does NOT re-rank prefix-before-substring: the
|
|
37
|
+
* model list arrives already ordered ⭐ preferred-first then alphabetical (CFG-14), and that order
|
|
38
|
+
* must be preserved within the filtered set, so the source order is kept rather than bucketed.
|
|
39
|
+
*/
|
|
40
|
+
export function filterSelectItems(items, query) {
|
|
41
|
+
return filteredItemIndices(items, query).map((i) => items[i]);
|
|
42
|
+
}
|
|
43
|
+
/** Window size used when the terminal height is unknown (non-TTY / tests). */
|
|
44
|
+
const DEFAULT_WINDOW = 10;
|
|
45
|
+
/**
|
|
46
|
+
* Rows reserved from the terminal height for chrome around the option window: the title line
|
|
47
|
+
* plus the two `↑ / ↓ N more` affordance lines, with one line of breathing room. The rest of
|
|
48
|
+
* the terminal is given to visible options.
|
|
49
|
+
*/
|
|
50
|
+
const RESERVED_ROWS = 4;
|
|
51
|
+
/**
|
|
52
|
+
* Derive how many option rows the scrolling window may show at once from the live terminal
|
|
53
|
+
* height. Ink/Node leaves `rows` `undefined` when not attached to a TTY (and in tests), in
|
|
54
|
+
* which case we fall back to {@link DEFAULT_WINDOW}. Always at least 1 so a tiny / mis-reported
|
|
55
|
+
* terminal can never collapse the window to zero.
|
|
56
|
+
*/
|
|
57
|
+
export function windowSize(rows) {
|
|
58
|
+
if (typeof rows === 'number' && Number.isFinite(rows)) {
|
|
59
|
+
return Math.max(1, Math.floor(rows) - RESERVED_ROWS);
|
|
60
|
+
}
|
|
61
|
+
return DEFAULT_WINDOW;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Standard "keep the cursor in view" windowing. Given the previous window start, the current
|
|
65
|
+
* cursor index, the window `size` and total item `count`, return the window start that keeps
|
|
66
|
+
* `cursor` visible while moving as little as possible (sticky: the window only shifts when the
|
|
67
|
+
* cursor crosses an edge). Clamped to `[0, count - size]` so it never scrolls past either end,
|
|
68
|
+
* which also makes wraparound (cursor jumping to the far end) resolve to that end cleanly.
|
|
69
|
+
*/
|
|
70
|
+
export function clampWindowStart(prevStart, cursor, size, count) {
|
|
71
|
+
const maxStart = Math.max(0, count - size);
|
|
72
|
+
let start = Math.min(Math.max(0, prevStart), maxStart);
|
|
73
|
+
if (cursor < start) {
|
|
74
|
+
start = cursor;
|
|
75
|
+
}
|
|
76
|
+
else if (cursor >= start + size) {
|
|
77
|
+
start = cursor - size + 1;
|
|
78
|
+
}
|
|
79
|
+
return Math.max(0, Math.min(start, maxStart));
|
|
80
|
+
}
|
|
4
81
|
/**
|
|
5
82
|
* CFG-11 — a minimal arrow-key + Enter selectable list for the first-run dialog. Up/Down
|
|
6
|
-
*
|
|
7
|
-
*
|
|
83
|
+
* move the highlight, Enter confirms.
|
|
84
|
+
*
|
|
85
|
+
* CFG-15 — the list renders within a bounded, scrolling viewport so a long option list (e.g.
|
|
86
|
+
* openrouter's discovered model list) never overflows the terminal. At most a window's worth of
|
|
87
|
+
* rows is shown; the window scrolls to keep the highlighted row in view, and dim `↑ N more` /
|
|
88
|
+
* `↓ N more` affordances signal hidden items above / below.
|
|
8
89
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
90
|
+
* CFG-20 — **type-to-filter**: typing letters incrementally filters the list (case-insensitive
|
|
91
|
+
* substring on the label — see {@link matchesFilter}), so a 200+-model catalog is reachable by
|
|
92
|
+
* typing a few chars instead of scrolling. Backspace edits the filter; Esc clears it (a second
|
|
93
|
+
* Esc, with the filter already empty, aborts — see `onCancel`). The filtered set keeps the source
|
|
94
|
+
* order (⭐ preferred-first then alphabetical). Because letters now feed the filter, the old j/k
|
|
95
|
+
* vim-nav shortcuts are gone — the arrow keys remain the movement keys.
|
|
96
|
+
*
|
|
97
|
+
* CFG-20 — **abort**: Ctrl+C (always) and Esc-on-empty-filter invoke `onCancel`, which the
|
|
98
|
+
* `runInkSelect` host turns into a {@link SelectCancelledError} so the dialog can abort without
|
|
99
|
+
* writing a config. Enter, Ctrl+C and Esc are handled as three DISTINCT keys (the previous
|
|
100
|
+
* default-`exitOnCtrlC` behaviour silently collapsed Ctrl+C into "selected the default").
|
|
101
|
+
*
|
|
102
|
+
* `onSelect` is called once with the chosen **absolute** index into `items` (never the
|
|
103
|
+
* filtered-view position); the caller is responsible for unmounting (the default `runInkSelect`
|
|
104
|
+
* host calls `useApp().exit()` via `onSelect`/`onCancel`).
|
|
11
105
|
*/
|
|
12
|
-
export function SelectList({ title, items, initialIndex = 0, onSelect, }) {
|
|
106
|
+
export function SelectList({ title, items, initialIndex = 0, onSelect, onCancel, viewportRows, }) {
|
|
107
|
+
const { stdout } = useStdout();
|
|
108
|
+
// CFG-20 — the active type-to-filter query. Empty = show everything.
|
|
109
|
+
const [filter, setFilter] = useState('');
|
|
110
|
+
// `cursor` is a position within the CURRENT filtered view, not an absolute index. With an empty
|
|
111
|
+
// filter the view is the identity of `items`, so the initial cursor is the caller's preferred
|
|
112
|
+
// default (initialIndex); a filter reset the cursor to the top of the narrowed set (below).
|
|
13
113
|
const [cursor, setCursor] = useState(initialIndex >= 0 && initialIndex < items.length ? initialIndex : 0);
|
|
114
|
+
const [windowStart, setWindowStart] = useState(0);
|
|
115
|
+
// Track the live terminal height in state and refresh it on resize, so the window resizes
|
|
116
|
+
// with the terminal (mirrors the resize-aware pattern in Rule.tsx).
|
|
117
|
+
const [rows, setRows] = useState(stdout?.rows);
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
if (!stdout)
|
|
120
|
+
return;
|
|
121
|
+
const onResize = () => setRows(stdout.rows);
|
|
122
|
+
onResize();
|
|
123
|
+
stdout.on('resize', onResize);
|
|
124
|
+
return () => {
|
|
125
|
+
stdout.off('resize', onResize);
|
|
126
|
+
};
|
|
127
|
+
}, [stdout]);
|
|
128
|
+
// The absolute indices of the rows that survive the current filter, in source order. Selection
|
|
129
|
+
// maps back through this array so `onSelect` always reports the original index into `items`. This
|
|
130
|
+
// is THE filter path — the same trimmed predicate the unit tests exercise via filterSelectItems.
|
|
131
|
+
const filteredIndices = useMemo(() => filteredItemIndices(items, filter), [items, filter]);
|
|
132
|
+
const count = filteredIndices.length;
|
|
133
|
+
// Re-filter the list from the current query, resetting the cursor to the top of the narrowed set.
|
|
134
|
+
const applyFilter = (next) => {
|
|
135
|
+
setFilter(next);
|
|
136
|
+
setCursor(0);
|
|
137
|
+
};
|
|
14
138
|
useInput((input, key) => {
|
|
15
|
-
|
|
16
|
-
|
|
139
|
+
// Ctrl+C is an unconditional abort (SIGINT semantics), regardless of any typed filter.
|
|
140
|
+
if (key.ctrl && input === 'c') {
|
|
141
|
+
onCancel?.();
|
|
142
|
+
return;
|
|
17
143
|
}
|
|
18
|
-
|
|
19
|
-
|
|
144
|
+
if (key.return) {
|
|
145
|
+
// Only confirm when the filtered set is non-empty (nothing to pick under a no-match filter).
|
|
146
|
+
if (count > 0)
|
|
147
|
+
onSelect(filteredIndices[cursor]);
|
|
148
|
+
return;
|
|
20
149
|
}
|
|
21
|
-
|
|
22
|
-
|
|
150
|
+
if (key.escape) {
|
|
151
|
+
// First Esc clears an active filter; a second Esc (empty filter) aborts.
|
|
152
|
+
if (filter)
|
|
153
|
+
applyFilter('');
|
|
154
|
+
else
|
|
155
|
+
onCancel?.();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (key.upArrow) {
|
|
159
|
+
if (count > 0)
|
|
160
|
+
setCursor((c) => (c - 1 + count) % count);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (key.downArrow) {
|
|
164
|
+
if (count > 0)
|
|
165
|
+
setCursor((c) => (c + 1) % count);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (key.backspace || key.delete) {
|
|
169
|
+
if (filter)
|
|
170
|
+
applyFilter(filter.slice(0, -1));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
// Any other printable, non-modified key extends the filter. Guard against control/meta combos
|
|
174
|
+
// and non-printable input (arrows/return/etc. are handled above and arrive with empty `input`).
|
|
175
|
+
if (input && !key.ctrl && !key.meta && !/[\x00-\x1f]/.test(input)) {
|
|
176
|
+
applyFilter(filter + input);
|
|
23
177
|
}
|
|
24
178
|
});
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
179
|
+
const size = typeof viewportRows === 'number' ? Math.max(1, viewportRows) : windowSize(rows);
|
|
180
|
+
// Derive the display window from the remembered start + current cursor. Computing it in render
|
|
181
|
+
// keeps the window valid even when the cursor jumps (wraparound), the filter narrows the list,
|
|
182
|
+
// or the terminal resizes.
|
|
183
|
+
const start = clampWindowStart(windowStart, cursor, size, count);
|
|
184
|
+
// Persist the derived start so the window stays sticky across moves (converges in one pass).
|
|
185
|
+
useEffect(() => {
|
|
186
|
+
if (start !== windowStart)
|
|
187
|
+
setWindowStart(start);
|
|
188
|
+
}, [start, windowStart]);
|
|
189
|
+
const visibleIndices = filteredIndices.slice(start, start + size);
|
|
190
|
+
const hiddenAbove = start;
|
|
191
|
+
const hiddenBelow = count - (start + visibleIndices.length);
|
|
192
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: title }), filter ? _jsx(Text, { dimColor: true, children: ` filter: ${filter}` }) : null, count === 0 ? (_jsx(Text, { dimColor: true, children: ` (no matches for "${filter}")` })) : (_jsxs(_Fragment, { children: [hiddenAbove > 0 ? _jsx(Text, { dimColor: true, children: ` ↑ ${hiddenAbove} more` }) : null, visibleIndices.map((absoluteIndex, i) => {
|
|
193
|
+
const selected = start + i === cursor;
|
|
194
|
+
return (_jsxs(Text, { color: selected ? 'cyan' : undefined, children: [selected ? '❯ ' : ' ', items[absoluteIndex].label] }, absoluteIndex));
|
|
195
|
+
}), hiddenBelow > 0 ? _jsx(Text, { dimColor: true, children: ` ↓ ${hiddenBelow} more` }) : null] }))] }));
|
|
29
196
|
}
|
|
30
197
|
/**
|
|
31
|
-
* Renders {@link SelectList} with Ink and resolves with the chosen index
|
|
32
|
-
*
|
|
33
|
-
*
|
|
198
|
+
* Renders {@link SelectList} with Ink and resolves with the chosen absolute index — or **rejects
|
|
199
|
+
* with a {@link SelectCancelledError}** when the user aborts (Ctrl+C, or Esc with an empty
|
|
200
|
+
* filter). Imports `ink` dynamically (unless a `render` is injected) so this module never pulls
|
|
201
|
+
* Ink into a readline-only run. Intended to be used only after the caller has confirmed an
|
|
202
|
+
* interactive TTY + Ink availability.
|
|
203
|
+
*
|
|
204
|
+
* `exitOnCtrlC: false` — Ink's default would unmount on Ctrl+C and let `waitUntilExit()` resolve
|
|
205
|
+
* normally, so the host would `resolve(chosen)` with the untouched default index (the CFG-20 bug:
|
|
206
|
+
* Ctrl+C read as "picked the default"). We own Ctrl+C in {@link SelectList} instead and route it
|
|
207
|
+
* to the reject path.
|
|
208
|
+
*
|
|
209
|
+
* @param render - injectable Ink renderer (defaults to Ink's `render`); for tests only.
|
|
34
210
|
*/
|
|
35
|
-
export async function runInkSelect(title, items, initialIndex = 0) {
|
|
36
|
-
const
|
|
37
|
-
return await new Promise((resolve) => {
|
|
211
|
+
export async function runInkSelect(title, items, initialIndex = 0, render) {
|
|
212
|
+
const renderFn = render ?? (await import('ink')).render;
|
|
213
|
+
return await new Promise((resolve, reject) => {
|
|
38
214
|
let chosen = initialIndex;
|
|
215
|
+
let cancelled = false;
|
|
39
216
|
const Host = () => {
|
|
40
217
|
const { exit } = useApp();
|
|
41
218
|
return (_jsx(SelectList, { title: title, items: items, initialIndex: initialIndex, onSelect: (index) => {
|
|
42
219
|
chosen = index;
|
|
43
220
|
exit();
|
|
221
|
+
}, onCancel: () => {
|
|
222
|
+
cancelled = true;
|
|
223
|
+
exit();
|
|
44
224
|
} }));
|
|
45
225
|
};
|
|
46
|
-
|
|
47
|
-
instance
|
|
226
|
+
// Own Ctrl+C ourselves (see the doc comment) rather than letting Ink silently unmount.
|
|
227
|
+
const instance = renderFn(_jsx(Host, {}), { exitOnCtrlC: false });
|
|
228
|
+
instance
|
|
229
|
+
.waitUntilExit()
|
|
230
|
+
.then(() => (cancelled ? reject(new SelectCancelledError()) : resolve(chosen)));
|
|
48
231
|
});
|
|
49
232
|
}
|
|
50
233
|
//# sourceMappingURL=SelectList.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SelectList.js","sourceRoot":"","sources":["../../../src/tui/components/SelectList.tsx"],"names":[],"mappings":";AAAA,OAAc,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"SelectList.js","sourceRoot":"","sources":["../../../src/tui/components/SelectList.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { SlashCommand } from '#src/tui/slashCommands.js';
|
|
3
|
+
/**
|
|
4
|
+
* TUI-C10 — the slash-command discovery menu. A read-only, presentational list of the commands
|
|
5
|
+
* that match what the user has typed so far (`/`, `/mo`, …), rendered just above the prompt line so
|
|
6
|
+
* a beginner can *discover* commands instead of having to already know their names (DL-9).
|
|
7
|
+
*
|
|
8
|
+
* It deliberately owns **no** keyboard state: unlike {@link SelectList} (a self-contained blocking
|
|
9
|
+
* dialog), this menu coexists with the live `<TextInput>` — the user keeps typing to filter — so
|
|
10
|
+
* `<PromptInput>` owns the arrow/Tab/Enter/Esc handling and just tells this component which row is
|
|
11
|
+
* highlighted. That keeps the selection logic in one place and this render trivially testable.
|
|
12
|
+
*
|
|
13
|
+
* Colour follows the shared palette (DL-8): the highlighted `/name` is **cyan** (the informational
|
|
14
|
+
* accent, matching SelectList's cursor) and every description is **dim** (secondary/contextual
|
|
15
|
+
* text). Names are padded to a common width so the descriptions line up into a readable column.
|
|
16
|
+
*/
|
|
17
|
+
export declare function SlashCommandMenu({ commands, selectedIndex, }: {
|
|
18
|
+
commands: SlashCommand[];
|
|
19
|
+
selectedIndex: number;
|
|
20
|
+
}): React.ReactElement | null;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
/**
|
|
4
|
+
* TUI-C10 — the slash-command discovery menu. A read-only, presentational list of the commands
|
|
5
|
+
* that match what the user has typed so far (`/`, `/mo`, …), rendered just above the prompt line so
|
|
6
|
+
* a beginner can *discover* commands instead of having to already know their names (DL-9).
|
|
7
|
+
*
|
|
8
|
+
* It deliberately owns **no** keyboard state: unlike {@link SelectList} (a self-contained blocking
|
|
9
|
+
* dialog), this menu coexists with the live `<TextInput>` — the user keeps typing to filter — so
|
|
10
|
+
* `<PromptInput>` owns the arrow/Tab/Enter/Esc handling and just tells this component which row is
|
|
11
|
+
* highlighted. That keeps the selection logic in one place and this render trivially testable.
|
|
12
|
+
*
|
|
13
|
+
* Colour follows the shared palette (DL-8): the highlighted `/name` is **cyan** (the informational
|
|
14
|
+
* accent, matching SelectList's cursor) and every description is **dim** (secondary/contextual
|
|
15
|
+
* text). Names are padded to a common width so the descriptions line up into a readable column.
|
|
16
|
+
*/
|
|
17
|
+
export function SlashCommandMenu({ commands, selectedIndex, }) {
|
|
18
|
+
if (commands.length === 0)
|
|
19
|
+
return null;
|
|
20
|
+
// Width of the widest "/name" so the description column aligns (the leading "/" is included).
|
|
21
|
+
const nameWidth = commands.reduce((w, c) => Math.max(w, c.name.length + 1), 0);
|
|
22
|
+
return (_jsx(Box, { flexDirection: "column", children: commands.map((command, index) => {
|
|
23
|
+
const selected = index === selectedIndex;
|
|
24
|
+
const label = `/${command.name}`.padEnd(nameWidth);
|
|
25
|
+
return (_jsxs(Box, { children: [_jsxs(Text, { color: selected ? 'cyan' : undefined, bold: selected, children: [selected ? '❯ ' : ' ', label] }), _jsxs(Text, { dimColor: true, children: [' ', command.description] })] }, command.name));
|
|
26
|
+
}) }));
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=SlashCommandMenu.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SlashCommandMenu.js","sourceRoot":"","sources":["../../../src/tui/components/SlashCommandMenu.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,KAAK,CAAC;AAGhC;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,gBAAgB,CAAC,EAC/B,QAAQ,EACR,aAAa,GAId;IACC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACvC,8FAA8F;IAC9F,MAAM,SAAS,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC/E,OAAO,CACL,KAAC,GAAG,IAAC,aAAa,EAAC,QAAQ,YACxB,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE;YAC/B,MAAM,QAAQ,GAAG,KAAK,KAAK,aAAa,CAAC;YACzC,MAAM,KAAK,GAAG,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;YACnD,OAAO,CACL,MAAC,GAAG,eACF,MAAC,IAAI,IAAC,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,EAAE,IAAI,EAAE,QAAQ,aACvD,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EACtB,KAAK,IACD,EACP,MAAC,IAAI,IAAC,QAAQ,mBACX,IAAI,EACJ,OAAO,CAAC,WAAW,IACf,KARC,OAAO,CAAC,IAAI,CAShB,CACP,CAAC;QACJ,CAAC,CAAC,GACE,CACP,CAAC;AACJ,CAAC"}
|
|
@@ -5,11 +5,17 @@ import React from 'react';
|
|
|
5
5
|
* and a turn counter — on a single dim line. Streaming progress itself is shown by the live
|
|
6
6
|
* turn, not here, so this bar stays stable (one line) and does not flicker.
|
|
7
7
|
*/
|
|
8
|
-
export declare function StatusBar({ running, mode, modelDisplayName, turnCount, debugHint, }: {
|
|
8
|
+
export declare function StatusBar({ running, mode, modelDisplayName, turnCount, debugHint, autoApprove, }: {
|
|
9
9
|
running: boolean;
|
|
10
10
|
mode: string;
|
|
11
11
|
modelDisplayName?: string;
|
|
12
12
|
turnCount?: number;
|
|
13
13
|
/** When the docked debug panel is open but unfocused, surface how to step into it. */
|
|
14
14
|
debugHint?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* EXT-12 — when session auto-approve is ON, surface a persistent, unmissable indicator so the
|
|
17
|
+
* user always knows shell commands run without asking. Shown in both the running and idle
|
|
18
|
+
* states (yellow, matching the warn tone of the /auto-approve ON notice).
|
|
19
|
+
*/
|
|
20
|
+
autoApprove?: boolean;
|
|
15
21
|
}): React.ReactElement;
|