@nghyane/arcane 0.1.19 → 0.1.20
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 +22 -0
- package/package.json +7 -7
- package/src/lsp/clients/biome-client.ts +1 -1
- package/src/lsp/edits.ts +1 -1
- package/src/lsp/index.ts +1 -1
- package/src/lsp/render.ts +3 -2
- package/src/lsp/utils.ts +1 -1
- package/src/main.ts +2 -2
- package/src/modes/components/assistant-message.ts +55 -25
- package/src/modes/components/bash-execution.ts +31 -0
- package/src/modes/components/context-group.ts +30 -3
- package/src/modes/components/model-selector.ts +35 -9
- package/src/modes/components/python-execution.ts +37 -0
- package/src/modes/components/tool-execution.ts +3 -4
- package/src/modes/controllers/event-controller.ts +43 -11
- package/src/modes/utils/ui-helpers.ts +1 -1
- package/src/patch/edit-tool.ts +13 -24
- package/src/patch/hashline.ts +105 -3
- package/src/patch/schemas.ts +2 -2
- package/src/prompts/agents/explore.md +1 -1
- package/src/prompts/agents/librarian.md +1 -1
- package/src/prompts/system/system-prompt.md +0 -1
- package/src/session/agent-session.ts +28 -27
- package/src/task/index.ts +1 -9
- package/src/task/render.ts +3 -3
- package/src/tools/ask.ts +0 -2
- package/src/tools/bash.ts +6 -3
- package/src/tools/browser.ts +1 -1
- package/src/tools/default-renderer.ts +7 -5
- package/src/tools/fetch.ts +5 -2
- package/src/tools/find-thread.ts +5 -2
- package/src/tools/find.ts +3 -3
- package/src/tools/gemini-image.ts +18 -10
- package/src/tools/github.ts +2 -2
- package/src/tools/grep.ts +3 -3
- package/src/tools/notebook.ts +8 -2
- package/src/tools/python.ts +3 -2
- package/src/tools/read-thread.ts +5 -2
- package/src/tools/read.ts +6 -3
- package/src/tools/render-mermaid.ts +3 -7
- package/src/tools/save-memory.ts +6 -3
- package/src/tools/ssh.ts +6 -3
- package/src/tools/todo-write.ts +6 -3
- package/src/tools/undo-edit.ts +5 -2
- package/src/ui/render-utils.ts +1 -1
- package/src/utils/file-mentions.ts +1 -1
- package/src/web/github-client.ts +2 -1
- package/src/web/scrapers/youtube.ts +1 -1
- package/src/web/search/render.ts +11 -2
- package/src/prompts/tools/render-mermaid.md +0 -9
|
@@ -11,14 +11,37 @@ import type { AgentSessionEvent } from "../../session/agent-session";
|
|
|
11
11
|
import { getSymbolTheme, theme } from "../../theme/theme";
|
|
12
12
|
import { getToolTier, isContextTool } from "../../ui/render-utils";
|
|
13
13
|
|
|
14
|
+
const STREAM_RENDER_INTERVAL_MS = 32;
|
|
15
|
+
|
|
14
16
|
export class EventController {
|
|
15
17
|
#lastThinkingCount = 0;
|
|
16
18
|
#renderedCustomMessages = new Set<string>();
|
|
17
19
|
#currentContextGroup?: ContextGroupComponent;
|
|
18
20
|
#toolGroups = new Map<string, ContextGroupComponent>();
|
|
21
|
+
#streamRenderTimer?: Timer;
|
|
22
|
+
#pendingStreamMessage?: AgentSessionEvent;
|
|
19
23
|
|
|
20
24
|
constructor(private ctx: InteractiveModeContext) {}
|
|
21
25
|
|
|
26
|
+
#flushStreamRender(): void {
|
|
27
|
+
const pending = this.#pendingStreamMessage;
|
|
28
|
+
if (pending && "message" in pending && pending.message.role === "assistant" && this.ctx.streamingComponent) {
|
|
29
|
+
this.#pendingStreamMessage = undefined;
|
|
30
|
+
this.ctx.streamingComponent.updateContent(pending.message);
|
|
31
|
+
this.ctx.ui.requestRender();
|
|
32
|
+
}
|
|
33
|
+
const timer = setTimeout(() => {
|
|
34
|
+
// Guard against orphan callbacks surviving clearTimeout after message_end
|
|
35
|
+
if (this.#streamRenderTimer !== timer) return;
|
|
36
|
+
if (this.#pendingStreamMessage) {
|
|
37
|
+
this.#flushStreamRender();
|
|
38
|
+
} else {
|
|
39
|
+
this.#streamRenderTimer = undefined;
|
|
40
|
+
}
|
|
41
|
+
}, STREAM_RENDER_INTERVAL_MS);
|
|
42
|
+
this.#streamRenderTimer = timer;
|
|
43
|
+
}
|
|
44
|
+
|
|
22
45
|
subscribeToAgent(): void {
|
|
23
46
|
this.ctx.unsubscribe = this.ctx.session.subscribe(async (event: AgentSessionEvent) => {
|
|
24
47
|
await this.handleEvent(event);
|
|
@@ -96,18 +119,10 @@ export class EventController {
|
|
|
96
119
|
case "message_update":
|
|
97
120
|
if (this.ctx.streamingComponent && event.message.role === "assistant") {
|
|
98
121
|
this.ctx.streamingMessage = event.message;
|
|
99
|
-
this.ctx.streamingComponent.updateContent(this.ctx.streamingMessage);
|
|
100
|
-
|
|
101
|
-
const thinkingCount = this.ctx.streamingMessage.content.filter(
|
|
102
|
-
content => content.type === "thinking" && content.thinking.trim(),
|
|
103
|
-
).length;
|
|
104
|
-
if (thinkingCount > this.#lastThinkingCount) {
|
|
105
|
-
this.#lastThinkingCount = thinkingCount;
|
|
106
|
-
}
|
|
107
122
|
|
|
123
|
+
// Tool calls need immediate processing (new tool components)
|
|
108
124
|
for (const content of this.ctx.streamingMessage.content) {
|
|
109
125
|
if (content.type !== "toolCall") continue;
|
|
110
|
-
|
|
111
126
|
if (!this.ctx.pendingTools.has(content.id)) {
|
|
112
127
|
const tool = this.ctx.session.getToolByName(content.name);
|
|
113
128
|
this.#appendTool(content.id, content.name, content.arguments, tool);
|
|
@@ -119,13 +134,30 @@ export class EventController {
|
|
|
119
134
|
}
|
|
120
135
|
}
|
|
121
136
|
|
|
122
|
-
this.ctx.
|
|
137
|
+
const thinkingCount = this.ctx.streamingMessage.content.filter(
|
|
138
|
+
content => content.type === "thinking" && content.thinking.trim(),
|
|
139
|
+
).length;
|
|
140
|
+
if (thinkingCount > this.#lastThinkingCount) {
|
|
141
|
+
this.#lastThinkingCount = thinkingCount;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Throttle text/thinking render updates to avoid re-parsing markdown every token
|
|
145
|
+
this.#pendingStreamMessage = event;
|
|
146
|
+
if (!this.#streamRenderTimer) {
|
|
147
|
+
this.#flushStreamRender();
|
|
148
|
+
}
|
|
123
149
|
}
|
|
124
150
|
break;
|
|
125
151
|
|
|
126
152
|
case "message_end":
|
|
127
153
|
if (event.message.role === "user") break;
|
|
128
154
|
if (this.ctx.streamingComponent && event.message.role === "assistant") {
|
|
155
|
+
// Flush any throttled stream render and stop the timer
|
|
156
|
+
if (this.#streamRenderTimer) {
|
|
157
|
+
clearTimeout(this.#streamRenderTimer);
|
|
158
|
+
this.#streamRenderTimer = undefined;
|
|
159
|
+
this.#pendingStreamMessage = undefined;
|
|
160
|
+
}
|
|
129
161
|
this.ctx.streamingMessage = event.message;
|
|
130
162
|
let errorMessage: string | undefined;
|
|
131
163
|
if (this.ctx.streamingMessage.stopReason === "aborted" && !this.ctx.session.isTtsrAbortPending) {
|
|
@@ -349,7 +381,7 @@ export class EventController {
|
|
|
349
381
|
|
|
350
382
|
if (isContextTool(toolName)) {
|
|
351
383
|
if (!this.#currentContextGroup) {
|
|
352
|
-
this.#currentContextGroup = new ContextGroupComponent();
|
|
384
|
+
this.#currentContextGroup = new ContextGroupComponent(this.ctx.ui);
|
|
353
385
|
this.#currentContextGroup.setExpanded(this.ctx.toolOutputExpanded);
|
|
354
386
|
this.ctx.chatContainer.addChild(this.#currentContextGroup);
|
|
355
387
|
}
|
|
@@ -231,7 +231,7 @@ export class UiHelpers {
|
|
|
231
231
|
|
|
232
232
|
if (isContextTool(content.name)) {
|
|
233
233
|
if (!currentGroup) {
|
|
234
|
-
currentGroup = new ContextGroupComponent();
|
|
234
|
+
currentGroup = new ContextGroupComponent(this.ctx.ui);
|
|
235
235
|
currentGroup.setExpanded(this.ctx.toolOutputExpanded);
|
|
236
236
|
this.ctx.chatContainer.addChild(currentGroup);
|
|
237
237
|
}
|
package/src/patch/edit-tool.ts
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
import { findMatch } from "./fuzzy";
|
|
31
31
|
import {
|
|
32
32
|
applyHashlineEdits,
|
|
33
|
+
buildCompactDiffPreview,
|
|
33
34
|
computeLineHash,
|
|
34
35
|
type HashlineEdit,
|
|
35
36
|
type LineTag,
|
|
@@ -212,7 +213,7 @@ export class EditTool implements AgentTool<TInput, any, Theme> {
|
|
|
212
213
|
}
|
|
213
214
|
|
|
214
215
|
description =
|
|
215
|
-
"Apply edits to existing files (create, update, delete, rename).
|
|
216
|
+
"Apply edits to existing files (create, update, delete, rename). Diff shown to user — do not repeat changes. Re-read before editing same file again (tags shift). On mismatch, retry with fresh tags from error.";
|
|
216
217
|
|
|
217
218
|
/**
|
|
218
219
|
* Dynamic parameters schema based on current edit mode (which depends on current model).
|
|
@@ -343,28 +344,12 @@ export class EditTool implements AgentTool<TInput, any, Theme> {
|
|
|
343
344
|
}
|
|
344
345
|
case "insert": {
|
|
345
346
|
const { before, after, content } = edit;
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
} else if (after && !before) {
|
|
353
|
-
anchorEdits.push({
|
|
354
|
-
op: "append",
|
|
355
|
-
after: parseTag(after),
|
|
356
|
-
content: hashlineParseContent(content),
|
|
357
|
-
});
|
|
358
|
-
} else if (before && after) {
|
|
359
|
-
anchorEdits.push({
|
|
360
|
-
op: "insert",
|
|
361
|
-
before: parseTag(before),
|
|
362
|
-
after: parseTag(after),
|
|
363
|
-
content: hashlineParseContent(content),
|
|
364
|
-
});
|
|
365
|
-
} else {
|
|
366
|
-
throw new Error(`Insert must have both before and after tags.`);
|
|
367
|
-
}
|
|
347
|
+
anchorEdits.push({
|
|
348
|
+
op: "insert",
|
|
349
|
+
before: parseTag(before),
|
|
350
|
+
after: parseTag(after),
|
|
351
|
+
content: hashlineParseContent(content),
|
|
352
|
+
});
|
|
368
353
|
break;
|
|
369
354
|
}
|
|
370
355
|
case "replaceText": {
|
|
@@ -493,11 +478,15 @@ export class EditTool implements AgentTool<TInput, any, Theme> {
|
|
|
493
478
|
.get();
|
|
494
479
|
|
|
495
480
|
const resultText = rename ? `Updated and moved ${path} to ${rename}` : `Updated ${path}`;
|
|
481
|
+
const preview = buildCompactDiffPreview(diffResult.diff);
|
|
482
|
+
const summaryLine = `Changes: +${preview.addedLines} -${preview.removedLines}`;
|
|
483
|
+
const previewBlock = preview.preview ? `\n\nDiff preview:\n${preview.preview}` : "";
|
|
484
|
+
const warningsBlock = result.warnings?.length ? `\n\nWarnings:\n${result.warnings.join("\n")}` : "";
|
|
496
485
|
return {
|
|
497
486
|
content: [
|
|
498
487
|
{
|
|
499
488
|
type: "text",
|
|
500
|
-
text: `${resultText}
|
|
489
|
+
text: `${resultText}\n${summaryLine}${previewBlock}${warningsBlock}`,
|
|
501
490
|
},
|
|
502
491
|
],
|
|
503
492
|
details: {
|
package/src/patch/hashline.ts
CHANGED
|
@@ -818,7 +818,7 @@ export function applyHashlineEdits(
|
|
|
818
818
|
let nextLines = merged.newLines;
|
|
819
819
|
nextLines = restoreIndentForPairedReplacement([origLines[0] ?? ""], nextLines);
|
|
820
820
|
|
|
821
|
-
if (origLines.every((line, i) => line === nextLines[i])) {
|
|
821
|
+
if (origLines.length === nextLines.length && origLines.every((line, i) => line === nextLines[i])) {
|
|
822
822
|
noopEdits.push({
|
|
823
823
|
editIndex: idx,
|
|
824
824
|
loc: `${edit.tag.line}#${edit.tag.hash}`,
|
|
@@ -838,7 +838,7 @@ export function applyHashlineEdits(
|
|
|
838
838
|
: edit.content;
|
|
839
839
|
stripped = autocorrect ? restoreOldWrappedLines(origLines, stripped) : stripped;
|
|
840
840
|
const newLines = autocorrect ? restoreIndentForPairedReplacement(origLines, stripped) : stripped;
|
|
841
|
-
if (origLines.every((line, i) => line === newLines[i])) {
|
|
841
|
+
if (origLines.length === newLines.length && origLines.every((line, i) => line === newLines[i])) {
|
|
842
842
|
noopEdits.push({
|
|
843
843
|
editIndex: idx,
|
|
844
844
|
loc: `${edit.tag.line}#${edit.tag.hash}`,
|
|
@@ -858,7 +858,7 @@ export function applyHashlineEdits(
|
|
|
858
858
|
: edit.content;
|
|
859
859
|
stripped = autocorrect ? restoreOldWrappedLines(origLines, stripped) : stripped;
|
|
860
860
|
const newLines = autocorrect ? restoreIndentForPairedReplacement(origLines, stripped) : stripped;
|
|
861
|
-
if (
|
|
861
|
+
if (origLines.length === newLines.length && origLines.every((line, i) => line === newLines[i])) {
|
|
862
862
|
noopEdits.push({
|
|
863
863
|
editIndex: idx,
|
|
864
864
|
loc: `${edit.first.line}#${edit.first.hash}`,
|
|
@@ -1012,3 +1012,105 @@ export function applyHashlineEdits(
|
|
|
1012
1012
|
return null;
|
|
1013
1013
|
}
|
|
1014
1014
|
}
|
|
1015
|
+
|
|
1016
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1017
|
+
// Compact diff preview for model-visible tool responses
|
|
1018
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1019
|
+
|
|
1020
|
+
export interface CompactDiffPreview {
|
|
1021
|
+
preview: string;
|
|
1022
|
+
addedLines: number;
|
|
1023
|
+
removedLines: number;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
export interface CompactDiffOptions {
|
|
1027
|
+
maxUnchangedRun?: number;
|
|
1028
|
+
maxAdditionRun?: number;
|
|
1029
|
+
maxDeletionRun?: number;
|
|
1030
|
+
maxOutputLines?: number;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
const NUMBERED_DIFF_LINE_RE = /^([ +-])\d+\|/;
|
|
1034
|
+
|
|
1035
|
+
type RunPosition = "first" | "last" | "middle";
|
|
1036
|
+
|
|
1037
|
+
function collapseRun(lines: string[], max: number, label: string, position: RunPosition): string[] {
|
|
1038
|
+
const len = lines.length;
|
|
1039
|
+
if (position === "first") {
|
|
1040
|
+
if (len <= max) return lines;
|
|
1041
|
+
return [` ... ${len - max} more ${label} lines`, ...lines.slice(-max)];
|
|
1042
|
+
}
|
|
1043
|
+
if (position === "last") {
|
|
1044
|
+
if (len <= max) return lines;
|
|
1045
|
+
return [...lines.slice(0, max), ` ... ${len - max} more ${label} lines`];
|
|
1046
|
+
}
|
|
1047
|
+
if (len <= max * 2) return lines;
|
|
1048
|
+
return [...lines.slice(0, max), ` ... ${len - max * 2} more ${label} lines`, ...lines.slice(-max)];
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
/**
|
|
1052
|
+
* Build a compact diff preview for model-visible tool responses.
|
|
1053
|
+
* Collapses long unchanged/added/removed runs so the model sees the shape
|
|
1054
|
+
* of edits without replaying full file content.
|
|
1055
|
+
*/
|
|
1056
|
+
export function buildCompactDiffPreview(diff: string, options: CompactDiffOptions = {}): CompactDiffPreview {
|
|
1057
|
+
const maxCtx = options.maxUnchangedRun ?? 2;
|
|
1058
|
+
const maxAdd = options.maxAdditionRun ?? 2;
|
|
1059
|
+
const maxDel = options.maxDeletionRun ?? 2;
|
|
1060
|
+
const maxOut = options.maxOutputLines ?? 16;
|
|
1061
|
+
|
|
1062
|
+
if (diff.length === 0) return { preview: "", addedLines: 0, removedLines: 0 };
|
|
1063
|
+
|
|
1064
|
+
const inputLines = diff.split("\n");
|
|
1065
|
+
|
|
1066
|
+
// Single-pass: group consecutive lines by kind into run spans
|
|
1067
|
+
type Kind = " " | "+" | "-" | "meta";
|
|
1068
|
+
const runs: { kind: Kind; start: number; end: number }[] = [];
|
|
1069
|
+
for (let i = 0; i < inputLines.length; i++) {
|
|
1070
|
+
const m = NUMBERED_DIFF_LINE_RE.exec(inputLines[i]);
|
|
1071
|
+
const kind: Kind = (m?.[1] as " " | "+" | "-" | undefined) ?? "meta";
|
|
1072
|
+
const prev = runs[runs.length - 1];
|
|
1073
|
+
if (prev && prev.kind === kind) {
|
|
1074
|
+
prev.end = i;
|
|
1075
|
+
} else {
|
|
1076
|
+
runs.push({ kind, start: i, end: i });
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
const out: string[] = [];
|
|
1081
|
+
let addedLines = 0;
|
|
1082
|
+
let removedLines = 0;
|
|
1083
|
+
|
|
1084
|
+
for (let ri = 0; ri < runs.length; ri++) {
|
|
1085
|
+
const { kind, start, end } = runs[ri];
|
|
1086
|
+
const slice = inputLines.slice(start, end + 1);
|
|
1087
|
+
switch (kind) {
|
|
1088
|
+
case "meta":
|
|
1089
|
+
out.push(...slice);
|
|
1090
|
+
break;
|
|
1091
|
+
case "+":
|
|
1092
|
+
addedLines += slice.length;
|
|
1093
|
+
out.push(...collapseRun(slice, maxAdd, "added", "last"));
|
|
1094
|
+
break;
|
|
1095
|
+
case "-":
|
|
1096
|
+
removedLines += slice.length;
|
|
1097
|
+
out.push(...collapseRun(slice, maxDel, "removed", "last"));
|
|
1098
|
+
break;
|
|
1099
|
+
case " ": {
|
|
1100
|
+
const pos: RunPosition = ri === 0 ? "first" : ri === runs.length - 1 ? "last" : "middle";
|
|
1101
|
+
out.push(...collapseRun(slice, maxCtx, "unchanged", pos));
|
|
1102
|
+
break;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
if (out.length > maxOut) {
|
|
1108
|
+
return {
|
|
1109
|
+
preview: [...out.slice(0, maxOut), ` ... ${out.length - maxOut} more preview lines`].join("\n"),
|
|
1110
|
+
addedLines,
|
|
1111
|
+
removedLines,
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
return { preview: out.join("\n"), addedLines, removedLines };
|
|
1116
|
+
}
|
package/src/patch/schemas.ts
CHANGED
|
@@ -144,8 +144,8 @@ const hashlineRangeEditSchema = Type.Object(
|
|
|
144
144
|
const hashlineInsertEditSchema = Type.Object(
|
|
145
145
|
{
|
|
146
146
|
op: Type.Literal("insert"),
|
|
147
|
-
before:
|
|
148
|
-
after:
|
|
147
|
+
before: hashlineTagFormat("line before which to insert"),
|
|
148
|
+
after: hashlineTagFormat("line after which to insert"),
|
|
149
149
|
content: hashlineInsertContentFormat("Inserted"),
|
|
150
150
|
},
|
|
151
151
|
{ additionalProperties: false },
|
|
@@ -57,4 +57,4 @@ Be comprehensive and direct. No filler.
|
|
|
57
57
|
Only your final message is returned to the caller. It must be self-contained with all findings, paths, and explanations. Do not reference tool names or intermediate steps — present conclusions directly. Your final message must contain ONLY the information found — no preamble.
|
|
58
58
|
|
|
59
59
|
Use "fluent" linking — embed file/PR/commit references in natural noun phrases, not raw URLs. Example: The [`handleAuth` function](file:///path/to/auth.ts#L42) validates tokens.
|
|
60
|
-
</critical>
|
|
60
|
+
</critical>
|
|
@@ -231,6 +231,7 @@ export class AgentSession {
|
|
|
231
231
|
// Verification loop state
|
|
232
232
|
#verificationReminderCount = 0;
|
|
233
233
|
#turnHasFileModifications = false;
|
|
234
|
+
#editErrorSteerCount = 0;
|
|
234
235
|
|
|
235
236
|
// Bash execution state
|
|
236
237
|
#bashAbortController: AbortController | undefined = undefined;
|
|
@@ -555,6 +556,25 @@ export class AgentSession {
|
|
|
555
556
|
{ deliverAs: "nextTurn" },
|
|
556
557
|
);
|
|
557
558
|
}
|
|
559
|
+
if (toolName === "edit" && isError && this.#editErrorSteerCount < 2) {
|
|
560
|
+
this.#editErrorSteerCount++;
|
|
561
|
+
const errorText = content?.find(part => part.type === "text")?.text;
|
|
562
|
+
const reminderText = [
|
|
563
|
+
"<system_reminder>",
|
|
564
|
+
"The edit tool failed. Re-read the file to get fresh line tags, then retry the edit.",
|
|
565
|
+
errorText ? `Failure: ${errorText}` : "Failure: edit returned an error.",
|
|
566
|
+
"</system_reminder>",
|
|
567
|
+
].join("\n");
|
|
568
|
+
await this.sendCustomMessage(
|
|
569
|
+
{
|
|
570
|
+
customType: "edit-error-reminder",
|
|
571
|
+
content: reminderText,
|
|
572
|
+
display: false,
|
|
573
|
+
details: { toolName, errorText },
|
|
574
|
+
},
|
|
575
|
+
{ deliverAs: "steer" },
|
|
576
|
+
);
|
|
577
|
+
}
|
|
558
578
|
}
|
|
559
579
|
}
|
|
560
580
|
|
|
@@ -1103,6 +1123,7 @@ export class AgentSession {
|
|
|
1103
1123
|
this.#todoReminderCount = 0;
|
|
1104
1124
|
this.#verificationReminderCount = 0;
|
|
1105
1125
|
this.#turnHasFileModifications = false;
|
|
1126
|
+
this.#editErrorSteerCount = 0;
|
|
1106
1127
|
|
|
1107
1128
|
// Validate model
|
|
1108
1129
|
if (!this.model) {
|
|
@@ -2335,33 +2356,13 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
2335
2356
|
const availableModels = this.#model.registry.getAvailable();
|
|
2336
2357
|
if (availableModels.length === 0) return undefined;
|
|
2337
2358
|
|
|
2338
|
-
const
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
candidates.push(candidate);
|
|
2346
|
-
};
|
|
2347
|
-
|
|
2348
|
-
addCandidate(this.#model.resolveContextPromotionTarget(currentModel, availableModels));
|
|
2349
|
-
|
|
2350
|
-
const sameProviderLarger = [...availableModels]
|
|
2351
|
-
.filter(
|
|
2352
|
-
m => m.provider === currentModel.provider && m.api === currentModel.api && m.contextWindow > contextWindow,
|
|
2353
|
-
)
|
|
2354
|
-
.sort((a, b) => a.contextWindow - b.contextWindow);
|
|
2355
|
-
addCandidate(sameProviderLarger[0]);
|
|
2356
|
-
for (const candidate of candidates) {
|
|
2357
|
-
if (modelsAreEqual(candidate, currentModel)) continue;
|
|
2358
|
-
if (candidate.contextWindow <= contextWindow) continue;
|
|
2359
|
-
const apiKey = await this.#model.registry.getApiKey(candidate, this.sessionId);
|
|
2360
|
-
if (!apiKey) continue;
|
|
2361
|
-
return candidate;
|
|
2362
|
-
}
|
|
2363
|
-
|
|
2364
|
-
return undefined;
|
|
2359
|
+
const candidate = this.#model.resolveContextPromotionTarget(currentModel, availableModels);
|
|
2360
|
+
if (!candidate) return undefined;
|
|
2361
|
+
if (modelsAreEqual(candidate, currentModel)) return undefined;
|
|
2362
|
+
if (candidate.contextWindow <= contextWindow) return undefined;
|
|
2363
|
+
const apiKey = await this.#model.registry.getApiKey(candidate, this.sessionId);
|
|
2364
|
+
if (!apiKey) return undefined;
|
|
2365
|
+
return candidate;
|
|
2365
2366
|
}
|
|
2366
2367
|
/**
|
|
2367
2368
|
* Internal: Run auto-compaction with events.
|
package/src/task/index.ts
CHANGED
|
@@ -227,16 +227,8 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
|
|
|
227
227
|
const totalDuration = Date.now() - startTime;
|
|
228
228
|
const output = agentOutput.trim() || result.stderr.trim() || "(no output)";
|
|
229
229
|
|
|
230
|
-
// Return structured result as JSON for code tool composability
|
|
231
|
-
const structured = {
|
|
232
|
-
exitCode: result.exitCode,
|
|
233
|
-
output,
|
|
234
|
-
tokens: result.tokens,
|
|
235
|
-
durationMs: result.durationMs,
|
|
236
|
-
};
|
|
237
|
-
|
|
238
230
|
return {
|
|
239
|
-
content: [{ type: "text", text:
|
|
231
|
+
content: [{ type: "text", text: output }],
|
|
240
232
|
details: {
|
|
241
233
|
results: [result],
|
|
242
234
|
totalDurationMs: totalDuration,
|
package/src/task/render.ts
CHANGED
|
@@ -24,7 +24,7 @@ import type { AgentProgress, SingleResult, TaskParams, TaskToolDetails } from ".
|
|
|
24
24
|
function getStatusIcon(status: AgentProgress["status"], theme: Theme, spinnerFrame?: number): string {
|
|
25
25
|
switch (status) {
|
|
26
26
|
case "pending":
|
|
27
|
-
return formatStatusIcon("
|
|
27
|
+
return formatStatusIcon("running", theme, spinnerFrame);
|
|
28
28
|
case "running":
|
|
29
29
|
return formatStatusIcon("running", theme, spinnerFrame);
|
|
30
30
|
case "completed":
|
|
@@ -274,7 +274,7 @@ export function renderResult(
|
|
|
274
274
|
lines.push(...renderConclusionMarkdown(fallbackText, width, expanded, theme));
|
|
275
275
|
}
|
|
276
276
|
} else {
|
|
277
|
-
const icon = formatStatusIcon("
|
|
277
|
+
const icon = formatStatusIcon("running", theme, spinnerFrame);
|
|
278
278
|
lines.push(...renderSubagentHeader(taskRenderConfig, args, { icon }, theme));
|
|
279
279
|
}
|
|
280
280
|
|
|
@@ -417,7 +417,7 @@ export function createUnifiedSubagentRenderer(config: SubagentRenderConfig): {
|
|
|
417
417
|
lines.push(...renderConclusionMarkdown(fallbackText, width, expanded, theme));
|
|
418
418
|
}
|
|
419
419
|
} else {
|
|
420
|
-
const icon = formatStatusIcon("
|
|
420
|
+
const icon = formatStatusIcon("running", theme, spinnerFrame);
|
|
421
421
|
lines.push(...renderSubagentHeader(config, params, { icon }, theme));
|
|
422
422
|
}
|
|
423
423
|
|
package/src/tools/ask.ts
CHANGED
|
@@ -73,8 +73,6 @@ export interface AskToolDetails {
|
|
|
73
73
|
|
|
74
74
|
const OTHER_OPTION = "Other (type your own)";
|
|
75
75
|
const RECOMMENDED_SUFFIX = " (Recommended)";
|
|
76
|
-
/** Default timeout in milliseconds (used when settings unavailable) */
|
|
77
|
-
const _DEFAULT_ASK_TIMEOUT_MS = 30000;
|
|
78
76
|
|
|
79
77
|
function getDoneOptionLabel(): string {
|
|
80
78
|
return `${theme.status.success} Done selecting`;
|
package/src/tools/bash.ts
CHANGED
|
@@ -26,7 +26,7 @@ export const BASH_DEFAULT_PREVIEW_LINES = 10;
|
|
|
26
26
|
|
|
27
27
|
const bashSchema = Type.Object({
|
|
28
28
|
command: Type.String({ description: "Shell command to execute" }),
|
|
29
|
-
timeout: Type.Optional(Type.Number({ description: "Timeout in
|
|
29
|
+
timeout: Type.Optional(Type.Number({ description: "Timeout in seconds" })),
|
|
30
30
|
cwd: Type.Optional(Type.String({ description: "Working directory" })),
|
|
31
31
|
head: Type.Optional(Type.Number({ description: "Return only the first N lines of output" })),
|
|
32
32
|
tail: Type.Optional(Type.Number({ description: "Return only the last N lines of output" })),
|
|
@@ -196,9 +196,12 @@ export class BashTool implements AgentTool<typeof bashSchema, BashToolDetails, T
|
|
|
196
196
|
return context;
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
-
renderCall(args: BashRenderArgs,
|
|
199
|
+
renderCall(args: BashRenderArgs, options: RenderResultOptions, uiTheme: Theme): Component {
|
|
200
200
|
const cmdText = formatBashCommand(args, uiTheme);
|
|
201
|
-
const text = renderStatusLine(
|
|
201
|
+
const text = renderStatusLine(
|
|
202
|
+
{ icon: "running", spinnerFrame: options.spinnerFrame, title: "Bash", description: cmdText },
|
|
203
|
+
uiTheme,
|
|
204
|
+
);
|
|
202
205
|
return new Text(text, 0, 0);
|
|
203
206
|
}
|
|
204
207
|
|
package/src/tools/browser.ts
CHANGED
|
@@ -349,7 +349,7 @@ const browserSchema = Type.Object({
|
|
|
349
349
|
value: Type.Optional(Type.String({ description: "Value to fill into input" })),
|
|
350
350
|
attribute: Type.Optional(Type.String({ description: "Attribute name to retrieve" })),
|
|
351
351
|
key: Type.Optional(Type.String({ description: "Key to press (e.g. Enter, Escape)" })),
|
|
352
|
-
timeout: Type.Optional(Type.Number({ description: "Timeout in
|
|
352
|
+
timeout: Type.Optional(Type.Number({ description: "Timeout in seconds" })),
|
|
353
353
|
wait_until: Type.Optional(
|
|
354
354
|
StringEnum(["load", "domcontentloaded", "networkidle0", "networkidle2"], {
|
|
355
355
|
description: "Navigation wait condition",
|
|
@@ -3,7 +3,7 @@ import { Text } from "@nghyane/arcane-tui";
|
|
|
3
3
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
4
4
|
import type { Theme } from "../theme/theme";
|
|
5
5
|
import { renderStatusLine } from "../tui";
|
|
6
|
-
import { formatMoreItems, PREVIEW_LIMITS, TRUNCATE_LENGTHS, truncateToWidth } from "../ui/render-utils";
|
|
6
|
+
import { formatMoreItems, PREVIEW_LIMITS, replaceTabs, TRUNCATE_LENGTHS, truncateToWidth } from "../ui/render-utils";
|
|
7
7
|
import {
|
|
8
8
|
formatArgsInline,
|
|
9
9
|
JSON_TREE_MAX_DEPTH_COLLAPSED,
|
|
@@ -36,7 +36,7 @@ export const defaultRenderer: DefaultRenderer = {
|
|
|
36
36
|
renderCall(args: unknown, options: RenderResultOptions, theme: Theme): Component {
|
|
37
37
|
const label = options.label ?? "Tool";
|
|
38
38
|
const lines: string[] = [];
|
|
39
|
-
lines.push(renderStatusLine({ icon: "
|
|
39
|
+
lines.push(renderStatusLine({ icon: "running", spinnerFrame: options.spinnerFrame, title: label }, theme));
|
|
40
40
|
|
|
41
41
|
const argsObject = asRecord(args);
|
|
42
42
|
if (argsObject && Object.keys(argsObject).length > 0) {
|
|
@@ -57,8 +57,10 @@ export const defaultRenderer: DefaultRenderer = {
|
|
|
57
57
|
const { expanded = false, isPartial = false } = options;
|
|
58
58
|
const label = options.label ?? "Tool";
|
|
59
59
|
const lines: string[] = [];
|
|
60
|
-
const icon = isPartial ? "
|
|
61
|
-
lines.push(
|
|
60
|
+
const icon = isPartial ? "running" : result.isError ? "error" : "success";
|
|
61
|
+
lines.push(
|
|
62
|
+
renderStatusLine({ icon, spinnerFrame: isPartial ? options.spinnerFrame : undefined, title: label }, theme),
|
|
63
|
+
);
|
|
62
64
|
|
|
63
65
|
// Output
|
|
64
66
|
const textContent = (result.content?.find(c => c.type === "text")?.text ?? "").trimEnd();
|
|
@@ -92,7 +94,7 @@ export const defaultRenderer: DefaultRenderer = {
|
|
|
92
94
|
const maxOutputLines = expanded ? PREVIEW_LIMITS.OUTPUT_EXPANDED : PREVIEW_LIMITS.OUTPUT_COLLAPSED;
|
|
93
95
|
const displayLines = outputLines.slice(0, maxOutputLines);
|
|
94
96
|
for (const line of displayLines) {
|
|
95
|
-
lines.push(theme.fg("toolOutput", truncateToWidth(line, TRUNCATE_LENGTHS.CONTENT)));
|
|
97
|
+
lines.push(theme.fg("toolOutput", truncateToWidth(replaceTabs(line), TRUNCATE_LENGTHS.CONTENT)));
|
|
96
98
|
}
|
|
97
99
|
if (outputLines.length > maxOutputLines) {
|
|
98
100
|
const remaining = outputLines.length - maxOutputLines;
|
package/src/tools/fetch.ts
CHANGED
|
@@ -959,7 +959,7 @@ function countNonEmptyLines(text: string): number {
|
|
|
959
959
|
/** Render fetch call (URL preview) */
|
|
960
960
|
function renderFetchCall(
|
|
961
961
|
args: { url?: string; timeout?: number; raw?: boolean },
|
|
962
|
-
|
|
962
|
+
options: RenderResultOptions,
|
|
963
963
|
uiTheme: Theme = theme,
|
|
964
964
|
): Component {
|
|
965
965
|
const url = args.url ?? "";
|
|
@@ -969,7 +969,10 @@ function renderFetchCall(
|
|
|
969
969
|
const meta: string[] = [];
|
|
970
970
|
if (args.raw) meta.push("raw");
|
|
971
971
|
if (args.timeout !== undefined) meta.push(`timeout:${args.timeout}s`);
|
|
972
|
-
const text = renderStatusLine(
|
|
972
|
+
const text = renderStatusLine(
|
|
973
|
+
{ icon: "running", spinnerFrame: options.spinnerFrame, title: "Fetch", description, meta },
|
|
974
|
+
uiTheme,
|
|
975
|
+
);
|
|
973
976
|
return new Text(text, 0, 0);
|
|
974
977
|
}
|
|
975
978
|
|
package/src/tools/find-thread.ts
CHANGED
|
@@ -79,9 +79,12 @@ export class FindThreadTool implements AgentTool<typeof findThreadSchema, FindTh
|
|
|
79
79
|
};
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
renderCall(args: FindThreadRenderArgs,
|
|
82
|
+
renderCall(args: FindThreadRenderArgs, options: RenderResultOptions, uiTheme: Theme): Component {
|
|
83
83
|
const meta = args.query ? [`"${args.query}"`] : [];
|
|
84
|
-
const text = renderStatusLine(
|
|
84
|
+
const text = renderStatusLine(
|
|
85
|
+
{ icon: "running", spinnerFrame: options.spinnerFrame, title: "Find Thread", meta },
|
|
86
|
+
uiTheme,
|
|
87
|
+
);
|
|
85
88
|
return new Text(text, 0, 0);
|
|
86
89
|
}
|
|
87
90
|
|
package/src/tools/find.ts
CHANGED
|
@@ -19,7 +19,7 @@ import { resolveToCwd } from "./path-utils";
|
|
|
19
19
|
import { ToolAbortError, ToolError, throwIfAborted } from "./tool-errors";
|
|
20
20
|
|
|
21
21
|
const findSchema = Type.Object({
|
|
22
|
-
pattern: Type.String({ description: "Glob pattern to match file paths" }),
|
|
22
|
+
pattern: Type.String({ description: "Glob pattern to match file paths", minLength: 1 }),
|
|
23
23
|
hidden: Type.Optional(Type.Boolean({ description: "Include hidden files and directories" })),
|
|
24
24
|
limit: Type.Optional(Type.Number({ description: "Max number of results to return" })),
|
|
25
25
|
});
|
|
@@ -381,12 +381,12 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails, T
|
|
|
381
381
|
});
|
|
382
382
|
}
|
|
383
383
|
|
|
384
|
-
renderCall(args: FindRenderArgs,
|
|
384
|
+
renderCall(args: FindRenderArgs, options: RenderResultOptions, uiTheme: Theme): Component {
|
|
385
385
|
const meta: string[] = [];
|
|
386
386
|
if (args.limit !== undefined) meta.push(`limit:${args.limit}`);
|
|
387
387
|
|
|
388
388
|
const text = renderStatusLine(
|
|
389
|
-
{ icon: "
|
|
389
|
+
{ icon: "running", spinnerFrame: options.spinnerFrame, title: "Find", description: args.pattern || "*", meta },
|
|
390
390
|
uiTheme,
|
|
391
391
|
);
|
|
392
392
|
return new Text(text, 0, 0);
|
|
@@ -33,14 +33,24 @@ const imageSizeSchema = StringEnum(["1024x1024", "1536x1024", "1024x1536"], {
|
|
|
33
33
|
description: "Image size, mainly for gemini-3-pro-image-preview.",
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
-
const inputImageSchema = Type.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
36
|
+
const inputImageSchema = Type.Union([
|
|
37
|
+
Type.Object(
|
|
38
|
+
{
|
|
39
|
+
path: Type.String({ description: "Path to an input image file." }),
|
|
40
|
+
data: Type.Optional(Type.String({ description: "Base64 image data or a data: URL." })),
|
|
41
|
+
mime_type: Type.Optional(Type.String({ description: "Required for raw base64 data." })),
|
|
42
|
+
},
|
|
43
|
+
{ additionalProperties: false },
|
|
44
|
+
),
|
|
45
|
+
Type.Object(
|
|
46
|
+
{
|
|
47
|
+
path: Type.Optional(Type.String({ description: "Path to an input image file." })),
|
|
48
|
+
data: Type.String({ description: "Base64 image data or a data: URL." }),
|
|
49
|
+
mime_type: Type.Optional(Type.String({ description: "Required for raw base64 data." })),
|
|
50
|
+
},
|
|
51
|
+
{ additionalProperties: false },
|
|
52
|
+
),
|
|
53
|
+
]);
|
|
44
54
|
|
|
45
55
|
const baseImageSchema = Type.Object(
|
|
46
56
|
{
|
|
@@ -574,8 +584,6 @@ interface AntigravitySseResult {
|
|
|
574
584
|
usage?: GeminiUsageMetadata;
|
|
575
585
|
}
|
|
576
586
|
|
|
577
|
-
const _prefix = Buffer.from("data: ", "utf-8");
|
|
578
|
-
|
|
579
587
|
async function parseAntigravitySseForImage(response: Response, signal?: AbortSignal): Promise<AntigravitySseResult> {
|
|
580
588
|
if (!response.body) {
|
|
581
589
|
throw new Error("No response body");
|