@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.
- package/CHANGELOG.md +24 -0
- package/dist/types/cli/worktree-cli.d.ts +8 -25
- package/dist/types/commands/gc.d.ts +4 -0
- package/dist/types/commands/worktree.d.ts +7 -32
- package/dist/types/gjc-runtime/gc-runtime.d.ts +9 -0
- package/dist/types/gjc-runtime/state-argv.d.ts +38 -0
- package/dist/types/modes/components/custom-editor.d.ts +8 -3
- package/dist/types/modes/components/tool-transcript-format.d.ts +19 -0
- package/dist/types/modes/controllers/input-controller.d.ts +8 -2
- package/dist/types/sdk/broker/broker.d.ts +1 -0
- package/dist/types/sdk/broker/discovery.d.ts +10 -0
- package/dist/types/sdk/broker/ensure.d.ts +24 -1
- package/dist/types/sdk/broker/internal.d.ts +3 -0
- package/dist/types/sdk/broker/session-index.d.ts +12 -0
- package/dist/types/sdk/broker/state-version.d.ts +2 -1
- package/dist/types/tools/ask.d.ts +39 -12
- package/dist/types/tools/browser/screenshot-format.d.ts +8 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +11 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +1 -1
- package/dist/types/tools/browser/tab-worker-path-resolver.d.ts +2 -0
- package/dist/types/tools/browser/tab-worker-smoke.d.ts +16 -0
- package/dist/types/tools/path-utils.d.ts +9 -14
- package/dist/types/tools/render-utils.d.ts +1 -8
- package/dist/types/tools/resource-gc.d.ts +14 -0
- package/dist/types/utils/image-loading.d.ts +23 -0
- package/dist/types/utils/image-resize.d.ts +2 -0
- package/dist/types/utils/pasted-image-loading.d.ts +39 -0
- package/dist/types/utils/pasted-image-path.d.ts +21 -19
- package/dist/types/web/scrapers/html-to-markdown.d.ts +5 -0
- package/dist/types/web/scrapers/types.d.ts +1 -5
- package/package.json +24 -8
- package/scripts/run-threat-model-check.ts +60 -0
- package/src/cli/worktree-cli.ts +9 -287
- package/src/cli.ts +10 -9
- package/src/commands/gc.ts +11 -1
- package/src/commands/sdk.ts +3 -2
- package/src/commands/worktree.ts +10 -53
- package/src/config/model-profiles.ts +15 -15
- package/src/eval/js/shared/runtime.ts +1 -1
- package/src/gjc-runtime/gc-render.ts +19 -1
- package/src/gjc-runtime/gc-runtime.ts +68 -3
- package/src/gjc-runtime/state-argv.ts +292 -0
- package/src/gjc-runtime/state-runtime.ts +42 -210
- package/src/modes/components/custom-editor.ts +103 -32
- package/src/modes/components/custom-provider-wizard.ts +26 -1
- package/src/modes/components/session-observer-overlay.ts +10 -23
- package/src/modes/components/tool-transcript-format.ts +72 -0
- package/src/modes/components/transcript-viewer-overlay.ts +42 -2
- package/src/modes/controllers/input-controller.ts +98 -37
- package/src/modes/controllers/selector-controller.ts +1 -1
- package/src/modes/interactive-mode.ts +13 -2
- package/src/sdk/broker/broker.ts +139 -32
- package/src/sdk/broker/discovery.ts +125 -0
- package/src/sdk/broker/ensure.ts +103 -19
- package/src/sdk/broker/internal.ts +10 -0
- package/src/sdk/broker/session-index.ts +228 -15
- package/src/sdk/broker/state-version.ts +5 -2
- package/src/sdk/bus/telegram-daemon.ts +50 -10
- package/src/session/internal/managed-session-scope.ts +15 -1
- package/src/tools/ask.ts +216 -41
- package/src/tools/bash-allowed-prefixes.ts +79 -29
- package/src/tools/browser/launch.ts +3 -1
- package/src/tools/browser/readable.ts +1 -1
- package/src/tools/browser/screenshot-format.ts +34 -0
- package/src/tools/browser/tab-protocol.ts +3 -0
- package/src/tools/browser/tab-supervisor.ts +125 -122
- package/src/tools/browser/tab-worker-entry.ts +33 -14
- package/src/tools/browser/tab-worker-path-resolver.ts +67 -0
- package/src/tools/browser/tab-worker-smoke.ts +87 -0
- package/src/tools/browser/tab-worker.ts +7 -5
- package/src/tools/path-utils.ts +42 -35
- package/src/tools/read.ts +7 -1
- package/src/tools/render-utils.ts +1 -27
- package/src/tools/resource-gc.ts +123 -34
- package/src/utils/image-loading.ts +85 -34
- package/src/utils/image-resize.ts +16 -9
- package/src/utils/pasted-image-loading.ts +514 -0
- package/src/utils/pasted-image-path.ts +223 -98
- package/src/web/scrapers/html-to-markdown.ts +68 -0
- package/src/web/scrapers/readthedocs.ts +1 -1
- package/src/web/scrapers/types.ts +1 -68
- 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
|
|
195
|
-
|
|
196
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
25
|
+
import { ensureSupportedImageInput, ImageInputTooLargeError } from "../../utils/image-loading";
|
|
26
26
|
import { resizeImage } from "../../utils/image-resize";
|
|
27
|
-
import {
|
|
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(
|
|
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,
|
|
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};
|
|
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
|
-
|
|
1527
|
-
|
|
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
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
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
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
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
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
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
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
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 (
|
|
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(
|
|
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
|
}
|
|
@@ -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:
|
|
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
|
}),
|