agent-sh 0.15.5 → 0.15.7
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/LICENSE +21 -0
- package/README.md +1 -1
- package/dist/agent/agent-loop.js +2 -5
- package/dist/agent/extensions/rolling-history/index.js +20 -8
- package/dist/agent/extensions/rolling-history/recall.d.ts +2 -2
- package/dist/agent/extensions/rolling-history/recall.js +17 -7
- package/dist/agent/providers/openai-compatible.d.ts +8 -0
- package/dist/agent/providers/openai-compatible.js +9 -2
- package/dist/agent/store.js +6 -1
- package/dist/agent/token-budget.d.ts +2 -1
- package/dist/agent/token-budget.js +6 -1
- package/dist/agent/types.d.ts +4 -1
- package/dist/cli/index.js +1 -1
- package/dist/core/event-bus.d.ts +16 -1
- package/dist/core/event-bus.js +73 -11
- package/dist/core/index.js +18 -0
- package/dist/shell/tui-renderer.js +116 -174
- package/dist/utils/diff-renderer.js +65 -30
- package/dist/utils/executor.js +19 -11
- package/dist/utils/floating-panel.d.ts +1 -0
- package/dist/utils/floating-panel.js +28 -26
- package/dist/utils/markdown.js +56 -44
- package/dist/utils/palette.d.ts +11 -0
- package/dist/utils/palette.js +11 -0
- package/docs/agent.md +13 -11
- package/docs/architecture.md +3 -5
- package/docs/extensions.md +21 -20
- package/docs/library.md +6 -3
- package/docs/troubleshooting.md +2 -2
- package/docs/tui-composition.md +11 -3
- package/docs/usage.md +70 -50
- package/examples/extensions/ashi/src/chat/assistant.ts +6 -4
- package/examples/extensions/ashi/src/compaction.ts +4 -7
- package/examples/extensions/ashi/src/frontend.ts +2 -0
- package/examples/extensions/ashi/src/schema.ts +8 -2
- package/examples/extensions/command-suggest.ts +90 -0
- package/examples/extensions/solarized-theme.ts +11 -0
- package/package.json +5 -5
- package/src/agent/agent-loop.ts +2 -5
- package/src/agent/extensions/rolling-history/index.ts +20 -8
- package/src/agent/extensions/rolling-history/recall.ts +28 -7
- package/src/agent/providers/openai-compatible.ts +19 -4
- package/src/agent/store.ts +5 -1
- package/src/agent/token-budget.ts +10 -1
- package/src/agent/types.ts +4 -1
- package/src/cli/index.ts +1 -1
- package/src/core/event-bus.ts +67 -12
- package/src/core/index.ts +18 -0
- package/src/shell/tui-renderer.ts +131 -207
- package/src/utils/diff-renderer.ts +62 -29
- package/src/utils/executor.ts +17 -14
- package/src/utils/floating-panel.ts +24 -22
- package/src/utils/markdown.ts +49 -40
- package/src/utils/palette.ts +30 -5
|
@@ -71,12 +71,9 @@ interface RenderState {
|
|
|
71
71
|
|
|
72
72
|
// ── Tool output ──
|
|
73
73
|
openTool: { callId: string; title: string; kind?: string; displayDetail?: string } | null;
|
|
74
|
-
/** Tools whose start line was closed before their complete fired
|
|
75
|
-
*
|
|
76
|
-
|
|
77
|
-
* ⎿ renders under a re-emitted "(cont.)" header to avoid looking
|
|
78
|
-
* like a child of whatever tool rendered in between. */
|
|
79
|
-
pendingToolCompletes: Map<string, { title: string; kind?: string; displayDetail?: string; orphaned?: boolean }>;
|
|
74
|
+
/** Tools whose start line was closed before their complete fired —
|
|
75
|
+
* their ✓ renders as a labeled ⎿ line instead of inline. */
|
|
76
|
+
pendingToolCompletes: Map<string, { title: string; kind?: string; displayDetail?: string }>;
|
|
80
77
|
currentToolKind: string | undefined;
|
|
81
78
|
toolStartTime: number;
|
|
82
79
|
toolExitCode: number | null;
|
|
@@ -85,21 +82,6 @@ interface RenderState {
|
|
|
85
82
|
commandOutputOverflow: number;
|
|
86
83
|
commandOverflowLines: string[];
|
|
87
84
|
|
|
88
|
-
/** Consecutive orphans of the same kind share one "(cont.)" header;
|
|
89
|
-
* cleared when any non-orphan render happens. */
|
|
90
|
-
orphanContHeaderKind: string | undefined;
|
|
91
|
-
|
|
92
|
-
// ── Tool grouping (collapse sequential same-type read-only tools) ──
|
|
93
|
-
toolGroupKind: string | undefined;
|
|
94
|
-
toolGroupCount: number;
|
|
95
|
-
/** Completes-seen count — skip aggregate if finalize fires at 0. */
|
|
96
|
-
toolGroupCompletedCount: number;
|
|
97
|
-
toolGroupAllOk: boolean;
|
|
98
|
-
/** Number of tools rendered individually in current group. */
|
|
99
|
-
toolGroupRendered: number;
|
|
100
|
-
/** Accumulated result summaries from grouped tools. */
|
|
101
|
-
toolGroupSummaries: string[];
|
|
102
|
-
|
|
103
85
|
// ── Thinking ──
|
|
104
86
|
isThinking: boolean;
|
|
105
87
|
showThinkingText: boolean;
|
|
@@ -118,7 +100,6 @@ function createRenderState(): RenderState {
|
|
|
118
100
|
spinnerStartTime: 0,
|
|
119
101
|
openTool: null,
|
|
120
102
|
pendingToolCompletes: new Map(),
|
|
121
|
-
orphanContHeaderKind: undefined,
|
|
122
103
|
currentToolKind: undefined,
|
|
123
104
|
toolStartTime: 0,
|
|
124
105
|
toolExitCode: null,
|
|
@@ -126,12 +107,6 @@ function createRenderState(): RenderState {
|
|
|
126
107
|
commandOutputLineCount: 0,
|
|
127
108
|
commandOutputOverflow: 0,
|
|
128
109
|
commandOverflowLines: [],
|
|
129
|
-
toolGroupKind: undefined,
|
|
130
|
-
toolGroupCount: 0,
|
|
131
|
-
toolGroupCompletedCount: 0,
|
|
132
|
-
toolGroupAllOk: true,
|
|
133
|
-
toolGroupRendered: 0,
|
|
134
|
-
toolGroupSummaries: [],
|
|
135
110
|
isThinking: false,
|
|
136
111
|
showThinkingText: false,
|
|
137
112
|
thinkingPending: false,
|
|
@@ -195,16 +170,6 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
195
170
|
return `${p.error}✗ exit ${exitCode}${p.reset}${summaryStr}${timer}`;
|
|
196
171
|
});
|
|
197
172
|
|
|
198
|
-
define("tui:render-tool-group-summary", (count: number, rendered: number, allOk: boolean, summaries: string[]): string => {
|
|
199
|
-
const mark = allOk ? `${p.success}✓${p.reset}` : `${p.error}✗${p.reset}`;
|
|
200
|
-
const summaryStr = summaries.length > 0 ? ` ${p.dim}${summaries.join(", ")}${p.reset}` : "";
|
|
201
|
-
const collapsed = count - rendered;
|
|
202
|
-
if (collapsed > 0) {
|
|
203
|
-
return ` ${p.muted}└${p.reset} ${p.dim}+${collapsed} more${p.reset} ${mark}${summaryStr}`;
|
|
204
|
-
}
|
|
205
|
-
return ` ${p.muted}└${p.reset} ${mark}${summaryStr}`;
|
|
206
|
-
});
|
|
207
|
-
|
|
208
173
|
define("tui:render-command-output", (line: string, _kind: string | undefined): string =>
|
|
209
174
|
`${p.dim} ${line}${p.reset}`);
|
|
210
175
|
|
|
@@ -333,34 +298,48 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
333
298
|
const GROUP_MAX_VISIBLE = 5;
|
|
334
299
|
const KIND_ICONS: Record<string, string> = { read: "◆", search: "⌕" };
|
|
335
300
|
|
|
336
|
-
//
|
|
337
|
-
|
|
301
|
+
// Read-only batch tools run in parallel, so their events arrive interleaved
|
|
302
|
+
// and append-only output can't un-interleave them. Buffer each group instead
|
|
303
|
+
// of streaming; mutating/streaming tools are sequential and still render live.
|
|
304
|
+
interface GroupMember { detail: string; ok: boolean; summary?: string; done: boolean; }
|
|
305
|
+
interface GroupState {
|
|
306
|
+
total: number;
|
|
307
|
+
completed: number;
|
|
308
|
+
finalized: boolean;
|
|
309
|
+
order: string[]; // callIds, in start order
|
|
310
|
+
members: Map<string, GroupMember>;
|
|
311
|
+
}
|
|
312
|
+
let batchGroups = new Map<string, GroupState>();
|
|
313
|
+
let batchSize = 0;
|
|
314
|
+
|
|
315
|
+
/** A lone tool has nothing to interleave with, so deferral applies only to
|
|
316
|
+
* read-only kinds in a multi-tool batch. */
|
|
317
|
+
function isDeferred(kind: string): boolean {
|
|
318
|
+
return batchSize > 1 && GROUPABLE_KINDS.has(kind) && batchGroups.has(kind);
|
|
319
|
+
}
|
|
338
320
|
|
|
339
321
|
bus.on("agent:tool-batch", (e) => {
|
|
340
322
|
if (!shouldRender()) return;
|
|
341
323
|
fencedTransform.flush();
|
|
342
|
-
|
|
343
|
-
|
|
324
|
+
finalizeAllGroups();
|
|
325
|
+
closeToolLine();
|
|
344
326
|
batchGroups = new Map();
|
|
327
|
+
batchSize = 0;
|
|
345
328
|
for (const group of e.groups) {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
rendered: 0,
|
|
349
|
-
headerShown: false,
|
|
350
|
-
});
|
|
329
|
+
batchSize += group.tools.length;
|
|
330
|
+
batchGroups.set(group.kind, { total: group.tools.length, completed: 0, finalized: false, order: [], members: new Map() });
|
|
351
331
|
}
|
|
352
332
|
});
|
|
353
333
|
|
|
354
334
|
bus.on("agent:tool-started", (e) => {
|
|
355
335
|
if (!shouldRender()) return;
|
|
356
336
|
fencedTransform.flush();
|
|
357
|
-
stopCurrentSpinner();
|
|
358
337
|
s.currentToolKind = e.kind;
|
|
359
338
|
s.toolStartTime = Date.now();
|
|
360
|
-
s.orphanContHeaderKind = undefined;
|
|
361
339
|
|
|
362
340
|
if (e.title === "user_shell") {
|
|
363
|
-
|
|
341
|
+
stopCurrentSpinner();
|
|
342
|
+
finalizeAllGroups();
|
|
364
343
|
closeToolLine();
|
|
365
344
|
if (!s.renderer) startAgentResponse();
|
|
366
345
|
contentGap("tool");
|
|
@@ -373,88 +352,55 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
373
352
|
}
|
|
374
353
|
|
|
375
354
|
const kind = e.kind ?? "execute";
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
if (!s.renderer) startAgentResponse();
|
|
385
|
-
showCollapsedThinking();
|
|
386
|
-
contentGap("tool");
|
|
387
|
-
s.renderer!.flush();
|
|
388
|
-
drain();
|
|
389
|
-
|
|
390
|
-
const icon = KIND_ICONS[kind] ?? "▶";
|
|
391
|
-
s.renderer!.writeLine(`${p.warning}${icon}${p.reset} ${kind}`);
|
|
392
|
-
drain();
|
|
393
|
-
|
|
394
|
-
group.headerShown = true;
|
|
395
|
-
s.toolGroupKind = kind;
|
|
396
|
-
s.toolGroupCount = 0;
|
|
397
|
-
s.toolGroupCompletedCount = 0;
|
|
398
|
-
s.toolGroupRendered = 0;
|
|
399
|
-
s.toolGroupAllOk = true;
|
|
400
|
-
s.toolGroupSummaries = [];
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
s.toolGroupCount++;
|
|
404
|
-
|
|
405
|
-
if (s.toolGroupRendered < GROUP_MAX_VISIBLE) {
|
|
406
|
-
showToolCall(e.title, "", { ...e, groupContinuation: true });
|
|
407
|
-
s.toolGroupRendered++;
|
|
408
|
-
}
|
|
409
|
-
if (e.toolCallId) {
|
|
410
|
-
s.pendingToolCompletes.set(e.toolCallId, {
|
|
411
|
-
title: e.title,
|
|
412
|
-
kind,
|
|
413
|
-
displayDetail: e.displayDetail ?? extractDetail(e),
|
|
414
|
-
});
|
|
415
|
-
}
|
|
416
|
-
} else {
|
|
417
|
-
// Standalone tool — single in its batch kind, or not groupable
|
|
418
|
-
finalizeToolGroup();
|
|
419
|
-
showToolCall(e.title, "", { ...e });
|
|
355
|
+
if (isDeferred(kind)) {
|
|
356
|
+
const group = batchGroups.get(kind)!;
|
|
357
|
+
const id = e.toolCallId ?? `${kind}-${group.order.length}`;
|
|
358
|
+
group.order.push(id);
|
|
359
|
+
group.members.set(id, { detail: e.displayDetail ?? extractDetail(e), ok: true, done: false });
|
|
360
|
+
s.hadToolCalls = true;
|
|
361
|
+
if (!s.spinner) startThinkingSpinner(); // nothing's drawn yet — stand in until the block renders
|
|
362
|
+
return;
|
|
420
363
|
}
|
|
364
|
+
|
|
365
|
+
// Eager: a single-tool turn, or a sequential mutating/streaming tool.
|
|
366
|
+
stopCurrentSpinner();
|
|
367
|
+
showToolCall(e.title, "", { ...e });
|
|
421
368
|
});
|
|
422
369
|
|
|
423
370
|
bus.on("agent:tool-completed", (e) => {
|
|
424
371
|
if (!shouldRender()) return;
|
|
425
372
|
s.toolExitCode = e.exitCode;
|
|
426
|
-
if (e.exitCode !== 0) s.toolGroupAllOk = false;
|
|
427
|
-
|
|
428
373
|
const resultDisplay = e.resultDisplay;
|
|
374
|
+
const kind = e.kind ?? "execute";
|
|
375
|
+
const group = batchGroups.get(kind);
|
|
429
376
|
|
|
430
|
-
if (
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
if (
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
// Finalize as soon as all members return so aggregate lands right
|
|
438
|
-
// after its children, not below out-of-band renders from the next tool.
|
|
439
|
-
const batchGroup = batchGroups.get(s.toolGroupKind);
|
|
440
|
-
if (batchGroup && s.toolGroupCompletedCount >= batchGroup.total) {
|
|
441
|
-
finalizeToolGroup();
|
|
377
|
+
if (isDeferred(kind) && group) {
|
|
378
|
+
const id = e.toolCallId ?? group.order[group.completed];
|
|
379
|
+
const member = id ? group.members.get(id) : undefined;
|
|
380
|
+
if (member) {
|
|
381
|
+
member.done = true;
|
|
382
|
+
member.ok = e.exitCode === 0;
|
|
383
|
+
member.summary = resultDisplay?.summary;
|
|
442
384
|
}
|
|
385
|
+
group.completed++;
|
|
386
|
+
s.currentToolKind = undefined;
|
|
387
|
+
flushReadyGroups();
|
|
388
|
+
if (!s.spinner) startThinkingSpinner(); // keep the spinner up for in-flight siblings
|
|
443
389
|
} else {
|
|
444
|
-
// Tools that lost the inline slot render as a labeled ⎿. Orphans
|
|
445
|
-
// (group finalized before they returned) reroute via showOrphanedComplete.
|
|
446
390
|
const pending = e.toolCallId ? s.pendingToolCompletes.get(e.toolCallId) : undefined;
|
|
447
391
|
if (pending) s.pendingToolCompletes.delete(e.toolCallId!);
|
|
448
|
-
|
|
449
|
-
showOrphanedComplete(e.exitCode, resultDisplay, pending.title, pending.kind, pending.displayDetail);
|
|
450
|
-
} else {
|
|
451
|
-
showToolComplete(e.exitCode, resultDisplay, pending?.displayDetail ?? pending?.title);
|
|
452
|
-
}
|
|
392
|
+
showToolComplete(e.exitCode, resultDisplay, pending?.displayDetail ?? pending?.title);
|
|
453
393
|
s.currentToolKind = undefined;
|
|
454
394
|
s.spinnerStartTime = 0;
|
|
455
395
|
startThinkingSpinner();
|
|
456
396
|
}
|
|
457
397
|
});
|
|
398
|
+
|
|
399
|
+
bus.on("agent:tool-batch-complete", () => {
|
|
400
|
+
if (!shouldRender()) return;
|
|
401
|
+
// Backstop for a group whose members didn't all complete (e.g. errored).
|
|
402
|
+
finalizeAllGroups();
|
|
403
|
+
});
|
|
458
404
|
bus.on("agent:tool-output-chunk", (e) => { if (shouldRender()) writeCommandOutput(e.chunk); });
|
|
459
405
|
bus.on("agent:tool-output", () => { if (shouldRender()) flushCommandOutput(); });
|
|
460
406
|
|
|
@@ -552,7 +498,7 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
552
498
|
}
|
|
553
499
|
|
|
554
500
|
function endAgentResponse(): void {
|
|
555
|
-
|
|
501
|
+
finalizeAllGroups();
|
|
556
502
|
closeToolLine();
|
|
557
503
|
stopCurrentSpinner();
|
|
558
504
|
if (s.renderer) {
|
|
@@ -589,7 +535,7 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
589
535
|
}
|
|
590
536
|
|
|
591
537
|
function writeAgentText(text: string): void {
|
|
592
|
-
|
|
538
|
+
finalizeAllGroups();
|
|
593
539
|
closeToolLine();
|
|
594
540
|
s.hadToolCalls = false;
|
|
595
541
|
if (s.isThinking) {
|
|
@@ -613,7 +559,7 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
613
559
|
flushForRaw();
|
|
614
560
|
contentGap("code");
|
|
615
561
|
if (language) {
|
|
616
|
-
s.renderer!.writeLine(`${p.
|
|
562
|
+
s.renderer!.writeLine(`${p.mdCodeBlockBorder}${language}${p.reset}`);
|
|
617
563
|
}
|
|
618
564
|
let highlighted: string;
|
|
619
565
|
try {
|
|
@@ -633,7 +579,10 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
633
579
|
console.error = origError;
|
|
634
580
|
}
|
|
635
581
|
} catch {
|
|
636
|
-
highlighted = code
|
|
582
|
+
highlighted = code
|
|
583
|
+
.split("\n")
|
|
584
|
+
.map((l) => `${p.mdCodeBlock}${l}${p.reset}`)
|
|
585
|
+
.join("\n");
|
|
637
586
|
}
|
|
638
587
|
const contentWidth = Math.min(90, width - 2);
|
|
639
588
|
for (const line of highlighted.split("\n")) {
|
|
@@ -731,6 +680,7 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
731
680
|
filePath,
|
|
732
681
|
maxLines,
|
|
733
682
|
trueColor: true,
|
|
683
|
+
gutterLine: false,
|
|
734
684
|
});
|
|
735
685
|
const body = diffLines.length > 1 ? ["", ...diffLines.slice(1), ""] : diffLines;
|
|
736
686
|
|
|
@@ -799,15 +749,13 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
799
749
|
displayDetail?: string;
|
|
800
750
|
batchIndex?: number;
|
|
801
751
|
batchTotal?: number;
|
|
802
|
-
groupContinuation?: boolean;
|
|
803
752
|
},
|
|
804
753
|
): void {
|
|
805
754
|
closeToolLine();
|
|
806
755
|
stopCurrentSpinner();
|
|
807
756
|
if (!s.renderer) startAgentResponse();
|
|
808
757
|
showCollapsedThinking();
|
|
809
|
-
|
|
810
|
-
if (!extra?.groupContinuation) contentGap("tool");
|
|
758
|
+
contentGap("tool");
|
|
811
759
|
s.renderer!.flush();
|
|
812
760
|
drain();
|
|
813
761
|
const lines = renderToolCall({
|
|
@@ -820,38 +768,19 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
820
768
|
displayDetail: extra?.displayDetail,
|
|
821
769
|
}, cappedW(), shellCwd);
|
|
822
770
|
|
|
823
|
-
if (extra?.groupContinuation && lines.length > 0) {
|
|
824
|
-
// Swap the colored kind icon for a muted tree connector,
|
|
825
|
-
// and strip the tool name prefix — show detail only.
|
|
826
|
-
const detail = extra.displayDetail || extractDetail(extra);
|
|
827
|
-
const maxW = Math.max(1, cappedW() - 6);
|
|
828
|
-
const text = detail.length > maxW ? detail.slice(0, maxW - 1) + "…" : detail;
|
|
829
|
-
lines[0] = detail
|
|
830
|
-
? `${p.muted}├${p.reset} ${p.dim}${text}${p.reset}`
|
|
831
|
-
: lines[0]!.replace(/^\x1b\[[^m]*m.\x1b\[0m/, `${p.muted}├${p.reset}`);
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
const batchPrefix = "";
|
|
835
|
-
|
|
836
771
|
for (let i = 0; i < lines.length - 1; i++) {
|
|
837
772
|
s.renderer!.writeLine(lines[i]!);
|
|
838
773
|
}
|
|
839
774
|
drain();
|
|
840
775
|
if (lines.length > 0) {
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
s.
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
callId: extra.toolCallId,
|
|
850
|
-
title,
|
|
851
|
-
kind: extra.kind,
|
|
852
|
-
displayDetail: extra.displayDetail ?? extractDetail(extra),
|
|
853
|
-
};
|
|
854
|
-
}
|
|
776
|
+
out().write(` ${lines[lines.length - 1]}`);
|
|
777
|
+
if (extra?.toolCallId) {
|
|
778
|
+
s.openTool = {
|
|
779
|
+
callId: extra.toolCallId,
|
|
780
|
+
title,
|
|
781
|
+
kind: extra.kind,
|
|
782
|
+
displayDetail: extra.displayDetail ?? extractDetail(extra),
|
|
783
|
+
};
|
|
855
784
|
}
|
|
856
785
|
}
|
|
857
786
|
s.hadToolCalls = true;
|
|
@@ -884,32 +813,6 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
884
813
|
if (resultDisplay?.body) renderResultBody(resultDisplay.body);
|
|
885
814
|
}
|
|
886
815
|
|
|
887
|
-
/** Late completion from a finalized group — re-emit the kind header
|
|
888
|
-
* in muted "(cont.)" form so the ⎿ has a legitimate parent, then
|
|
889
|
-
* render the completion as a normal labeled ⎿. Subsequent orphans
|
|
890
|
-
* of the same kind reuse the existing (cont.) header. */
|
|
891
|
-
function showOrphanedComplete(
|
|
892
|
-
exitCode: number | null,
|
|
893
|
-
resultDisplay: ToolResultDisplay | undefined,
|
|
894
|
-
title: string,
|
|
895
|
-
kind: string | undefined,
|
|
896
|
-
displayDetail: string | undefined,
|
|
897
|
-
): void {
|
|
898
|
-
if (s.orphanContHeaderKind !== kind) {
|
|
899
|
-
stopCurrentSpinner();
|
|
900
|
-
closeToolLine();
|
|
901
|
-
flushCommandOutput();
|
|
902
|
-
if (!s.renderer) startAgentResponse();
|
|
903
|
-
showCollapsedThinking();
|
|
904
|
-
const icon = (kind && KIND_ICONS[kind]) ?? "▶";
|
|
905
|
-
const label = kind ?? "tool";
|
|
906
|
-
s.renderer!.writeLine(`${p.muted}${icon} ${label} (cont.)${p.reset}`);
|
|
907
|
-
drain();
|
|
908
|
-
s.orphanContHeaderKind = kind;
|
|
909
|
-
}
|
|
910
|
-
showToolComplete(exitCode, resultDisplay, displayDetail || title);
|
|
911
|
-
}
|
|
912
|
-
|
|
913
816
|
function renderResultBody(body: ToolResultBody): void {
|
|
914
817
|
if (!s.renderer) return;
|
|
915
818
|
const lines: string[] = ctx.call("render:result-body", body, cappedW()) ?? [];
|
|
@@ -957,7 +860,7 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
957
860
|
function closeToolLine(): void {
|
|
958
861
|
if (s.openTool) {
|
|
959
862
|
out().write("\n");
|
|
960
|
-
// Stash identity so the completion renders as ⎿ labeled, not
|
|
863
|
+
// Stash identity so the completion renders as ⎿ labeled, not inline ✓.
|
|
961
864
|
s.pendingToolCompletes.set(s.openTool.callId, {
|
|
962
865
|
title: s.openTool.title,
|
|
963
866
|
kind: s.openTool.kind,
|
|
@@ -967,41 +870,62 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
967
870
|
}
|
|
968
871
|
}
|
|
969
872
|
|
|
970
|
-
/** Render
|
|
971
|
-
* completed
|
|
972
|
-
function
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
if (s.toolGroupCount <= 1 || skipAggregate) {
|
|
982
|
-
s.toolGroupKind = undefined;
|
|
983
|
-
s.toolGroupCount = 0;
|
|
984
|
-
s.toolGroupCompletedCount = 0;
|
|
985
|
-
s.toolGroupRendered = 0;
|
|
986
|
-
s.toolGroupAllOk = true;
|
|
987
|
-
s.toolGroupSummaries = [];
|
|
988
|
-
return;
|
|
989
|
-
}
|
|
873
|
+
/** Render a deferred group as one contiguous block, skipping a group with no
|
|
874
|
+
* completed members (e.g. all errored before their events fired). */
|
|
875
|
+
function finalizeGroup(kind: string): void {
|
|
876
|
+
const group = batchGroups.get(kind);
|
|
877
|
+
if (!group || group.finalized) return;
|
|
878
|
+
group.finalized = true;
|
|
879
|
+
const members = group.order
|
|
880
|
+
.map((id) => group.members.get(id))
|
|
881
|
+
.filter((m): m is GroupMember => !!m && m.done);
|
|
882
|
+
if (members.length === 0) return;
|
|
883
|
+
|
|
990
884
|
stopCurrentSpinner();
|
|
991
885
|
closeToolLine();
|
|
992
886
|
if (!s.renderer) startAgentResponse();
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
);
|
|
997
|
-
|
|
887
|
+
showCollapsedThinking();
|
|
888
|
+
contentGap("tool");
|
|
889
|
+
s.renderer!.flush();
|
|
890
|
+
drain();
|
|
891
|
+
|
|
892
|
+
const icon = KIND_ICONS[kind] ?? "▶";
|
|
893
|
+
const mark = (m: GroupMember): string => ctx.call("tui:render-tool-complete", m.ok ? 0 : 1, "", m.summary);
|
|
894
|
+
|
|
895
|
+
if (members.length === 1) {
|
|
896
|
+
const m = members[0]!;
|
|
897
|
+
s.renderer!.writeLine(`${p.warning}${icon}${p.reset} ${kind} ${p.dim}${m.detail}${p.reset} ${mark(m)}`);
|
|
898
|
+
} else {
|
|
899
|
+
s.renderer!.writeLine(`${p.warning}${icon}${p.reset} ${kind}`);
|
|
900
|
+
const shown = Math.min(members.length, GROUP_MAX_VISIBLE);
|
|
901
|
+
const allShown = members.length <= GROUP_MAX_VISIBLE;
|
|
902
|
+
for (let i = 0; i < shown; i++) {
|
|
903
|
+
const m = members[i]!;
|
|
904
|
+
const connector = allShown && i === shown - 1 ? "└" : "├";
|
|
905
|
+
s.renderer!.writeLine(` ${p.muted}${connector}${p.reset} ${p.dim}${m.detail}${p.reset} ${mark(m)}`);
|
|
906
|
+
}
|
|
907
|
+
if (!allShown) {
|
|
908
|
+
s.renderer!.writeLine(` ${p.muted}└${p.reset} ${p.dim}+${members.length - shown} more${p.reset}`);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
998
911
|
drain();
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function finalizeAllGroups(): void {
|
|
915
|
+
for (const kind of batchGroups.keys()) finalizeGroup(kind);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/** Finalize ready groups in batch order, stopping at the first still in
|
|
919
|
+
* flight — so blocks render in dispatch order, not completion order. */
|
|
920
|
+
function flushReadyGroups(): void {
|
|
921
|
+
for (const [kind, group] of batchGroups) {
|
|
922
|
+
if (group.finalized) continue;
|
|
923
|
+
// Eager (non-groupable) groups never increment `completed`, so one earlier
|
|
924
|
+
// in batch order is a barrier here: a deferred group after it renders at
|
|
925
|
+
// the tool-batch-complete backstop, not incrementally. Order is preserved.
|
|
926
|
+
if (group.completed < group.total) break;
|
|
927
|
+
finalizeGroup(kind);
|
|
928
|
+
}
|
|
1005
929
|
}
|
|
1006
930
|
|
|
1007
931
|
function renderCommandLine(line: string): string {
|
|
@@ -140,6 +140,39 @@ function tokenLcs(
|
|
|
140
140
|
return { oldMatch, newMatch };
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Anchors the shared prefix/suffix as unchanged and runs the O(m·n) LCS only on
|
|
145
|
+
* the differing middle. Null when that middle exceeds maxProduct.
|
|
146
|
+
*/
|
|
147
|
+
function inlineMatches(
|
|
148
|
+
a: Token[],
|
|
149
|
+
b: Token[],
|
|
150
|
+
maxProduct: number,
|
|
151
|
+
): { oldMatch: boolean[]; newMatch: boolean[] } | null {
|
|
152
|
+
const m = a.length;
|
|
153
|
+
const n = b.length;
|
|
154
|
+
let pre = 0;
|
|
155
|
+
while (pre < m && pre < n && a[pre].text === b[pre].text) pre++;
|
|
156
|
+
let suf = 0;
|
|
157
|
+
while (suf < m - pre && suf < n - pre && a[m - 1 - suf].text === b[n - 1 - suf].text) suf++;
|
|
158
|
+
|
|
159
|
+
const midM = m - pre - suf;
|
|
160
|
+
const midN = n - pre - suf;
|
|
161
|
+
if (midM * midN > maxProduct) return null;
|
|
162
|
+
|
|
163
|
+
const oldMatch = new Array<boolean>(m).fill(false);
|
|
164
|
+
const newMatch = new Array<boolean>(n).fill(false);
|
|
165
|
+
for (let i = 0; i < pre; i++) { oldMatch[i] = true; newMatch[i] = true; }
|
|
166
|
+
for (let i = 0; i < suf; i++) { oldMatch[m - 1 - i] = true; newMatch[n - 1 - i] = true; }
|
|
167
|
+
|
|
168
|
+
if (midM > 0 && midN > 0) {
|
|
169
|
+
const mid = tokenLcs(a.slice(pre, m - suf), b.slice(pre, n - suf));
|
|
170
|
+
for (let i = 0; i < midM; i++) oldMatch[pre + i] = mid.oldMatch[i];
|
|
171
|
+
for (let j = 0; j < midN; j++) newMatch[pre + j] = mid.newMatch[j];
|
|
172
|
+
}
|
|
173
|
+
return { oldMatch, newMatch };
|
|
174
|
+
}
|
|
175
|
+
|
|
143
176
|
/**
|
|
144
177
|
* Rewrite full ANSI resets (\x1b[0m) to foreground-only resets,
|
|
145
178
|
* preserving the given background color across the line.
|
|
@@ -193,15 +226,14 @@ function highlightInlineChanges(
|
|
|
193
226
|
};
|
|
194
227
|
}
|
|
195
228
|
|
|
196
|
-
|
|
197
|
-
if (
|
|
229
|
+
const matches = inlineMatches(oldTokens, newTokens, 1_000_000);
|
|
230
|
+
if (!matches) {
|
|
198
231
|
return {
|
|
199
232
|
old: language ? highlightLine(oldLine, language) : oldLine,
|
|
200
233
|
new: language ? highlightLine(newLine, language) : newLine,
|
|
201
234
|
};
|
|
202
235
|
}
|
|
203
|
-
|
|
204
|
-
const { oldMatch, newMatch } = tokenLcs(oldTokens, newTokens);
|
|
236
|
+
const { oldMatch, newMatch } = matches;
|
|
205
237
|
|
|
206
238
|
const buildHighlighted = (
|
|
207
239
|
tokens: Token[],
|
|
@@ -340,12 +372,19 @@ function renderUnifiedHunk(hunk: DiffHunk, layout: UnifiedLayout): string[] {
|
|
|
340
372
|
const bgWidth = Math.max(1, textWidth - noW - 3);
|
|
341
373
|
const gutter = (n: string): string => `${p.dim}${n} │${p.reset} `;
|
|
342
374
|
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
375
|
+
const continuationNo = " ".repeat(noW);
|
|
376
|
+
|
|
377
|
+
// Wrapped rows after the first blank the line number and sigil.
|
|
378
|
+
const change = (no: string, sigil: string, bg: string, fg: string, text: string): string[] => {
|
|
379
|
+
return wrapLine(text, lineTextW).map((seg, r) => {
|
|
380
|
+
const n = r === 0 ? no : continuationNo;
|
|
381
|
+
const sg = r === 0 ? sigil : " ";
|
|
382
|
+
if (!gutterLine) {
|
|
383
|
+
return `${bg}${padToWidth(`${fg}${n} ${sg}${p.diffText} ${preserveBg(seg, bg)}`, textWidth)}${p.reset}`;
|
|
384
|
+
}
|
|
385
|
+
if (useTrueColor) return gutter(n) + padToWidth(`${bg}${fg}${sg}${p.diffText} ${preserveBg(seg, bg)}`, bgWidth) + p.reset;
|
|
386
|
+
return `${gutter(n)}${fg}${sg} ${seg}${p.reset}`;
|
|
387
|
+
});
|
|
349
388
|
};
|
|
350
389
|
|
|
351
390
|
const hlCache = new Map<ChangePair, { old: string; new: string }>();
|
|
@@ -367,31 +406,25 @@ function renderUnifiedHunk(hunk: DiffHunk, layout: UnifiedLayout): string[] {
|
|
|
367
406
|
).padStart(noW);
|
|
368
407
|
|
|
369
408
|
if (line.type === "context") {
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
409
|
+
const text = lang ? highlightLine(line.text, lang) : line.text;
|
|
410
|
+
wrapLine(text, lineTextW).forEach((seg, r) => {
|
|
411
|
+
const n = r === 0 ? no : continuationNo;
|
|
412
|
+
out.push(!gutterLine ? `${p.dim}${n}${p.reset} ${seg}` : `${gutter(n)} ${p.dim}${seg}${p.reset}`);
|
|
413
|
+
});
|
|
373
414
|
continue;
|
|
374
415
|
}
|
|
375
416
|
|
|
376
417
|
const pair = pairs.get(i);
|
|
377
418
|
if (line.type === "removed") {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
const raw = truncateText(line.text, lineTextW);
|
|
383
|
-
removedText = lang ? highlightLine(raw, lang) : raw;
|
|
384
|
-
}
|
|
385
|
-
out.push(change(no, "-", p.errorBg, p.error, removedText));
|
|
419
|
+
const removedText = pair
|
|
420
|
+
? highlightedPair(pair).old
|
|
421
|
+
: (lang ? highlightLine(line.text, lang) : line.text);
|
|
422
|
+
out.push(...change(no, "-", p.errorBg, p.error, removedText));
|
|
386
423
|
} else {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
const raw = truncateText(line.text, lineTextW);
|
|
392
|
-
addedText = lang ? highlightLine(raw, lang) : raw;
|
|
393
|
-
}
|
|
394
|
-
out.push(change(no, "+", p.successBg, p.success, addedText));
|
|
424
|
+
const addedText = pair
|
|
425
|
+
? highlightedPair(pair).new
|
|
426
|
+
: (lang ? highlightLine(line.text, lang) : line.text);
|
|
427
|
+
out.push(...change(no, "+", p.successBg, p.success, addedText));
|
|
395
428
|
}
|
|
396
429
|
}
|
|
397
430
|
return out;
|