@linxiraos/pi-tui 1.2.1 → 1.3.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 (280) hide show
  1. package/CHANGELOG.md +245 -224
  2. package/README.md +36 -0
  3. package/THIRD-PARTY-NOTICES.txt +55 -5
  4. package/dist/types/app-keybindings.d.ts +18 -8
  5. package/dist/types/apps/git/diff-pane.d.ts +5 -0
  6. package/dist/types/apps/git/git-tui.d.ts +16 -2
  7. package/dist/types/apps/git/state.d.ts +11 -0
  8. package/dist/types/autocomplete.d.ts +2 -0
  9. package/dist/types/charts/chart-svg.d.ts +7 -13
  10. package/dist/types/chat/advisor-message.d.ts +4 -1
  11. package/dist/types/chat/assistant-message.d.ts +9 -4
  12. package/dist/types/chat/figure-markdown.d.ts +7 -2
  13. package/dist/types/chat/messages.d.ts +3 -1
  14. package/dist/types/chat/read-tool-group.d.ts +2 -2
  15. package/dist/types/chat/table-chart.d.ts +11 -15
  16. package/dist/types/chat/tool-execution.d.ts +32 -20
  17. package/dist/types/chat/user-message.d.ts +3 -2
  18. package/dist/types/chrome/countdown-timer.d.ts +3 -2
  19. package/dist/types/chrome/diff.d.ts +2 -3
  20. package/dist/types/chrome/status-notice.d.ts +2 -0
  21. package/dist/types/chrome/transcript-container.d.ts +2 -0
  22. package/dist/types/components/image.d.ts +2 -0
  23. package/dist/types/components/markdown.d.ts +49 -3
  24. package/dist/types/components/text.d.ts +2 -0
  25. package/dist/types/debug-server.d.ts +2 -0
  26. package/dist/types/keybinding-matchers.d.ts +6 -0
  27. package/dist/types/multiplexers/tmux.d.ts +1 -0
  28. package/dist/types/multiplexers/types.d.ts +6 -0
  29. package/dist/types/native/backend.d.ts +5 -4
  30. package/dist/types/native/blobs.d.ts +24 -12
  31. package/dist/types/overlays/agent-hub-projection.d.ts +10 -5
  32. package/dist/types/overlays/annotation-overlay.d.ts +19 -0
  33. package/dist/types/overlays/approval-dialog.d.ts +79 -0
  34. package/dist/types/overlays/ask-dialog.d.ts +11 -6
  35. package/dist/types/overlays/btw-history-panel.d.ts +2 -6
  36. package/dist/types/overlays/hook-editor.d.ts +7 -2
  37. package/dist/types/overlays/hook-input.d.ts +12 -4
  38. package/dist/types/overlays/job-tree.d.ts +50 -0
  39. package/dist/types/overlays/jobs-panel.d.ts +25 -11
  40. package/dist/types/overlays/login-dialog.d.ts +8 -3
  41. package/dist/types/overlays/mcp-add-wizard.d.ts +9 -1
  42. package/dist/types/overlays/model-browser.d.ts +6 -1
  43. package/dist/types/overlays/model-picker.d.ts +2 -0
  44. package/dist/types/overlays/pause-screen.d.ts +5 -0
  45. package/dist/types/overlays/plan-review-overlay.d.ts +4 -0
  46. package/dist/types/overlays/report-panel.d.ts +5 -1
  47. package/dist/types/overlays/reset-usage-selector.d.ts +7 -5
  48. package/dist/types/overlays/session-account-selector.d.ts +4 -0
  49. package/dist/types/overlays/session-info-overlay.d.ts +7 -3
  50. package/dist/types/overlays/session-selector.d.ts +3 -1
  51. package/dist/types/overlays/usage-dashboard.d.ts +6 -6
  52. package/dist/types/prompt/composer.d.ts +23 -20
  53. package/dist/types/prompt/custom-editor.d.ts +20 -10
  54. package/dist/types/prompt/image-references.d.ts +22 -0
  55. package/dist/types/prompt/internal-url-autocomplete.d.ts +1 -0
  56. package/dist/types/prompt/macos-spelling.d.ts +2 -1
  57. package/dist/types/prompt/prose-gate.d.ts +3 -0
  58. package/dist/types/prompt/queued-messages.d.ts +1 -1
  59. package/dist/types/prompt/typing-word.d.ts +25 -0
  60. package/dist/types/prompt/welcome.d.ts +26 -6
  61. package/dist/types/render/hanging-wrap.d.ts +7 -0
  62. package/dist/types/render/hyperlink.d.ts +2 -0
  63. package/dist/types/render/render-utils.d.ts +9 -1
  64. package/dist/types/render/utils.d.ts +1 -1
  65. package/dist/types/setup/scenes/splash.d.ts +6 -5
  66. package/dist/types/status-line/context-usage.d.ts +4 -1
  67. package/dist/types/status-line/host.d.ts +6 -4
  68. package/dist/types/status-line/startup.d.ts +6 -6
  69. package/dist/types/status-line/types.d.ts +3 -0
  70. package/dist/types/stdin-buffer.d.ts +7 -0
  71. package/dist/types/terminal-capabilities.d.ts +16 -1
  72. package/dist/types/terminal-multiplexer.d.ts +7 -0
  73. package/dist/types/terminal.d.ts +8 -4
  74. package/dist/types/theme/symbols.d.ts +1 -1
  75. package/dist/types/theme/theme.d.ts +11 -0
  76. package/dist/types/thinking.d.ts +5 -0
  77. package/dist/types/tmux.d.ts +39 -4
  78. package/dist/types/tools/ast-edit.d.ts +2 -2
  79. package/dist/types/tools/ast-grep.d.ts +2 -2
  80. package/dist/types/tools/bash.d.ts +6 -1
  81. package/dist/types/tools/edit.d.ts +2 -0
  82. package/dist/types/tools/eval.d.ts +53 -4
  83. package/dist/types/tools/find.d.ts +14 -4
  84. package/dist/types/tools/glob.d.ts +3 -3
  85. package/dist/types/tools/grep.d.ts +2 -2
  86. package/dist/types/tools/grouped-file-output.d.ts +9 -4
  87. package/dist/types/tools/native-view.d.ts +1 -1
  88. package/dist/types/tools/read.d.ts +32 -10
  89. package/dist/types/tools/renderer.d.ts +2 -0
  90. package/dist/types/tools/task.d.ts +31 -0
  91. package/dist/types/tools/web-search-types.d.ts +1 -0
  92. package/dist/types/tools/write.d.ts +2 -0
  93. package/dist/types/tui.d.ts +20 -3
  94. package/package.json +10 -10
  95. package/src/app-keybindings.d.ts +18 -8
  96. package/src/app-keybindings.ts +20 -8
  97. package/src/apps/autoresearch-dashboard.ts +444 -27
  98. package/src/apps/debug/raw-sse.ts +2 -2
  99. package/src/apps/git/diff-pane.ts +39 -41
  100. package/src/apps/git/git-tui.ts +80 -14
  101. package/src/apps/git/sidebar.ts +10 -1
  102. package/src/apps/git/state.ts +12 -0
  103. package/src/autocomplete.d.ts +2 -0
  104. package/src/autocomplete.ts +21 -19
  105. package/src/bracketed-paste.ts +9 -2
  106. package/src/charts/chart-svg.d.ts +7 -13
  107. package/src/charts/chart-svg.ts +19 -22
  108. package/src/charts/table-data.ts +17 -6
  109. package/src/chat/advisor-message.d.ts +4 -1
  110. package/src/chat/advisor-message.ts +77 -24
  111. package/src/chat/assistant-message.d.ts +9 -4
  112. package/src/chat/assistant-message.ts +127 -31
  113. package/src/chat/bash-execution.ts +7 -2
  114. package/src/chat/figure-markdown.d.ts +7 -2
  115. package/src/chat/figure-markdown.ts +10 -3
  116. package/src/chat/image-loading.ts +9 -6
  117. package/src/chat/messages.d.ts +3 -1
  118. package/src/chat/messages.ts +4 -1
  119. package/src/chat/read-tool-group.d.ts +2 -2
  120. package/src/chat/read-tool-group.ts +97 -42
  121. package/src/chat/svg-figure.ts +3 -1
  122. package/src/chat/svg-source.ts +6 -3
  123. package/src/chat/table-chart.d.ts +11 -15
  124. package/src/chat/table-chart.ts +24 -57
  125. package/src/chat/tool-execution.d.ts +32 -20
  126. package/src/chat/tool-execution.ts +103 -36
  127. package/src/chat/user-message.d.ts +3 -2
  128. package/src/chat/user-message.ts +54 -34
  129. package/src/chrome/countdown-timer.ts +3 -3
  130. package/src/chrome/diff.d.ts +2 -3
  131. package/src/chrome/diff.ts +4 -9
  132. package/src/chrome/status-notice.ts +5 -0
  133. package/src/chrome/transcript-container.d.ts +2 -0
  134. package/src/chrome/transcript-container.ts +212 -28
  135. package/src/components/editor.ts +12 -12
  136. package/src/components/image.d.ts +2 -0
  137. package/src/components/image.ts +54 -20
  138. package/src/components/markdown.d.ts +49 -3
  139. package/src/components/markdown.ts +482 -12
  140. package/src/components/settings-list.ts +2 -4
  141. package/src/components/text.d.ts +2 -0
  142. package/src/components/text.ts +11 -1
  143. package/src/debug-server.d.ts +2 -0
  144. package/src/debug-server.ts +51 -69
  145. package/src/hotkeys-markdown.ts +2 -0
  146. package/src/keybinding-matchers.d.ts +6 -0
  147. package/src/keybinding-matchers.ts +19 -0
  148. package/src/multiplexers/tmux.d.ts +1 -0
  149. package/src/multiplexers/tmux.ts +3 -0
  150. package/src/multiplexers/types.d.ts +6 -0
  151. package/src/multiplexers/types.ts +6 -0
  152. package/src/native/backend.d.ts +5 -4
  153. package/src/native/backend.ts +13 -5
  154. package/src/native/blobs.d.ts +24 -12
  155. package/src/native/blobs.ts +56 -7
  156. package/src/native/reconcile.ts +43 -21
  157. package/src/overlays/advisor-config.ts +259 -8
  158. package/src/overlays/agent-hub-projection.d.ts +10 -5
  159. package/src/overlays/agent-hub-projection.ts +63 -12
  160. package/src/overlays/annotation-overlay.ts +422 -168
  161. package/src/overlays/approval-dialog.ts +389 -0
  162. package/src/overlays/ask-dialog.ts +244 -105
  163. package/src/overlays/btw-history-panel.ts +208 -147
  164. package/src/overlays/extensions/extension-list.ts +4 -6
  165. package/src/overlays/hook-editor.ts +90 -32
  166. package/src/overlays/hook-input.ts +105 -19
  167. package/src/overlays/job-tree.ts +177 -0
  168. package/src/overlays/jobs-panel.ts +222 -142
  169. package/src/overlays/login-dialog.ts +201 -91
  170. package/src/overlays/mcp-add-wizard.ts +562 -279
  171. package/src/overlays/model-browser.ts +32 -7
  172. package/src/overlays/model-hub.ts +4 -10
  173. package/src/overlays/model-picker.ts +58 -21
  174. package/src/overlays/pause-screen.ts +81 -23
  175. package/src/overlays/plan-review-overlay.ts +16 -0
  176. package/src/overlays/plugin-selector.ts +17 -1
  177. package/src/overlays/queue-mode-selector.ts +10 -0
  178. package/src/overlays/report-panel.ts +6 -2
  179. package/src/overlays/reset-usage-selector.ts +130 -21
  180. package/src/overlays/session-account-selector.ts +36 -0
  181. package/src/overlays/session-info-overlay.ts +199 -76
  182. package/src/overlays/session-selector.ts +15 -10
  183. package/src/overlays/settings-selector.ts +3 -0
  184. package/src/overlays/show-images-selector.ts +1 -0
  185. package/src/overlays/snapcompact-shape-preview.ts +13 -6
  186. package/src/overlays/theme-selector.ts +6 -1
  187. package/src/overlays/thinking-selector.ts +8 -4
  188. package/src/overlays/tree-selector.ts +117 -2
  189. package/src/overlays/usage-dashboard.ts +498 -144
  190. package/src/prompt/composer-cache.ts +4 -1
  191. package/src/prompt/composer.ts +200 -74
  192. package/src/prompt/custom-editor.ts +168 -65
  193. package/src/prompt/image-references.d.ts +22 -0
  194. package/src/prompt/image-references.ts +79 -2
  195. package/src/prompt/internal-url-autocomplete.ts +1 -0
  196. package/src/prompt/macos-spelling.ts +28 -5
  197. package/src/prompt/prose-gate.ts +3 -0
  198. package/src/prompt/queued-messages.ts +9 -2
  199. package/src/prompt/tips.txt +1 -2
  200. package/src/prompt/typing-word.ts +59 -0
  201. package/src/prompt/welcome.ts +97 -45
  202. package/src/render/code-cell.ts +18 -5
  203. package/src/render/hanging-wrap.d.ts +7 -0
  204. package/src/render/hanging-wrap.ts +58 -0
  205. package/src/render/hyperlink.d.ts +2 -0
  206. package/src/render/hyperlink.ts +5 -0
  207. package/src/render/render-utils.d.ts +9 -1
  208. package/src/render/render-utils.ts +16 -2
  209. package/src/render/utils.d.ts +1 -1
  210. package/src/render/utils.ts +1 -1
  211. package/src/render/width-aware-text.ts +6 -5
  212. package/src/setup/scenes/splash.ts +22 -8
  213. package/src/status-line/component.ts +79 -11
  214. package/src/status-line/context-usage.ts +176 -73
  215. package/src/status-line/footer.ts +18 -35
  216. package/src/status-line/host.ts +6 -1
  217. package/src/status-line/segments.ts +16 -5
  218. package/src/status-line/startup.ts +6 -6
  219. package/src/status-line/types.ts +1 -0
  220. package/src/stdin-buffer.d.ts +7 -0
  221. package/src/stdin-buffer.ts +300 -62
  222. package/src/terminal-capabilities.d.ts +16 -1
  223. package/src/terminal-capabilities.ts +66 -28
  224. package/src/terminal-multiplexer.d.ts +7 -0
  225. package/src/terminal-multiplexer.ts +14 -0
  226. package/src/terminal.d.ts +8 -4
  227. package/src/terminal.ts +190 -63
  228. package/src/theme/symbols.d.ts +1 -1
  229. package/src/theme/symbols.ts +1 -0
  230. package/src/theme/theme-class.ts +1 -0
  231. package/src/theme/theme.d.ts +11 -0
  232. package/src/theme/theme.ts +46 -14
  233. package/src/thinking.d.ts +5 -0
  234. package/src/thinking.ts +9 -1
  235. package/src/tmux.d.ts +39 -4
  236. package/src/tmux.ts +110 -16
  237. package/src/tools/ask.ts +20 -9
  238. package/src/tools/ast-edit.d.ts +2 -2
  239. package/src/tools/ast-edit.ts +30 -7
  240. package/src/tools/ast-grep.d.ts +2 -2
  241. package/src/tools/ast-grep.ts +39 -35
  242. package/src/tools/bash.d.ts +6 -1
  243. package/src/tools/bash.ts +15 -9
  244. package/src/tools/debug.ts +2 -1
  245. package/src/tools/default-renderer.ts +3 -2
  246. package/src/tools/edit.d.ts +2 -0
  247. package/src/tools/edit.ts +100 -21
  248. package/src/tools/eval.d.ts +53 -4
  249. package/src/tools/eval.ts +173 -137
  250. package/src/tools/find.d.ts +14 -4
  251. package/src/tools/find.ts +71 -18
  252. package/src/tools/glob.d.ts +3 -3
  253. package/src/tools/glob.ts +51 -17
  254. package/src/tools/grep.d.ts +2 -2
  255. package/src/tools/grep.ts +99 -22
  256. package/src/tools/grouped-file-output.d.ts +9 -4
  257. package/src/tools/grouped-file-output.ts +21 -6
  258. package/src/tools/lsp.ts +30 -18
  259. package/src/tools/mcp.ts +2 -2
  260. package/src/tools/native-view.d.ts +1 -1
  261. package/src/tools/native-view.ts +2 -2
  262. package/src/tools/read.d.ts +32 -10
  263. package/src/tools/read.ts +89 -43
  264. package/src/tools/renderer.d.ts +2 -0
  265. package/src/tools/renderer.ts +2 -0
  266. package/src/tools/task.d.ts +31 -0
  267. package/src/tools/task.ts +40 -7
  268. package/src/tools/todo.ts +3 -2
  269. package/src/tools/wait.ts +16 -30
  270. package/src/tools/web-search-types.d.ts +1 -0
  271. package/src/tools/web-search-types.ts +1 -0
  272. package/src/tools/web-search.ts +2 -1
  273. package/src/tools/write.d.ts +2 -0
  274. package/src/tools/write.ts +36 -12
  275. package/src/tools/yield.ts +2 -2
  276. package/src/tui.d.ts +20 -3
  277. package/src/tui.ts +160 -43
  278. package/src/utils.ts +5 -3
  279. package/dist/types/tools/eval-format/code-mode-declarations.d.ts +0 -5
  280. package/src/tools/eval-format/code-mode-declarations.ts +0 -61
@@ -81,7 +81,7 @@ import {
81
81
  import { optionMarker } from "../tools/ask";
82
82
  import { CountdownTimer } from "../chrome/countdown-timer";
83
83
  import { editorKey, editorKeys } from "../chrome/keybinding-hints";
84
- import { formatKeyHint, formatKeyHints } from "../app-keybindings";
84
+ import { formatKeyHint, formatKeyHints, formatTooltipKey } from "../app-keybindings";
85
85
  import { OverlayPanel, PanelDivider, PanelRows } from "../chrome/overlay-box";
86
86
  import { handleTabSwitchKey } from "../chrome/selector-helpers";
87
87
  import { col, node, span, text } from "../native/describe";
@@ -367,16 +367,49 @@ function renderAnswerSummary(question: ExtensionAskDialogQuestion, state: Questi
367
367
  return selected[0] ?? theme.fg("warning", "unanswered");
368
368
  }
369
369
 
370
- /** {@link renderAnswerSummary} as spans for the native review list. */
370
+ /** Inline markdown code (`` `x` ``) as mono `code` spans, the rest plain, as the ANSI rows render it. */
371
+ function inlineCodeSpans(value: string): TspSpan[] {
372
+ return value
373
+ .split(/(`[^`]+`)/)
374
+ .flatMap(piece =>
375
+ piece === ""
376
+ ? []
377
+ : piece.length > 2 && piece.startsWith("`") && piece.endsWith("`")
378
+ ? [span(piece.slice(1, -1), "code")]
379
+ : [span(piece)],
380
+ );
381
+ }
382
+
383
+ /** An option's native label: the `(Recommended)` suffix goes (the row's badge says it), inline code runs mono. */
384
+ function optionLabelSpans(question: ExtensionAskDialogQuestion, optionIndex: number, label: string): TspSpan[] {
385
+ const recommended = question.recommended === optionIndex && label.endsWith(RECOMMENDED_SUFFIX);
386
+ return inlineCodeSpans(recommended ? label.slice(0, -RECOMMENDED_SUFFIX.length) : label);
387
+ }
388
+
389
+ function isAnswered(state: QuestionState): boolean {
390
+ return state.selectedOptions.size > 0 || state.customInput !== undefined;
391
+ }
392
+
393
+ /** The answer to one question for the Review tab: the chosen labels (code mono), the custom answer, the note. */
371
394
  function describeAnswer(question: ExtensionAskDialogQuestion, state: QuestionState): readonly TspSpan[] {
372
- if (state.selectedOptions.size === 0 && state.customInput === undefined) return [span("unanswered", "warning")];
373
- const spans = [span(plainText(renderAnswerSummary(question, state)))];
395
+ if (!isAnswered(state)) return [span("Unanswered", "warning")];
396
+ const labels = displayOptionLabels(question);
397
+ const spans: TspSpan[] = [];
398
+ const push = (parts: readonly TspSpan[]): void => {
399
+ if (spans.length > 0) spans.push(span(", ", "dim"));
400
+ spans.push(...parts);
401
+ };
402
+ question.options.forEach((option, index) => {
403
+ if (state.selectedOptions.has(option.label))
404
+ push(optionLabelSpans(question, index, labels[index] ?? option.label));
405
+ });
406
+ if (state.customInput !== undefined) push([span(`“${normalizedInlineInput(state.customInput)}”`)]);
374
407
  const note = noteForSubmittedAnswer(question, state)?.trim();
375
- if (note) spans.push(span(` · Note: ${normalizedInlineInput(note)}`, "muted"));
408
+ if (note) spans.push(span(" ✎ ", "dim"), span(normalizedInlineInput(note), "em"));
376
409
  return spans;
377
410
  }
378
411
 
379
- /** Native `item.detail` of an option row: its description, the typed custom answer, and its note. */
412
+ /** Native `item.detail` of an option row: its description or the typed custom answer, then its note (`em`). */
380
413
  function describeRowDetail(
381
414
  rowItem: QuestionRow,
382
415
  question: ExtensionAskDialogQuestion,
@@ -385,12 +418,12 @@ function describeRowDetail(
385
418
  const spans: TspSpan[] = [];
386
419
  if (rowItem.kind === "option") {
387
420
  const description = question.options[rowItem.optionIndex ?? -1]?.description?.trim();
388
- if (description) spans.push(span(normalizedInlineInput(description), "muted"));
421
+ if (description) spans.push(...inlineCodeSpans(normalizedInlineInput(description)));
389
422
  } else if (state.customInput !== undefined) {
390
- spans.push(span(`“${normalizedInlineInput(state.customInput)}”`, "muted"));
423
+ spans.push(span(`“${normalizedInlineInput(state.customInput)}”`));
391
424
  }
392
425
  if (state.note && state.noteRowKey === rowItem.key) {
393
- spans.push(span(`${spans.length > 0 ? " · " : ""}✎ ${normalizedInlineInput(state.note)}`, "success"));
426
+ spans.push(span(`${spans.length > 0 ? " " : ""}✎ `, "dim"), span(normalizedInlineInput(state.note), "em"));
394
427
  }
395
428
  return spans.length > 0 ? spans : undefined;
396
429
  }
@@ -524,7 +557,6 @@ export function normalizeDialogQuestions(questions: ExtensionAskDialogQuestion[]
524
557
  }
525
558
 
526
559
  export class AskDialogComponent implements Component {
527
- readonly retireDisplacedTranscript = true;
528
560
  #states: QuestionState[];
529
561
  #activeTabIndex = 0;
530
562
  #submitScrollOffset = 0;
@@ -543,6 +575,8 @@ export class AskDialogComponent implements Component {
543
575
  #contentWidth = 76;
544
576
  #headerExpandable = false;
545
577
  #descExpandable = false;
578
+ /** Collapsed to one line (`app.ask.hide` / the Hide button) while the question stays pending. */
579
+ #hidden = false;
546
580
  /** Memoized native description; dropped whenever dialog state may have changed. */
547
581
  #native: NativeNode | undefined;
548
582
  /** Input-guard blocked state {@link #native} was described with (the guard flips outside this dialog). */
@@ -632,7 +666,7 @@ export class AskDialogComponent implements Component {
632
666
  }
633
667
 
634
668
  #canToggleExpansion(): boolean {
635
- if (this.#closed || this.#isSubmitTab()) return false;
669
+ if (this.#closed || this.#hidden || this.#isSubmitTab()) return false;
636
670
  const question = this.#questions[this.#currentQuestionIndex()];
637
671
  if (!question) return false;
638
672
  if (this.#expanded) return true;
@@ -652,6 +686,10 @@ export class AskDialogComponent implements Component {
652
686
  this.#finishCancel();
653
687
  return;
654
688
  }
689
+ if (getKeybindings().matches(keyData, "app.ask.hide")) {
690
+ this.#setHidden(!this.#hidden);
691
+ return;
692
+ }
655
693
  // Expand before the draft-input guard so Ctrl+O can reveal a truncated
656
694
  // question even while a pending prompt still owns typing.
657
695
  if (matchesAppToolsExpand(keyData)) {
@@ -664,6 +702,12 @@ export class AskDialogComponent implements Component {
664
702
  this.#requestRender();
665
703
  return;
666
704
  }
705
+ if (this.#hidden) {
706
+ if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
707
+ this.#setHidden(false);
708
+ }
709
+ return;
710
+ }
667
711
  if (this.#hasSubmitTab() && handleTabSwitchKey(keyData, direction => this.#switchTab(direction))) {
668
712
  this.#requestRender();
669
713
  return;
@@ -680,6 +724,7 @@ export class AskDialogComponent implements Component {
680
724
  // renders as the next sibling in the same container, so this lands in the
681
725
  // same frame).
682
726
  this.options.inputGuard?.syncPresentation?.();
727
+ if (this.#hidden) return this.#renderHidden(width);
683
728
  const innerWidth = Math.max(1, width - 4);
684
729
  this.#contentWidth = innerWidth;
685
730
  // Fixed panel height: measured from the tallest tab at spawn and
@@ -712,10 +757,15 @@ export class AskDialogComponent implements Component {
712
757
 
713
758
  /**
714
759
  * In the dock, in the composer's place and framed as the composer is: a
715
- * `col` with the prompt editor's root role (`zeta.editor`), so the terminal
716
- * gives it the composer's insets and spacing. Not a modal `overlay` sheet:
717
- * one anchored at the bottom covered the transcript rows that explain the
718
- * question and blocked scrolling until the question was answered.
760
+ * `col` with an `zeta.editor.*` root role (`zeta.editor.ask`, collapsed
761
+ * `zeta.editor.ask.hidden`), so the terminal draws the composer's shell
762
+ * around it. Not a modal `overlay` sheet: one anchored at the bottom
763
+ * covered the transcript rows that explain the question and blocked
764
+ * scrolling until the question was answered.
765
+ *
766
+ * Anatomy: the question tabs (several questions), the question, its
767
+ * answers as radio/check rows, and the action bar as the footer (the
768
+ * countdown, Hide, then Note, Skip and the primary Submit/Next).
719
769
  */
720
770
  describe(cx: DescribeContext): NativeNode {
721
771
  const inputGuard = this.options.inputGuard;
@@ -725,61 +775,100 @@ export class AskDialogComponent implements Component {
725
775
  const ring = this.#countdown !== undefined && cx.supports("meter");
726
776
  if (ring) this.#startRingTick();
727
777
  if (this.#native && this.#nativeBlocked === blocked) return this.#native;
778
+ if (this.#hidden) {
779
+ this.#native = this.#describeHidden(ring);
780
+ this.#nativeBlocked = blocked;
781
+ return this.#native;
782
+ }
728
783
  const children: NativeChild[] = [];
729
- const head = this.#describeHead(ring);
784
+ const head = this.#describeHead();
730
785
  if (head) children.push(head);
731
786
  if (this.#isSubmitTab()) this.#describeSubmitBody(children);
732
787
  else this.#describeQuestionBody(children);
733
- children.push(this.#describeActions(blocked));
734
- this.#native = col(children, { role: "zeta.editor", gap: "md" });
788
+ children.push(this.#describeActions(blocked, ring));
789
+ this.#native = col(children, { role: "zeta.editor.ask", gap: "md" });
735
790
  this.#nativeBlocked = blocked;
736
791
  return this.#native;
737
792
  }
738
793
 
739
- /** Question tabs (plus Review) and the countdown: a ring when the terminal draws meters, else a clocked `elapsed`. */
740
- #describeHead(ring: boolean): NativeNode | undefined {
741
- const children: NativeChild[] = [];
742
- if (this.#hasSubmitTab()) {
743
- const items = this.#questions.map((question, index) => ({
794
+ /** The question tabs, an answered one checked, then Review; none for a lone single-choice question. */
795
+ #describeHead(): NativeNode | undefined {
796
+ if (!this.#hasSubmitTab()) return undefined;
797
+ const items = this.#questions.map((question, index) => {
798
+ const name = questionTabName(question, index);
799
+ const state = this.#states[index];
800
+ return {
744
801
  id: String(index),
745
- label: questionTabName(question, index),
746
- }));
747
- items.push({ id: "submit", label: REVIEW_TAB });
748
- const active = this.#isSubmitTab() ? "submit" : String(this.#activeTabIndex);
749
- children.push(
802
+ label: state && isAnswered(state) ? [span("✓ ", "success"), span(name)] : [span(name)],
803
+ };
804
+ });
805
+ items.push({ id: "submit", label: [span(REVIEW_TAB)] });
806
+ const active = this.#isSubmitTab() ? "submit" : String(this.#activeTabIndex);
807
+ return node(
808
+ "row",
809
+ { role: "zeta.ask.head", align: "center" },
810
+ [
750
811
  node(
751
812
  "tabs",
752
- { items, active, role: "zeta.ask.questions", actions: { click: "select" } },
813
+ {
814
+ items,
815
+ active,
816
+ role: "zeta.ask.questions",
817
+ title: "Next question tab · Previous shift+tab",
818
+ actions: { click: "select" },
819
+ },
753
820
  undefined,
754
821
  "tabs",
755
822
  ),
756
- );
757
- }
758
- if (this.#countdown) {
759
- children.push(node("spacer", { grow: 1 }));
760
- if (ring) {
761
- const total = Math.max(1, this.options.timeout ?? 1);
762
- const left = Math.max(0, this.#countdownDeadline - Date.now());
763
- children.push(
764
- node(
765
- "meter",
766
- {
767
- value: Math.round((left / total) * 1000) / 1000,
768
- style: "ring",
769
- size: "sm",
770
- label: `${Math.ceil(left / 1000)}s`,
771
- title: "Answers the recommended option when the time runs out",
772
- },
773
- undefined,
774
- "timer",
775
- ),
776
- );
777
- } else {
778
- children.push(this.#countdown.describe());
779
- }
780
- }
781
- if (children.length === 0) return undefined;
782
- return node("row", { role: "zeta.ask.head", gap: "sm", align: "center" }, children, "head");
823
+ ],
824
+ "head",
825
+ );
826
+ }
827
+
828
+ /** The countdown: a ring when the terminal draws meters, else a clocked `elapsed`. */
829
+ #describeTimer(countdown: CountdownTimer, ring: boolean): NativeNode {
830
+ if (!ring) return countdown.describe();
831
+ const total = Math.max(1, this.options.timeout ?? 1);
832
+ const left = Math.max(0, this.#countdownDeadline - Date.now());
833
+ return node(
834
+ "meter",
835
+ {
836
+ value: Math.round((left / total) * 1000) / 1000,
837
+ style: "ring",
838
+ size: "sm",
839
+ label: `${Math.ceil(left / 1000)}s`,
840
+ title: "Answers the recommended option when the time runs out",
841
+ },
842
+ undefined,
843
+ "timer",
844
+ );
845
+ }
846
+
847
+ /** Hidden: one row in the composer's place — the countdown, the question clipped to a line, Skip and Show. */
848
+ #describeHidden(ring: boolean): NativeNode {
849
+ const question = this.#questions[this.#currentQuestionIndex()];
850
+ const more = this.#questions.length > 1 ? ` +${this.#questions.length - 1} more` : "";
851
+ const summary = text(
852
+ [
853
+ span("Question ", "muted"),
854
+ span(normalizedInlineInput(sanitizeCarriageReturns(question?.question ?? ""))),
855
+ ...(more ? [span(more, "muted")] : []),
856
+ ],
857
+ { truncate: "end", role: "zeta.ask.summary" },
858
+ );
859
+ const hideKey = getKeybindings().getKeys("app.ask.hide")[0];
860
+ const show = actionButton("Show", "show", {
861
+ tone: "accent",
862
+ ...(hideKey ? { keys: hideKey } : {}),
863
+ title: `Show the question${hideKey ? ` ${formatTooltipKey(hideKey)}` : ""}`,
864
+ });
865
+ const cancelKey = getKeybindings().getKeys("tui.select.cancel")[0];
866
+ const skip = actionButton("Skip", "cancel", cancelKey ? { keys: cancelKey } : {});
867
+ const items: (NativeNode | null)[] = [];
868
+ if (this.#countdown) items.push(this.#describeTimer(this.#countdown, ring));
869
+ // The summary grows (`zeta.ask.summary`), end-aligning Skip and Show.
870
+ items.push(summary, skip, show);
871
+ return col([actionBar(items)], { role: "zeta.editor.ask.hidden", gap: "md" });
783
872
  }
784
873
 
785
874
  /** Question tab: the question as markdown and its answers as radio/check rows (label, description, badge). */
@@ -797,7 +886,8 @@ export class AskDialogComponent implements Component {
797
886
  "question",
798
887
  ),
799
888
  );
800
- const optionRole = question.multi ? "zeta.ask.check" : "zeta.ask.option";
889
+ // Radios (`zeta.ask.option`, `zeta.ask.other`) or checks (`zeta.ask.check`, `zeta.ask.other.check`); a chosen
890
+ // row is `accent`-toned (a check also carries the check glyph), apart from the highlighted row (`selected`).
801
891
  const items = rows.map(rowItem => {
802
892
  const option = rowItem.kind === "option" ? question.options[rowItem.optionIndex ?? -1] : undefined;
803
893
  const checked =
@@ -805,33 +895,29 @@ export class AskDialogComponent implements Component {
805
895
  ? state.customInput !== undefined
806
896
  : option !== undefined && state.selectedOptions.has(option.label);
807
897
  const recommended = rowItem.kind === "option" && question.recommended === rowItem.optionIndex;
808
- // The badge replaces the ANSI `(Recommended)` suffix.
809
- const label =
810
- rowItem.kind === "other"
811
- ? OTHER_LABEL
812
- : recommended && rowItem.label.endsWith(RECOMMENDED_SUFFIX)
813
- ? rowItem.label.slice(0, -RECOMMENDED_SUFFIX.length)
814
- : rowItem.label;
815
898
  const detail =
816
899
  describeRowDetail(rowItem, question, state) ??
817
- (rowItem.kind === "other" ? [span("Type your own answer", "dim")] : undefined);
900
+ (rowItem.kind === "other" ? [span("Type your own answer")] : undefined);
901
+ const role =
902
+ rowItem.kind === "other"
903
+ ? question.multi
904
+ ? "zeta.ask.other.check"
905
+ : "zeta.ask.other"
906
+ : question.multi
907
+ ? "zeta.ask.check"
908
+ : "zeta.ask.option";
818
909
  return node(
819
910
  "item",
820
911
  {
821
- // Inline code runs mono, as the ANSI row renders inline markdown.
822
- label: label
823
- .split(/(`[^`]+`)/)
824
- .flatMap(piece =>
825
- piece === ""
826
- ? []
827
- : piece.length > 2 && piece.startsWith("`") && piece.endsWith("`")
828
- ? [span(piece.slice(1, -1), "code")]
829
- : [span(piece)],
830
- ),
831
- role: rowItem.kind === "other" ? "zeta.ask.other" : optionRole,
912
+ // The badge replaces the ANSI `(Recommended)` suffix.
913
+ label:
914
+ rowItem.kind === "other"
915
+ ? [span(OTHER_LABEL)]
916
+ : optionLabelSpans(question, rowItem.optionIndex ?? -1, rowItem.label),
917
+ role,
832
918
  ...(detail ? { detail } : {}),
833
- ...(recommended ? { value: [span("Recommended", "accent")] } : {}),
834
- ...(checked ? { icon: "check", tone: "success" as const } : {}),
919
+ ...(recommended ? { value: [span("Recommended")] } : {}),
920
+ ...(checked ? { tone: "accent" as const, ...(question.multi ? { icon: "check" } : {}) } : {}),
835
921
  },
836
922
  undefined,
837
923
  rowItem.key,
@@ -849,69 +935,87 @@ export class AskDialogComponent implements Component {
849
935
  const highlighted = rows[state.cursorIndex];
850
936
  const option = highlighted?.kind === "option" ? question.options[highlighted.optionIndex ?? -1] : undefined;
851
937
  if (option?.preview?.trim()) {
852
- children.push(node("md", { text: replaceTabs(option.preview) }, undefined, "preview"));
938
+ children.push(
939
+ node("md", { text: replaceTabs(option.preview), role: "zeta.ask.preview" }, undefined, "preview"),
940
+ );
853
941
  }
854
942
  }
855
943
 
856
- /** Review tab: every answer as key/value pairs; the action bar's Submit sends them. */
944
+ /** Review tab: what is still unanswered, then every answer as question/answer pairs; Submit sends them. */
857
945
  #describeSubmitBody(children: NativeChild[]): void {
858
946
  const unanswered = this.#unansweredCount();
859
- if (unanswered > 0) {
860
- const noun = `question${unanswered === 1 ? "" : "s"}`;
861
- children.push(
862
- text([span(`${unanswered} unanswered ${noun}; ${formatKeyHint("enter")} still submits.`, "warning")]),
863
- );
864
- }
947
+ const notice =
948
+ unanswered > 0
949
+ ? [
950
+ span(`${unanswered} unanswered`, "warning"),
951
+ span(` · Submit still sends ${unanswered === this.#questions.length ? "the questions" : "the rest"}`),
952
+ ]
953
+ : [span("All answered · Submit sends them")];
954
+ children.push(node("text", { spans: notice, role: "zeta.ask.notice" }, undefined, "notice"));
865
955
  const answers: { k: string; v: readonly TspSpan[] }[] = [];
866
956
  for (let index = 0; index < this.#questions.length; index++) {
867
957
  const question = this.#questions[index];
868
958
  const state = this.#states[index];
869
959
  if (!question || !state) continue;
870
- answers.push({ k: `${index + 1}. ${questionTabName(question, index)}`, v: describeAnswer(question, state) });
960
+ answers.push({ k: questionTabName(question, index), v: describeAnswer(question, state) });
871
961
  }
872
- children.push(node("kv", { items: answers }, undefined, "answers"));
962
+ children.push(node("kv", { items: answers, role: "zeta.ask.answers" }, undefined, "answers"));
873
963
  }
874
964
 
875
965
  /**
876
- * The action bar: Submit ⏎ (Enter's action on this tab; `Next` while more
877
- * questions follow), Note n, Skip esc. Keyboard-only keys live in the
878
- * buttons' tooltips; a draft that owns input shows its hint instead.
966
+ * The action bar, the sheet's footer: the countdown and Hide (apart from
967
+ * the answer actions), then Note, Skip and the primary Submit (Enter's
968
+ * action on this tab; `Next` while more questions follow). Buttons say
969
+ * what they do; their keys live in their tooltips. A draft that owns input
970
+ * shows its hint in place of the answer actions.
879
971
  */
880
- #describeActions(blocked: boolean): NativeNode {
972
+ #describeActions(blocked: boolean, ring: boolean): NativeNode {
881
973
  const cancelKey = getKeybindings().getKeys("tui.select.cancel")[0];
882
- const skip = actionButton("Skip", "cancel", cancelKey ? { keys: cancelKey } : {});
974
+ const skip = actionButton("Skip", "cancel", {
975
+ ...(cancelKey ? { keys: cancelKey } : {}),
976
+ title: `Skip without answering${cancelKey ? ` ${formatTooltipKey(cancelKey)}` : ""}`,
977
+ });
978
+ const hideKey = getKeybindings().getKeys("app.ask.hide")[0];
979
+ const hide = actionButton("Hide", "hide", {
980
+ ...(hideKey ? { keys: hideKey } : {}),
981
+ title: `Hide until you are ready to answer${hideKey ? ` ${formatTooltipKey(hideKey)}` : ""}`,
982
+ });
983
+ const lead: NativeNode[] = this.#countdown ? [this.#describeTimer(this.#countdown, ring), hide] : [hide];
883
984
  const inputGuard = this.options.inputGuard;
884
985
  if (blocked && inputGuard) {
885
- return actionBar([text([span(plainText(inputGuard.hint), "muted")], { truncate: "end" }), null, skip]);
986
+ return actionBar([
987
+ ...lead,
988
+ text([span(plainText(inputGuard.hint), "muted")], { truncate: "end", role: "zeta.ask.summary" }),
989
+ skip,
990
+ ]);
886
991
  }
887
- const moves = ["up/down move"];
888
- if (this.#hasSubmitTab()) moves.push("tab switches question");
889
992
  if (this.#isSubmitTab()) {
890
993
  const submit = actionButton("Submit", "submit", {
891
994
  keys: "enter",
892
995
  tone: "accent",
893
996
  title: "Submit answers enter",
894
997
  });
895
- return actionBar([null, skip, submit]);
998
+ return actionBar([...lead, null, skip, submit]);
896
999
  }
897
1000
  const question = this.#questions[this.#currentQuestionIndex()];
898
1001
  const last = this.#questions.length === 1;
899
1002
  const enterLabel = last ? "Submit" : "Next";
900
- const tips = question?.multi ? ["space toggles", ...moves] : moves;
1003
+ // A multi-select toggles on Space; Enter moves on.
901
1004
  const submit = actionButton(enterLabel, "submit", {
902
1005
  keys: "enter",
903
1006
  tone: "accent",
904
- title: `${enterLabel} enter · ${tips.join(" · ")}`,
1007
+ title: question?.multi ? `${enterLabel} enter · Toggle space` : `${enterLabel} enter`,
905
1008
  });
906
- const note = actionButton("Note", "note", { keys: "n", title: `Add a note to the highlighted answer n` });
907
- return actionBar([null, note, skip, submit]);
1009
+ const note = actionButton("Note", "note", { keys: "n", title: "Add a note to the highlighted answer n" });
1010
+ return actionBar([...lead, null, note, skip, submit]);
908
1011
  }
909
1012
 
910
1013
  /**
911
1014
  * Pointer actions: a tab click switches to it; a click on an answer row is
912
1015
  * Enter on it (Space for a multi-select option, so it toggles; its
913
1016
  * double-click `activate` is Enter); the action bar's Submit, Note and Skip
914
- * run exactly what Enter, `n` and Esc run on the highlighted row.
1017
+ * run exactly what Enter, `n` and Esc run on the highlighted row; Hide/Show
1018
+ * collapse and restore the dialog.
915
1019
  */
916
1020
  handleNativeEvent(event: NativeUiEvent): void {
917
1021
  if (this.#closed || this.#promptActive) return;
@@ -920,13 +1024,17 @@ export class AskDialogComponent implements Component {
920
1024
  this.#finishCancel();
921
1025
  return;
922
1026
  }
923
- if (this.options.inputGuard?.isBlocked()) return;
1027
+ if (event.act === "hide" || event.act === "show") {
1028
+ this.#setHidden(event.act === "hide");
1029
+ return;
1030
+ }
1031
+ if (this.#hidden || this.options.inputGuard?.isBlocked()) return;
924
1032
  if (event.act === "submit") this.#pressEnter();
925
1033
  else if (event.act === "note") this.#pressNote();
926
1034
  return;
927
1035
  }
928
1036
  if (event.type !== "select" && event.type !== "activate") return;
929
- if (this.options.inputGuard?.isBlocked()) return;
1037
+ if (this.#hidden || this.options.inputGuard?.isBlocked()) return;
930
1038
  const key = leafKey(event.key);
931
1039
  if (key === "tabs") {
932
1040
  if (!this.#hasSubmitTab()) return;
@@ -1054,6 +1162,35 @@ export class AskDialogComponent implements Component {
1054
1162
  return this.#remainingSeconds === undefined ? "Ask" : `Ask (${this.#remainingSeconds}s)`;
1055
1163
  }
1056
1164
 
1165
+ #setHidden(hidden: boolean): void {
1166
+ if (this.#hidden === hidden) return;
1167
+ this.#hidden = hidden;
1168
+ this.#countdown?.reset();
1169
+ this.#requestRender();
1170
+ }
1171
+
1172
+ /** Hidden: one line in the composer's place — title (with countdown), the question clipped to fit, and how to bring it back. */
1173
+ #renderHidden(width: number): readonly string[] {
1174
+ const hideKey = editorKey("app.ask.hide");
1175
+ const enter = formatKeyHint("enter");
1176
+ const inputGuard = this.options.inputGuard;
1177
+ const blocked = inputGuard?.isBlocked() ?? false;
1178
+ // While a draft owns input, Enter goes to the draft, so only the hide key restores.
1179
+ const show = blocked ? hideKey : hideKey ? `${hideKey}/${enter}` : enter;
1180
+ const hints: string[] = [];
1181
+ if (blocked && inputGuard) hints.push(inputGuard.hint);
1182
+ if (show) hints.push(`${show} show`);
1183
+ hints.push(`${editorKey("tui.select.cancel")} cancel`);
1184
+ const hint = theme.fg("dim", ` · ${hints.join(" · ")}`);
1185
+ const label = theme.fg("accent", `${this.#titleText()} hidden:`);
1186
+ const question = this.#questions[this.#currentQuestionIndex()];
1187
+ const more = this.#questions.length > 1 ? theme.fg("dim", ` (+${this.#questions.length - 1} more)`) : "";
1188
+ const summary = normalizedInlineInput(sanitizeCarriageReturns(question?.question ?? ""));
1189
+ const summaryWidth = Math.max(1, width - visibleWidth(label) - visibleWidth(more) - visibleWidth(hint) - 1);
1190
+ const clipped = truncateToWidth(summary, summaryWidth, Ellipsis.Unicode);
1191
+ return [truncateToWidth(`${label} ${theme.fg("muted", clipped)}${more}${hint}`, width, Ellipsis.Unicode)];
1192
+ }
1193
+
1057
1194
  #hasSubmitTab(): boolean {
1058
1195
  // Multi questions confirm on the Submit tab (Enter toggles, never
1059
1196
  // submits), so any multi question forces the tab even when there is
@@ -1119,7 +1256,9 @@ export class AskDialogComponent implements Component {
1119
1256
  }
1120
1257
 
1121
1258
  #footerHintText(indicator: string): string {
1122
- const cancel = `${editorKey("tui.select.cancel")} cancel`;
1259
+ // Hide rides along with cancel: both leave the question view without answering.
1260
+ const hideKey = editorKey("app.ask.hide");
1261
+ const cancel = `${editorKey("tui.select.cancel")} cancel${hideKey ? ` · ${hideKey} hide` : ""}`;
1123
1262
  const enter = formatKeyHint("enter");
1124
1263
  const upDown = editorKeys("tui.select.up", "tui.select.down");
1125
1264
  const inputGuard = this.options.inputGuard;