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
|
@@ -50,7 +50,6 @@ function createRenderState() {
|
|
|
50
50
|
spinnerStartTime: 0,
|
|
51
51
|
openTool: null,
|
|
52
52
|
pendingToolCompletes: new Map(),
|
|
53
|
-
orphanContHeaderKind: undefined,
|
|
54
53
|
currentToolKind: undefined,
|
|
55
54
|
toolStartTime: 0,
|
|
56
55
|
toolExitCode: null,
|
|
@@ -58,12 +57,6 @@ function createRenderState() {
|
|
|
58
57
|
commandOutputLineCount: 0,
|
|
59
58
|
commandOutputOverflow: 0,
|
|
60
59
|
commandOverflowLines: [],
|
|
61
|
-
toolGroupKind: undefined,
|
|
62
|
-
toolGroupCount: 0,
|
|
63
|
-
toolGroupCompletedCount: 0,
|
|
64
|
-
toolGroupAllOk: true,
|
|
65
|
-
toolGroupRendered: 0,
|
|
66
|
-
toolGroupSummaries: [],
|
|
67
60
|
isThinking: false,
|
|
68
61
|
showThinkingText: false,
|
|
69
62
|
thinkingPending: false,
|
|
@@ -113,15 +106,6 @@ export default function activate(ctx) {
|
|
|
113
106
|
return `${p.success}✓${p.reset}${summaryStr}${timer}`;
|
|
114
107
|
return `${p.error}✗ exit ${exitCode}${p.reset}${summaryStr}${timer}`;
|
|
115
108
|
});
|
|
116
|
-
define("tui:render-tool-group-summary", (count, rendered, allOk, summaries) => {
|
|
117
|
-
const mark = allOk ? `${p.success}✓${p.reset}` : `${p.error}✗${p.reset}`;
|
|
118
|
-
const summaryStr = summaries.length > 0 ? ` ${p.dim}${summaries.join(", ")}${p.reset}` : "";
|
|
119
|
-
const collapsed = count - rendered;
|
|
120
|
-
if (collapsed > 0) {
|
|
121
|
-
return ` ${p.muted}└${p.reset} ${p.dim}+${collapsed} more${p.reset} ${mark}${summaryStr}`;
|
|
122
|
-
}
|
|
123
|
-
return ` ${p.muted}└${p.reset} ${mark}${summaryStr}`;
|
|
124
|
-
});
|
|
125
109
|
define("tui:render-command-output", (line, _kind) => `${p.dim} ${line}${p.reset}`);
|
|
126
110
|
define("tui:render-spinner", (label, frame, elapsed, hint) => {
|
|
127
111
|
const timer = elapsed ? ` ${p.dim}${elapsed}${p.reset}` : "";
|
|
@@ -245,33 +229,35 @@ export default function activate(ctx) {
|
|
|
245
229
|
const GROUPABLE_KINDS = new Set(["read", "search"]);
|
|
246
230
|
const GROUP_MAX_VISIBLE = 5;
|
|
247
231
|
const KIND_ICONS = { read: "◆", search: "⌕" };
|
|
248
|
-
// Batch groups: kind → { total, rendered, headerShown }
|
|
249
232
|
let batchGroups = new Map();
|
|
233
|
+
let batchSize = 0;
|
|
234
|
+
/** A lone tool has nothing to interleave with, so deferral applies only to
|
|
235
|
+
* read-only kinds in a multi-tool batch. */
|
|
236
|
+
function isDeferred(kind) {
|
|
237
|
+
return batchSize > 1 && GROUPABLE_KINDS.has(kind) && batchGroups.has(kind);
|
|
238
|
+
}
|
|
250
239
|
bus.on("agent:tool-batch", (e) => {
|
|
251
240
|
if (!shouldRender())
|
|
252
241
|
return;
|
|
253
242
|
fencedTransform.flush();
|
|
254
|
-
|
|
255
|
-
|
|
243
|
+
finalizeAllGroups();
|
|
244
|
+
closeToolLine();
|
|
256
245
|
batchGroups = new Map();
|
|
246
|
+
batchSize = 0;
|
|
257
247
|
for (const group of e.groups) {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
rendered: 0,
|
|
261
|
-
headerShown: false,
|
|
262
|
-
});
|
|
248
|
+
batchSize += group.tools.length;
|
|
249
|
+
batchGroups.set(group.kind, { total: group.tools.length, completed: 0, finalized: false, order: [], members: new Map() });
|
|
263
250
|
}
|
|
264
251
|
});
|
|
265
252
|
bus.on("agent:tool-started", (e) => {
|
|
266
253
|
if (!shouldRender())
|
|
267
254
|
return;
|
|
268
255
|
fencedTransform.flush();
|
|
269
|
-
stopCurrentSpinner();
|
|
270
256
|
s.currentToolKind = e.kind;
|
|
271
257
|
s.toolStartTime = Date.now();
|
|
272
|
-
s.orphanContHeaderKind = undefined;
|
|
273
258
|
if (e.title === "user_shell") {
|
|
274
|
-
|
|
259
|
+
stopCurrentSpinner();
|
|
260
|
+
finalizeAllGroups();
|
|
275
261
|
closeToolLine();
|
|
276
262
|
if (!s.renderer)
|
|
277
263
|
startAgentResponse();
|
|
@@ -284,89 +270,57 @@ export default function activate(ctx) {
|
|
|
284
270
|
return;
|
|
285
271
|
}
|
|
286
272
|
const kind = e.kind ?? "execute";
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
showCollapsedThinking();
|
|
297
|
-
contentGap("tool");
|
|
298
|
-
s.renderer.flush();
|
|
299
|
-
drain();
|
|
300
|
-
const icon = KIND_ICONS[kind] ?? "▶";
|
|
301
|
-
s.renderer.writeLine(`${p.warning}${icon}${p.reset} ${kind}`);
|
|
302
|
-
drain();
|
|
303
|
-
group.headerShown = true;
|
|
304
|
-
s.toolGroupKind = kind;
|
|
305
|
-
s.toolGroupCount = 0;
|
|
306
|
-
s.toolGroupCompletedCount = 0;
|
|
307
|
-
s.toolGroupRendered = 0;
|
|
308
|
-
s.toolGroupAllOk = true;
|
|
309
|
-
s.toolGroupSummaries = [];
|
|
310
|
-
}
|
|
311
|
-
s.toolGroupCount++;
|
|
312
|
-
if (s.toolGroupRendered < GROUP_MAX_VISIBLE) {
|
|
313
|
-
showToolCall(e.title, "", { ...e, groupContinuation: true });
|
|
314
|
-
s.toolGroupRendered++;
|
|
315
|
-
}
|
|
316
|
-
if (e.toolCallId) {
|
|
317
|
-
s.pendingToolCompletes.set(e.toolCallId, {
|
|
318
|
-
title: e.title,
|
|
319
|
-
kind,
|
|
320
|
-
displayDetail: e.displayDetail ?? extractDetail(e),
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
else {
|
|
325
|
-
// Standalone tool — single in its batch kind, or not groupable
|
|
326
|
-
finalizeToolGroup();
|
|
327
|
-
showToolCall(e.title, "", { ...e });
|
|
273
|
+
if (isDeferred(kind)) {
|
|
274
|
+
const group = batchGroups.get(kind);
|
|
275
|
+
const id = e.toolCallId ?? `${kind}-${group.order.length}`;
|
|
276
|
+
group.order.push(id);
|
|
277
|
+
group.members.set(id, { detail: e.displayDetail ?? extractDetail(e), ok: true, done: false });
|
|
278
|
+
s.hadToolCalls = true;
|
|
279
|
+
if (!s.spinner)
|
|
280
|
+
startThinkingSpinner(); // nothing's drawn yet — stand in until the block renders
|
|
281
|
+
return;
|
|
328
282
|
}
|
|
283
|
+
// Eager: a single-tool turn, or a sequential mutating/streaming tool.
|
|
284
|
+
stopCurrentSpinner();
|
|
285
|
+
showToolCall(e.title, "", { ...e });
|
|
329
286
|
});
|
|
330
287
|
bus.on("agent:tool-completed", (e) => {
|
|
331
288
|
if (!shouldRender())
|
|
332
289
|
return;
|
|
333
290
|
s.toolExitCode = e.exitCode;
|
|
334
|
-
if (e.exitCode !== 0)
|
|
335
|
-
s.toolGroupAllOk = false;
|
|
336
291
|
const resultDisplay = e.resultDisplay;
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
if (
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
// Finalize as soon as all members return so aggregate lands right
|
|
347
|
-
// after its children, not below out-of-band renders from the next tool.
|
|
348
|
-
const batchGroup = batchGroups.get(s.toolGroupKind);
|
|
349
|
-
if (batchGroup && s.toolGroupCompletedCount >= batchGroup.total) {
|
|
350
|
-
finalizeToolGroup();
|
|
292
|
+
const kind = e.kind ?? "execute";
|
|
293
|
+
const group = batchGroups.get(kind);
|
|
294
|
+
if (isDeferred(kind) && group) {
|
|
295
|
+
const id = e.toolCallId ?? group.order[group.completed];
|
|
296
|
+
const member = id ? group.members.get(id) : undefined;
|
|
297
|
+
if (member) {
|
|
298
|
+
member.done = true;
|
|
299
|
+
member.ok = e.exitCode === 0;
|
|
300
|
+
member.summary = resultDisplay?.summary;
|
|
351
301
|
}
|
|
302
|
+
group.completed++;
|
|
303
|
+
s.currentToolKind = undefined;
|
|
304
|
+
flushReadyGroups();
|
|
305
|
+
if (!s.spinner)
|
|
306
|
+
startThinkingSpinner(); // keep the spinner up for in-flight siblings
|
|
352
307
|
}
|
|
353
308
|
else {
|
|
354
|
-
// Tools that lost the inline slot render as a labeled ⎿. Orphans
|
|
355
|
-
// (group finalized before they returned) reroute via showOrphanedComplete.
|
|
356
309
|
const pending = e.toolCallId ? s.pendingToolCompletes.get(e.toolCallId) : undefined;
|
|
357
310
|
if (pending)
|
|
358
311
|
s.pendingToolCompletes.delete(e.toolCallId);
|
|
359
|
-
|
|
360
|
-
showOrphanedComplete(e.exitCode, resultDisplay, pending.title, pending.kind, pending.displayDetail);
|
|
361
|
-
}
|
|
362
|
-
else {
|
|
363
|
-
showToolComplete(e.exitCode, resultDisplay, pending?.displayDetail ?? pending?.title);
|
|
364
|
-
}
|
|
312
|
+
showToolComplete(e.exitCode, resultDisplay, pending?.displayDetail ?? pending?.title);
|
|
365
313
|
s.currentToolKind = undefined;
|
|
366
314
|
s.spinnerStartTime = 0;
|
|
367
315
|
startThinkingSpinner();
|
|
368
316
|
}
|
|
369
317
|
});
|
|
318
|
+
bus.on("agent:tool-batch-complete", () => {
|
|
319
|
+
if (!shouldRender())
|
|
320
|
+
return;
|
|
321
|
+
// Backstop for a group whose members didn't all complete (e.g. errored).
|
|
322
|
+
finalizeAllGroups();
|
|
323
|
+
});
|
|
370
324
|
bus.on("agent:tool-output-chunk", (e) => { if (shouldRender())
|
|
371
325
|
writeCommandOutput(e.chunk); });
|
|
372
326
|
bus.on("agent:tool-output", () => { if (shouldRender())
|
|
@@ -465,7 +419,7 @@ export default function activate(ctx) {
|
|
|
465
419
|
}
|
|
466
420
|
}
|
|
467
421
|
function endAgentResponse() {
|
|
468
|
-
|
|
422
|
+
finalizeAllGroups();
|
|
469
423
|
closeToolLine();
|
|
470
424
|
stopCurrentSpinner();
|
|
471
425
|
if (s.renderer) {
|
|
@@ -502,7 +456,7 @@ export default function activate(ctx) {
|
|
|
502
456
|
}
|
|
503
457
|
}
|
|
504
458
|
function writeAgentText(text) {
|
|
505
|
-
|
|
459
|
+
finalizeAllGroups();
|
|
506
460
|
closeToolLine();
|
|
507
461
|
s.hadToolCalls = false;
|
|
508
462
|
if (s.isThinking) {
|
|
@@ -526,7 +480,7 @@ export default function activate(ctx) {
|
|
|
526
480
|
flushForRaw();
|
|
527
481
|
contentGap("code");
|
|
528
482
|
if (language) {
|
|
529
|
-
s.renderer.writeLine(`${p.
|
|
483
|
+
s.renderer.writeLine(`${p.mdCodeBlockBorder}${language}${p.reset}`);
|
|
530
484
|
}
|
|
531
485
|
let highlighted;
|
|
532
486
|
try {
|
|
@@ -549,7 +503,10 @@ export default function activate(ctx) {
|
|
|
549
503
|
}
|
|
550
504
|
}
|
|
551
505
|
catch {
|
|
552
|
-
highlighted = code
|
|
506
|
+
highlighted = code
|
|
507
|
+
.split("\n")
|
|
508
|
+
.map((l) => `${p.mdCodeBlock}${l}${p.reset}`)
|
|
509
|
+
.join("\n");
|
|
553
510
|
}
|
|
554
511
|
const contentWidth = Math.min(90, width - 2);
|
|
555
512
|
for (const line of highlighted.split("\n")) {
|
|
@@ -644,6 +601,7 @@ export default function activate(ctx) {
|
|
|
644
601
|
filePath,
|
|
645
602
|
maxLines,
|
|
646
603
|
trueColor: true,
|
|
604
|
+
gutterLine: false,
|
|
647
605
|
});
|
|
648
606
|
const body = diffLines.length > 1 ? ["", ...diffLines.slice(1), ""] : diffLines;
|
|
649
607
|
return renderBoxFrame(body, {
|
|
@@ -710,9 +668,7 @@ export default function activate(ctx) {
|
|
|
710
668
|
if (!s.renderer)
|
|
711
669
|
startAgentResponse();
|
|
712
670
|
showCollapsedThinking();
|
|
713
|
-
|
|
714
|
-
if (!extra?.groupContinuation)
|
|
715
|
-
contentGap("tool");
|
|
671
|
+
contentGap("tool");
|
|
716
672
|
s.renderer.flush();
|
|
717
673
|
drain();
|
|
718
674
|
const lines = renderToolCall({
|
|
@@ -724,37 +680,19 @@ export default function activate(ctx) {
|
|
|
724
680
|
rawInput: extra?.rawInput,
|
|
725
681
|
displayDetail: extra?.displayDetail,
|
|
726
682
|
}, cappedW(), shellCwd);
|
|
727
|
-
if (extra?.groupContinuation && lines.length > 0) {
|
|
728
|
-
// Swap the colored kind icon for a muted tree connector,
|
|
729
|
-
// and strip the tool name prefix — show detail only.
|
|
730
|
-
const detail = extra.displayDetail || extractDetail(extra);
|
|
731
|
-
const maxW = Math.max(1, cappedW() - 6);
|
|
732
|
-
const text = detail.length > maxW ? detail.slice(0, maxW - 1) + "…" : detail;
|
|
733
|
-
lines[0] = detail
|
|
734
|
-
? `${p.muted}├${p.reset} ${p.dim}${text}${p.reset}`
|
|
735
|
-
: lines[0].replace(/^\x1b\[[^m]*m.\x1b\[0m/, `${p.muted}├${p.reset}`);
|
|
736
|
-
}
|
|
737
|
-
const batchPrefix = "";
|
|
738
683
|
for (let i = 0; i < lines.length - 1; i++) {
|
|
739
684
|
s.renderer.writeLine(lines[i]);
|
|
740
685
|
}
|
|
741
686
|
drain();
|
|
742
687
|
if (lines.length > 0) {
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
s.
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
s.openTool = {
|
|
752
|
-
callId: extra.toolCallId,
|
|
753
|
-
title,
|
|
754
|
-
kind: extra.kind,
|
|
755
|
-
displayDetail: extra.displayDetail ?? extractDetail(extra),
|
|
756
|
-
};
|
|
757
|
-
}
|
|
688
|
+
out().write(` ${lines[lines.length - 1]}`);
|
|
689
|
+
if (extra?.toolCallId) {
|
|
690
|
+
s.openTool = {
|
|
691
|
+
callId: extra.toolCallId,
|
|
692
|
+
title,
|
|
693
|
+
kind: extra.kind,
|
|
694
|
+
displayDetail: extra.displayDetail ?? extractDetail(extra),
|
|
695
|
+
};
|
|
758
696
|
}
|
|
759
697
|
}
|
|
760
698
|
s.hadToolCalls = true;
|
|
@@ -782,26 +720,6 @@ export default function activate(ctx) {
|
|
|
782
720
|
if (resultDisplay?.body)
|
|
783
721
|
renderResultBody(resultDisplay.body);
|
|
784
722
|
}
|
|
785
|
-
/** Late completion from a finalized group — re-emit the kind header
|
|
786
|
-
* in muted "(cont.)" form so the ⎿ has a legitimate parent, then
|
|
787
|
-
* render the completion as a normal labeled ⎿. Subsequent orphans
|
|
788
|
-
* of the same kind reuse the existing (cont.) header. */
|
|
789
|
-
function showOrphanedComplete(exitCode, resultDisplay, title, kind, displayDetail) {
|
|
790
|
-
if (s.orphanContHeaderKind !== kind) {
|
|
791
|
-
stopCurrentSpinner();
|
|
792
|
-
closeToolLine();
|
|
793
|
-
flushCommandOutput();
|
|
794
|
-
if (!s.renderer)
|
|
795
|
-
startAgentResponse();
|
|
796
|
-
showCollapsedThinking();
|
|
797
|
-
const icon = (kind && KIND_ICONS[kind]) ?? "▶";
|
|
798
|
-
const label = kind ?? "tool";
|
|
799
|
-
s.renderer.writeLine(`${p.muted}${icon} ${label} (cont.)${p.reset}`);
|
|
800
|
-
drain();
|
|
801
|
-
s.orphanContHeaderKind = kind;
|
|
802
|
-
}
|
|
803
|
-
showToolComplete(exitCode, resultDisplay, displayDetail || title);
|
|
804
|
-
}
|
|
805
723
|
function renderResultBody(body) {
|
|
806
724
|
if (!s.renderer)
|
|
807
725
|
return;
|
|
@@ -848,7 +766,7 @@ export default function activate(ctx) {
|
|
|
848
766
|
function closeToolLine() {
|
|
849
767
|
if (s.openTool) {
|
|
850
768
|
out().write("\n");
|
|
851
|
-
// Stash identity so the completion renders as ⎿ labeled, not
|
|
769
|
+
// Stash identity so the completion renders as ⎿ labeled, not inline ✓.
|
|
852
770
|
s.pendingToolCompletes.set(s.openTool.callId, {
|
|
853
771
|
title: s.openTool.title,
|
|
854
772
|
kind: s.openTool.kind,
|
|
@@ -857,40 +775,64 @@ export default function activate(ctx) {
|
|
|
857
775
|
s.openTool = null;
|
|
858
776
|
}
|
|
859
777
|
}
|
|
860
|
-
/** Render
|
|
861
|
-
* completed
|
|
862
|
-
function
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
const skipAggregate = s.toolGroupCount > 1 && s.toolGroupCompletedCount === 0;
|
|
872
|
-
if (s.toolGroupCount <= 1 || skipAggregate) {
|
|
873
|
-
s.toolGroupKind = undefined;
|
|
874
|
-
s.toolGroupCount = 0;
|
|
875
|
-
s.toolGroupCompletedCount = 0;
|
|
876
|
-
s.toolGroupRendered = 0;
|
|
877
|
-
s.toolGroupAllOk = true;
|
|
878
|
-
s.toolGroupSummaries = [];
|
|
778
|
+
/** Render a deferred group as one contiguous block, skipping a group with no
|
|
779
|
+
* completed members (e.g. all errored before their events fired). */
|
|
780
|
+
function finalizeGroup(kind) {
|
|
781
|
+
const group = batchGroups.get(kind);
|
|
782
|
+
if (!group || group.finalized)
|
|
783
|
+
return;
|
|
784
|
+
group.finalized = true;
|
|
785
|
+
const members = group.order
|
|
786
|
+
.map((id) => group.members.get(id))
|
|
787
|
+
.filter((m) => !!m && m.done);
|
|
788
|
+
if (members.length === 0)
|
|
879
789
|
return;
|
|
880
|
-
}
|
|
881
790
|
stopCurrentSpinner();
|
|
882
791
|
closeToolLine();
|
|
883
792
|
if (!s.renderer)
|
|
884
793
|
startAgentResponse();
|
|
885
|
-
|
|
886
|
-
|
|
794
|
+
showCollapsedThinking();
|
|
795
|
+
contentGap("tool");
|
|
796
|
+
s.renderer.flush();
|
|
887
797
|
drain();
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
798
|
+
const icon = KIND_ICONS[kind] ?? "▶";
|
|
799
|
+
const mark = (m) => ctx.call("tui:render-tool-complete", m.ok ? 0 : 1, "", m.summary);
|
|
800
|
+
if (members.length === 1) {
|
|
801
|
+
const m = members[0];
|
|
802
|
+
s.renderer.writeLine(`${p.warning}${icon}${p.reset} ${kind} ${p.dim}${m.detail}${p.reset} ${mark(m)}`);
|
|
803
|
+
}
|
|
804
|
+
else {
|
|
805
|
+
s.renderer.writeLine(`${p.warning}${icon}${p.reset} ${kind}`);
|
|
806
|
+
const shown = Math.min(members.length, GROUP_MAX_VISIBLE);
|
|
807
|
+
const allShown = members.length <= GROUP_MAX_VISIBLE;
|
|
808
|
+
for (let i = 0; i < shown; i++) {
|
|
809
|
+
const m = members[i];
|
|
810
|
+
const connector = allShown && i === shown - 1 ? "└" : "├";
|
|
811
|
+
s.renderer.writeLine(` ${p.muted}${connector}${p.reset} ${p.dim}${m.detail}${p.reset} ${mark(m)}`);
|
|
812
|
+
}
|
|
813
|
+
if (!allShown) {
|
|
814
|
+
s.renderer.writeLine(` ${p.muted}└${p.reset} ${p.dim}+${members.length - shown} more${p.reset}`);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
drain();
|
|
818
|
+
}
|
|
819
|
+
function finalizeAllGroups() {
|
|
820
|
+
for (const kind of batchGroups.keys())
|
|
821
|
+
finalizeGroup(kind);
|
|
822
|
+
}
|
|
823
|
+
/** Finalize ready groups in batch order, stopping at the first still in
|
|
824
|
+
* flight — so blocks render in dispatch order, not completion order. */
|
|
825
|
+
function flushReadyGroups() {
|
|
826
|
+
for (const [kind, group] of batchGroups) {
|
|
827
|
+
if (group.finalized)
|
|
828
|
+
continue;
|
|
829
|
+
// Eager (non-groupable) groups never increment `completed`, so one earlier
|
|
830
|
+
// in batch order is a barrier here: a deferred group after it renders at
|
|
831
|
+
// the tool-batch-complete backstop, not incrementally. Order is preserved.
|
|
832
|
+
if (group.completed < group.total)
|
|
833
|
+
break;
|
|
834
|
+
finalizeGroup(kind);
|
|
835
|
+
}
|
|
894
836
|
}
|
|
895
837
|
function renderCommandLine(line) {
|
|
896
838
|
return ctx.call("tui:render-command-output", line, s.currentToolKind);
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { highlight } from "cli-highlight";
|
|
9
9
|
import { visibleLen, charWidth } from "./ansi.js";
|
|
10
10
|
import { palette as p } from "./palette.js";
|
|
11
|
+
import { wrapLine } from "./markdown.js";
|
|
11
12
|
// ── Constants ────────────────────────────────────────────────────
|
|
12
13
|
const SPLIT_MIN_WIDTH = 120;
|
|
13
14
|
const UNIFIED_MIN_WIDTH = 40;
|
|
@@ -102,6 +103,42 @@ function tokenLcs(a, b) {
|
|
|
102
103
|
}
|
|
103
104
|
return { oldMatch, newMatch };
|
|
104
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Anchors the shared prefix/suffix as unchanged and runs the O(m·n) LCS only on
|
|
108
|
+
* the differing middle. Null when that middle exceeds maxProduct.
|
|
109
|
+
*/
|
|
110
|
+
function inlineMatches(a, b, maxProduct) {
|
|
111
|
+
const m = a.length;
|
|
112
|
+
const n = b.length;
|
|
113
|
+
let pre = 0;
|
|
114
|
+
while (pre < m && pre < n && a[pre].text === b[pre].text)
|
|
115
|
+
pre++;
|
|
116
|
+
let suf = 0;
|
|
117
|
+
while (suf < m - pre && suf < n - pre && a[m - 1 - suf].text === b[n - 1 - suf].text)
|
|
118
|
+
suf++;
|
|
119
|
+
const midM = m - pre - suf;
|
|
120
|
+
const midN = n - pre - suf;
|
|
121
|
+
if (midM * midN > maxProduct)
|
|
122
|
+
return null;
|
|
123
|
+
const oldMatch = new Array(m).fill(false);
|
|
124
|
+
const newMatch = new Array(n).fill(false);
|
|
125
|
+
for (let i = 0; i < pre; i++) {
|
|
126
|
+
oldMatch[i] = true;
|
|
127
|
+
newMatch[i] = true;
|
|
128
|
+
}
|
|
129
|
+
for (let i = 0; i < suf; i++) {
|
|
130
|
+
oldMatch[m - 1 - i] = true;
|
|
131
|
+
newMatch[n - 1 - i] = true;
|
|
132
|
+
}
|
|
133
|
+
if (midM > 0 && midN > 0) {
|
|
134
|
+
const mid = tokenLcs(a.slice(pre, m - suf), b.slice(pre, n - suf));
|
|
135
|
+
for (let i = 0; i < midM; i++)
|
|
136
|
+
oldMatch[pre + i] = mid.oldMatch[i];
|
|
137
|
+
for (let j = 0; j < midN; j++)
|
|
138
|
+
newMatch[pre + j] = mid.newMatch[j];
|
|
139
|
+
}
|
|
140
|
+
return { oldMatch, newMatch };
|
|
141
|
+
}
|
|
105
142
|
/**
|
|
106
143
|
* Rewrite full ANSI resets (\x1b[0m) to foreground-only resets,
|
|
107
144
|
* preserving the given background color across the line.
|
|
@@ -139,14 +176,14 @@ function highlightInlineChanges(oldLine, newLine, oldPalette, newPalette, useTru
|
|
|
139
176
|
new: language ? highlightLine(newLine, language) : newLine,
|
|
140
177
|
};
|
|
141
178
|
}
|
|
142
|
-
|
|
143
|
-
if (
|
|
179
|
+
const matches = inlineMatches(oldTokens, newTokens, 1_000_000);
|
|
180
|
+
if (!matches) {
|
|
144
181
|
return {
|
|
145
182
|
old: language ? highlightLine(oldLine, language) : oldLine,
|
|
146
183
|
new: language ? highlightLine(newLine, language) : newLine,
|
|
147
184
|
};
|
|
148
185
|
}
|
|
149
|
-
const { oldMatch, newMatch } =
|
|
186
|
+
const { oldMatch, newMatch } = matches;
|
|
150
187
|
const buildHighlighted = (tokens, matched, palette) => {
|
|
151
188
|
let result = "";
|
|
152
189
|
for (let i = 0; i < tokens.length; i++) {
|
|
@@ -248,13 +285,19 @@ function renderUnifiedHunk(hunk, layout) {
|
|
|
248
285
|
const pairs = findChangePairs(hunk);
|
|
249
286
|
const bgWidth = Math.max(1, textWidth - noW - 3);
|
|
250
287
|
const gutter = (n) => `${p.dim}${n} │${p.reset} `;
|
|
288
|
+
const continuationNo = " ".repeat(noW);
|
|
289
|
+
// Wrapped rows after the first blank the line number and sigil.
|
|
251
290
|
const change = (no, sigil, bg, fg, text) => {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
291
|
+
return wrapLine(text, lineTextW).map((seg, r) => {
|
|
292
|
+
const n = r === 0 ? no : continuationNo;
|
|
293
|
+
const sg = r === 0 ? sigil : " ";
|
|
294
|
+
if (!gutterLine) {
|
|
295
|
+
return `${bg}${padToWidth(`${fg}${n} ${sg}${p.diffText} ${preserveBg(seg, bg)}`, textWidth)}${p.reset}`;
|
|
296
|
+
}
|
|
297
|
+
if (useTrueColor)
|
|
298
|
+
return gutter(n) + padToWidth(`${bg}${fg}${sg}${p.diffText} ${preserveBg(seg, bg)}`, bgWidth) + p.reset;
|
|
299
|
+
return `${gutter(n)}${fg}${sg} ${seg}${p.reset}`;
|
|
300
|
+
});
|
|
258
301
|
};
|
|
259
302
|
const hlCache = new Map();
|
|
260
303
|
const highlightedPair = (pair) => {
|
|
@@ -269,33 +312,25 @@ function renderUnifiedHunk(hunk, layout) {
|
|
|
269
312
|
const line = hunk.lines[i];
|
|
270
313
|
const no = String(line.type === "removed" ? (line.oldNo ?? "") : (line.newNo ?? line.oldNo ?? "")).padStart(noW);
|
|
271
314
|
if (line.type === "context") {
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
315
|
+
const text = lang ? highlightLine(line.text, lang) : line.text;
|
|
316
|
+
wrapLine(text, lineTextW).forEach((seg, r) => {
|
|
317
|
+
const n = r === 0 ? no : continuationNo;
|
|
318
|
+
out.push(!gutterLine ? `${p.dim}${n}${p.reset} ${seg}` : `${gutter(n)} ${p.dim}${seg}${p.reset}`);
|
|
319
|
+
});
|
|
275
320
|
continue;
|
|
276
321
|
}
|
|
277
322
|
const pair = pairs.get(i);
|
|
278
323
|
if (line.type === "removed") {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
else {
|
|
284
|
-
const raw = truncateText(line.text, lineTextW);
|
|
285
|
-
removedText = lang ? highlightLine(raw, lang) : raw;
|
|
286
|
-
}
|
|
287
|
-
out.push(change(no, "-", p.errorBg, p.error, removedText));
|
|
324
|
+
const removedText = pair
|
|
325
|
+
? highlightedPair(pair).old
|
|
326
|
+
: (lang ? highlightLine(line.text, lang) : line.text);
|
|
327
|
+
out.push(...change(no, "-", p.errorBg, p.error, removedText));
|
|
288
328
|
}
|
|
289
329
|
else {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
else {
|
|
295
|
-
const raw = truncateText(line.text, lineTextW);
|
|
296
|
-
addedText = lang ? highlightLine(raw, lang) : raw;
|
|
297
|
-
}
|
|
298
|
-
out.push(change(no, "+", p.successBg, p.success, addedText));
|
|
330
|
+
const addedText = pair
|
|
331
|
+
? highlightedPair(pair).new
|
|
332
|
+
: (lang ? highlightLine(line.text, lang) : line.text);
|
|
333
|
+
out.push(...change(no, "+", p.successBg, p.success, addedText));
|
|
299
334
|
}
|
|
300
335
|
}
|
|
301
336
|
return out;
|
package/dist/utils/executor.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn, spawnSync } from "node:child_process";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
+
import { StringDecoder } from "node:string_decoder";
|
|
3
4
|
import { stripAnsi } from "./ansi.js";
|
|
4
5
|
// Node reports a missing cwd as `spawn <binary> ENOENT` — disambiguate.
|
|
5
6
|
function explainSpawnError(err, cwd) {
|
|
@@ -77,21 +78,23 @@ export function executeCommand(opts) {
|
|
|
77
78
|
return { session, done };
|
|
78
79
|
}
|
|
79
80
|
session.process = child;
|
|
80
|
-
const
|
|
81
|
-
|
|
81
|
+
const handleText = (raw) => {
|
|
82
|
+
if (!raw)
|
|
83
|
+
return;
|
|
82
84
|
const clean = stripAnsi(raw);
|
|
83
|
-
// Accumulate cleaned output for the agent
|
|
84
85
|
session.output += clean;
|
|
85
|
-
// Enforce output cap — truncate from beginning, keep tail
|
|
86
86
|
if (session.output.length > maxOutput) {
|
|
87
87
|
session.output = session.output.slice(-maxOutput);
|
|
88
88
|
session.truncated = true;
|
|
89
89
|
}
|
|
90
|
-
// Real-time streaming callback
|
|
91
90
|
opts.onOutput?.(raw);
|
|
92
91
|
};
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
const outDecoder = new StringDecoder("utf-8");
|
|
93
|
+
const errDecoder = new StringDecoder("utf-8");
|
|
94
|
+
child.stdout?.on("data", (d) => handleText(outDecoder.write(d)));
|
|
95
|
+
child.stderr?.on("data", (d) => handleText(errDecoder.write(d)));
|
|
96
|
+
child.stdout?.on("end", () => handleText(outDecoder.end()));
|
|
97
|
+
child.stderr?.on("end", () => handleText(errDecoder.end()));
|
|
95
98
|
let cancelKill;
|
|
96
99
|
const timer = setTimeout(() => {
|
|
97
100
|
if (!session.done) {
|
|
@@ -169,8 +172,9 @@ export function executeArgv(opts) {
|
|
|
169
172
|
return { session, done };
|
|
170
173
|
}
|
|
171
174
|
session.process = child;
|
|
172
|
-
const
|
|
173
|
-
|
|
175
|
+
const handleText = (raw) => {
|
|
176
|
+
if (!raw)
|
|
177
|
+
return;
|
|
174
178
|
const clean = stripAnsi(raw);
|
|
175
179
|
session.output += clean;
|
|
176
180
|
if (session.output.length > maxOutput) {
|
|
@@ -179,8 +183,12 @@ export function executeArgv(opts) {
|
|
|
179
183
|
}
|
|
180
184
|
opts.onOutput?.(raw);
|
|
181
185
|
};
|
|
182
|
-
|
|
183
|
-
|
|
186
|
+
const outDecoder = new StringDecoder("utf-8");
|
|
187
|
+
const errDecoder = new StringDecoder("utf-8");
|
|
188
|
+
child.stdout?.on("data", (d) => handleText(outDecoder.write(d)));
|
|
189
|
+
child.stderr?.on("data", (d) => handleText(errDecoder.write(d)));
|
|
190
|
+
child.stdout?.on("end", () => handleText(outDecoder.end()));
|
|
191
|
+
child.stderr?.on("end", () => handleText(errDecoder.end()));
|
|
184
192
|
const timer = setTimeout(() => {
|
|
185
193
|
if (!session.done && session.process) {
|
|
186
194
|
try {
|