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.
Files changed (127) hide show
  1. package/README.md +55 -5
  2. package/cli.js +43 -10
  3. package/dist/cli.js +33 -2
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/adkEvalRunner.d.ts +66 -0
  6. package/dist/commands/adkEvalRunner.js +69 -0
  7. package/dist/commands/adkEvalRunner.js.map +1 -0
  8. package/dist/commands/agUiEvalRunner.d.ts +73 -0
  9. package/dist/commands/agUiEvalRunner.js +216 -0
  10. package/dist/commands/agUiEvalRunner.js.map +1 -0
  11. package/dist/commands/askCommand.d.ts +3 -2
  12. package/dist/commands/askCommand.js +26 -7
  13. package/dist/commands/askCommand.js.map +1 -1
  14. package/dist/commands/batchCommand.d.ts +78 -0
  15. package/dist/commands/batchCommand.js +245 -0
  16. package/dist/commands/batchCommand.js.map +1 -0
  17. package/dist/commands/cliOptionParsers.d.ts +12 -0
  18. package/dist/commands/cliOptionParsers.js +18 -0
  19. package/dist/commands/cliOptionParsers.js.map +1 -0
  20. package/dist/commands/commandIntrospection.d.ts +6 -6
  21. package/dist/commands/commandIntrospection.js +12 -12
  22. package/dist/commands/commandIntrospection.js.map +1 -1
  23. package/dist/commands/commandUtils.d.ts +16 -16
  24. package/dist/commands/commandUtils.js +20 -20
  25. package/dist/commands/commandUtils.js.map +1 -1
  26. package/dist/commands/configCommand.d.ts +24 -0
  27. package/dist/commands/configCommand.js +117 -0
  28. package/dist/commands/configCommand.js.map +1 -0
  29. package/dist/commands/configSetup.d.ts +1 -1
  30. package/dist/commands/configSetup.js +17 -2
  31. package/dist/commands/configSetup.js.map +1 -1
  32. package/dist/commands/evalCommand.d.ts +41 -0
  33. package/dist/commands/evalCommand.js +483 -0
  34. package/dist/commands/evalCommand.js.map +1 -0
  35. package/dist/commands/execCommand.js +4 -1
  36. package/dist/commands/execCommand.js.map +1 -1
  37. package/dist/commands/firstRunDialog.d.ts +39 -5
  38. package/dist/commands/firstRunDialog.js +72 -10
  39. package/dist/commands/firstRunDialog.js.map +1 -1
  40. package/dist/commands/getCommand.js +7 -7
  41. package/dist/commands/getCommand.js.map +1 -1
  42. package/dist/commands/historyCommand.d.ts +17 -0
  43. package/dist/commands/historyCommand.js +102 -0
  44. package/dist/commands/historyCommand.js.map +1 -0
  45. package/dist/commands/initCommand.d.ts +3 -0
  46. package/dist/commands/initCommand.js +9 -4
  47. package/dist/commands/initCommand.js.map +1 -1
  48. package/dist/commands/insightsCommand.d.ts +8 -0
  49. package/dist/commands/insightsCommand.js +32 -0
  50. package/dist/commands/insightsCommand.js.map +1 -0
  51. package/dist/commands/modelsCommand.d.ts +25 -0
  52. package/dist/commands/modelsCommand.js +96 -0
  53. package/dist/commands/modelsCommand.js.map +1 -0
  54. package/dist/commands/prCommand.js +14 -15
  55. package/dist/commands/prCommand.js.map +1 -1
  56. package/dist/commands/prDiscovery.d.ts +2 -2
  57. package/dist/commands/prDiscovery.js +27 -29
  58. package/dist/commands/prDiscovery.js.map +1 -1
  59. package/dist/commands/reviewCommand.js +15 -10
  60. package/dist/commands/reviewCommand.js.map +1 -1
  61. package/dist/commands/workflowCommand.d.ts +19 -0
  62. package/dist/commands/workflowCommand.js +77 -0
  63. package/dist/commands/workflowCommand.js.map +1 -0
  64. package/dist/helpers/jira/jiraLogWork.js.map +1 -1
  65. package/dist/modules/startSession.js +9 -7
  66. package/dist/modules/startSession.js.map +1 -1
  67. package/dist/tools/gthJiraLogWorkTool.js +3 -3
  68. package/dist/tools/gthJiraLogWorkTool.js.map +1 -1
  69. package/dist/tui/components/App.js +321 -21
  70. package/dist/tui/components/App.js.map +1 -1
  71. package/dist/tui/components/ApprovalPrompt.d.ts +18 -0
  72. package/dist/tui/components/ApprovalPrompt.js +26 -0
  73. package/dist/tui/components/ApprovalPrompt.js.map +1 -0
  74. package/dist/tui/components/DebugPanel.d.ts +29 -9
  75. package/dist/tui/components/DebugPanel.js +46 -23
  76. package/dist/tui/components/DebugPanel.js.map +1 -1
  77. package/dist/tui/components/FetchProgress.d.ts +36 -0
  78. package/dist/tui/components/FetchProgress.js +39 -0
  79. package/dist/tui/components/FetchProgress.js.map +1 -0
  80. package/dist/tui/components/LiveTurn.d.ts +26 -4
  81. package/dist/tui/components/LiveTurn.js +104 -18
  82. package/dist/tui/components/LiveTurn.js.map +1 -1
  83. package/dist/tui/components/NoticeBar.d.ts +32 -0
  84. package/dist/tui/components/NoticeBar.js +39 -0
  85. package/dist/tui/components/NoticeBar.js.map +1 -0
  86. package/dist/tui/components/PromptInput.d.ts +26 -1
  87. package/dist/tui/components/PromptInput.js +93 -7
  88. package/dist/tui/components/PromptInput.js.map +1 -1
  89. package/dist/tui/components/Rule.js.map +1 -1
  90. package/dist/tui/components/SelectList.d.ts +98 -9
  91. package/dist/tui/components/SelectList.js +209 -26
  92. package/dist/tui/components/SelectList.js.map +1 -1
  93. package/dist/tui/components/SlashCommandMenu.d.ts +20 -0
  94. package/dist/tui/components/SlashCommandMenu.js +28 -0
  95. package/dist/tui/components/SlashCommandMenu.js.map +1 -0
  96. package/dist/tui/components/StatusBar.d.ts +7 -1
  97. package/dist/tui/components/StatusBar.js +7 -3
  98. package/dist/tui/components/StatusBar.js.map +1 -1
  99. package/dist/tui/components/Transcript.js +6 -1
  100. package/dist/tui/components/Transcript.js.map +1 -1
  101. package/dist/tui/debugRender.d.ts +41 -6
  102. package/dist/tui/debugRender.js +126 -12
  103. package/dist/tui/debugRender.js.map +1 -1
  104. package/dist/tui/debugSearch.d.ts +40 -0
  105. package/dist/tui/debugSearch.js +73 -0
  106. package/dist/tui/debugSearch.js.map +1 -0
  107. package/dist/tui/markdown.js +0 -0
  108. package/dist/tui/markdown.js.map +1 -1
  109. package/dist/tui/pasteParser.d.ts +21 -0
  110. package/dist/tui/pasteParser.js +24 -0
  111. package/dist/tui/pasteParser.js.map +1 -0
  112. package/dist/tui/selectCancelled.d.ts +13 -0
  113. package/dist/tui/selectCancelled.js +17 -0
  114. package/dist/tui/selectCancelled.js.map +1 -0
  115. package/dist/tui/slashCommands.d.ts +220 -1
  116. package/dist/tui/slashCommands.js +408 -1
  117. package/dist/tui/slashCommands.js.map +1 -1
  118. package/dist/tui/tuiSessionModule.js +249 -24
  119. package/dist/tui/tuiSessionModule.js.map +1 -1
  120. package/dist/tui/types.d.ts +96 -7
  121. package/dist/tui/viewModel.d.ts +34 -0
  122. package/dist/tui/viewModel.js +67 -0
  123. package/dist/tui/viewModel.js.map +1 -1
  124. package/dist/utils/stdinPolicy.d.ts +31 -0
  125. package/dist/utils/stdinPolicy.js +37 -0
  126. package/dist/utils/stdinPolicy.js.map +1 -0
  127. 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
- return (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: ' > ' }), _jsx(TextInput, { value: value, onChange: setValue, onSubmit: (submitted) => {
12
- setValue('');
13
- onSubmit(submitted);
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;AACxC,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,KAAK,CAAC;AAChC,OAAO,SAAS,MAAM,gBAAgB,CAAC;AAEvC;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,EAC1B,QAAQ,GAGT;IACC,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAC;IACvC,OAAO,CACL,MAAC,GAAG,eACF,KAAC,IAAI,IAAC,KAAK,EAAC,MAAM,YAAE,MAAM,GAAQ,EAClC,KAAC,SAAS,IACR,KAAK,EAAE,KAAK,EACZ,QAAQ,EAAE,QAAQ,EAClB,QAAQ,EAAE,CAAC,SAAS,EAAE,EAAE;oBACtB,QAAQ,CAAC,EAAE,CAAC,CAAC;oBACb,QAAQ,CAAC,SAAS,CAAC,CAAC;gBACtB,CAAC,GACD,IACE,CACP,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"PromptInput.js","sourceRoot":"","sources":["../../../src/tui/components/PromptInput.tsx"],"names":[],"mappings":";AAAA,OAAc,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACnD,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,KAAK,CAAC;AACpD,OAAO,SAAS,MAAM,gBAAgB,CAAC;AACvC,OAAO,EAAE,gBAAgB,EAAE,MAAM,yCAAyC,CAAC;AAC3E,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAqB,MAAM,2BAA2B,CAAC;AACnG,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAE9D;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,UAAU,WAAW,CAAC,EAC1B,QAAQ,EACR,QAAQ,GAAG,EAAE,EACb,iBAAiB,GAQlB;IACC,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAC;IACvC,8FAA8F;IAC9F,+EAA+E;IAC/E,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;IACxC,2FAA2F;IAC3F,kEAAkE;IAClE,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAElD,MAAM,KAAK,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;IACpC,MAAM,OAAO,GAAG,KAAK,KAAK,IAAI,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,mBAAmB,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACzF,MAAM,UAAU,GAAG,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC;IACtC,uFAAuF;IACvF,MAAM,aAAa,GAAG,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAE5E,gGAAgG;IAChG,SAAS,CAAC,GAAG,EAAE;QACb,iBAAiB,EAAE,CAAC,UAAU,CAAC,CAAC;IAClC,CAAC,EAAE,CAAC,UAAU,EAAE,iBAAiB,CAAC,CAAC,CAAC;IAEpC,0FAA0F;IAC1F,8FAA8F;IAC9F,kGAAkG;IAClG,gGAAgG;IAChG,8FAA8F;IAC9F,oFAAoF;IACpF,QAAQ,CAAC,CAAC,IAAI,EAAE,EAAE;QAChB,MAAM,MAAM,GAAG,mBAAmB,CAAC,IAAI,CAAC,CAAC;QACzC,IAAI,CAAC,MAAM;YAAE,OAAO;QACpB,QAAQ,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,GAAG,MAAM,CAAC,CAAC;QACxC,2FAA2F;QAC3F,YAAY,CAAC,KAAK,CAAC,CAAC;QACpB,SAAS,CAAC,CAAC,CAAC,CAAC;IACf,CAAC,CAAC,CAAC;IAEH,QAAQ,CACN,CAAC,MAAM,EAAE,GAAG,EAAE,EAAE;QACd,qFAAqF;QACrF,IAAI,CAAC,UAAU;YAAE,OAAO;QACxB,IAAI,GAAG,CAAC,OAAO,EAAE,CAAC;YAChB,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;QAC5F,CAAC;aAAM,IAAI,GAAG,CAAC,SAAS,EAAE,CAAC;YACzB,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;QAC3E,CAAC;aAAM,IAAI,GAAG,CAAC,GAAG,EAAE,CAAC;YACnB,yFAAyF;YACzF,uFAAuF;YACvF,QAAQ,CAAC,IAAI,OAAO,CAAC,aAAa,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC;YAC7C,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,SAAS,CAAC,CAAC,CAAC,CAAC;QACf,CAAC;aAAM,IAAI,GAAG,CAAC,MAAM,EAAE,CAAC;YACtB,YAAY,CAAC,IAAI,CAAC,CAAC;QACrB,CAAC;QACD,yFAAyF;IAC3F,CAAC;IACD,4FAA4F;IAC5F,EAAE,QAAQ,EAAE,IAAI,EAAE,CACnB,CAAC;IAEF,OAAO,CACL,MAAC,GAAG,IAAC,aAAa,EAAC,QAAQ,aACxB,UAAU,CAAC,CAAC,CAAC,KAAC,gBAAgB,IAAC,QAAQ,EAAE,OAAO,EAAE,aAAa,EAAE,aAAa,GAAI,CAAC,CAAC,CAAC,IAAI,EAC1F,MAAC,GAAG,eACF,KAAC,IAAI,IAAC,KAAK,EAAC,MAAM,YAAE,MAAM,GAAQ,EAClC,KAAC,SAAS,IACR,KAAK,EAAE,KAAK,EACZ,QAAQ,EAAE,CAAC,IAAI,EAAE,EAAE;4BACjB,QAAQ,CAAC,IAAI,CAAC,CAAC;4BACf,YAAY,CAAC,KAAK,CAAC,CAAC;4BACpB,SAAS,CAAC,CAAC,CAAC,CAAC;wBACf,CAAC,EACD,QAAQ,EAAE,CAAC,SAAS,EAAE,EAAE;4BACtB,oFAAoF;4BACpF,qFAAqF;4BACrF,kFAAkF;4BAClF,MAAM,QAAQ,GAAG,UAAU,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,aAAa,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;4BAC5E,QAAQ,CAAC,EAAE,CAAC,CAAC;4BACb,YAAY,CAAC,KAAK,CAAC,CAAC;4BACpB,SAAS,CAAC,CAAC,CAAC,CAAC;4BACb,QAAQ,CAAC,QAAQ,CAAC,CAAC;wBACrB,CAAC,GACD,IACE,IACF,CACP,CAAC;AACJ,CAAC"}
@@ -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,KAAyB,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"}
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
- * (and k/j) move the highlight, Enter confirms. It deliberately has no number-typing path:
9
- * the readline number menu remains the non-TTY fallback, this is the keyboard-driven TUI one.
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; the caller is responsible for unmounting
12
- * (the default `runInkSelect` host calls `useApp().exit()` via `onSelect`).
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
- * Renders {@link SelectList} with Ink and resolves with the chosen index. Imports `ink`
22
- * dynamically so this module never pulls Ink into a readline-only run. Intended to be used
23
- * only after the caller has confirmed an interactive TTY + Ink availability.
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
- * (and k/j) move the highlight, Enter confirms. It deliberately has no number-typing path:
7
- * the readline number menu remains the non-TTY fallback, this is the keyboard-driven TUI one.
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
- * `onSelect` is called once with the chosen index; the caller is responsible for unmounting
10
- * (the default `runInkSelect` host calls `useApp().exit()` via `onSelect`).
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
- if (key.upArrow || input === 'k') {
16
- setCursor((c) => (c - 1 + items.length) % items.length);
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
- else if (key.downArrow || input === 'j') {
19
- setCursor((c) => (c + 1) % items.length);
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
- else if (key.return) {
22
- onSelect(cursor);
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
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: title }), items.map((item, index) => {
26
- const selected = index === cursor;
27
- return (_jsxs(Text, { color: selected ? 'cyan' : undefined, children: [selected ? '❯ ' : ' ', item.label] }, index));
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. Imports `ink`
32
- * dynamically so this module never pulls Ink into a readline-only run. Intended to be used
33
- * only after the caller has confirmed an interactive TTY + Ink availability.
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 { render } = await import('ink');
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
- const instance = render(_jsx(Host, {}));
47
- instance.waitUntilExit().then(() => resolve(chosen));
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;AACxC,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,CAAC;AAOlD;;;;;;;GAOG;AACH,MAAM,UAAU,UAAU,CAAC,EACzB,KAAK,EACL,KAAK,EACL,YAAY,GAAG,CAAC,EAChB,QAAQ,GAMT;IACC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CAClC,YAAY,IAAI,CAAC,IAAI,YAAY,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CACpE,CAAC;IAEF,QAAQ,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;QACtB,IAAI,GAAG,CAAC,OAAO,IAAI,KAAK,KAAK,GAAG,EAAE,CAAC;YACjC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC;QAC1D,CAAC;aAAM,IAAI,GAAG,CAAC,SAAS,IAAI,KAAK,KAAK,GAAG,EAAE,CAAC;YAC1C,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC;QAC3C,CAAC;aAAM,IAAI,GAAG,CAAC,MAAM,EAAE,CAAC;YACtB,QAAQ,CAAC,MAAM,CAAC,CAAC;QACnB,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,OAAO,CACL,MAAC,GAAG,IAAC,aAAa,EAAC,QAAQ,aACzB,KAAC,IAAI,cAAE,KAAK,GAAQ,EACnB,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;gBACzB,MAAM,QAAQ,GAAG,KAAK,KAAK,MAAM,CAAC;gBAClC,OAAO,CACL,MAAC,IAAI,IAAa,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,aACnD,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EACtB,IAAI,CAAC,KAAK,KAFF,KAAK,CAGT,CACR,CAAC;YACJ,CAAC,CAAC,IACE,CACP,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,KAAa,EACb,KAAmB,EACnB,YAAY,GAAG,CAAC;IAEhB,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,CAAC;IACvC,OAAO,MAAM,IAAI,OAAO,CAAS,CAAC,OAAO,EAAE,EAAE;QAC3C,IAAI,MAAM,GAAG,YAAY,CAAC;QAC1B,MAAM,IAAI,GAAG,GAAuB,EAAE;YACpC,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,EAAE,CAAC;YAC1B,OAAO,CACL,KAAC,UAAU,IACT,KAAK,EAAE,KAAK,EACZ,KAAK,EAAE,KAAK,EACZ,YAAY,EAAE,YAAY,EAC1B,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE;oBAClB,MAAM,GAAG,KAAK,CAAC;oBACf,IAAI,EAAE,CAAC;gBACT,CAAC,GACD,CACH,CAAC;QACJ,CAAC,CAAC;QACF,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAC,IAAI,KAAG,CAAC,CAAC;QAClC,QAAQ,CAAC,aAAa,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;AACL,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;