@agi-fans/dsh-tui 0.22.0 → 0.23.1

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 (282) hide show
  1. package/LICENSE +26 -0
  2. package/lib/CHANGELOG.md +219 -1
  3. package/lib/chrome/box.d.ts +1 -18
  4. package/lib/chrome/box.js +2 -45
  5. package/lib/chrome/box.js.map +1 -1
  6. package/lib/chrome/code-highlight.js +4 -4
  7. package/lib/chrome/code-highlight.js.map +1 -1
  8. package/lib/chrome/command-output.d.ts +0 -1
  9. package/lib/chrome/command-output.js +11 -5
  10. package/lib/chrome/command-output.js.map +1 -1
  11. package/lib/chrome/goal-bar.js +1 -1
  12. package/lib/chrome/goal-bar.js.map +1 -1
  13. package/lib/chrome/main-screen-renderer.js +4 -3
  14. package/lib/chrome/main-screen-renderer.js.map +1 -1
  15. package/lib/chrome/markdown.d.ts +3 -3
  16. package/lib/chrome/markdown.js +85 -36
  17. package/lib/chrome/markdown.js.map +1 -1
  18. package/lib/chrome/md-style.d.ts +5 -2
  19. package/lib/chrome/md-style.js +13 -4
  20. package/lib/chrome/md-style.js.map +1 -1
  21. package/lib/chrome/renderer.d.ts +37 -1
  22. package/lib/chrome/renderer.js.map +1 -1
  23. package/lib/chrome/status-line.d.ts +3 -3
  24. package/lib/chrome/status-line.js +5 -5
  25. package/lib/chrome/status-line.js.map +1 -1
  26. package/lib/chrome/theme.d.ts +8 -3
  27. package/lib/chrome/theme.js +51 -153
  28. package/lib/chrome/theme.js.map +1 -1
  29. package/lib/commands/auth-account.d.ts +9 -0
  30. package/lib/commands/auth-account.js +202 -0
  31. package/lib/commands/auth-account.js.map +1 -0
  32. package/lib/commands/auth.js +238 -414
  33. package/lib/commands/auth.js.map +1 -1
  34. package/lib/commands/diff.js +22 -16
  35. package/lib/commands/diff.js.map +1 -1
  36. package/lib/commands/export.d.ts +0 -1
  37. package/lib/commands/export.js +99 -32
  38. package/lib/commands/export.js.map +1 -1
  39. package/lib/commands/files.js +11 -9
  40. package/lib/commands/files.js.map +1 -1
  41. package/lib/commands/integrations.d.ts +12 -1
  42. package/lib/commands/integrations.js +120 -23
  43. package/lib/commands/integrations.js.map +1 -1
  44. package/lib/commands/jobs-browser.d.ts +3 -0
  45. package/lib/commands/jobs-browser.js +152 -0
  46. package/lib/commands/jobs-browser.js.map +1 -0
  47. package/lib/commands/jobs.d.ts +3 -14
  48. package/lib/commands/jobs.js +26 -18
  49. package/lib/commands/jobs.js.map +1 -1
  50. package/lib/commands/model.d.ts +3 -0
  51. package/lib/commands/model.js +212 -128
  52. package/lib/commands/model.js.map +1 -1
  53. package/lib/commands/open-external.d.ts +2 -0
  54. package/lib/commands/open-external.js +19 -0
  55. package/lib/commands/open-external.js.map +1 -0
  56. package/lib/commands/permission.js +65 -54
  57. package/lib/commands/permission.js.map +1 -1
  58. package/lib/commands/plugins.js +381 -139
  59. package/lib/commands/plugins.js.map +1 -1
  60. package/lib/commands/schedule-catalog.d.ts +12 -0
  61. package/lib/commands/schedule-catalog.js +41 -0
  62. package/lib/commands/schedule-catalog.js.map +1 -0
  63. package/lib/commands/schedule-history.d.ts +4 -0
  64. package/lib/commands/schedule-history.js +62 -0
  65. package/lib/commands/schedule-history.js.map +1 -0
  66. package/lib/commands/schedule.d.ts +12 -15
  67. package/lib/commands/schedule.js +220 -187
  68. package/lib/commands/schedule.js.map +1 -1
  69. package/lib/commands/session-configuration.d.ts +3 -0
  70. package/lib/commands/session-configuration.js +62 -28
  71. package/lib/commands/session-configuration.js.map +1 -1
  72. package/lib/commands/session.js +116 -66
  73. package/lib/commands/session.js.map +1 -1
  74. package/lib/commands/terminal.js +14 -8
  75. package/lib/commands/terminal.js.map +1 -1
  76. package/lib/definition.d.ts +115 -15
  77. package/lib/index.d.ts +1 -0
  78. package/lib/index.js +1 -0
  79. package/lib/index.js.map +1 -1
  80. package/lib/input/actions.d.ts +1 -1
  81. package/lib/input/actions.js +5 -2
  82. package/lib/input/actions.js.map +1 -1
  83. package/lib/input/composer-files.d.ts +2 -0
  84. package/lib/input/composer-files.js +13 -3
  85. package/lib/input/composer-files.js.map +1 -1
  86. package/lib/input/image-paste.d.ts +5 -12
  87. package/lib/input/image-paste.js +32 -28
  88. package/lib/input/image-paste.js.map +1 -1
  89. package/lib/input/key-test.d.ts +5 -0
  90. package/lib/input/key-test.js +19 -0
  91. package/lib/input/key-test.js.map +1 -0
  92. package/lib/input/keys.d.ts +2 -0
  93. package/lib/input/keys.js +4 -1
  94. package/lib/input/keys.js.map +1 -1
  95. package/lib/locale/index.d.ts +6 -0
  96. package/lib/locale/index.js +32 -0
  97. package/lib/locale/index.js.map +1 -0
  98. package/lib/locale/prompt.d.ts +4 -0
  99. package/lib/locale/prompt.js +24 -0
  100. package/lib/locale/prompt.js.map +1 -0
  101. package/lib/locale/zh.d.ts +1114 -0
  102. package/lib/locale/zh.js +1115 -0
  103. package/lib/locale/zh.js.map +1 -0
  104. package/lib/prompt-intents.d.ts +4 -0
  105. package/lib/prompt-intents.js +4 -0
  106. package/lib/prompt-intents.js.map +1 -1
  107. package/lib/runtime/account-settings.d.ts +18 -0
  108. package/lib/runtime/account-settings.js +241 -0
  109. package/lib/runtime/account-settings.js.map +1 -0
  110. package/lib/runtime/agent-profile.d.ts +3 -3
  111. package/lib/runtime/agent-profile.js +4 -4
  112. package/lib/runtime/agent-profile.js.map +1 -1
  113. package/lib/runtime/btw-policy.js +2 -1
  114. package/lib/runtime/btw-policy.js.map +1 -1
  115. package/lib/runtime/btw.js +1 -2
  116. package/lib/runtime/btw.js.map +1 -1
  117. package/lib/runtime/composer-images.d.ts +2 -2
  118. package/lib/runtime/composer-images.js +4 -4
  119. package/lib/runtime/composer-images.js.map +1 -1
  120. package/lib/runtime/composer-shell.js +27 -1
  121. package/lib/runtime/composer-shell.js.map +1 -1
  122. package/lib/runtime/file-browser.d.ts +19 -0
  123. package/lib/runtime/file-browser.js +291 -0
  124. package/lib/runtime/file-browser.js.map +1 -0
  125. package/lib/runtime/file-review.d.ts +6 -9
  126. package/lib/runtime/file-review.js +91 -7
  127. package/lib/runtime/file-review.js.map +1 -1
  128. package/lib/runtime/help-reader.d.ts +12 -0
  129. package/lib/runtime/help-reader.js +50 -0
  130. package/lib/runtime/help-reader.js.map +1 -0
  131. package/lib/runtime/human-interaction.js +1 -1
  132. package/lib/runtime/human-interaction.js.map +1 -1
  133. package/lib/runtime/model-catalog-editor.d.ts +12 -0
  134. package/lib/runtime/model-catalog-editor.js +215 -0
  135. package/lib/runtime/model-catalog-editor.js.map +1 -0
  136. package/lib/runtime/model-settings.d.ts +11 -0
  137. package/lib/runtime/model-settings.js +404 -0
  138. package/lib/runtime/model-settings.js.map +1 -0
  139. package/lib/runtime/plugin-settings.d.ts +4 -1
  140. package/lib/runtime/plugin-settings.js +164 -88
  141. package/lib/runtime/plugin-settings.js.map +1 -1
  142. package/lib/runtime/prompt-browser.d.ts +9 -0
  143. package/lib/runtime/prompt-browser.js +19 -0
  144. package/lib/runtime/prompt-browser.js.map +1 -0
  145. package/lib/runtime/provider-local.d.ts +27 -27
  146. package/lib/runtime/provider-local.js +1288 -308
  147. package/lib/runtime/provider-local.js.map +1 -1
  148. package/lib/runtime/reference-reader.d.ts +15 -0
  149. package/lib/runtime/reference-reader.js +78 -0
  150. package/lib/runtime/reference-reader.js.map +1 -0
  151. package/lib/runtime/runner.js +38 -29
  152. package/lib/runtime/runner.js.map +1 -1
  153. package/lib/runtime/session-runtime.js +1 -1
  154. package/lib/runtime/session-runtime.js.map +1 -1
  155. package/lib/runtime/settings-draft.d.ts +21 -0
  156. package/lib/runtime/settings-draft.js +166 -0
  157. package/lib/runtime/settings-draft.js.map +1 -0
  158. package/lib/runtime/settings-preferences.js +1 -1
  159. package/lib/runtime/settings-preferences.js.map +1 -1
  160. package/lib/runtime/startup-notices.d.ts +0 -1
  161. package/lib/runtime/startup-notices.js +35 -6
  162. package/lib/runtime/startup-notices.js.map +1 -1
  163. package/lib/runtime/subagent-model-selection.js +2 -1
  164. package/lib/runtime/subagent-model-selection.js.map +1 -1
  165. package/lib/runtime/subagent-settings.d.ts +14 -0
  166. package/lib/runtime/subagent-settings.js +288 -0
  167. package/lib/runtime/subagent-settings.js.map +1 -0
  168. package/lib/runtime/terminal-notifications.d.ts +3 -0
  169. package/lib/runtime/terminal-notifications.js +13 -8
  170. package/lib/runtime/terminal-notifications.js.map +1 -1
  171. package/lib/runtime/terminal-probe.d.ts +2 -0
  172. package/lib/runtime/terminal-probe.js +9 -0
  173. package/lib/runtime/terminal-probe.js.map +1 -1
  174. package/lib/runtime/tool-presentation.js.map +1 -1
  175. package/lib/runtime/tool-reader.d.ts +5 -0
  176. package/lib/runtime/tool-reader.js +27 -0
  177. package/lib/runtime/tool-reader.js.map +1 -0
  178. package/lib/runtime/web-settings.d.ts +14 -0
  179. package/lib/runtime/web-settings.js +339 -0
  180. package/lib/runtime/web-settings.js.map +1 -0
  181. package/lib/session/command-attachments.d.ts +17 -0
  182. package/lib/session/command-attachments.js +50 -0
  183. package/lib/session/command-attachments.js.map +1 -0
  184. package/lib/session/context-diagnostics.d.ts +16 -0
  185. package/lib/session/context-diagnostics.js +15 -0
  186. package/lib/session/context-diagnostics.js.map +1 -1
  187. package/lib/session/external-agents.d.ts +3 -1
  188. package/lib/session/external-agents.js +35 -10
  189. package/lib/session/external-agents.js.map +1 -1
  190. package/lib/session/generation-continuation.d.ts +11 -0
  191. package/lib/session/generation-continuation.js +34 -0
  192. package/lib/session/generation-continuation.js.map +1 -0
  193. package/lib/session/goal-controls.d.ts +5 -0
  194. package/lib/session/goal-controls.js +81 -0
  195. package/lib/session/goal-controls.js.map +1 -0
  196. package/lib/session/interaction-adapter.d.ts +7 -3
  197. package/lib/session/interaction-adapter.js +121 -49
  198. package/lib/session/interaction-adapter.js.map +1 -1
  199. package/lib/session/message-queue.d.ts +2 -0
  200. package/lib/session/message-queue.js +102 -17
  201. package/lib/session/message-queue.js.map +1 -1
  202. package/lib/session/pending-inputs.d.ts +30 -0
  203. package/lib/session/pending-inputs.js +183 -0
  204. package/lib/session/pending-inputs.js.map +1 -0
  205. package/lib/session/session-configuration.js +1 -1
  206. package/lib/session/session-configuration.js.map +1 -1
  207. package/lib/session/session-controller.d.ts +33 -20
  208. package/lib/session/session-controller.js +500 -129
  209. package/lib/session/session-controller.js.map +1 -1
  210. package/lib/session/session-library.d.ts +3 -1
  211. package/lib/session/session-library.js +15 -0
  212. package/lib/session/session-library.js.map +1 -1
  213. package/lib/session/subagent-roster.d.ts +1 -0
  214. package/lib/session/subagent-roster.js +16 -5
  215. package/lib/session/subagent-roster.js.map +1 -1
  216. package/lib/session/subagent-tracker.d.ts +2 -0
  217. package/lib/session/subagent-tracker.js +18 -0
  218. package/lib/session/subagent-tracker.js.map +1 -1
  219. package/lib/session/tui-settings.d.ts +9 -5
  220. package/lib/session/tui-settings.js +3 -1
  221. package/lib/session/tui-settings.js.map +1 -1
  222. package/lib/session/workflow-projection.d.ts +2 -1
  223. package/lib/session/workflow-projection.js +34 -4
  224. package/lib/session/workflow-projection.js.map +1 -1
  225. package/lib/views/autocomplete.d.ts +12 -12
  226. package/lib/views/autocomplete.js +54 -45
  227. package/lib/views/autocomplete.js.map +1 -1
  228. package/lib/views/context-overview.d.ts +4 -0
  229. package/lib/views/context-overview.js +43 -0
  230. package/lib/views/context-overview.js.map +1 -0
  231. package/lib/views/copy-targets.d.ts +6 -2
  232. package/lib/views/copy-targets.js +19 -14
  233. package/lib/views/copy-targets.js.map +1 -1
  234. package/lib/views/document-reader.d.ts +10 -1
  235. package/lib/views/document-reader.js +63 -6
  236. package/lib/views/document-reader.js.map +1 -1
  237. package/lib/views/event-views.js +29 -11
  238. package/lib/views/event-views.js.map +1 -1
  239. package/lib/views/file-actions.js +21 -0
  240. package/lib/views/file-actions.js.map +1 -1
  241. package/lib/views/file-browser.d.ts +98 -0
  242. package/lib/views/file-browser.js +120 -0
  243. package/lib/views/file-browser.js.map +1 -0
  244. package/lib/views/history-search.js +4 -4
  245. package/lib/views/history-search.js.map +1 -1
  246. package/lib/views/hotkey-format.d.ts +3 -2
  247. package/lib/views/hotkey-format.js +4 -3
  248. package/lib/views/hotkey-format.js.map +1 -1
  249. package/lib/views/hotkeys.js +10 -12
  250. package/lib/views/hotkeys.js.map +1 -1
  251. package/lib/views/list-view.js +16 -11
  252. package/lib/views/list-view.js.map +1 -1
  253. package/lib/views/prompt-selector.d.ts +23 -1
  254. package/lib/views/prompt-selector.js +123 -60
  255. package/lib/views/prompt-selector.js.map +1 -1
  256. package/lib/views/settings-list.d.ts +6 -1
  257. package/lib/views/settings-list.js +69 -46
  258. package/lib/views/settings-list.js.map +1 -1
  259. package/lib/views/tool-actions.d.ts +16 -0
  260. package/lib/views/tool-actions.js +29 -0
  261. package/lib/views/tool-actions.js.map +1 -0
  262. package/lib/views/trajectory-timeline.d.ts +5 -6
  263. package/lib/views/trajectory-timeline.js +25 -15
  264. package/lib/views/trajectory-timeline.js.map +1 -1
  265. package/lib/views/trajectory.d.ts +23 -2
  266. package/lib/views/trajectory.js +404 -186
  267. package/lib/views/trajectory.js.map +1 -1
  268. package/lib/views/transcript-render.d.ts +39 -15
  269. package/lib/views/transcript-render.js +406 -208
  270. package/lib/views/transcript-render.js.map +1 -1
  271. package/lib/views/transcript-types.d.ts +3 -2
  272. package/lib/views/transcript-types.js.map +1 -1
  273. package/lib/views/workflow.d.ts +16 -0
  274. package/lib/views/workflow.js +42 -0
  275. package/lib/views/workflow.js.map +1 -0
  276. package/package.json +61 -58
  277. package/lib/chrome/welcome-tips.d.ts +0 -23
  278. package/lib/chrome/welcome-tips.js +0 -24
  279. package/lib/chrome/welcome-tips.js.map +0 -1
  280. package/lib/session/steering.d.ts +0 -3
  281. package/lib/session/steering.js +0 -9
  282. package/lib/session/steering.js.map +0 -1
@@ -1,4 +1,5 @@
1
1
  /** Interactive terminal selector used by resume, approval, and user questions. */
2
+ import { uiText } from "../locale/index.js";
2
3
  import { imageSize } from "../chrome/terminal-images.js";
3
4
  import { documentLayout, documentMatches, documentModel, documentPosition, documentStart } from "./document-reader.js";
4
5
  import { promptTreeRows } from "./prompt-tree.js";
@@ -6,7 +7,7 @@ import { rankSearchResults } from "../input/fuzzy-search.js";
6
7
  import { renderEditor, renderFramedBlock } from "../chrome/box.js";
7
8
  import { renderMarkdown } from "../chrome/markdown.js";
8
9
  import { BOX, SYMBOL } from "../chrome/theme.js";
9
- import { padToWidth, sliceCells, stripAnsi, truncateToWidth, visibleWidth, wrapTextStable } from "../chrome/width.js";
10
+ import { expandTabs, padToWidth, sliceCells, stripAnsi, truncateToWidth, visibleWidth, wrapTextStable } from "../chrome/width.js";
10
11
  import { formatHotkeyKeys, formatOverlayHint } from "./hotkey-format.js";
11
12
  import { renderListRow } from "./list-view.js";
12
13
  /** Maximum option rows retained in the prompt overlay before it windows. */
@@ -21,6 +22,7 @@ const HOTKEY_CHOOSE = { keys: '←→', action: 'Choose — move between the pla
21
22
  const HOTKEY_PAGE = { keys: 'PgUp / PgDn', action: 'Scroll — page the options or the plan' };
22
23
  const HOTKEY_EDGE = { keys: 'Home / End', action: 'Jump to the first or last option, or the plan edges' };
23
24
  const HOTKEY_TOGGLE = { keys: 'Space', action: 'Toggle — check or clear a multi-select option' };
25
+ const HOTKEY_ANSWER_TOGGLE = { keys: 'Ctrl+G', action: 'Toggle — check or clear a multi-select option' };
24
26
  const HOTKEY_SELECT = { keys: 'Enter', action: 'Select the highlighted option' };
25
27
  const HOTKEY_SUBMIT = { keys: 'Enter', action: 'Submit — send revision feedback, or keep planning when empty' };
26
28
  const HOTKEY_SUBMIT_NEWLINE = { keys: 'Ctrl+J', action: 'Submit — send revision feedback like Enter' };
@@ -38,6 +40,7 @@ export const PROMPT_SELECTOR_HOTKEYS = [
38
40
  HOTKEY_PAGE,
39
41
  HOTKEY_EDGE,
40
42
  HOTKEY_TOGGLE,
43
+ HOTKEY_ANSWER_TOGGLE,
41
44
  HOTKEY_SELECT,
42
45
  HOTKEY_SUBMIT,
43
46
  HOTKEY_SUBMIT_NEWLINE,
@@ -58,6 +61,8 @@ export function filteredPromptOptions(request, query, collapsed) {
58
61
  const options = request.options ?? [];
59
62
  if (request.filterable !== true)
60
63
  return options;
64
+ if (request.filterOptions !== undefined)
65
+ return request.filterOptions(query);
61
66
  return rankSearchResults(options, query, option => [option.label, option.value, option.preview, option.description, option.badge?.label]
62
67
  .filter((value) => value !== undefined));
63
68
  }
@@ -75,7 +80,7 @@ export function refreshPromptOptions(state, options, query) {
75
80
  const previewCache = new WeakMap();
76
81
  function treePreview(option, theme, width) {
77
82
  if (option === undefined)
78
- return ['No matching conversation turns.'];
83
+ return [theme.text('No matching conversation turns.')];
79
84
  let cache = previewCache.get(option);
80
85
  if (cache === undefined) {
81
86
  cache = new Map();
@@ -97,9 +102,9 @@ function treePreview(option, theme, width) {
97
102
  cache.set(key, lines);
98
103
  return lines;
99
104
  }
100
- function treePageLayout(request, width, height) {
105
+ function treePageLayout(request, width, height, language = 'en') {
101
106
  const actions = (request.actions ?? []).map(item => `${item.key} ${item.label}`).join(' · ');
102
- const hint = `↑↓ · ←→ fold${width >= 90 ? ' · type search' : ''}${actions === '' ? '' : ` · ${actions}`}${width >= 90 ? ' · Ctrl+↑↓ preview' : ''}`;
107
+ const hint = [uiText(language, '↑↓ · ←→ fold'), ...(width >= 90 ? [uiText(language, 'type search')] : []), ...(actions === '' ? [] : [actions]), ...(width >= 90 ? [uiText(language, 'Ctrl+↑↓ preview')] : [])].join(' · ');
103
108
  const hints = wrapTextStable(hint, width - 4).slice(0, Math.max(1, Math.floor((height - 6) / 2)));
104
109
  const middle = height - 6 - hints.length;
105
110
  const listHeight = width >= 90 ? middle : Math.max(1, Math.floor((middle - 1) / 2));
@@ -107,8 +112,8 @@ function treePageLayout(request, width, height) {
107
112
  return { hints, middle, listHeight, previewHeight };
108
113
  }
109
114
  /** Page movement follows the list space remaining after wrapped action hints. */
110
- export function promptTreePageSize(request, width, height) {
111
- return height < 10 || width < 20 ? Math.max(1, height - 8) : treePageLayout(request, width, height).listHeight;
115
+ export function promptTreePageSize(request, width, height, language = 'en') {
116
+ return height < 10 || width < 20 ? Math.max(1, height - 8) : treePageLayout(request, width, height, language).listHeight;
112
117
  }
113
118
  /** A hierarchical choice with an independently scrollable content preview. */
114
119
  export function renderPromptTreePage(state, theme, width, height, input, inputCursor, appName) {
@@ -119,7 +124,7 @@ export function renderPromptTreePage(state, theme, width, height, input, inputCu
119
124
  const option = rows[selected]?.option;
120
125
  const split = width >= 90;
121
126
  const inner = width - 4;
122
- const { hints, middle, listHeight, previewHeight } = treePageLayout(state.request, width, height);
127
+ const { hints, middle, listHeight, previewHeight } = treePageLayout(state.request, width, height, theme.language);
123
128
  const listWidth = split ? Math.floor((inner - 3) * 0.45) : inner;
124
129
  const previewWidth = split ? inner - listWidth - 3 : inner;
125
130
  const range = promptSelectorVisibleRange(rows.length, selected, listHeight);
@@ -155,7 +160,7 @@ export function renderPromptTreePage(state, theme, width, height, input, inputCu
155
160
  list.push(fit(head + text + description, listWidth));
156
161
  }
157
162
  if (rows.length === 0)
158
- list.push(fit(theme.fg('dim', state.request.emptyText ?? 'No matching conversation turns.'), listWidth));
163
+ list.push(fit(theme.fg('dim', state.request.emptyText ?? theme.text('No matching conversation turns.')), listWidth));
159
164
  const preview = treePreview(option, theme, Math.max(1, previewWidth));
160
165
  const maxScroll = Math.max(0, preview.length - previewHeight);
161
166
  const scroll = Math.max(0, Math.min(state.previewScroll ?? 0, maxScroll));
@@ -173,8 +178,9 @@ export function renderPromptTreePage(state, theme, width, height, input, inputCu
173
178
  lines.push(pageRow(theme, preview[scroll + at] ?? '', width));
174
179
  }
175
180
  const action = option?.submitLabel ?? state.request.submitLabel ?? 'select';
176
- lines.push(pageRow(theme, theme.fg('dim', `Enter ${action} · Esc back`), width), ...hints.map(hint => pageRow(theme, theme.fg('dim', hint), width)), pageBottom(theme, width));
177
- return { lines, cursor: { row: 2, column: Math.min(width - 3, 4 + visibleWidth(input.slice(0, inputCursor))) },
181
+ const navigation = option === undefined || option.disabled === true ? theme.text('Esc back') : theme.text(`Enter ${action} · Esc back`);
182
+ lines.push(pageRow(theme, theme.fg('dim', navigation), width), ...hints.map(hint => pageRow(theme, theme.fg('dim', hint), width)), pageBottom(theme, width));
183
+ return { lines, choices: rows.slice(range.start, range.end).map((row, index) => ({ row: 4 + index, column: 2, width: listWidth, index: range.start + index, value: row.option.value ?? row.option.label })), cursor: { row: 2, column: Math.min(width - 3, 4 + visibleWidth(input.slice(0, inputCursor))) },
178
184
  document: { start: scroll, maxStart: maxScroll, pageSize: previewHeight }, cursorVisible: true };
179
185
  }
180
186
  /** Fold/unfold a subtree or move to its parent, preserving selection by value. */
@@ -344,6 +350,7 @@ export function promptNeedsPage(request) {
344
350
  /** Render a bounded, scrollable Markdown document with fixed actions. */
345
351
  function documentActionRows(state, theme, width, pageHeight) {
346
352
  const actionRows = [];
353
+ const actions = [];
347
354
  let actionRow = '';
348
355
  const actionWidth = Math.max(1, width - 5);
349
356
  for (const [index, option] of (state.request.options ?? []).entries()) {
@@ -352,6 +359,7 @@ function documentActionRows(state, theme, width, pageHeight) {
352
359
  actionRows.push(actionRow);
353
360
  actionRow = '';
354
361
  }
362
+ actions.push({ row: actionRows.length, column: 3 + visibleWidth(actionRow) + (actionRow === '' ? 0 : 3), width: visibleWidth(label), index });
355
363
  const painted = index === state.selected ? theme.bold(theme.fg('accent', label)) : theme.fg('muted', label);
356
364
  actionRow += (actionRow === '' ? '' : theme.fg('dim', ' ')) + painted;
357
365
  }
@@ -360,8 +368,9 @@ function documentActionRows(state, theme, width, pageHeight) {
360
368
  const selected = state.request.options?.[state.selected];
361
369
  actionRows.splice(0, actionRows.length, selected === undefined ? ''
362
370
  : theme.bold(theme.fg('accent', truncateToWidth(`› [ ${selected.label} ]`, actionWidth))));
371
+ actions.splice(0, actions.length, ...(selected === undefined ? [] : [{ row: 0, column: 3, width: visibleWidth(actionRows[0]), index: state.selected }]));
363
372
  }
364
- return actionRows;
373
+ return { lines: actionRows, actions };
365
374
  }
366
375
  /** Reserve a bordered, padded rectangle; graphics never become display-row content. */
367
376
  export function renderImagePreviewPage(state, theme, width, height, appName, protocol, cell) {
@@ -369,7 +378,7 @@ export function renderImagePreviewPage(state, theme, width, height, appName, pro
369
378
  if (width < 16 || height < 10)
370
379
  return renderPromptSelector({ ...state, request: { ...state.request,
371
380
  detail: image.description + '\nEnlarge the terminal or use Open to view the image.' } }, theme, width, '', 0, Math.max(1, height - 8));
372
- const actions = documentActionRows(state, theme, width, height);
381
+ const { lines: actions } = documentActionRows(state, theme, width, height);
373
382
  const hints = 'Tab/←→ actions · Enter activate · Esc back';
374
383
  const bodyRows = Math.max(1, height - 8 - actions.length);
375
384
  const lines = [pageTop(theme, width, appName + ' · ' + state.request.title), pageRow(theme, state.request.question, width),
@@ -393,11 +402,19 @@ export function renderPlanReviewPage(state, theme, width, height, input, inputCu
393
402
  return renderPromptSelector(source === undefined ? state : { ...state, request: { ...state.request, detail: source.text } }, theme, width, input, inputCursor, Math.max(1, pageHeight - 8));
394
403
  }
395
404
  const feedback = state.feedback === true;
396
- const actionRows = documentActionRows(state, theme, width, pageHeight);
405
+ const actionLayout = documentActionRows(state, theme, width, pageHeight);
406
+ const actionRows = actionLayout.lines;
397
407
  const readerInput = state.documentInput !== undefined;
398
408
  const customHints = (state.request.actions ?? []).map(action => `${action.key.toUpperCase()} ${action.label}`);
409
+ const renderedReader = source === undefined && state.request.presentation === 'document';
410
+ let renderedHints = renderedReader ? wrapTextStable(readerInput ? 'Enter find · Esc cancel'
411
+ : ['↑↓/PgUp/PgDn/wheel scroll', '/ find', 'Ctrl+N/P match', ...customHints,
412
+ ...(state.request.options && state.request.options.length > 1 ? ['Tab actions · Enter'] : []), 'Esc back'].join(' · '), Math.max(1, width - 4)) : [];
413
+ const maxRenderedHints = Math.max(1, pageHeight - (state.request.question === '' ? 5 : 6) - actionRows.length - (readerInput || state.documentQuery ? 1 : 0));
414
+ if (renderedHints.length > maxRenderedHints)
415
+ renderedHints = [...renderedHints.slice(0, maxRenderedHints - 1), '/ find · Esc back'];
399
416
  let readerHints = source === undefined ? [] : wrapTextStable(readerInput ? 'Enter apply · Esc cancel'
400
- : (state.request.interrupt === undefined ? '' : 'Ctrl+C interrupt · ') + 'W wrap · Alt+←→ pan · ↑↓/PgUp/PgDn scroll · ' + (state.request.documentTail === true ? 'Home start · End follow' : 'Home/End edges') + ' · / find · Ctrl+N/P match · G line' + (source.diff ? ' · [/] hunk' : '')
417
+ : (state.request.interrupt === undefined ? '' : 'Ctrl+C interrupt · ') + 'W wrap · Alt+←→ pan · ↑↓/PgUp/PgDn/wheel scroll · ' + (state.request.documentTail === true ? 'Home start · End follow' : 'Home/End edges') + ' · / find · Ctrl+N/P match · G line' + (source.diff ? ' · [/] hunk' : '')
401
418
  + ' · ' + ['Tab/←→ actions', 'Enter activate', ...customHints, 'Esc back'].join(' · '), width - 4);
402
419
  const maxHints = Math.max(1, Math.min(4, pageHeight - 8 - actionRows.length));
403
420
  if (readerHints.length > maxHints)
@@ -405,21 +422,28 @@ export function renderPlanReviewPage(state, theme, width, height, input, inputCu
405
422
  const compactReader = source !== undefined && pageHeight < 10;
406
423
  if (compactReader)
407
424
  readerHints = [];
408
- const footerRows = feedback ? 5 : actionRows.length + 3 + (source === undefined ? 0 : readerHints.length);
409
- const bodyRows = Math.max(1, pageHeight - (source === undefined ? 5 : compactReader ? 2 : 3) - footerRows);
425
+ const renderedSearchRows = readerInput || state.documentQuery ? 1 : 0;
426
+ const hintRows = renderedReader ? renderedHints.length - 1 + renderedSearchRows : readerHints.length;
427
+ const footerRows = feedback ? 5 : actionRows.length + 3 + hintRows;
428
+ const headerRows = renderedReader ? 2 + Number(state.request.question !== '')
429
+ : source === undefined ? 5 : compactReader ? 2 : 3;
430
+ const bodyRows = Math.max(1, pageHeight - headerRows - footerRows);
410
431
  const markdownWidth = Math.max(1, width - 6);
411
432
  const layout = source === undefined ? undefined : documentLayout(source, theme, markdownWidth, {
412
433
  wordWrap: state.documentWordWrap ?? state.request.documentPosition?.wordWrap ?? true,
413
434
  column: state.documentColumn ?? state.request.documentPosition?.column ?? 0,
414
435
  });
415
- const document = layout?.rows ?? documentRows(state.request, theme, markdownWidth);
436
+ const textDocument = layout === undefined
437
+ ? state.renderDocument?.(theme, markdownWidth) ?? documentRows(state.request, theme, markdownWidth)
438
+ : undefined;
439
+ const document = layout?.rows ?? textDocument ?? [];
416
440
  const maxStart = Math.max(0, document.length - bodyRows);
417
441
  const start = Math.max(0, Math.min(state.documentScroll === Number.POSITIVE_INFINITY ? maxStart : layout !== undefined && state.documentAnchor !== undefined
418
442
  ? documentStart(layout, state.documentAnchor) : state.documentScroll ?? state.request.documentPosition?.row ?? 0, maxStart));
419
443
  const visible = document.slice(start, start + bodyRows);
420
444
  const query = state.documentQuery ?? '';
421
445
  const matches = source === undefined ? [] : documentMatches(source, query);
422
- const textMatches = source === undefined ? findInRows(document, query) : [];
446
+ const textMatches = textDocument === undefined ? [] : findInRows(textDocument, query);
423
447
  const matching = new Set(matches);
424
448
  if (layout !== undefined)
425
449
  for (let at = 0; at < visible.length; at++) {
@@ -447,16 +471,17 @@ export function renderPlanReviewPage(state, theme, width, height, input, inputCu
447
471
  }
448
472
  const lines = [
449
473
  pageTop(theme, width, `${appName} · ${compactReader ? state.request.question : state.request.title}`),
450
- ...(source === undefined ? [pageRow(theme, '', width)] : []),
451
- ...(compactReader ? [] : [pageRow(theme, ' ' + theme.bold(state.request.question), width)]),
452
- ...(source === undefined ? [pageRow(theme, '', width)] : []),
474
+ ...(source === undefined && !renderedReader ? [pageRow(theme, '', width)] : []),
475
+ ...(compactReader || renderedReader && state.request.question === '' ? [] : [pageRow(theme, ' ' + theme.bold(state.request.question), width)]),
476
+ ...(source === undefined && !renderedReader ? [pageRow(theme, '', width)] : []),
453
477
  pageDivider(theme, width),
454
478
  ...visible.map(line => pageRow(theme, ' ' + line, width)),
455
479
  pageDivider(theme, width),
456
480
  ];
481
+ const actions = [];
457
482
  let cursor = { row: Math.max(0, lines.length - 1), column: 1 };
458
483
  if (feedback) {
459
- lines.push(pageRow(theme, ' ' + theme.bold('Revision feedback') + theme.fg('dim', ' · optional'), width));
484
+ lines.push(pageRow(theme, ' ' + theme.bold(theme.text('Revision feedback')) + theme.fg('dim', ' · optional'), width));
460
485
  const prefix = theme.fg('accent', '> ');
461
486
  const available = Math.max(1, width - 8);
462
487
  const displayInput = input.replace(/\r?\n/gu, ' ↵ ');
@@ -465,7 +490,7 @@ export function renderPlanReviewPage(state, theme, width, height, input, inputCu
465
490
  const inputRow = lines.length;
466
491
  lines.push(pageRow(theme, ' ' + prefix + value, width));
467
492
  // "empty Enter keeps planning" is prose, but it names the key through the catalog.
468
- lines.push(pageRow(theme, theme.fg('dim', `[${formatOverlayHint([HOTKEY_SUBMIT, HOTKEY_BACK, HOTKEY_CANCEL_CTRL])} · Ctrl+X editor · empty ${formatHotkeyKeys(HOTKEY_SUBMIT)} keeps planning]`), width));
493
+ lines.push(pageRow(theme, theme.fg('dim', `[${formatOverlayHint([HOTKEY_SUBMIT, HOTKEY_BACK, HOTKEY_CANCEL_CTRL], 'lowercase', theme.language)} · Ctrl+X editor · empty ${formatHotkeyKeys(HOTKEY_SUBMIT)} keeps planning]`), width));
469
494
  lines.push(pageBottom(theme, width));
470
495
  cursor = {
471
496
  row: inputRow,
@@ -478,7 +503,7 @@ export function renderPlanReviewPage(state, theme, width, height, input, inputCu
478
503
  // itself: the query, how many hits it has, and where the reader is in them.
479
504
  const index = textMatches.findIndex(match => match.row === state.documentTarget);
480
505
  const hits = textMatches.length === 0
481
- ? 'No match'
506
+ ? theme.text('No match')
482
507
  : `${index < 0 ? 1 : index + 1}/${textMatches.length} matching rows`;
483
508
  const label = state.documentInput === 'search' ? 'Find: ' : '';
484
509
  const value = input.replace(/\r?\n/gu, ' ');
@@ -502,23 +527,26 @@ export function renderPlanReviewPage(state, theme, width, height, input, inputCu
502
527
  cursor = { row: lines.length, column: Math.min(width - 3, 2 + visibleWidth(label + value.slice(0, inputCursor))) };
503
528
  lines.push(pageRow(theme, theme.fg('muted', status), width));
504
529
  }
530
+ actions.push(...actionLayout.actions.map(action => ({ ...action, row: action.row + lines.length })));
505
531
  lines.push(...actionRows.map(row => pageRow(theme, ' ' + row, width)));
506
532
  const shortcuts = state.request.presentation === 'document' && state.request.actions?.length
507
533
  ? 'Tab/←→ select · Enter activate · ' + state.request.actions.map(action => `${action.key.toUpperCase()} ${action.label}`).join(' · ') + ' · Esc back'
508
- : formatOverlayHint([HOTKEY_PAGE, HOTKEY_NAVIGATE_TAB, HOTKEY_SELECT, HOTKEY_CANCEL])
509
- + (state.request.presentation === 'plan-review' ? ' · Ctrl+X editor' : '');
534
+ : formatOverlayHint([HOTKEY_PAGE, HOTKEY_NAVIGATE_TAB, HOTKEY_SELECT, HOTKEY_CANCEL], 'lowercase', theme.language)
535
+ + (state.request.presentation === 'plan-review' && state.request.allowCustom !== false ? ' · Ctrl+X editor' : '');
510
536
  // A page long enough to need scrolling is a page someone will want to search.
511
537
  const pageHint = source === undefined && state.request.presentation === 'document'
512
538
  ? `[${shortcuts} · / find · Ctrl+N/P match]`
513
539
  : `[${shortcuts}]`;
514
- lines.push(...(source === undefined ? [pageHint] : readerHints).map(hint => pageRow(theme, theme.fg('dim', hint), width)));
540
+ const hints = renderedReader ? renderedHints : source === undefined ? [pageHint] : readerHints;
541
+ lines.push(...hints.map(hint => pageRow(theme, theme.fg('dim', hint), width)));
515
542
  lines.push(pageBottom(theme, width));
516
543
  }
517
544
  return {
518
545
  lines,
519
546
  cursor,
547
+ actions,
520
548
  document: { start, maxStart, pageSize: Math.max(1, bodyRows - 1),
521
- ...(layout === undefined ? state.request.presentation === 'document' ? { position: { row: start, wrap: 0, query: '' } } : {} : { position: { ...documentPosition(layout, start, query),
549
+ ...(layout === undefined ? state.request.presentation === 'document' ? { position: { row: start, wrap: 0, query } } : {} : { position: { ...documentPosition(layout, start, query),
522
550
  ...(state.request.documentTail === true ? { following: state.documentScroll === Number.POSITIVE_INFINITY } : {}) } }) },
523
551
  cursorVisible: feedback || readerInput,
524
552
  };
@@ -535,39 +563,48 @@ export function renderPromptSelector(state, theme, width, input, inputCursor, ma
535
563
  const filtering = input !== '';
536
564
  const contentWidth = Math.max(1, width - 4);
537
565
  const body = [state.request.question];
566
+ const choices = [];
567
+ const actions = [];
568
+ const bodyRow = () => 1 + body.reduce((count, line) => count + wrapTextStable(expandTabs(line, 8, 2), contentWidth).length, 0);
538
569
  if (state.request.wait !== undefined) {
539
570
  const remaining = Math.max(0, Math.ceil((state.request.wait.deadline - Date.now()) / 1_000));
540
571
  body.push(theme.fg('dim', state.waitHeld === true
541
- ? 'Take your time · waiting for your answer'
542
- : `Continues in ${remaining}s · ${formatHotkeyKeys(HOTKEY_TAKE_TIME)} take time`));
572
+ ? theme.text('Take your time · waiting for your answer')
573
+ : theme.text('Continues in {seconds}s · {key} take time', { seconds: remaining, key: formatHotkeyKeys(HOTKEY_TAKE_TIME) })));
543
574
  }
544
575
  if (state.request.detail !== undefined && state.request.detail !== '')
545
576
  body.push('', state.request.detail);
546
577
  if (options.length > 0) {
547
578
  const { start, end } = promptSelectorVisibleRange(options.length, state.selected, maxVisible);
548
- body.push('', ...options.slice(start, end).map((option, offset) => optionRow(option, start + offset, state, theme, contentWidth, (state.request.options ?? []).indexOf(option))));
579
+ body.push('');
580
+ for (let index = start; index < end; index++) {
581
+ const option = options[index];
582
+ choices.push({ row: bodyRow(), column: 2, width: contentWidth, index, value: option.value ?? option.label });
583
+ body.push(optionRow(option, index, state, theme, contentWidth, (state.request.options ?? []).indexOf(option)));
584
+ }
549
585
  if (options.length > maxVisible) {
550
- body.push(theme.fg('dim', ` ${state.selected + 1}/${options.length} · scroll for more`));
586
+ body.push(theme.fg('dim', ` ${state.selected + 1}/${options.length} · ${theme.text('scroll for more')}`));
551
587
  }
552
588
  // The paged list draws the selected row's preview beside the rows; the card
553
589
  // gives it three lines underneath, so demoting a previewed list keeps the
554
590
  // content it was being browsed for.
555
591
  const preview = options[state.selected]?.preview;
556
592
  if (preview !== undefined && preview !== '') {
557
- const wrapped = state.request.options?.[state.selected]?.previewFormat === 'markdown'
558
- ? renderMarkdown(preview, theme, contentWidth)
559
- : wrapTextStable(preview, contentWidth);
593
+ const previewWidth = Math.max(1, contentWidth - 2);
594
+ const wrapped = options[state.selected]?.previewFormat === 'markdown'
595
+ ? renderMarkdown(preview, theme, previewWidth)
596
+ : wrapTextStable(expandTabs(preview, 8, 4), previewWidth);
560
597
  body.push('', ...wrapped.slice(0, CARD_PREVIEW_LINES).map(line => theme.fg('dim', ' ' + line)));
561
598
  if (wrapped.length > CARD_PREVIEW_LINES) {
562
599
  body.push(theme.fg('dim', ` … ${wrapped.length - CARD_PREVIEW_LINES} more lines`));
563
600
  }
564
601
  }
565
602
  }
566
- else {
603
+ else if (state.request.options !== undefined) {
567
604
  // A filter that matches nothing has to say so. An empty frame reads as a
568
605
  // broken list, and the reader cannot tell a typo from an empty transcript.
569
606
  body.push('', theme.fg('dim', ` ${state.request.emptyText
570
- ?? (filtering ? `No match for ${quote(input)}.` : 'Nothing to choose.')}`));
607
+ ?? (filtering ? theme.text('No match for {query}.', { query: quote(input) }) : theme.text('Nothing to choose.'))}`));
571
608
  }
572
609
  /**
573
610
  * The query, drawn where the reader types it.
@@ -578,40 +615,57 @@ export function renderPromptSelector(state, theme, width, input, inputCursor, ma
578
615
  * hits looked broken rather than empty.
579
616
  */
580
617
  const filterRow = state.request.filterable === true && state.request.presentation !== 'progress'
581
- ? body.length
618
+ ? bodyRow() + 1
582
619
  : undefined;
583
620
  if (filterRow !== undefined) {
584
621
  // The query and what it kept belong on one line: the reader typed one thing
585
622
  // and the list answered with a count.
586
623
  const count = filtering ? theme.fg('dim', ` · ${options.length === 0 ? 'no match' : `${state.selected + 1}/${options.length}`}`) : '';
587
624
  body.push('', input === ''
588
- ? theme.fg('muted', ' ⌕ type to filter') + count
589
- : theme.fg('text', ' ⌕ ') + input + count);
625
+ ? theme.fg('muted', ' ' + theme.text('type to filter'))
626
+ : ' ' + theme.fg('text', input) + count);
590
627
  }
591
628
  // A request that has options but no match has nothing to submit: advertising
592
629
  // `enter select` there promises a key the handler swallows.
593
- const exhausted = options.length === 0 && (state.request.options?.length ?? 0) > 0;
594
- const submit = state.request.submitLabel?.trim()
595
- || (options.length === 0 ? 'answer' : state.request.multiSelect === true ? 'confirm' : 'select');
630
+ const exhausted = options.length === 0 && state.request.options !== undefined && state.request.allowCustom === false;
631
+ const submit = (state.request.multiSelect === true ? state.request.submitLabel : options[state.selected]?.submitLabel ?? state.request.submitLabel)?.trim()
632
+ || (options.length === 0 ? theme.text('answer') : state.request.multiSelect === true ? theme.text('confirm') : theme.text('select'));
596
633
  // The request supplies the submit verb; the key itself comes from the catalog.
597
634
  const select = `${formatHotkeyKeys(HOTKEY_SELECT)} ${submit}`;
598
635
  const navigation = state.request.presentation === 'progress'
599
- ? formatOverlayHint([HOTKEY_CANCEL, HOTKEY_CANCEL_CTRL], 'catalog')
636
+ ? formatOverlayHint([HOTKEY_CANCEL, HOTKEY_CANCEL_CTRL], 'catalog', theme.language)
600
637
  : exhausted
601
- ? formatOverlayHint([HOTKEY_CANCEL])
638
+ ? formatOverlayHint([HOTKEY_CANCEL], 'lowercase', theme.language)
602
639
  : options.length === 0
603
- ? `${select} · ${formatOverlayHint([HOTKEY_CANCEL])}`
640
+ ? `${select} · ${formatOverlayHint([HOTKEY_CANCEL], 'lowercase', theme.language)}`
604
641
  : state.request.multiSelect === true
605
- ? `${formatOverlayHint([HOTKEY_NAVIGATE])} · ${formatOverlayHint([HOTKEY_TOGGLE])} · ${select} · ${formatOverlayHint([HOTKEY_CANCEL])}`
606
- : `${formatOverlayHint([HOTKEY_NAVIGATE])} · ${select} · ${formatOverlayHint([HOTKEY_CANCEL])}`;
642
+ ? `${formatOverlayHint([HOTKEY_NAVIGATE], 'lowercase', theme.language)} · ${formatOverlayHint([state.request.answerFormat === 'json' ? HOTKEY_ANSWER_TOGGLE : HOTKEY_TOGGLE], 'lowercase', theme.language)} · ${formatOverlayHint([HOTKEY_CANCEL], 'lowercase', theme.language)}`
643
+ : `${formatOverlayHint([HOTKEY_NAVIGATE], 'lowercase', theme.language)} · ${formatOverlayHint([HOTKEY_CANCEL], 'lowercase', theme.language)}`;
607
644
  const dismiss = state.request.dismissLabel;
608
645
  const actionHints = (state.request.actions ?? []).map(action => `${action.key} ${action.label}`);
609
646
  const hints = [
610
- dismiss === undefined ? navigation : navigation.replace(formatOverlayHint([HOTKEY_CANCEL]), `${formatHotkeyKeys(HOTKEY_CANCEL)} ${dismiss.toLowerCase()}`),
611
- ...(state.request.skippable === true ? [formatOverlayHint([HOTKEY_SKIP])] : []),
612
- ...actionHints,
647
+ dismiss === undefined ? navigation : navigation.replace(formatOverlayHint([HOTKEY_CANCEL], 'lowercase', theme.language), `${formatHotkeyKeys(HOTKEY_CANCEL)} ${dismiss.toLowerCase()}`),
648
+ ...(state.request.skippable === true ? [formatOverlayHint([HOTKEY_SKIP], 'lowercase', theme.language)] : []),
613
649
  ];
614
- body.push('', theme.fg('dim', hints.join(' · ')));
650
+ body.push('', theme.fg('dim', theme.text(hints.join(' · '))));
651
+ if (state.request.options !== undefined) {
652
+ const buttons = [...(options.length === 0 ? [] : [{ key: 'enter', label: submit }]), ...(state.request.actions ?? []).filter(action => options.length > 0 || action.scope === 'list')];
653
+ let line = '';
654
+ for (const button of buttons) {
655
+ const label = truncateToWidth(`[ ${button.key === 'enter' ? 'Enter' : button.key} ${button.label} ]`, contentWidth);
656
+ if (line !== '' && visibleWidth(line + ' ' + label) > contentWidth) {
657
+ body.push(line);
658
+ line = '';
659
+ }
660
+ const gap = line === '' ? '' : ' ';
661
+ actions.push({ row: bodyRow(), column: 2 + visibleWidth(line + gap), width: visibleWidth(label), index: -1, key: button.key.toLowerCase() });
662
+ line += gap + theme.fg('accent', label);
663
+ }
664
+ if (line !== '')
665
+ body.push(line);
666
+ }
667
+ else if (actionHints.length > 0)
668
+ body.push(theme.fg('dim', actionHints.join(' · ')));
615
669
  const card = renderFramedBlock({
616
670
  header: state.request.title,
617
671
  state: 'warning',
@@ -620,14 +674,14 @@ export function renderPromptSelector(state, theme, width, input, inputCursor, ma
620
674
  applyBg: false,
621
675
  }, theme);
622
676
  if (state.request.allowCustom === false || state.request.presentation === 'progress') {
623
- // With a filter line the caret belongs at its end, where the next character
624
- // will land; without one it stays on body padding, because the selector glyph
677
+ // With a filter line the caret follows the editor's insertion point;
678
+ // without one it stays on body padding, because the selector glyph
625
679
  // owns focus while no text editor is present.
626
680
  return {
627
- lines: card,
681
+ lines: card, choices, actions,
628
682
  cursor: filterRow === undefined
629
683
  ? { row: Math.min(1, Math.max(0, card.length - 1)), column: 1 }
630
- : { row: filterRow + 2, column: 4 + visibleWidth(input) },
684
+ : { row: filterRow, column: 4 + visibleWidth(input.slice(0, inputCursor)) },
631
685
  cursorVisible: filterRow !== undefined,
632
686
  };
633
687
  }
@@ -656,10 +710,10 @@ export function renderPromptSelector(state, theme, width, input, inputCursor, ma
656
710
  input: displayInput,
657
711
  inputCursor,
658
712
  status: ' ' + theme.fg('accent', label) + ' ',
659
- ...(state.request.question === '' ? {} : { inlineHint: state.request.question }),
713
+ ...(displayInput !== '' || state.request.question === '' ? {} : { inlineHint: state.request.question }),
660
714
  // The keys live on the bottom rule so the frame says how to answer without
661
715
  // spending a row on it.
662
- ...(hints.length === 0 ? {} : { note: hints.join(' · ') }),
716
+ ...(hints.length === 0 ? {} : { note: theme.text(hints.join(' · ')) }),
663
717
  border: 'accent',
664
718
  }, theme);
665
719
  return { lines: editor.lines, cursor: editor.cursor, cursorVisible: true };
@@ -668,12 +722,12 @@ export function renderPromptSelector(state, theme, width, input, inputCursor, ma
668
722
  width,
669
723
  input: displayInput,
670
724
  inputCursor,
671
- status: theme.fg('muted', secret ? 'API key · hidden' : input === '' ? 'answer' : 'custom answer'),
725
+ status: theme.fg('muted', secret ? theme.text('API key · hidden') : input === '' ? theme.text('answer') : theme.text('custom answer')),
672
726
  border: 'accent',
673
727
  }, theme);
674
728
  const editorStart = card.length + 1;
675
729
  return {
676
- lines: [...card, '', ...editor.lines],
730
+ lines: [...card, '', ...editor.lines], choices, actions,
677
731
  cursor: { row: editorStart + editor.cursor.row, column: editor.cursor.column },
678
732
  cursorVisible: true,
679
733
  };
@@ -700,6 +754,15 @@ export function togglePromptSelection(state, query = '') {
700
754
  checked.add(index);
701
755
  return { ...state, checked };
702
756
  }
757
+ /** Keep option values and free text separate, including punctuation in labels. */
758
+ export function structuredPromptAnswer(state, custom) {
759
+ const options = state.request.options ?? [];
760
+ const selected = state.request.multiSelect === true
761
+ ? options.flatMap((option, index) => state.checked.has(index) && !option.disabled ? [option.value ?? option.label] : [])
762
+ : custom.trim() !== '' || options[state.selected] === undefined || options[state.selected]?.disabled === true
763
+ ? [] : [options[state.selected].value ?? options[state.selected].label];
764
+ return { selected, ...(custom.trim() === '' ? {} : { custom: custom.trim() }) };
765
+ }
703
766
  /** Resolve the selected labels in stable option order. */
704
767
  export function selectedPromptAnswer(state) {
705
768
  const options = state.request.presentation === 'tree'