@gajae-code/coding-agent 0.11.3 → 0.11.4

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 (82) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/types/cli/worktree-cli.d.ts +8 -25
  3. package/dist/types/commands/gc.d.ts +4 -0
  4. package/dist/types/commands/worktree.d.ts +7 -32
  5. package/dist/types/gjc-runtime/gc-runtime.d.ts +9 -0
  6. package/dist/types/gjc-runtime/state-argv.d.ts +38 -0
  7. package/dist/types/modes/components/custom-editor.d.ts +8 -3
  8. package/dist/types/modes/components/tool-transcript-format.d.ts +19 -0
  9. package/dist/types/modes/controllers/input-controller.d.ts +8 -2
  10. package/dist/types/sdk/broker/broker.d.ts +1 -0
  11. package/dist/types/sdk/broker/discovery.d.ts +10 -0
  12. package/dist/types/sdk/broker/ensure.d.ts +24 -1
  13. package/dist/types/sdk/broker/internal.d.ts +3 -0
  14. package/dist/types/sdk/broker/session-index.d.ts +12 -0
  15. package/dist/types/sdk/broker/state-version.d.ts +2 -1
  16. package/dist/types/tools/ask.d.ts +39 -12
  17. package/dist/types/tools/browser/screenshot-format.d.ts +8 -0
  18. package/dist/types/tools/browser/tab-protocol.d.ts +11 -0
  19. package/dist/types/tools/browser/tab-supervisor.d.ts +1 -1
  20. package/dist/types/tools/browser/tab-worker-path-resolver.d.ts +2 -0
  21. package/dist/types/tools/browser/tab-worker-smoke.d.ts +16 -0
  22. package/dist/types/tools/path-utils.d.ts +9 -14
  23. package/dist/types/tools/render-utils.d.ts +1 -8
  24. package/dist/types/tools/resource-gc.d.ts +14 -0
  25. package/dist/types/utils/image-loading.d.ts +23 -0
  26. package/dist/types/utils/image-resize.d.ts +2 -0
  27. package/dist/types/utils/pasted-image-loading.d.ts +39 -0
  28. package/dist/types/utils/pasted-image-path.d.ts +21 -19
  29. package/dist/types/web/scrapers/html-to-markdown.d.ts +5 -0
  30. package/dist/types/web/scrapers/types.d.ts +1 -5
  31. package/package.json +24 -8
  32. package/scripts/run-threat-model-check.ts +60 -0
  33. package/src/cli/worktree-cli.ts +9 -287
  34. package/src/cli.ts +10 -9
  35. package/src/commands/gc.ts +11 -1
  36. package/src/commands/sdk.ts +3 -2
  37. package/src/commands/worktree.ts +10 -53
  38. package/src/config/model-profiles.ts +15 -15
  39. package/src/eval/js/shared/runtime.ts +1 -1
  40. package/src/gjc-runtime/gc-render.ts +19 -1
  41. package/src/gjc-runtime/gc-runtime.ts +68 -3
  42. package/src/gjc-runtime/state-argv.ts +292 -0
  43. package/src/gjc-runtime/state-runtime.ts +42 -210
  44. package/src/modes/components/custom-editor.ts +103 -32
  45. package/src/modes/components/custom-provider-wizard.ts +26 -1
  46. package/src/modes/components/session-observer-overlay.ts +10 -23
  47. package/src/modes/components/tool-transcript-format.ts +72 -0
  48. package/src/modes/components/transcript-viewer-overlay.ts +42 -2
  49. package/src/modes/controllers/input-controller.ts +98 -37
  50. package/src/modes/controllers/selector-controller.ts +1 -1
  51. package/src/modes/interactive-mode.ts +13 -2
  52. package/src/sdk/broker/broker.ts +139 -32
  53. package/src/sdk/broker/discovery.ts +125 -0
  54. package/src/sdk/broker/ensure.ts +103 -19
  55. package/src/sdk/broker/internal.ts +10 -0
  56. package/src/sdk/broker/session-index.ts +228 -15
  57. package/src/sdk/broker/state-version.ts +5 -2
  58. package/src/sdk/bus/telegram-daemon.ts +50 -10
  59. package/src/session/internal/managed-session-scope.ts +15 -1
  60. package/src/tools/ask.ts +216 -41
  61. package/src/tools/bash-allowed-prefixes.ts +79 -29
  62. package/src/tools/browser/launch.ts +3 -1
  63. package/src/tools/browser/readable.ts +1 -1
  64. package/src/tools/browser/screenshot-format.ts +34 -0
  65. package/src/tools/browser/tab-protocol.ts +3 -0
  66. package/src/tools/browser/tab-supervisor.ts +125 -122
  67. package/src/tools/browser/tab-worker-entry.ts +33 -14
  68. package/src/tools/browser/tab-worker-path-resolver.ts +67 -0
  69. package/src/tools/browser/tab-worker-smoke.ts +87 -0
  70. package/src/tools/browser/tab-worker.ts +7 -5
  71. package/src/tools/path-utils.ts +42 -35
  72. package/src/tools/read.ts +7 -1
  73. package/src/tools/render-utils.ts +1 -27
  74. package/src/tools/resource-gc.ts +123 -34
  75. package/src/utils/image-loading.ts +85 -34
  76. package/src/utils/image-resize.ts +16 -9
  77. package/src/utils/pasted-image-loading.ts +514 -0
  78. package/src/utils/pasted-image-path.ts +223 -98
  79. package/src/web/scrapers/html-to-markdown.ts +68 -0
  80. package/src/web/scrapers/readthedocs.ts +1 -1
  81. package/src/web/scrapers/types.ts +1 -68
  82. package/src/web/search/providers/openai-compatible.ts +76 -23
@@ -8,10 +8,9 @@ import type { SessionMessageEntry } from "../../session/session-manager";
8
8
  import { parseSessionEntries } from "../../session/session-manager";
9
9
  import type { ObservableSession, SessionObserverRegistry } from "../session-observer-registry";
10
10
  import { theme } from "../theme/theme";
11
+ import { composeToolText } from "./tool-transcript-format";
11
12
  import { type TranscriptViewerEntry, TranscriptViewerOverlay } from "./transcript-viewer-overlay";
12
13
 
13
- const MAX_TOOL_ARGS_CHARS = 500;
14
-
15
14
  /** Session-observer adapter. The shared viewer owns navigation and fold state. */
16
15
  export class SessionObserverOverlayComponent extends TranscriptViewerOverlay {
17
16
  #registry: SessionObserverRegistry;
@@ -191,15 +190,20 @@ function entriesFromMessages(entries: readonly SessionMessageEntry[]): Transcrip
191
190
  .map(part => part.text)
192
191
  .join("\n")
193
192
  .trim() ?? "";
194
- const summary = formatToolArgs(content.name, content.arguments);
195
- const callText = [summary, content.intent].filter(Boolean).join("\n");
196
- const resultDisplay = result?.isError ? `✗ ${resultText || "Error"}` : resultText || "✓ done";
193
+ const text = composeToolText({
194
+ name: content.name,
195
+ args: content.arguments,
196
+ intent: content.intent,
197
+ resultText,
198
+ isError: result?.isError ?? false,
199
+ hasResult: results.has(content.id),
200
+ });
197
201
  output.push({
198
202
  id: `tool:${content.id}`,
199
203
  kind: "tool",
200
204
  label: content.name,
201
205
  payload: {
202
- text: [callText, resultDisplay].filter(Boolean).join("\n"),
206
+ text,
203
207
  metadata: {
204
208
  name: content.name,
205
209
  arguments: content.arguments,
@@ -234,23 +238,6 @@ function entriesFromMessages(entries: readonly SessionMessageEntry[]): Transcrip
234
238
  return output;
235
239
  }
236
240
 
237
- function formatToolArgs(name: string, args: Record<string, unknown>): string {
238
- if (name === "read" || name === "write" || name === "edit") return args.path ? `path: ${args.path}` : "";
239
- if (name === "bash") return typeof args.command === "string" ? args.command.replaceAll("\t", " ") : "";
240
- if (name === "search")
241
- return [
242
- args.pattern ? `pattern: ${args.pattern}` : "",
243
- Array.isArray(args.paths) ? `paths: ${args.paths.join(", ")}` : "",
244
- ]
245
- .filter(Boolean)
246
- .join(", ");
247
- return Object.entries(args)
248
- .filter(([key]) => !key.startsWith("_"))
249
- .map(([key, value]) => `${key}: ${typeof value === "string" ? value : JSON.stringify(value)}`)
250
- .join(", ")
251
- .slice(0, MAX_TOOL_ARGS_CHARS);
252
- }
253
-
254
241
  function truncateThinking(text: string, expanded: boolean): string {
255
242
  const limit = expanded ? 4_000 : 200;
256
243
  return text.length > limit ? `${text.slice(0, limit)}...` : text;
@@ -0,0 +1,72 @@
1
+ export const MAX_TOOL_ARGS_CHARS = 500;
2
+ export const TOOL_RESULT_MAX_EXPANDED_LINES = 100;
3
+
4
+ type ToolCallFields = {
5
+ name: string;
6
+ args: Record<string, unknown>;
7
+ intent?: string;
8
+ };
9
+
10
+ type ToolResultFields = {
11
+ resultText: string;
12
+ isError: boolean;
13
+ hasResult: boolean;
14
+ };
15
+
16
+ export type ToolTranscriptFields = ToolCallFields & ToolResultFields;
17
+
18
+ export function formatToolArgs(name: string, args: Record<string, unknown>): string {
19
+ if (name === "read" || name === "write" || name === "edit") return args.path ? `path: ${args.path}` : "";
20
+ if (name === "bash") return typeof args.command === "string" ? args.command.replaceAll("\t", " ") : "";
21
+ if (name === "search")
22
+ return [
23
+ args.pattern ? `pattern: ${args.pattern}` : "",
24
+ Array.isArray(args.paths) ? `paths: ${args.paths.join(", ")}` : "",
25
+ ]
26
+ .filter(Boolean)
27
+ .join(", ");
28
+ return Object.entries(args)
29
+ .filter(([key]) => !key.startsWith("_"))
30
+ .map(([key, value]) => `${key}: ${typeof value === "string" ? value : JSON.stringify(value)}`)
31
+ .join(", ")
32
+ .slice(0, MAX_TOOL_ARGS_CHARS);
33
+ }
34
+
35
+ export function composeToolCall({ name, args, intent }: ToolCallFields): string {
36
+ return [formatToolArgs(name, args), intent].filter(Boolean).join("\n");
37
+ }
38
+
39
+ export function composeToolResult({ resultText, isError, hasResult }: ToolResultFields): string {
40
+ const text = resultText.trim();
41
+ if (!hasResult) return "⏳ pending";
42
+ if (isError) return `✗ ${text || "Error"}`;
43
+ return text || "✓ done";
44
+ }
45
+
46
+ export function composeToolText(fields: ToolTranscriptFields): string {
47
+ return [composeToolCall(fields), composeToolResult(fields)].filter(Boolean).join("\n");
48
+ }
49
+
50
+ /**
51
+ * Caps result output without allocating an array for every source line. The final
52
+ * line count still requires scanning the result so the omitted-line count is exact.
53
+ */
54
+ function expandedToolResult(result: string): string {
55
+ let lineCount = 1;
56
+ let prefixEnd = result.length;
57
+ for (let index = 0; index < result.length; index++) {
58
+ if (result[index] !== "\n") continue;
59
+ if (lineCount === TOOL_RESULT_MAX_EXPANDED_LINES) prefixEnd = index;
60
+ lineCount += 1;
61
+ }
62
+ const prefix = result.slice(0, prefixEnd);
63
+ return lineCount > TOOL_RESULT_MAX_EXPANDED_LINES
64
+ ? `${prefix}\n... ${lineCount - TOOL_RESULT_MAX_EXPANDED_LINES} more lines`
65
+ : prefix;
66
+ }
67
+
68
+ export function toolDisplayText(fields: ToolTranscriptFields, expanded: boolean): string {
69
+ const call = composeToolCall(fields);
70
+ if (!expanded) return call;
71
+ return [call, expandedToolResult(composeToolResult(fields))].filter(Boolean).join("\n");
72
+ }
@@ -11,6 +11,7 @@ import { sanitizeText } from "@gajae-code/utils";
11
11
  import { getMarkdownTheme, theme } from "../theme/theme";
12
12
  import type { TranscriptItemRegistry, TranscriptSourcePayload } from "../transcript-item-registry";
13
13
  import { DynamicBorder } from "./dynamic-border";
14
+ import { toolDisplayText } from "./tool-transcript-format";
14
15
 
15
16
  const INDENT = " ";
16
17
  const PAGE_SIZE = 15;
@@ -363,6 +364,29 @@ export class TranscriptViewerOverlay extends Container {
363
364
  }
364
365
  }
365
366
 
367
+ type ToolTranscriptMetadata = {
368
+ name: string;
369
+ arguments: Record<string, unknown>;
370
+ intent?: unknown;
371
+ resultText: string;
372
+ isError: boolean;
373
+ hasResult: boolean;
374
+ };
375
+
376
+ function hasToolTranscriptMetadata(
377
+ metadata: Readonly<Record<string, unknown>>,
378
+ ): metadata is Readonly<ToolTranscriptMetadata> {
379
+ return (
380
+ typeof metadata.name === "string" &&
381
+ metadata.arguments !== null &&
382
+ typeof metadata.arguments === "object" &&
383
+ !Array.isArray(metadata.arguments) &&
384
+ typeof metadata.resultText === "string" &&
385
+ typeof metadata.isError === "boolean" &&
386
+ typeof metadata.hasResult === "boolean"
387
+ );
388
+ }
389
+
366
390
  /** Main-session adapter over the registry's canonical payload resolver. */
367
391
  export function transcriptViewerEntries(registry: TranscriptItemRegistry): TranscriptViewerEntry[] {
368
392
  return registry.items().flatMap(item => {
@@ -375,10 +399,26 @@ export function transcriptViewerEntries(registry: TranscriptItemRegistry): Trans
375
399
  : item.kind === "assistant-thinking"
376
400
  ? "Thinking"
377
401
  : item.kind === "tool"
378
- ? "Tool"
402
+ ? String(payload.metadata.name ?? "Tool")
379
403
  : item.kind === "user"
380
404
  ? "User"
381
405
  : item.kind;
382
- return [{ id: item.id, kind: item.kind, label, payload, ...capabilities }];
406
+ const toolMetadata = hasToolTranscriptMetadata(payload.metadata) ? payload.metadata : undefined;
407
+ const getDisplayText =
408
+ item.kind === "tool" && toolMetadata
409
+ ? (expanded: boolean) =>
410
+ toolDisplayText(
411
+ {
412
+ name: toolMetadata.name,
413
+ args: toolMetadata.arguments,
414
+ intent: typeof toolMetadata.intent === "string" ? toolMetadata.intent : undefined,
415
+ resultText: toolMetadata.resultText,
416
+ isError: toolMetadata.isError,
417
+ hasResult: toolMetadata.hasResult,
418
+ },
419
+ expanded,
420
+ )
421
+ : undefined;
422
+ return [{ id: item.id, kind: item.kind, label, payload, ...capabilities, getDisplayText }];
383
423
  });
384
424
  }
@@ -2,7 +2,7 @@ import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
3
  import { type AgentMessage, ThinkingLevel } from "@gajae-code/agent-core";
4
4
  import { type AutocompleteProvider, matchesKey, type SlashCommand } from "@gajae-code/tui";
5
- import { $env, sanitizeText } from "@gajae-code/utils";
5
+ import { $env, logger, sanitizeText } from "@gajae-code/utils";
6
6
  import { type AppKeybinding, KEYBINDINGS } from "../../config/keybindings";
7
7
  import { isSettingsInitialized, settings } from "../../config/settings";
8
8
  import { resolveSubskillActivationForSkillInvocation } from "../../extensibility/gjc-plugins";
@@ -22,12 +22,14 @@ import { getUserMessageViewportAnchorIds } from "../../session/session-manager";
22
22
  import { executeBuiltinSlashCommand } from "../../slash-commands/builtin-registry";
23
23
  import { copyToClipboard, readImageFromClipboard } from "../../utils/clipboard";
24
24
  import { getEditorCommand, openInEditor } from "../../utils/external-editor";
25
- import { ensureSupportedImageInput, ImageInputTooLargeError, loadImageInput } from "../../utils/image-loading";
25
+ import { ensureSupportedImageInput, ImageInputTooLargeError } from "../../utils/image-loading";
26
26
  import { resizeImage } from "../../utils/image-resize";
27
- import { formatPastedImageReference, resolvePastedImagePath } from "../../utils/pasted-image-path";
27
+ import { loadPastedImageBatch, PastedImageBatchError } from "../../utils/pasted-image-loading";
28
+ import { formatPastedImageReference, parsePastedImagePaths } from "../../utils/pasted-image-path";
28
29
  import { generateSessionTitle, setSessionTerminalTitle } from "../../utils/title-generator";
29
30
  import { ActionRegistry, APP_ACTION_METADATA } from "../action-registry";
30
31
  import { CommandPalette, type CommandPaletteAction, type CommandPaletteEntry } from "../components/command-palette";
32
+ import type { PasteTextContext } from "../components/custom-editor";
31
33
  import { QueuePaneComponent } from "../components/queue-pane";
32
34
  import { type QueuedMessageMoveDirection, QueuedMessageSelectorComponent } from "../components/queued-message-selector";
33
35
 
@@ -44,14 +46,23 @@ const EMPTY_EDITOR_DOUBLE_ESCAPE_WINDOW_MS = 500;
44
46
  const IMAGE_PLACEHOLDER_PATTERN = /\[image ([1-9]\d*)\]/g;
45
47
  const IMAGE_PLACEHOLDER_PRESENT_PATTERN = /\[image [1-9]\d*\]/;
46
48
 
49
+ interface InputControllerDependencies {
50
+ loadPastedImageBatch?: typeof loadPastedImageBatch;
51
+ }
52
+
47
53
  function isExpandable(obj: unknown): obj is Expandable {
48
54
  return typeof obj === "object" && obj !== null && "setExpanded" in obj && typeof obj.setExpanded === "function";
49
55
  }
50
56
 
51
57
  export class InputController {
52
58
  readonly actionRegistry: ActionRegistry<void>;
59
+ readonly #loadPastedImageBatch: typeof loadPastedImageBatch;
53
60
 
54
- constructor(private ctx: InteractiveModeContext) {
61
+ constructor(
62
+ private ctx: InteractiveModeContext,
63
+ dependencies: InputControllerDependencies = {},
64
+ ) {
65
+ this.#loadPastedImageBatch = dependencies.loadPastedImageBatch ?? loadPastedImageBatch;
55
66
  this.actionRegistry = new ActionRegistry({
56
67
  context: undefined,
57
68
  showError: actionId => this.ctx.showError(actionId),
@@ -585,11 +596,11 @@ export class InputController {
585
596
  );
586
597
  this.ctx.editor.onCopyPrompt = () => this.#executeAction("app.clipboard.copyPrompt");
587
598
  this.#registerCommandPaletteAction("app.clipboard.copyPrompt", copyPrompt);
588
- this.ctx.editor.onPasteText = text => this.handleTextPaste(text);
589
- this.ctx.editor.onPastePendingInputCleared = (reason, droppedInputCount) => {
599
+ this.ctx.editor.onPasteText = (text, context) => this.handleTextPaste(text, context);
600
+ this.ctx.editor.onPastePendingInputCleared = (reason, restoredInputCount) => {
590
601
  const reasonText = reason === "timeout" ? "timed out" : "exceeded the input queue limit";
591
602
  this.ctx.showWarning(
592
- `Paste handling ${reasonText}; discarded ${droppedInputCount} buffered input event${droppedInputCount === 1 ? "" : "s"}.`,
603
+ `Paste handling ${reasonText}; restored the original paste and ${restoredInputCount} buffered input event${restoredInputCount === 1 ? "" : "s"}.`,
593
604
  );
594
605
  };
595
606
  const expandTools = () => this.toggleToolOutputExpansion();
@@ -1522,46 +1533,96 @@ export class InputController {
1522
1533
  return true;
1523
1534
  }
1524
1535
 
1525
- handleTextPaste(text: string): boolean | Promise<boolean> {
1526
- const imagePath = resolvePastedImagePath(text, { cwd: this.ctx.sessionManager.getCwd() });
1527
- return imagePath ? this.#attachPastedImagePath(imagePath) : false;
1536
+ handleTextPaste(text: string, context: PasteTextContext): boolean | Promise<boolean> {
1537
+ if (this.ctx.isBashMode || this.ctx.isPythonMode) return false;
1538
+ const parsed = parsePastedImagePaths(text, { cwd: this.ctx.sessionManager.getCwd() });
1539
+ if (!parsed) return false;
1540
+ if (parsed.kind === "too-many") {
1541
+ this.ctx.showStatus(`Cannot attach more than ${parsed.maxCandidates} pasted images.`);
1542
+ return false;
1543
+ }
1544
+ return this.#attachPastedImagePaths(parsed.paths, parsed.requiresConfirmation, context);
1528
1545
  }
1529
1546
 
1530
- /**
1531
- * Returns `false` on every failure path so the editor replays the original
1532
- * bracketed paste — the raw path text must never be lost when attachment
1533
- * is impossible (unsupported content, oversized image, load error).
1534
- */
1535
- async #attachPastedImagePath(imagePath: string): Promise<boolean> {
1547
+ /** Return false on every failure so CustomEditor restores the exact paste. */
1548
+ async #attachPastedImagePaths(
1549
+ imagePaths: readonly string[],
1550
+ requiresConfirmation: boolean,
1551
+ context: PasteTextContext,
1552
+ ): Promise<boolean> {
1553
+ const signal = context.signal;
1554
+ const editor = this.ctx.editor;
1555
+ const editorText = editor.getText();
1556
+ const pendingImages = this.ctx.pendingImages;
1557
+ const pendingImageCount = pendingImages.length;
1536
1558
  try {
1537
- const image = await loadImageInput({
1538
- path: imagePath,
1539
- cwd: this.ctx.sessionManager.getCwd(),
1559
+ if (requiresConfirmation) {
1560
+ const selection = await this.ctx.showHookSelector(
1561
+ `Attach ${imagePaths.length} pasted images?\nOnly attach files you intend to send to the model.`,
1562
+ ["Attach images", "Paste paths literally"],
1563
+ { signal, helpText: "Enter: choose · Esc: paste paths literally" },
1564
+ );
1565
+ signal.throwIfAborted();
1566
+ if (selection !== "Attach images") return false;
1567
+ }
1568
+
1569
+ const loaded = await this.#loadPastedImageBatch({
1570
+ paths: imagePaths,
1540
1571
  autoResize: this.ctx.settings.get("images.autoResize"),
1572
+ sourcePolicy: requiresConfirmation ? "confirmed" : "automatic-temp",
1573
+ signal,
1541
1574
  });
1542
- if (!image) {
1543
- this.ctx.showStatus("Unsupported pasted image file");
1544
- return false;
1545
- }
1575
+ signal.throwIfAborted();
1576
+ return context.commit(() => {
1577
+ if (
1578
+ this.ctx.editor !== editor ||
1579
+ editor.getText() !== editorText ||
1580
+ this.ctx.pendingImages !== pendingImages ||
1581
+ pendingImages.length !== pendingImageCount
1582
+ ) {
1583
+ return false;
1584
+ }
1546
1585
 
1547
- this.ctx.pendingImages = [
1548
- ...this.ctx.pendingImages,
1549
- {
1550
- type: "image",
1551
- data: image.data,
1552
- mimeType: image.mimeType,
1553
- },
1554
- ];
1555
- this.ctx.editor.insertText(`${formatPastedImageReference(this.#nextImagePlaceholder(), image.resolvedPath)} `);
1556
- this.ctx.showStatus(`Attached image: ${path.basename(image.resolvedPath)}`, { dim: true });
1557
- this.ctx.ui.requestRender();
1558
- return true;
1586
+ const firstImageNumber = pendingImages.length + 1;
1587
+ const references = loaded.sourcePaths.map((sourcePath, index) =>
1588
+ formatPastedImageReference(`[image ${firstImageNumber + index}]`, sourcePath),
1589
+ );
1590
+ try {
1591
+ editor.insertText(`${references.join(" ")} `);
1592
+ this.ctx.pendingImages = [...pendingImages, ...loaded.images];
1593
+ } catch (error) {
1594
+ try {
1595
+ editor.setText(editorText);
1596
+ } catch {
1597
+ // Preserve the original mutation error when rollback itself fails.
1598
+ } finally {
1599
+ this.ctx.pendingImages = pendingImages;
1600
+ }
1601
+ throw error;
1602
+ }
1603
+
1604
+ try {
1605
+ this.ctx.showStatus(
1606
+ loaded.images.length === 1
1607
+ ? `Attached image: ${path.basename(loaded.sourcePaths[0] ?? "")}`
1608
+ : `Attached ${loaded.images.length} images`,
1609
+ { dim: true },
1610
+ );
1611
+ this.ctx.ui.requestRender();
1612
+ } catch (error) {
1613
+ logger.warn("Pasted images attached but status rendering failed", { error: String(error) });
1614
+ }
1615
+ return true;
1616
+ });
1559
1617
  } catch (error) {
1560
- if (error instanceof ImageInputTooLargeError) {
1618
+ if (signal.aborted) return false;
1619
+ if (error instanceof ImageInputTooLargeError || error instanceof PastedImageBatchError) {
1561
1620
  this.ctx.showStatus(error.message);
1562
1621
  return false;
1563
1622
  }
1564
- this.ctx.showStatus("Failed to attach pasted image");
1623
+ this.ctx.showStatus(
1624
+ imagePaths.length === 1 ? "Failed to attach pasted image" : "Failed to attach pasted images",
1625
+ );
1565
1626
  return false;
1566
1627
  }
1567
1628
  }
@@ -1060,7 +1060,7 @@ export class SelectorController {
1060
1060
  };
1061
1061
  wizard = new CustomProviderWizardComponent(
1062
1062
  input => {
1063
- void submit(input);
1063
+ return submit(input);
1064
1064
  },
1065
1065
  () => {
1066
1066
  done();
@@ -62,6 +62,7 @@ import {
62
62
  } from "./components/pet-capability";
63
63
  import type { ToolExecutionHandle } from "./components/tool-execution";
64
64
  import { StatusLineComponent } from "./components/tool-status-header";
65
+ import { composeToolText } from "./components/tool-transcript-format";
65
66
  import {
66
67
  WelcomeComponent,
67
68
  type WelcomeLogoMode,
@@ -1817,17 +1818,27 @@ export class InteractiveMode implements InteractiveModeContext {
1817
1818
  result?.content
1818
1819
  .filter(part => part.type === "text")
1819
1820
  .map(part => part.text)
1820
- .join("\n") ?? "";
1821
+ .join("\n")
1822
+ .trim() ?? "";
1821
1823
  items.push({
1822
1824
  kind: "tool",
1823
1825
  source: { toolCallId: content.id, content, result },
1824
1826
  getPayload: () => ({
1825
- text: resultText || JSON.stringify(content.arguments, null, 2),
1827
+ text: composeToolText({
1828
+ name: content.name,
1829
+ args: content.arguments,
1830
+ intent: content.intent,
1831
+ resultText,
1832
+ isError: result?.isError ?? false,
1833
+ hasResult: toolResults.has(content.id),
1834
+ }),
1826
1835
  metadata: {
1827
1836
  name: content.name,
1828
1837
  arguments: content.arguments,
1829
1838
  intent: content.intent,
1830
1839
  isError: result?.isError ?? false,
1840
+ resultText,
1841
+ hasResult: toolResults.has(content.id),
1831
1842
  },
1832
1843
  source: { content, result },
1833
1844
  }),