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