agent-sh 0.5.0 → 0.6.0
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/README.md +12 -43
- package/dist/agent/agent-loop.d.ts +1 -0
- package/dist/agent/agent-loop.js +119 -26
- package/dist/agent/subagent.js +3 -1
- package/dist/agent/system-prompt.d.ts +1 -1
- package/dist/agent/system-prompt.js +21 -16
- package/dist/agent/tools/bash.js +10 -1
- package/dist/agent/tools/display.d.ts +13 -0
- package/dist/agent/tools/display.js +70 -0
- package/dist/agent/tools/edit-file.js +60 -7
- package/dist/agent/tools/glob.js +39 -7
- package/dist/agent/tools/grep.js +111 -20
- package/dist/agent/tools/ls.js +31 -2
- package/dist/agent/tools/read-file.d.ts +9 -1
- package/dist/agent/tools/read-file.js +50 -4
- package/dist/agent/tools/user-shell.js +40 -13
- package/dist/agent/tools/write-file.js +9 -1
- package/dist/agent/types.d.ts +35 -1
- package/dist/core.d.ts +1 -3
- package/dist/core.js +7 -11
- package/dist/event-bus.d.ts +18 -3
- package/dist/extension-loader.d.ts +1 -1
- package/dist/extension-loader.js +1 -3
- package/dist/extensions/tui-renderer.js +341 -83
- package/dist/index.js +41 -36
- package/dist/input-handler.js +4 -2
- package/dist/settings.js +1 -1
- package/dist/shell.js +2 -2
- package/dist/utils/diff.js +10 -0
- package/dist/utils/markdown.d.ts +1 -0
- package/dist/utils/markdown.js +23 -1
- package/dist/utils/tool-display.d.ts +4 -0
- package/dist/utils/tool-display.js +22 -5
- package/examples/extensions/claude-code-bridge/index.ts +8 -12
- package/examples/extensions/pi-bridge/index.ts +10 -12
- package/examples/extensions/secret-guard.ts +100 -0
- package/package.json +1 -1
|
@@ -14,7 +14,7 @@ import { highlight } from "cli-highlight";
|
|
|
14
14
|
import { MarkdownRenderer, wrapLine } from "../utils/markdown.js";
|
|
15
15
|
import { createFencedBlockTransform } from "../utils/stream-transform.js";
|
|
16
16
|
import { palette as p } from "../utils/palette.js";
|
|
17
|
-
import { renderToolCall, createSpinner, renderSpinnerLine, } from "../utils/tool-display.js";
|
|
17
|
+
import { renderToolCall, createSpinner, renderSpinnerLine, formatElapsed, } from "../utils/tool-display.js";
|
|
18
18
|
import { renderDiff } from "../utils/diff-renderer.js";
|
|
19
19
|
import { renderBoxFrame } from "../utils/box-frame.js";
|
|
20
20
|
import { getSettings } from "../settings.js";
|
|
@@ -42,6 +42,7 @@ function createRenderState() {
|
|
|
42
42
|
return {
|
|
43
43
|
renderer: null,
|
|
44
44
|
hadToolCalls: false,
|
|
45
|
+
lastContentKind: null,
|
|
45
46
|
spinner: null,
|
|
46
47
|
spinnerLabel: "",
|
|
47
48
|
spinnerOpts: {},
|
|
@@ -49,9 +50,17 @@ function createRenderState() {
|
|
|
49
50
|
spinnerStartTime: 0,
|
|
50
51
|
toolLineOpen: false,
|
|
51
52
|
currentToolKind: undefined,
|
|
53
|
+
toolStartTime: 0,
|
|
54
|
+
toolExitCode: null,
|
|
52
55
|
commandOutputBuffer: "",
|
|
53
56
|
commandOutputLineCount: 0,
|
|
54
57
|
commandOutputOverflow: 0,
|
|
58
|
+
commandOverflowLines: [],
|
|
59
|
+
toolGroupKind: undefined,
|
|
60
|
+
toolGroupCount: 0,
|
|
61
|
+
toolGroupAllOk: true,
|
|
62
|
+
toolGroupRendered: 0,
|
|
63
|
+
toolGroupSummaries: [],
|
|
55
64
|
isThinking: false,
|
|
56
65
|
showThinkingText: false,
|
|
57
66
|
thinkingPending: false,
|
|
@@ -77,7 +86,7 @@ export default function activate(ctx) {
|
|
|
77
86
|
// ── Event subscriptions ─────────────────────────────────────
|
|
78
87
|
bus.on("agent:query", (e) => {
|
|
79
88
|
s.spinnerStartTime = 0;
|
|
80
|
-
showUserQuery(e.query
|
|
89
|
+
showUserQuery(e.query);
|
|
81
90
|
startAgentResponse();
|
|
82
91
|
startThinkingSpinner();
|
|
83
92
|
});
|
|
@@ -154,29 +163,104 @@ export default function activate(ctx) {
|
|
|
154
163
|
}
|
|
155
164
|
endAgentResponse();
|
|
156
165
|
});
|
|
166
|
+
// ── Tool batch grouping ──────────────────────────────────────────
|
|
167
|
+
const GROUPABLE_KINDS = new Set(["read", "search"]);
|
|
168
|
+
const GROUP_MAX_VISIBLE = 5;
|
|
169
|
+
const KIND_ICONS = { read: "◆", search: "⌕" };
|
|
170
|
+
// Batch groups: kind → { total, rendered, headerShown }
|
|
171
|
+
let batchGroups = new Map();
|
|
172
|
+
bus.on("agent:tool-batch", (e) => {
|
|
173
|
+
fencedTransform.flush();
|
|
174
|
+
finalizeToolGroup();
|
|
175
|
+
batchGroups = new Map();
|
|
176
|
+
for (const group of e.groups) {
|
|
177
|
+
batchGroups.set(group.kind, {
|
|
178
|
+
total: group.tools.length,
|
|
179
|
+
rendered: 0,
|
|
180
|
+
headerShown: false,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
});
|
|
157
184
|
bus.on("agent:tool-started", (e) => {
|
|
158
185
|
fencedTransform.flush();
|
|
159
186
|
stopCurrentSpinner();
|
|
160
187
|
s.currentToolKind = e.kind;
|
|
188
|
+
s.toolStartTime = Date.now();
|
|
161
189
|
if (e.title === "user_shell") {
|
|
190
|
+
finalizeToolGroup();
|
|
162
191
|
closeToolLine();
|
|
163
192
|
if (!s.renderer)
|
|
164
193
|
startAgentResponse();
|
|
194
|
+
contentGap("tool");
|
|
165
195
|
s.renderer.flush();
|
|
166
196
|
const cmd = e.rawInput?.command || "";
|
|
167
197
|
s.renderer.writeLine(`${p.dim}▶ user_shell: ${cmd}${p.reset}`);
|
|
168
198
|
drain();
|
|
169
199
|
s.hadToolCalls = true;
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const kind = e.kind ?? "execute";
|
|
203
|
+
const group = batchGroups.get(kind);
|
|
204
|
+
const isGrouped = group && group.total > 1 && GROUPABLE_KINDS.has(kind);
|
|
205
|
+
if (isGrouped) {
|
|
206
|
+
// Render group header on first tool of this kind in the batch
|
|
207
|
+
if (!group.headerShown) {
|
|
208
|
+
finalizeToolGroup();
|
|
209
|
+
closeToolLine();
|
|
210
|
+
if (!s.renderer)
|
|
211
|
+
startAgentResponse();
|
|
212
|
+
showCollapsedThinking();
|
|
213
|
+
contentGap("tool");
|
|
214
|
+
s.renderer.flush();
|
|
215
|
+
drain();
|
|
216
|
+
const icon = KIND_ICONS[kind] ?? "▶";
|
|
217
|
+
s.renderer.writeLine(`${p.warning}${icon}${p.reset} ${kind}`);
|
|
218
|
+
drain();
|
|
219
|
+
group.headerShown = true;
|
|
220
|
+
s.toolGroupKind = kind;
|
|
221
|
+
s.toolGroupCount = 0;
|
|
222
|
+
s.toolGroupRendered = 0;
|
|
223
|
+
s.toolGroupAllOk = true;
|
|
224
|
+
s.toolGroupSummaries = [];
|
|
225
|
+
}
|
|
226
|
+
s.toolGroupCount++;
|
|
227
|
+
if (s.toolGroupRendered < GROUP_MAX_VISIBLE) {
|
|
228
|
+
showToolCall(e.title, "", {
|
|
229
|
+
...e,
|
|
230
|
+
batchIndex: e.batchIndex,
|
|
231
|
+
batchTotal: e.batchTotal,
|
|
232
|
+
groupContinuation: true,
|
|
233
|
+
});
|
|
234
|
+
s.toolGroupRendered++;
|
|
235
|
+
}
|
|
170
236
|
}
|
|
171
237
|
else {
|
|
172
|
-
|
|
238
|
+
// Standalone tool — single in its batch kind, or not groupable
|
|
239
|
+
finalizeToolGroup();
|
|
240
|
+
showToolCall(e.title, "", {
|
|
241
|
+
...e,
|
|
242
|
+
batchIndex: e.batchIndex,
|
|
243
|
+
batchTotal: e.batchTotal,
|
|
244
|
+
});
|
|
173
245
|
}
|
|
174
246
|
});
|
|
175
247
|
bus.on("agent:tool-completed", (e) => {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
248
|
+
s.toolExitCode = e.exitCode;
|
|
249
|
+
if (e.exitCode !== 0)
|
|
250
|
+
s.toolGroupAllOk = false;
|
|
251
|
+
if (s.toolGroupKind) {
|
|
252
|
+
// Grouped tool — track success/failure and summaries, show aggregate on ⎿ line.
|
|
253
|
+
// Don't restart spinner between grouped tools — it's already running from group start.
|
|
254
|
+
if (e.resultDisplay?.summary)
|
|
255
|
+
s.toolGroupSummaries.push(e.resultDisplay.summary);
|
|
256
|
+
s.currentToolKind = undefined;
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
showToolComplete(e.exitCode, e.resultDisplay);
|
|
260
|
+
s.currentToolKind = undefined;
|
|
261
|
+
s.spinnerStartTime = 0;
|
|
262
|
+
startThinkingSpinner();
|
|
263
|
+
}
|
|
180
264
|
});
|
|
181
265
|
bus.on("agent:tool-output-chunk", (e) => writeCommandOutput(e.chunk));
|
|
182
266
|
bus.on("agent:tool-output", () => flushCommandOutput());
|
|
@@ -196,6 +280,7 @@ export default function activate(ctx) {
|
|
|
196
280
|
showCollapsedThinking();
|
|
197
281
|
if (!s.renderer)
|
|
198
282
|
startAgentResponse();
|
|
283
|
+
contentGap("info");
|
|
199
284
|
s.renderer.writeLine(`${p.error}Error: ${e.message}${p.reset}`);
|
|
200
285
|
s.renderer.writeLine("");
|
|
201
286
|
drain();
|
|
@@ -238,24 +323,43 @@ export default function activate(ctx) {
|
|
|
238
323
|
return;
|
|
239
324
|
for (const line of s.renderer.drainLines()) {
|
|
240
325
|
writer.write(line + "\n");
|
|
326
|
+
// Track whether we just emitted a blank line (for contentGap dedup).
|
|
327
|
+
// Lines from the renderer are indented (" "), so a blank line is " " or empty.
|
|
328
|
+
lastEmittedLineBlank = line.trimEnd() === "" || line.trimEnd().replace(/\x1b\[[^m]*m/g, "").trim() === "";
|
|
241
329
|
}
|
|
242
330
|
}
|
|
243
331
|
function startAgentResponse() {
|
|
244
332
|
s.renderer = new MarkdownRenderer(writer.columns);
|
|
245
333
|
s.hadToolCalls = false;
|
|
334
|
+
// Preserve lastContentKind across responses so text→tool gaps work
|
|
246
335
|
s.renderer.printTopBorder();
|
|
247
336
|
drain();
|
|
248
337
|
}
|
|
338
|
+
/**
|
|
339
|
+
* Insert an empty line when transitioning between different content kinds
|
|
340
|
+
* (e.g., text → tool, tool → text, diff → tool) for visual breathing room.
|
|
341
|
+
* Avoids double-blanks by checking if the last emitted line was already empty.
|
|
342
|
+
*/
|
|
343
|
+
let lastEmittedLineBlank = false;
|
|
344
|
+
function contentGap(kind) {
|
|
345
|
+
if (s.lastContentKind && s.lastContentKind !== kind) {
|
|
346
|
+
if (s.renderer) {
|
|
347
|
+
s.renderer.flush();
|
|
348
|
+
drain();
|
|
349
|
+
}
|
|
350
|
+
writer.write("\n");
|
|
351
|
+
}
|
|
352
|
+
s.lastContentKind = kind;
|
|
353
|
+
}
|
|
249
354
|
function showCollapsedThinking() {
|
|
250
355
|
if (s.thinkingPending && !s.showThinkingText) {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
s.renderer.writeLine(`${p.muted}… thinking${p.reset}`);
|
|
254
|
-
s.renderer.writeLine("");
|
|
356
|
+
// Just clear the pending flag — the spinner already indicates thinking.
|
|
357
|
+
// No need for a separate "… thinking" label that clutters the output.
|
|
255
358
|
s.thinkingPending = false;
|
|
256
359
|
}
|
|
257
360
|
}
|
|
258
361
|
function endAgentResponse() {
|
|
362
|
+
finalizeToolGroup();
|
|
259
363
|
closeToolLine();
|
|
260
364
|
stopCurrentSpinner();
|
|
261
365
|
if (s.renderer) {
|
|
@@ -266,10 +370,10 @@ export default function activate(ctx) {
|
|
|
266
370
|
s.renderer = null;
|
|
267
371
|
}
|
|
268
372
|
}
|
|
269
|
-
function showUserQuery(query
|
|
373
|
+
function showUserQuery(query) {
|
|
270
374
|
const boxW = writer.columns;
|
|
271
375
|
const contentW = boxW - 4;
|
|
272
|
-
|
|
376
|
+
let lines = [];
|
|
273
377
|
for (const raw of query.split("\n")) {
|
|
274
378
|
if (raw.length <= contentW) {
|
|
275
379
|
lines.push(`${p.accent}${raw}${p.reset}`);
|
|
@@ -287,12 +391,18 @@ export default function activate(ctx) {
|
|
|
287
391
|
lines.push(`${p.accent}${remaining}${p.reset}`);
|
|
288
392
|
}
|
|
289
393
|
}
|
|
394
|
+
// Truncate very long queries to keep the response visible
|
|
395
|
+
const MAX_QUERY_LINES = 20;
|
|
396
|
+
if (lines.length > MAX_QUERY_LINES) {
|
|
397
|
+
const overflow = lines.length - MAX_QUERY_LINES;
|
|
398
|
+
lines = [
|
|
399
|
+
...lines.slice(0, MAX_QUERY_LINES),
|
|
400
|
+
`${p.dim}… ${overflow} more lines${p.reset}`,
|
|
401
|
+
];
|
|
402
|
+
}
|
|
290
403
|
// Mode-specific border color and title
|
|
291
|
-
const
|
|
292
|
-
const
|
|
293
|
-
const title = modeLabel
|
|
294
|
-
? `${borderColor}${p.bold} ${modeLabel} ${p.reset}`
|
|
295
|
-
: `${p.accent}${p.bold}❯${p.reset}`;
|
|
404
|
+
const borderColor = p.accent;
|
|
405
|
+
const title = `${p.accent}${p.bold}❯${p.reset}`;
|
|
296
406
|
// Backend/model label on the right (backend/model, highlighted)
|
|
297
407
|
const model = backendInfo?.model ?? llmClient?.model;
|
|
298
408
|
const backend = backendInfo?.name;
|
|
@@ -319,8 +429,8 @@ export default function activate(ctx) {
|
|
|
319
429
|
}
|
|
320
430
|
}
|
|
321
431
|
function writeAgentText(text) {
|
|
432
|
+
finalizeToolGroup();
|
|
322
433
|
closeToolLine();
|
|
323
|
-
const needsGap = s.hadToolCalls;
|
|
324
434
|
s.hadToolCalls = false;
|
|
325
435
|
if (s.isThinking) {
|
|
326
436
|
s.isThinking = false;
|
|
@@ -335,28 +445,24 @@ export default function activate(ctx) {
|
|
|
335
445
|
stopCurrentSpinner();
|
|
336
446
|
if (!s.renderer)
|
|
337
447
|
startAgentResponse();
|
|
338
|
-
|
|
339
|
-
writer.write("\n");
|
|
448
|
+
contentGap("text");
|
|
340
449
|
s.renderer.push(text);
|
|
341
450
|
drain();
|
|
342
451
|
}
|
|
343
452
|
define("render:code-block", (language, code, width) => {
|
|
344
453
|
flushForRaw();
|
|
454
|
+
contentGap("code");
|
|
345
455
|
if (language) {
|
|
346
456
|
s.renderer.writeLine(`${p.dim}${language}${p.reset}`);
|
|
347
457
|
}
|
|
348
458
|
let highlighted;
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
459
|
+
try {
|
|
460
|
+
highlighted = language
|
|
461
|
+
? highlight(code, { language })
|
|
462
|
+
: highlight(code); // auto-detect
|
|
352
463
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
highlighted = highlight(code, { language });
|
|
356
|
-
}
|
|
357
|
-
catch {
|
|
358
|
-
highlighted = `${p.success}${code}${p.reset}`;
|
|
359
|
-
}
|
|
464
|
+
catch {
|
|
465
|
+
highlighted = code;
|
|
360
466
|
}
|
|
361
467
|
const contentWidth = Math.min(90, width - 2);
|
|
362
468
|
for (const line of highlighted.split("\n")) {
|
|
@@ -389,41 +495,168 @@ export default function activate(ctx) {
|
|
|
389
495
|
function writeInlineImage(data) {
|
|
390
496
|
ctx.call("render:image", data);
|
|
391
497
|
}
|
|
498
|
+
/**
|
|
499
|
+
* Default renderer for tool result bodies. Extensions can advise this handler
|
|
500
|
+
* to override rendering for specific body kinds or add new ones:
|
|
501
|
+
*
|
|
502
|
+
* ctx.advise("render:result-body", (next, body, width) => {
|
|
503
|
+
* if (body.kind === "diff") return myCustomDiffRenderer(body, width);
|
|
504
|
+
* return next(body, width);
|
|
505
|
+
* });
|
|
506
|
+
*/
|
|
507
|
+
define("render:result-body", (body, width) => {
|
|
508
|
+
if (body.kind === "diff") {
|
|
509
|
+
return renderDiffBody(body.diff, body.filePath, width);
|
|
510
|
+
}
|
|
511
|
+
if (body.kind === "lines") {
|
|
512
|
+
return renderLinesBody(body.lines, width, body.maxLines);
|
|
513
|
+
}
|
|
514
|
+
return [];
|
|
515
|
+
});
|
|
516
|
+
/** Render a diff as framed box lines (pure — no TUI state side effects). */
|
|
517
|
+
function renderDiffBody(diff, filePath, width) {
|
|
518
|
+
if (diff.isIdentical)
|
|
519
|
+
return [];
|
|
520
|
+
const boxW = Math.min(120, width);
|
|
521
|
+
const contentW = boxW - 4;
|
|
522
|
+
const diffLines = renderDiff(diff, {
|
|
523
|
+
width: contentW,
|
|
524
|
+
filePath,
|
|
525
|
+
maxLines: getSettings().diffMaxLines,
|
|
526
|
+
trueColor: true,
|
|
527
|
+
});
|
|
528
|
+
const lastLine = diffLines[diffLines.length - 1] ?? "";
|
|
529
|
+
const isTruncated = lastLine.includes("… ");
|
|
530
|
+
if (isTruncated) {
|
|
531
|
+
s.lastTruncatedDiff = { filePath, diff, expanded: false };
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
s.lastTruncatedDiff = null;
|
|
535
|
+
}
|
|
536
|
+
const body = diffLines.length > 1 ? ["", ...diffLines.slice(1), ""] : diffLines;
|
|
537
|
+
const footer = isTruncated
|
|
538
|
+
? [` ${p.dim}ctrl+o to expand${p.reset}`]
|
|
539
|
+
: undefined;
|
|
540
|
+
return renderBoxFrame(body, {
|
|
541
|
+
width: boxW,
|
|
542
|
+
style: "rounded",
|
|
543
|
+
borderColor: p.dim,
|
|
544
|
+
title: diffTitle(filePath, diff),
|
|
545
|
+
footer,
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
/** Render output lines with truncation. */
|
|
549
|
+
function renderLinesBody(lines, width, maxLines) {
|
|
550
|
+
const max = maxLines ?? 10;
|
|
551
|
+
const shown = lines.slice(0, max);
|
|
552
|
+
const contentW = Math.max(1, width - 6);
|
|
553
|
+
const output = [];
|
|
554
|
+
for (const line of shown) {
|
|
555
|
+
const text = line.length > contentW ? line.slice(0, contentW - 1) + "…" : line;
|
|
556
|
+
output.push(` ${p.dim} ${text}${p.reset}`);
|
|
557
|
+
}
|
|
558
|
+
if (lines.length > max) {
|
|
559
|
+
output.push(` ${p.dim} … ${lines.length - max} more lines${p.reset}`);
|
|
560
|
+
}
|
|
561
|
+
return output;
|
|
562
|
+
}
|
|
563
|
+
/** Extract a detail string from tool args for group continuation display. */
|
|
564
|
+
function extractDetail(extra) {
|
|
565
|
+
if (extra.locations && extra.locations.length > 0) {
|
|
566
|
+
const loc = extra.locations[0];
|
|
567
|
+
const cwd = process.cwd();
|
|
568
|
+
const home = process.env.HOME;
|
|
569
|
+
let fp = loc.path;
|
|
570
|
+
if (fp.startsWith(cwd + "/"))
|
|
571
|
+
fp = fp.slice(cwd.length + 1);
|
|
572
|
+
else if (home && fp.startsWith(home + "/"))
|
|
573
|
+
fp = "~/" + fp.slice(home.length + 1);
|
|
574
|
+
return loc.line ? `${fp}:${loc.line}` : fp;
|
|
575
|
+
}
|
|
576
|
+
const raw = extra.rawInput;
|
|
577
|
+
if (!raw)
|
|
578
|
+
return "";
|
|
579
|
+
if (typeof raw.command === "string")
|
|
580
|
+
return `$ ${raw.command}`;
|
|
581
|
+
if (typeof raw.pattern === "string")
|
|
582
|
+
return raw.pattern;
|
|
583
|
+
if (typeof raw.path === "string") {
|
|
584
|
+
const cwd = process.cwd();
|
|
585
|
+
const home = process.env.HOME;
|
|
586
|
+
let fp = raw.path;
|
|
587
|
+
if (fp.startsWith(cwd + "/"))
|
|
588
|
+
fp = fp.slice(cwd.length + 1);
|
|
589
|
+
else if (home && fp.startsWith(home + "/"))
|
|
590
|
+
fp = "~/" + fp.slice(home.length + 1);
|
|
591
|
+
return fp;
|
|
592
|
+
}
|
|
593
|
+
if (typeof raw.query === "string")
|
|
594
|
+
return `"${raw.query}"`;
|
|
595
|
+
return "";
|
|
596
|
+
}
|
|
392
597
|
function showToolCall(title, command, extra) {
|
|
393
598
|
closeToolLine();
|
|
394
599
|
stopCurrentSpinner();
|
|
395
600
|
if (!s.renderer)
|
|
396
601
|
startAgentResponse();
|
|
397
602
|
showCollapsedThinking();
|
|
603
|
+
// No gap between grouped tools — they're visually connected
|
|
604
|
+
if (!extra?.groupContinuation)
|
|
605
|
+
contentGap("tool");
|
|
398
606
|
s.renderer.flush();
|
|
399
607
|
drain();
|
|
400
608
|
const lines = renderToolCall({
|
|
401
609
|
title,
|
|
402
610
|
command: command || undefined,
|
|
403
611
|
kind: extra?.kind,
|
|
612
|
+
icon: extra?.icon,
|
|
404
613
|
locations: extra?.locations,
|
|
405
614
|
rawInput: extra?.rawInput,
|
|
615
|
+
displayDetail: extra?.displayDetail,
|
|
406
616
|
}, writer.columns);
|
|
617
|
+
if (extra?.groupContinuation && lines.length > 0) {
|
|
618
|
+
// Swap the colored kind icon for a muted tree connector,
|
|
619
|
+
// and strip the tool name prefix — show detail only.
|
|
620
|
+
const detail = extra.displayDetail || extractDetail(extra);
|
|
621
|
+
const maxW = Math.max(1, writer.columns - 6);
|
|
622
|
+
const text = detail.length > maxW ? detail.slice(0, maxW - 1) + "…" : detail;
|
|
623
|
+
lines[0] = detail
|
|
624
|
+
? `${p.muted}├${p.reset} ${p.dim}${text}${p.reset}`
|
|
625
|
+
: lines[0].replace(/^\x1b\[[^m]*m.\x1b\[0m/, `${p.muted}├${p.reset}`);
|
|
626
|
+
}
|
|
627
|
+
const batchPrefix = "";
|
|
407
628
|
for (let i = 0; i < lines.length - 1; i++) {
|
|
408
629
|
s.renderer.writeLine(lines[i]);
|
|
409
630
|
}
|
|
410
631
|
drain();
|
|
411
632
|
if (lines.length > 0) {
|
|
412
|
-
|
|
413
|
-
|
|
633
|
+
if (extra?.groupContinuation) {
|
|
634
|
+
// Grouped tools: close the line immediately — checkmarks go on the ⎿ summary
|
|
635
|
+
s.renderer.writeLine(` ${batchPrefix}${lines[lines.length - 1]}`);
|
|
636
|
+
drain();
|
|
637
|
+
s.toolLineOpen = false;
|
|
638
|
+
}
|
|
639
|
+
else {
|
|
640
|
+
writer.write(` ${batchPrefix}${lines[lines.length - 1]}`);
|
|
641
|
+
s.toolLineOpen = true;
|
|
642
|
+
}
|
|
414
643
|
}
|
|
415
644
|
s.hadToolCalls = true;
|
|
416
645
|
s.commandOutputLineCount = 0;
|
|
417
646
|
s.commandOutputOverflow = 0;
|
|
418
647
|
}
|
|
419
|
-
function showToolComplete(exitCode) {
|
|
648
|
+
function showToolComplete(exitCode, resultDisplay) {
|
|
420
649
|
if (!s.renderer)
|
|
421
650
|
return;
|
|
651
|
+
stopCurrentSpinner();
|
|
652
|
+
const elapsed = s.toolStartTime ? formatElapsed(Date.now() - s.toolStartTime) : "";
|
|
653
|
+
const timer = elapsed ? ` ${p.dim}${elapsed}${p.reset}` : "";
|
|
654
|
+
const summary = resultDisplay?.summary ? ` ${p.dim}${resultDisplay.summary}${p.reset}` : "";
|
|
422
655
|
const mark = exitCode === null
|
|
423
656
|
? `${p.muted}(timed out)${p.reset}`
|
|
424
657
|
: exitCode === 0
|
|
425
|
-
? `${p.success}✓${p.reset}`
|
|
426
|
-
: `${p.error}✗ exit ${exitCode}${p.reset}`;
|
|
658
|
+
? `${p.success}✓${p.reset}${summary}${timer}`
|
|
659
|
+
: `${p.error}✗ exit ${exitCode}${p.reset}${summary}${timer}`;
|
|
427
660
|
if (s.toolLineOpen && s.commandOutputLineCount === 0) {
|
|
428
661
|
writer.write(` ${mark}\n`);
|
|
429
662
|
s.toolLineOpen = false;
|
|
@@ -434,6 +667,20 @@ export default function activate(ctx) {
|
|
|
434
667
|
s.renderer.writeLine(` ${mark}`);
|
|
435
668
|
drain();
|
|
436
669
|
}
|
|
670
|
+
// Render structured body if present
|
|
671
|
+
if (resultDisplay?.body) {
|
|
672
|
+
renderResultBody(resultDisplay.body);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
function renderResultBody(body) {
|
|
676
|
+
if (!s.renderer)
|
|
677
|
+
return;
|
|
678
|
+
const lines = ctx.call("render:result-body", body, writer.columns) ?? [];
|
|
679
|
+
for (const line of lines) {
|
|
680
|
+
s.renderer.writeLine(line);
|
|
681
|
+
}
|
|
682
|
+
if (lines.length > 0)
|
|
683
|
+
drain();
|
|
437
684
|
}
|
|
438
685
|
// Thinking is always assumed available — the TUI renders thinking
|
|
439
686
|
// tokens whenever they arrive, regardless of backend.
|
|
@@ -474,6 +721,40 @@ export default function activate(ctx) {
|
|
|
474
721
|
s.toolLineOpen = false;
|
|
475
722
|
}
|
|
476
723
|
}
|
|
724
|
+
/** Finalize a group of collapsed tool calls, rendering the summary. */
|
|
725
|
+
function finalizeToolGroup() {
|
|
726
|
+
if (s.toolGroupCount <= 1) {
|
|
727
|
+
// 0–1 tools: standalone, nothing to finalize
|
|
728
|
+
s.toolGroupKind = undefined;
|
|
729
|
+
s.toolGroupCount = 0;
|
|
730
|
+
s.toolGroupRendered = 0;
|
|
731
|
+
s.toolGroupSummaries = [];
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
closeToolLine();
|
|
735
|
+
if (!s.renderer)
|
|
736
|
+
startAgentResponse();
|
|
737
|
+
const mark = s.toolGroupAllOk
|
|
738
|
+
? `${p.success}✓${p.reset}`
|
|
739
|
+
: `${p.error}✗${p.reset}`;
|
|
740
|
+
const summary = s.toolGroupSummaries.length > 0
|
|
741
|
+
? ` ${p.dim}${s.toolGroupSummaries.join(", ")}${p.reset}`
|
|
742
|
+
: "";
|
|
743
|
+
const collapsed = s.toolGroupCount - s.toolGroupRendered;
|
|
744
|
+
if (collapsed > 0) {
|
|
745
|
+
s.renderer.writeLine(` ${p.muted}└${p.reset} ${p.dim}+${collapsed} more${p.reset} ${mark}${summary}`);
|
|
746
|
+
}
|
|
747
|
+
else {
|
|
748
|
+
// All items visible — close the tree with └ mark + summary
|
|
749
|
+
s.renderer.writeLine(` ${p.muted}└${p.reset} ${mark}${summary}`);
|
|
750
|
+
}
|
|
751
|
+
drain();
|
|
752
|
+
s.toolGroupKind = undefined;
|
|
753
|
+
s.toolGroupCount = 0;
|
|
754
|
+
s.toolGroupAllOk = true;
|
|
755
|
+
s.toolGroupRendered = 0;
|
|
756
|
+
s.toolGroupSummaries = [];
|
|
757
|
+
}
|
|
477
758
|
function writeCommandOutput(chunk) {
|
|
478
759
|
if (!s.renderer)
|
|
479
760
|
return;
|
|
@@ -491,10 +772,13 @@ export default function activate(ctx) {
|
|
|
491
772
|
}
|
|
492
773
|
else {
|
|
493
774
|
s.commandOutputOverflow++;
|
|
775
|
+
s.commandOverflowLines.push(line);
|
|
494
776
|
}
|
|
495
777
|
}
|
|
496
778
|
drain();
|
|
497
779
|
}
|
|
780
|
+
/** Max overflow lines to show when a command fails. */
|
|
781
|
+
const FAIL_OVERFLOW_MAX = 20;
|
|
498
782
|
function flushCommandOutput() {
|
|
499
783
|
if (!s.renderer)
|
|
500
784
|
return;
|
|
@@ -508,13 +792,28 @@ export default function activate(ctx) {
|
|
|
508
792
|
}
|
|
509
793
|
else {
|
|
510
794
|
s.commandOutputOverflow++;
|
|
795
|
+
s.commandOverflowLines.push(s.commandOutputBuffer);
|
|
511
796
|
}
|
|
512
797
|
s.commandOutputBuffer = "";
|
|
513
798
|
}
|
|
514
|
-
|
|
799
|
+
// On failure, show the tail of the overflow so the user can see the error
|
|
800
|
+
const failed = s.toolExitCode !== null && s.toolExitCode !== 0;
|
|
801
|
+
if (failed && s.commandOverflowLines.length > 0) {
|
|
802
|
+
const tail = s.commandOverflowLines.slice(-FAIL_OVERFLOW_MAX);
|
|
803
|
+
const skipped = s.commandOverflowLines.length - tail.length;
|
|
804
|
+
if (skipped > 0) {
|
|
805
|
+
s.renderer.writeLine(`${p.dim} … ${skipped} lines hidden${p.reset}`);
|
|
806
|
+
}
|
|
807
|
+
for (const line of tail) {
|
|
808
|
+
s.renderer.writeLine(`${p.dim} ${line}${p.reset}`);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
else if (s.commandOutputOverflow > 0 && maxLines > 0) {
|
|
515
812
|
s.renderer.writeLine(`${p.dim} … ${s.commandOutputOverflow} more lines${p.reset}`);
|
|
516
813
|
}
|
|
517
814
|
s.commandOutputOverflow = 0;
|
|
815
|
+
s.commandOverflowLines = [];
|
|
816
|
+
s.toolExitCode = null;
|
|
518
817
|
drain();
|
|
519
818
|
}
|
|
520
819
|
function diffTitle(filePath, diff) {
|
|
@@ -526,36 +825,11 @@ export default function activate(ctx) {
|
|
|
526
825
|
function showFileDiff(filePath, diff) {
|
|
527
826
|
if (diff.isIdentical)
|
|
528
827
|
return;
|
|
529
|
-
|
|
530
|
-
const
|
|
531
|
-
const diffLines = renderDiff(diff, {
|
|
532
|
-
width: contentW,
|
|
533
|
-
filePath,
|
|
534
|
-
maxLines: getSettings().diffMaxLines,
|
|
535
|
-
trueColor: true,
|
|
536
|
-
});
|
|
537
|
-
const lastLine = diffLines[diffLines.length - 1] ?? "";
|
|
538
|
-
const isTruncated = lastLine.includes("… ");
|
|
539
|
-
if (isTruncated) {
|
|
540
|
-
s.lastTruncatedDiff = { filePath, diff, expanded: false };
|
|
541
|
-
}
|
|
542
|
-
else {
|
|
543
|
-
s.lastTruncatedDiff = null;
|
|
544
|
-
}
|
|
545
|
-
const body = diffLines.length > 1 ? ["", ...diffLines.slice(1), ""] : diffLines;
|
|
546
|
-
const footer = isTruncated
|
|
547
|
-
? [` ${p.dim}ctrl+o to expand${p.reset}`]
|
|
548
|
-
: undefined;
|
|
549
|
-
const framed = renderBoxFrame(body, {
|
|
550
|
-
width: boxW,
|
|
551
|
-
style: "rounded",
|
|
552
|
-
borderColor: p.dim,
|
|
553
|
-
title: diffTitle(filePath, diff),
|
|
554
|
-
footer,
|
|
555
|
-
});
|
|
828
|
+
contentGap("diff");
|
|
829
|
+
const lines = ctx.call("render:result-body", { kind: "diff", diff, filePath }, writer.columns) ?? [];
|
|
556
830
|
if (!s.renderer)
|
|
557
831
|
startAgentResponse();
|
|
558
|
-
for (const line of
|
|
832
|
+
for (const line of lines) {
|
|
559
833
|
s.renderer.writeLine(line);
|
|
560
834
|
}
|
|
561
835
|
drain();
|
|
@@ -594,25 +868,9 @@ export default function activate(ctx) {
|
|
|
594
868
|
}
|
|
595
869
|
}
|
|
596
870
|
function showFileDiffCached(entry) {
|
|
597
|
-
const {
|
|
598
|
-
const boxW = Math.min(120, writer.columns);
|
|
599
|
-
const contentW = boxW - 4;
|
|
600
|
-
const diffLines = renderDiff(diff, {
|
|
601
|
-
width: contentW,
|
|
602
|
-
filePath,
|
|
603
|
-
maxLines: getSettings().diffMaxLines,
|
|
604
|
-
trueColor: true,
|
|
605
|
-
});
|
|
606
|
-
const body = diffLines.length > 1 ? ["", ...diffLines.slice(1), ""] : diffLines;
|
|
607
|
-
const framed = renderBoxFrame(body, {
|
|
608
|
-
width: boxW,
|
|
609
|
-
style: "rounded",
|
|
610
|
-
borderColor: p.dim,
|
|
611
|
-
title: diffTitle(filePath, diff),
|
|
612
|
-
footer: [` ${p.dim}ctrl+o to expand${p.reset}`],
|
|
613
|
-
});
|
|
871
|
+
const lines = ctx.call("render:result-body", { kind: "diff", diff: entry.diff, filePath: entry.filePath }, writer.columns) ?? [];
|
|
614
872
|
writer.write("\n");
|
|
615
|
-
for (const line of
|
|
873
|
+
for (const line of lines) {
|
|
616
874
|
writer.write(line + "\n");
|
|
617
875
|
}
|
|
618
876
|
}
|