@oh-my-pi/pi-coding-agent 15.10.0 → 15.10.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 (176) hide show
  1. package/CHANGELOG.md +75 -1
  2. package/dist/types/cli/dry-balance-cli.d.ts +15 -1
  3. package/dist/types/commit/analysis/conventional.d.ts +2 -2
  4. package/dist/types/commit/analysis/summary.d.ts +2 -2
  5. package/dist/types/commit/changelog/generate.d.ts +2 -2
  6. package/dist/types/commit/changelog/index.d.ts +2 -2
  7. package/dist/types/commit/map-reduce/index.d.ts +3 -3
  8. package/dist/types/commit/map-reduce/map-phase.d.ts +2 -2
  9. package/dist/types/commit/map-reduce/reduce-phase.d.ts +2 -2
  10. package/dist/types/commit/model-selection.d.ts +10 -4
  11. package/dist/types/config/api-key-resolver.d.ts +34 -0
  12. package/dist/types/config/model-registry.d.ts +17 -1
  13. package/dist/types/config/settings-schema.d.ts +9 -0
  14. package/dist/types/dap/config.d.ts +14 -1
  15. package/dist/types/dap/types.d.ts +10 -0
  16. package/dist/types/lsp/utils.d.ts +3 -2
  17. package/dist/types/modes/components/chat-block.d.ts +64 -0
  18. package/dist/types/modes/components/custom-editor.d.ts +3 -0
  19. package/dist/types/modes/components/overlay-box.d.ts +17 -0
  20. package/dist/types/modes/components/plan-review-overlay.d.ts +59 -0
  21. package/dist/types/modes/components/plan-toc.d.ts +41 -0
  22. package/dist/types/modes/components/read-tool-group.d.ts +2 -0
  23. package/dist/types/modes/components/transcript-container.d.ts +11 -0
  24. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  25. package/dist/types/modes/controllers/event-controller.d.ts +0 -1
  26. package/dist/types/modes/controllers/extension-ui-controller.d.ts +0 -1
  27. package/dist/types/modes/controllers/input-controller.d.ts +1 -1
  28. package/dist/types/modes/controllers/streaming-reveal.d.ts +22 -0
  29. package/dist/types/modes/controllers/tan-command-controller.d.ts +6 -0
  30. package/dist/types/modes/interactive-mode.d.ts +15 -5
  31. package/dist/types/modes/theme/theme.d.ts +1 -1
  32. package/dist/types/modes/types.d.ts +18 -5
  33. package/dist/types/modes/utils/copy-targets.d.ts +21 -1
  34. package/dist/types/plan-mode/approved-plan.d.ts +27 -8
  35. package/dist/types/plan-mode/plan-protection.d.ts +4 -4
  36. package/dist/types/sdk.d.ts +2 -0
  37. package/dist/types/session/agent-session.d.ts +21 -0
  38. package/dist/types/session/messages.d.ts +12 -0
  39. package/dist/types/session/session-manager.d.ts +3 -1
  40. package/dist/types/slash-commands/types.d.ts +4 -6
  41. package/dist/types/task/executor.d.ts +7 -0
  42. package/dist/types/task/index.d.ts +1 -0
  43. package/dist/types/task/render.d.ts +3 -2
  44. package/dist/types/tools/archive-reader.d.ts +5 -0
  45. package/dist/types/tools/ast-edit.d.ts +3 -0
  46. package/dist/types/tools/ast-grep.d.ts +3 -0
  47. package/dist/types/tools/bash.d.ts +1 -0
  48. package/dist/types/tools/find.d.ts +8 -4
  49. package/dist/types/tools/grouped-file-output.d.ts +95 -12
  50. package/dist/types/tools/memory-render.d.ts +4 -1
  51. package/dist/types/tools/plan-mode-guard.d.ts +8 -9
  52. package/dist/types/tools/render-utils.d.ts +5 -9
  53. package/dist/types/tools/search.d.ts +4 -0
  54. package/dist/types/tools/sqlite-reader.d.ts +1 -0
  55. package/dist/types/tools/todo.d.ts +3 -2
  56. package/dist/types/tools/write.d.ts +3 -0
  57. package/dist/types/tui/output-block.d.ts +16 -4
  58. package/dist/types/tui/status-line.d.ts +3 -0
  59. package/dist/types/utils/enhanced-paste.d.ts +20 -0
  60. package/dist/types/web/search/providers/kimi.d.ts +1 -1
  61. package/package.json +9 -9
  62. package/src/auto-thinking/classifier.ts +5 -1
  63. package/src/cli/dry-balance-cli.ts +52 -17
  64. package/src/cli/gallery-cli.ts +4 -1
  65. package/src/cli/gallery-fixtures/misc.ts +29 -0
  66. package/src/commit/analysis/conventional.ts +2 -2
  67. package/src/commit/analysis/summary.ts +2 -2
  68. package/src/commit/changelog/generate.ts +2 -2
  69. package/src/commit/changelog/index.ts +2 -2
  70. package/src/commit/map-reduce/index.ts +3 -3
  71. package/src/commit/map-reduce/map-phase.ts +2 -2
  72. package/src/commit/map-reduce/reduce-phase.ts +2 -2
  73. package/src/commit/model-selection.ts +33 -9
  74. package/src/commit/pipeline.ts +4 -4
  75. package/src/config/api-key-resolver.ts +58 -0
  76. package/src/config/model-registry.ts +25 -2
  77. package/src/config/settings-schema.ts +10 -0
  78. package/src/config/settings.ts +20 -2
  79. package/src/dap/config.ts +41 -2
  80. package/src/dap/defaults.json +1 -0
  81. package/src/dap/session.ts +1 -0
  82. package/src/dap/types.ts +10 -0
  83. package/src/debug/index.ts +40 -54
  84. package/src/edit/renderer.ts +82 -78
  85. package/src/eval/__tests__/llm-bridge.test.ts +90 -31
  86. package/src/eval/llm-bridge.ts +8 -3
  87. package/src/goals/tools/goal-tool.ts +36 -26
  88. package/src/internal-urls/docs-index.generated.ts +6 -6
  89. package/src/lsp/utils.ts +3 -2
  90. package/src/main.ts +9 -7
  91. package/src/memories/index.ts +12 -5
  92. package/src/mnemopi/backend.ts +5 -1
  93. package/src/modes/acp/acp-agent.ts +33 -26
  94. package/src/modes/components/assistant-message.ts +2 -9
  95. package/src/modes/components/chat-block.ts +111 -0
  96. package/src/modes/components/copy-selector.ts +1 -44
  97. package/src/modes/components/custom-editor.ts +23 -0
  98. package/src/modes/components/custom-message.ts +1 -3
  99. package/src/modes/components/execution-shared.ts +1 -2
  100. package/src/modes/components/hook-message.ts +1 -3
  101. package/src/modes/components/overlay-box.ts +108 -0
  102. package/src/modes/components/plan-review-overlay.ts +799 -0
  103. package/src/modes/components/plan-toc.ts +138 -0
  104. package/src/modes/components/read-tool-group.ts +20 -4
  105. package/src/modes/components/skill-message.ts +0 -1
  106. package/src/modes/components/tips.txt +1 -0
  107. package/src/modes/components/todo-reminder.ts +0 -2
  108. package/src/modes/components/tool-execution.ts +68 -88
  109. package/src/modes/components/transcript-container.ts +84 -24
  110. package/src/modes/components/user-message.ts +1 -2
  111. package/src/modes/controllers/command-controller-shared.ts +7 -6
  112. package/src/modes/controllers/command-controller.ts +57 -55
  113. package/src/modes/controllers/event-controller.ts +41 -40
  114. package/src/modes/controllers/extension-ui-controller.ts +10 -73
  115. package/src/modes/controllers/input-controller.ts +124 -119
  116. package/src/modes/controllers/mcp-command-controller.ts +69 -60
  117. package/src/modes/controllers/selector-controller.ts +23 -25
  118. package/src/modes/controllers/streaming-reveal.ts +212 -0
  119. package/src/modes/controllers/tan-command-controller.ts +173 -0
  120. package/src/modes/interactive-mode.ts +169 -94
  121. package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
  122. package/src/modes/theme/theme-schema.json +1 -1
  123. package/src/modes/theme/theme.ts +8 -4
  124. package/src/modes/types.ts +18 -7
  125. package/src/modes/utils/copy-targets.ts +133 -27
  126. package/src/modes/utils/ui-helpers.ts +44 -46
  127. package/src/plan-mode/approved-plan.ts +66 -43
  128. package/src/plan-mode/plan-protection.ts +4 -4
  129. package/src/prompts/system/background-tan-dispatch.md +8 -0
  130. package/src/prompts/system/plan-mode-active.md +67 -58
  131. package/src/prompts/system/plan-mode-approved.md +1 -1
  132. package/src/sdk.ts +11 -37
  133. package/src/session/agent-session.ts +82 -6
  134. package/src/session/messages.ts +26 -0
  135. package/src/session/session-manager.ts +13 -5
  136. package/src/slash-commands/builtin-registry.ts +36 -9
  137. package/src/slash-commands/types.ts +4 -6
  138. package/src/task/executor.ts +5 -2
  139. package/src/task/index.ts +4 -0
  140. package/src/task/render.ts +212 -147
  141. package/src/tools/archive-reader.ts +64 -0
  142. package/src/tools/ask.ts +119 -164
  143. package/src/tools/ast-edit.ts +98 -71
  144. package/src/tools/ast-grep.ts +37 -43
  145. package/src/tools/bash.ts +50 -6
  146. package/src/tools/debug.ts +20 -8
  147. package/src/tools/fetch.ts +297 -7
  148. package/src/tools/find.ts +44 -30
  149. package/src/tools/gh-renderer.ts +81 -42
  150. package/src/tools/grouped-file-output.ts +272 -48
  151. package/src/tools/image-gen.ts +150 -103
  152. package/src/tools/inspect-image-renderer.ts +63 -41
  153. package/src/tools/inspect-image.ts +8 -1
  154. package/src/tools/job.ts +3 -4
  155. package/src/tools/memory-render.ts +4 -1
  156. package/src/tools/plan-mode-guard.ts +21 -39
  157. package/src/tools/read.ts +23 -16
  158. package/src/tools/render-utils.ts +21 -37
  159. package/src/tools/resolve.ts +14 -0
  160. package/src/tools/search-tool-bm25.ts +36 -23
  161. package/src/tools/search.ts +80 -78
  162. package/src/tools/sqlite-reader.ts +9 -12
  163. package/src/tools/todo.ts +118 -52
  164. package/src/tools/write.ts +81 -62
  165. package/src/tui/output-block.ts +60 -13
  166. package/src/tui/status-line.ts +5 -1
  167. package/src/utils/commit-message-generator.ts +9 -1
  168. package/src/utils/enhanced-paste.ts +202 -0
  169. package/src/utils/title-generator.ts +2 -1
  170. package/src/web/search/providers/anthropic.ts +25 -19
  171. package/src/web/search/providers/exa.ts +11 -3
  172. package/src/web/search/providers/kimi.ts +28 -17
  173. package/src/web/search/providers/parallel.ts +35 -24
  174. package/src/web/search/providers/synthetic.ts +8 -6
  175. package/src/web/search/providers/tavily.ts +9 -8
  176. package/src/web/search/providers/zai.ts +8 -6
@@ -1,5 +1,9 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
1
4
  import { inflateSync, strFromU8 } from "fflate";
2
5
 
6
+ import { formatBytes } from "./render-utils";
3
7
  import { ToolError } from "./tool-errors";
4
8
 
5
9
  export type ArchiveFormat = "zip" | "tar" | "tar.gz";
@@ -123,11 +127,21 @@ function getArchiveFormatFromPath(filePath: string): ArchiveFormat | undefined {
123
127
  return undefined;
124
128
  }
125
129
 
130
+ export function formatArchiveEntryLines(entries: readonly ArchiveDirectoryEntry[]): string[] {
131
+ return entries.map(entry => {
132
+ if (entry.isDirectory) return `${entry.name}/`;
133
+
134
+ const sizeSuffix = entry.size > 0 ? ` (${formatBytes(entry.size)})` : "";
135
+ return `${entry.name}${sizeSuffix}`;
136
+ });
137
+ }
138
+
126
139
  const ZIP_LOCAL_FILE_HEADER_SIGNATURE = 0x04034b50;
127
140
  const ZIP_CENTRAL_DIRECTORY_HEADER_SIGNATURE = 0x02014b50;
128
141
  const ZIP64_EOCD_SIGNATURE = 0x06064b50;
129
142
  const ZIP64_EOCD_LOCATOR_SIGNATURE = 0x07064b50;
130
143
  const ZIP_EOCD_SIGNATURE = 0x06054b50;
144
+ const ZIP_DATA_DESCRIPTOR_SIGNATURE = 0x08074b50;
131
145
  const ZIP_EOCD_MIN_LENGTH = 22;
132
146
  const ZIP_EOCD_MAX_COMMENT_LENGTH = 0xffff;
133
147
  const ZIP64_EOCD_LOCATOR_LENGTH = 20;
@@ -167,6 +181,37 @@ function readUInt32LE(bytes: Uint8Array, offset: number): number {
167
181
  return (bytes[offset]! | (bytes[offset + 1]! << 8) | (bytes[offset + 2]! << 16) | (bytes[offset + 3]! << 24)) >>> 0;
168
182
  }
169
183
 
184
+ function bytesMatchAscii(bytes: Uint8Array, offset: number, value: string): boolean {
185
+ if (bytes.byteLength < offset + value.length) return false;
186
+ for (let index = 0; index < value.length; index++) {
187
+ if (bytes[offset + index] !== value.charCodeAt(index)) return false;
188
+ }
189
+ return true;
190
+ }
191
+
192
+ export function sniffArchiveFormat(bytes: Uint8Array): ArchiveFormat | undefined {
193
+ if (bytes.byteLength >= 4) {
194
+ const signature = readUInt32LE(bytes, 0);
195
+ if (
196
+ signature === ZIP_LOCAL_FILE_HEADER_SIGNATURE ||
197
+ signature === ZIP_EOCD_SIGNATURE ||
198
+ signature === ZIP_DATA_DESCRIPTOR_SIGNATURE
199
+ ) {
200
+ return "zip";
201
+ }
202
+ }
203
+
204
+ if (bytes.byteLength >= 2 && bytes[0] === 0x1f && bytes[1] === 0x8b) {
205
+ return "tar.gz";
206
+ }
207
+
208
+ if (bytesMatchAscii(bytes, 257, "ustar")) {
209
+ return "tar";
210
+ }
211
+
212
+ return undefined;
213
+ }
214
+
170
215
  function readUInt64LEAsNumber(bytes: Uint8Array, offset: number): number {
171
216
  const value = readUInt32LE(bytes, offset) + readUInt32LE(bytes, offset + 4) * ZIP_UINT32_RANGE;
172
217
  if (!Number.isSafeInteger(value)) {
@@ -627,3 +672,22 @@ export async function openArchive(filePath: string): Promise<ArchiveReader> {
627
672
  format === "zip" ? await readZipEntries(filePath) : await readTarEntries(await Bun.file(filePath).bytes());
628
673
  return new ArchiveReader(format, entries);
629
674
  }
675
+
676
+ export async function listArchiveRoot(
677
+ bytes: Uint8Array,
678
+ format: ArchiveFormat,
679
+ opts: { limit?: number } = {},
680
+ ): Promise<string> {
681
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "omp-archive-"));
682
+ const tempPath = path.join(tempDir, `payload.${format}`);
683
+ try {
684
+ await Bun.write(tempPath, bytes);
685
+ const archive = await openArchive(tempPath);
686
+ const entries = archive.listDirectory("");
687
+ const limitedEntries = opts.limit !== undefined && opts.limit > 0 ? entries.slice(0, opts.limit) : entries;
688
+ const lines = formatArchiveEntryLines(limitedEntries);
689
+ return lines.length > 0 ? lines.join("\n") : "(empty archive directory)";
690
+ } finally {
691
+ await fs.rm(tempDir, { recursive: true, force: true });
692
+ }
693
+ }
package/src/tools/ask.ts CHANGED
@@ -16,22 +16,14 @@
16
16
  */
17
17
 
18
18
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
19
- import {
20
- type Component,
21
- Container,
22
- Markdown,
23
- type MarkdownTheme,
24
- renderInlineMarkdown,
25
- TERMINAL,
26
- Text,
27
- } from "@oh-my-pi/pi-tui";
19
+ import { type Component, Markdown, type MarkdownTheme, renderInlineMarkdown, TERMINAL, Text } from "@oh-my-pi/pi-tui";
28
20
  import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
29
21
  import * as z from "zod/v4";
30
22
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
31
23
  import type { ExtensionUISelectItem } from "../extensibility/extensions";
32
24
  import { getMarkdownTheme, type Theme, theme } from "../modes/theme/theme";
33
25
  import askDescription from "../prompts/tools/ask.md" with { type: "text" };
34
- import { renderStatusLine } from "../tui";
26
+ import { framedBlock, renderStatusLine } from "../tui";
35
27
  import type { ToolSession } from ".";
36
28
  import { formatErrorMessage, formatMeta, formatTitle } from "./render-utils";
37
29
  import { ToolAbortError } from "./tool-errors";
@@ -626,23 +618,14 @@ interface AskRenderArgs {
626
618
  }>;
627
619
  }
628
620
 
629
- /** Render custom input as a single block with continuation lines (not one entry per line) */
630
- function renderCustomInput(
631
- uiTheme: Theme,
632
- prefix: string,
633
- customInput: string,
634
- isLastEntry: boolean,
635
- includeLeadingNewline = true,
636
- ): string {
621
+ /** Render a custom free-text answer as a status line plus indented continuation rows. */
622
+ function renderCustomInputLines(uiTheme: Theme, customInput: string): string[] {
637
623
  const lines = customInput.split("\n");
638
- const branch = isLastEntry ? uiTheme.tree.last : uiTheme.tree.branch;
639
- const firstLine = lines[0] ?? "";
640
- let text = `${includeLeadingNewline ? "\n" : ""}${prefix}${uiTheme.fg("dim", branch)} ${uiTheme.styledSymbol("status.success", "success")} ${uiTheme.fg("toolOutput", firstLine)}`;
641
- const continuationIndent = isLastEntry ? " " : `${uiTheme.fg("dim", uiTheme.tree.vertical)} `;
642
- for (let i = 1; i < lines.length; i++) {
643
- text += `\n${prefix}${continuationIndent} ${uiTheme.fg("toolOutput", lines[i])}`;
644
- }
645
- return text;
624
+ const out: string[] = [
625
+ ` ${uiTheme.styledSymbol("status.success", "success")} ${uiTheme.fg("toolOutput", lines[0] ?? "")}`,
626
+ ];
627
+ for (let i = 1; i < lines.length; i++) out.push(` ${uiTheme.fg("toolOutput", lines[i])}`);
628
+ return out;
646
629
  }
647
630
 
648
631
  /**
@@ -654,25 +637,38 @@ function optionMarker(uiTheme: Theme, multi: boolean | undefined, selected: bool
654
637
  return selected ? uiTheme.radio.selected : uiTheme.radio.unselected;
655
638
  }
656
639
 
640
+ /** Render the offered options for a question form as flat marker bullets (no tree guides). */
641
+ function renderQuestionOptionLines(
642
+ uiTheme: Theme,
643
+ mdTheme: MarkdownTheme,
644
+ options: AskRenderOption[],
645
+ multi: boolean | undefined,
646
+ ): string[] {
647
+ const out: string[] = [];
648
+ for (const opt of options) {
649
+ const optLabel = renderInlineMarkdown(opt.label, mdTheme, t => uiTheme.fg("muted", t));
650
+ out.push(` ${uiTheme.fg("dim", optionMarker(uiTheme, multi, false))} ${optLabel}`);
651
+ if (opt.description?.trim()) {
652
+ const description = renderInlineMarkdown(opt.description.trim(), mdTheme, t => uiTheme.fg("dim", t));
653
+ out.push(` ${uiTheme.fg("dim", "↳")} ${description}`);
654
+ }
655
+ }
656
+ return out;
657
+ }
658
+
657
659
  /**
658
660
  * Render the answered option list for a question: every offered option with its
659
- * selection marker filled in, plus any custom free-text answer. This keeps the
660
- * result visually identical to the question form (`renderCall`) so the answer
661
- * reads in place rather than as a detached summary block.
662
- *
663
- * `linePrefix` is the indent that precedes each entry's tree branch — a single
664
- * leading space for top-level (single-question) entries, or the question's
665
- * vertical continuation for nested (multi-question) entries.
661
+ * selection marker filled in, plus any custom free-text answer. Flat marker
662
+ * bullets the frame is the container, so no tree guides are drawn.
666
663
  */
667
- function renderAnswerOptions(
664
+ function renderAnswerOptionLines(
668
665
  uiTheme: Theme,
669
666
  mdTheme: MarkdownTheme,
670
- linePrefix: string,
671
667
  options: string[] | undefined,
672
668
  selectedOptions: string[] | undefined,
673
669
  multi: boolean | undefined,
674
670
  customInput: string | undefined,
675
- ): string {
671
+ ): string[] {
676
672
  const selected = new Set(selectedOptions ?? []);
677
673
  // Prefer the full recorded option set; fall back to the selected labels when
678
674
  // details omit the options array.
@@ -680,26 +676,21 @@ function renderAnswerOptions(
680
676
 
681
677
  // Nothing was chosen (and no custom answer) → a lone cancelled marker.
682
678
  if (selected.size === 0 && customInput === undefined) {
683
- return `${linePrefix}${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.styledSymbol("status.warning", "warning")} ${uiTheme.fg("warning", "Cancelled")}`;
679
+ return [` ${uiTheme.styledSymbol("status.warning", "warning")} ${uiTheme.fg("warning", "Cancelled")}`];
684
680
  }
685
681
 
686
- let text = "";
687
- for (let i = 0; i < list.length; i++) {
688
- const label = list[i];
682
+ const out: string[] = [];
683
+ for (const label of list) {
689
684
  const isSelected = selected.has(label);
690
- const isLastEntry = i === list.length - 1 && customInput === undefined;
691
- const branch = isLastEntry ? uiTheme.tree.last : uiTheme.tree.branch;
692
685
  const marker = optionMarker(uiTheme, multi, isSelected);
693
686
  const markerStyled = isSelected ? uiTheme.fg("success", marker) : uiTheme.fg("dim", marker);
694
687
  const labelStyled = renderInlineMarkdown(label, mdTheme, t =>
695
688
  isSelected ? uiTheme.fg("toolOutput", t) : uiTheme.fg("muted", t),
696
689
  );
697
- text += `${text ? "\n" : ""}${linePrefix}${uiTheme.fg("dim", branch)} ${markerStyled} ${labelStyled}`;
690
+ out.push(` ${markerStyled} ${labelStyled}`);
698
691
  }
699
- if (customInput !== undefined) {
700
- text += renderCustomInput(uiTheme, linePrefix, customInput, true, text.length > 0);
701
- }
702
- return text;
692
+ if (customInput !== undefined) out.push(...renderCustomInputLines(uiTheme, customInput));
693
+ return out;
703
694
  }
704
695
 
705
696
  export const askToolRenderer = {
@@ -708,80 +699,58 @@ export const askToolRenderer = {
708
699
  const label = formatTitle("Ask", uiTheme);
709
700
  const mdTheme = getMarkdownTheme();
710
701
  const accentStyle = { color: (t: string) => uiTheme.fg("accent", t) };
702
+ const md = (text: string, width: number) =>
703
+ new Markdown(text, 1, 0, mdTheme, accentStyle).render(Math.max(1, width - 3 + 1));
711
704
 
712
- // Multi-part questions
705
+ // Multi-part questions: one divider-labelled section per question.
713
706
  if (args.questions && args.questions.length > 0) {
714
- const container = new Container();
715
- container.addChild(new Text(`${label} ${uiTheme.fg("muted", `${args.questions.length} questions`)}`, 0, 0));
716
-
717
- for (let i = 0; i < args.questions.length; i++) {
718
- const q = args.questions[i];
719
- const isLastQ = i === args.questions.length - 1;
720
- const qBranch = isLastQ ? uiTheme.tree.last : uiTheme.tree.branch;
721
- const continuation = isLastQ ? " " : uiTheme.tree.vertical;
722
-
723
- const meta: string[] = [];
724
- if (q.multi) meta.push("multi");
725
- if (q.options?.length) meta.push(`options:${q.options.length}`);
726
- const metaStr = meta.length > 0 ? uiTheme.fg("dim", ` · ${meta.join(" · ")}`) : "";
727
-
728
- container.addChild(
729
- new Text(` ${uiTheme.fg("dim", qBranch)} ${uiTheme.fg("dim", `[${q.id}]`)}${metaStr}`, 0, 0),
730
- );
731
- container.addChild(new Markdown(q.question, 3, 0, mdTheme, accentStyle));
732
-
733
- if (q.options?.length) {
734
- let optText = "";
735
- for (let j = 0; j < q.options.length; j++) {
736
- const opt = q.options[j];
737
- const isLastOpt = j === q.options.length - 1;
738
- const optBranch = isLastOpt ? uiTheme.tree.last : uiTheme.tree.branch;
739
- const optLabel = renderInlineMarkdown(opt.label, mdTheme, t => uiTheme.fg("muted", t));
740
- optText += `\n ${uiTheme.fg("dim", continuation)} ${uiTheme.fg("dim", optBranch)} ${uiTheme.fg("dim", optionMarker(uiTheme, q.multi, false))} ${optLabel}`;
741
- if (opt.description?.trim()) {
742
- const optContinuation = isLastOpt ? " " : uiTheme.tree.vertical;
743
- const description = renderInlineMarkdown(opt.description.trim(), mdTheme, t =>
744
- uiTheme.fg("dim", t),
745
- );
746
- optText += `\n ${uiTheme.fg("dim", continuation)} ${uiTheme.fg("dim", optContinuation)} ${uiTheme.fg("dim", "↳")} ${description}`;
747
- }
748
- }
749
- container.addChild(new Text(optText, 0, 0));
750
- }
751
- }
752
- return container;
707
+ const questions = args.questions;
708
+ const header = `${label} ${uiTheme.fg("muted", `${questions.length} questions`)}`;
709
+ return framedBlock(uiTheme, width => {
710
+ const sections = questions.map(q => {
711
+ const meta: string[] = [];
712
+ if (q.multi) meta.push("multi");
713
+ if (q.options?.length) meta.push(`options:${q.options.length}`);
714
+ const metaStr = meta.length > 0 ? uiTheme.fg("dim", ` · ${meta.join(" · ")}`) : "";
715
+ const lines = md(q.question, width);
716
+ if (q.options?.length) lines.push(...renderQuestionOptionLines(uiTheme, mdTheme, q.options, q.multi));
717
+ return { label: `${uiTheme.fg("dim", `[${q.id}]`)}${metaStr}`, lines };
718
+ });
719
+ return { header, sections, state: "pending", borderColor: "borderMuted", width };
720
+ });
753
721
  }
754
722
 
755
723
  // Single question
756
724
  if (!args.question) {
757
- return new Text(formatErrorMessage("No question provided", uiTheme), 0, 0);
725
+ const errorLine = formatErrorMessage("No question provided", uiTheme);
726
+ return framedBlock(uiTheme, width => ({
727
+ header: errorLine,
728
+ sections: [],
729
+ state: "error",
730
+ borderColor: "error",
731
+ width,
732
+ }));
758
733
  }
759
734
 
760
- const container = new Container();
735
+ const question = args.question;
761
736
  const meta: string[] = [];
762
737
  if (args.multi) meta.push("multi");
763
738
  if (args.options?.length) meta.push(`options:${args.options.length}`);
764
- container.addChild(new Text(`${label}${formatMeta(meta, uiTheme)}`, 0, 0));
765
- container.addChild(new Markdown(args.question, 1, 0, mdTheme, accentStyle));
766
-
767
- if (args.options?.length) {
768
- let optText = "";
769
- for (let i = 0; i < args.options.length; i++) {
770
- const opt = args.options[i];
771
- const isLast = i === args.options.length - 1;
772
- const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
773
- const optLabel = renderInlineMarkdown(opt.label, mdTheme, t => uiTheme.fg("muted", t));
774
- optText += `\n ${uiTheme.fg("dim", branch)} ${uiTheme.fg("dim", optionMarker(uiTheme, args.multi, false))} ${optLabel}`;
775
- if (opt.description?.trim()) {
776
- const continuation = isLast ? " " : uiTheme.tree.vertical;
777
- const description = renderInlineMarkdown(opt.description.trim(), mdTheme, t => uiTheme.fg("dim", t));
778
- optText += `\n ${uiTheme.fg("dim", continuation)} ${uiTheme.fg("dim", "↳")} ${description}`;
779
- }
780
- }
781
- container.addChild(new Text(optText, 0, 0));
782
- }
783
-
784
- return container;
739
+ const header = `${label}${formatMeta(meta, uiTheme)}`;
740
+ const questionOptions = args.options;
741
+ const multi = args.multi;
742
+ return framedBlock(uiTheme, width => {
743
+ const bodyLines = md(question, width);
744
+ if (questionOptions?.length)
745
+ bodyLines.push(...renderQuestionOptionLines(uiTheme, mdTheme, questionOptions, multi));
746
+ return {
747
+ header,
748
+ sections: bodyLines.length > 0 ? [{ lines: bodyLines }] : [],
749
+ state: "pending",
750
+ borderColor: "borderMuted",
751
+ width,
752
+ };
753
+ });
785
754
  },
786
755
 
787
756
  renderResult(
@@ -792,56 +761,47 @@ export const askToolRenderer = {
792
761
  const { details } = result;
793
762
  const mdTheme = getMarkdownTheme();
794
763
  const accentStyle = { color: (t: string) => uiTheme.fg("accent", t) };
764
+ const md = (text: string, width: number) =>
765
+ new Markdown(text, 1, 0, mdTheme, accentStyle).render(Math.max(1, width - 3 + 1));
795
766
 
796
767
  if (!details) {
797
768
  const txt = result.content[0];
798
769
  const fallback = txt?.type === "text" && txt.text ? txt.text : "";
799
770
  const header = renderStatusLine({ icon: "warning", title: "Ask" }, uiTheme);
800
- return new Text(`${header}\n${uiTheme.fg("dim", fallback)}`, 0, 0);
771
+ const body = fallback ? `\n${uiTheme.fg("dim", fallback)}` : "";
772
+ return new Text(`${header}${body}`, 0, 0);
801
773
  }
802
774
 
803
- // Multi-part results
775
+ // Multi-part results: one divider-labelled section per question.
804
776
  if (details.results && details.results.length > 0) {
805
- const hasAnySelection = details.results.some(
777
+ const results = details.results;
778
+ const hasAnySelection = results.some(
806
779
  r => r.customInput !== undefined || (r.selectedOptions && r.selectedOptions.length > 0),
807
780
  );
808
781
  const header = renderStatusLine(
809
782
  {
810
783
  icon: hasAnySelection ? "success" : "warning",
811
784
  title: "Ask",
812
- meta: [`${details.results.length} questions`],
785
+ meta: [`${results.length} questions`],
813
786
  },
814
787
  uiTheme,
815
788
  );
816
- const container = new Container();
817
- container.addChild(new Text(header, 0, 0));
818
-
819
- for (let i = 0; i < details.results.length; i++) {
820
- const r = details.results[i];
821
- const isLastQuestion = i === details.results.length - 1;
822
- const qBranch = isLastQuestion ? uiTheme.tree.last : uiTheme.tree.branch;
823
- const continuation = isLastQuestion ? " " : uiTheme.tree.vertical;
824
- const linePrefix = ` ${uiTheme.fg("dim", continuation)} `;
825
-
826
- container.addChild(new Text(` ${uiTheme.fg("dim", qBranch)} ${uiTheme.fg("dim", `[${r.id}]`)}`, 0, 0));
827
- container.addChild(new Markdown(r.question, 3, 0, mdTheme, accentStyle));
828
- container.addChild(
829
- new Text(
830
- renderAnswerOptions(
831
- uiTheme,
832
- mdTheme,
833
- linePrefix,
834
- r.options,
835
- r.selectedOptions,
836
- r.multi,
837
- r.customInput,
838
- ),
839
- 0,
840
- 0,
841
- ),
842
- );
843
- }
844
- return container;
789
+ return framedBlock(uiTheme, width => {
790
+ const sections = results.map(r => {
791
+ const lines = md(r.question, width);
792
+ lines.push(
793
+ ...renderAnswerOptionLines(uiTheme, mdTheme, r.options, r.selectedOptions, r.multi, r.customInput),
794
+ );
795
+ return { label: uiTheme.fg("dim", `[${r.id}]`), lines };
796
+ });
797
+ return {
798
+ header,
799
+ sections,
800
+ state: hasAnySelection ? "success" : "warning",
801
+ borderColor: "borderMuted",
802
+ width,
803
+ };
804
+ });
845
805
  }
846
806
 
847
807
  // Single question result
@@ -851,29 +811,24 @@ export const askToolRenderer = {
851
811
  return new Text(fallback, 0, 0);
852
812
  }
853
813
 
814
+ const question = details.question;
854
815
  const hasSelection =
855
816
  details.customInput !== undefined || (details.selectedOptions && details.selectedOptions.length > 0);
856
817
  const header = renderStatusLine({ icon: hasSelection ? "success" : "warning", title: "Ask" }, uiTheme);
857
- const container = new Container();
858
- container.addChild(new Text(header, 0, 0));
859
- container.addChild(new Markdown(details.question, 1, 0, mdTheme, accentStyle));
860
-
861
- container.addChild(
862
- new Text(
863
- renderAnswerOptions(
864
- uiTheme,
865
- mdTheme,
866
- " ",
867
- details.options,
868
- details.selectedOptions,
869
- details.multi,
870
- details.customInput,
871
- ),
872
- 0,
873
- 0,
874
- ),
875
- );
876
-
877
- return container;
818
+ const dOptions = details.options;
819
+ const dSelected = details.selectedOptions;
820
+ const dMulti = details.multi;
821
+ const dCustom = details.customInput;
822
+ return framedBlock(uiTheme, width => {
823
+ const bodyLines = md(question, width);
824
+ bodyLines.push(...renderAnswerOptionLines(uiTheme, mdTheme, dOptions, dSelected, dMulti, dCustom));
825
+ return {
826
+ header,
827
+ sections: bodyLines.length > 0 ? [{ lines: bodyLines }] : [],
828
+ state: hasSelection ? "success" : "warning",
829
+ borderColor: "borderMuted",
830
+ width,
831
+ };
832
+ });
878
833
  },
879
834
  };