agent-sh 0.15.6 → 0.15.8
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.d.ts +3 -0
- package/dist/agent/agent-loop.js +19 -6
- package/dist/agent/events.d.ts +3 -0
- 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/host-types.d.ts +6 -0
- package/dist/agent/index.js +5 -1
- package/dist/agent/llm-client.d.ts +2 -0
- package/dist/agent/llm-client.js +2 -2
- package/dist/agent/providers/openai-compatible.d.ts +8 -0
- package/dist/agent/providers/openai-compatible.js +9 -2
- package/dist/agent/providers/openrouter.js +11 -1
- 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/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/strategies/bash.js +10 -2
- package/dist/shell/tui-renderer.js +115 -174
- 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 +19 -21
- 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/package.json +1 -1
- package/examples/extensions/ashi/src/chat/assistant.ts +8 -4
- package/examples/extensions/ashi/src/cli.ts +8 -0
- package/examples/extensions/ashi/src/compaction.ts +4 -7
- package/examples/extensions/ashi/src/frontend.ts +6 -3
- package/examples/extensions/ashi/src/renderers/pi-tui/inline-image.ts +145 -0
- package/examples/extensions/ashi/src/renderers/pi-tui/nodes.ts +51 -1
- package/examples/extensions/ashi/src/schema.ts +8 -2
- package/examples/extensions/ashi/src/user-shell-intents.ts +4 -1
- package/examples/extensions/command-suggest.ts +4 -0
- package/examples/extensions/latex-images.ts +152 -7
- package/examples/extensions/solarized-theme.ts +11 -0
- package/package.json +1 -1
- package/src/agent/agent-loop.ts +19 -6
- package/src/agent/events.ts +1 -0
- package/src/agent/extensions/rolling-history/index.ts +20 -8
- package/src/agent/extensions/rolling-history/recall.ts +28 -7
- package/src/agent/host-types.ts +2 -0
- package/src/agent/index.ts +7 -1
- package/src/agent/llm-client.ts +4 -2
- package/src/agent/providers/openai-compatible.ts +19 -4
- package/src/agent/providers/openrouter.ts +10 -1
- package/src/agent/store.ts +5 -1
- package/src/agent/token-budget.ts +10 -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/strategies/bash.ts +10 -2
- package/src/shell/tui-renderer.ts +130 -207
- package/src/utils/executor.ts +17 -14
- package/src/utils/floating-panel.ts +24 -22
- package/src/utils/markdown.ts +17 -20
- 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")) {
|
|
@@ -800,15 +749,13 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
800
749
|
displayDetail?: string;
|
|
801
750
|
batchIndex?: number;
|
|
802
751
|
batchTotal?: number;
|
|
803
|
-
groupContinuation?: boolean;
|
|
804
752
|
},
|
|
805
753
|
): void {
|
|
806
754
|
closeToolLine();
|
|
807
755
|
stopCurrentSpinner();
|
|
808
756
|
if (!s.renderer) startAgentResponse();
|
|
809
757
|
showCollapsedThinking();
|
|
810
|
-
|
|
811
|
-
if (!extra?.groupContinuation) contentGap("tool");
|
|
758
|
+
contentGap("tool");
|
|
812
759
|
s.renderer!.flush();
|
|
813
760
|
drain();
|
|
814
761
|
const lines = renderToolCall({
|
|
@@ -821,38 +768,19 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
821
768
|
displayDetail: extra?.displayDetail,
|
|
822
769
|
}, cappedW(), shellCwd);
|
|
823
770
|
|
|
824
|
-
if (extra?.groupContinuation && lines.length > 0) {
|
|
825
|
-
// Swap the colored kind icon for a muted tree connector,
|
|
826
|
-
// and strip the tool name prefix — show detail only.
|
|
827
|
-
const detail = extra.displayDetail || extractDetail(extra);
|
|
828
|
-
const maxW = Math.max(1, cappedW() - 6);
|
|
829
|
-
const text = detail.length > maxW ? detail.slice(0, maxW - 1) + "…" : detail;
|
|
830
|
-
lines[0] = detail
|
|
831
|
-
? `${p.muted}├${p.reset} ${p.dim}${text}${p.reset}`
|
|
832
|
-
: lines[0]!.replace(/^\x1b\[[^m]*m.\x1b\[0m/, `${p.muted}├${p.reset}`);
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
const batchPrefix = "";
|
|
836
|
-
|
|
837
771
|
for (let i = 0; i < lines.length - 1; i++) {
|
|
838
772
|
s.renderer!.writeLine(lines[i]!);
|
|
839
773
|
}
|
|
840
774
|
drain();
|
|
841
775
|
if (lines.length > 0) {
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
s.
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
callId: extra.toolCallId,
|
|
851
|
-
title,
|
|
852
|
-
kind: extra.kind,
|
|
853
|
-
displayDetail: extra.displayDetail ?? extractDetail(extra),
|
|
854
|
-
};
|
|
855
|
-
}
|
|
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
|
+
};
|
|
856
784
|
}
|
|
857
785
|
}
|
|
858
786
|
s.hadToolCalls = true;
|
|
@@ -885,32 +813,6 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
885
813
|
if (resultDisplay?.body) renderResultBody(resultDisplay.body);
|
|
886
814
|
}
|
|
887
815
|
|
|
888
|
-
/** Late completion from a finalized group — re-emit the kind header
|
|
889
|
-
* in muted "(cont.)" form so the ⎿ has a legitimate parent, then
|
|
890
|
-
* render the completion as a normal labeled ⎿. Subsequent orphans
|
|
891
|
-
* of the same kind reuse the existing (cont.) header. */
|
|
892
|
-
function showOrphanedComplete(
|
|
893
|
-
exitCode: number | null,
|
|
894
|
-
resultDisplay: ToolResultDisplay | undefined,
|
|
895
|
-
title: string,
|
|
896
|
-
kind: string | undefined,
|
|
897
|
-
displayDetail: string | undefined,
|
|
898
|
-
): void {
|
|
899
|
-
if (s.orphanContHeaderKind !== kind) {
|
|
900
|
-
stopCurrentSpinner();
|
|
901
|
-
closeToolLine();
|
|
902
|
-
flushCommandOutput();
|
|
903
|
-
if (!s.renderer) startAgentResponse();
|
|
904
|
-
showCollapsedThinking();
|
|
905
|
-
const icon = (kind && KIND_ICONS[kind]) ?? "▶";
|
|
906
|
-
const label = kind ?? "tool";
|
|
907
|
-
s.renderer!.writeLine(`${p.muted}${icon} ${label} (cont.)${p.reset}`);
|
|
908
|
-
drain();
|
|
909
|
-
s.orphanContHeaderKind = kind;
|
|
910
|
-
}
|
|
911
|
-
showToolComplete(exitCode, resultDisplay, displayDetail || title);
|
|
912
|
-
}
|
|
913
|
-
|
|
914
816
|
function renderResultBody(body: ToolResultBody): void {
|
|
915
817
|
if (!s.renderer) return;
|
|
916
818
|
const lines: string[] = ctx.call("render:result-body", body, cappedW()) ?? [];
|
|
@@ -958,7 +860,7 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
958
860
|
function closeToolLine(): void {
|
|
959
861
|
if (s.openTool) {
|
|
960
862
|
out().write("\n");
|
|
961
|
-
// Stash identity so the completion renders as ⎿ labeled, not
|
|
863
|
+
// Stash identity so the completion renders as ⎿ labeled, not inline ✓.
|
|
962
864
|
s.pendingToolCompletes.set(s.openTool.callId, {
|
|
963
865
|
title: s.openTool.title,
|
|
964
866
|
kind: s.openTool.kind,
|
|
@@ -968,41 +870,62 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
968
870
|
}
|
|
969
871
|
}
|
|
970
872
|
|
|
971
|
-
/** Render
|
|
972
|
-
* completed
|
|
973
|
-
function
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
if (s.toolGroupCount <= 1 || skipAggregate) {
|
|
983
|
-
s.toolGroupKind = undefined;
|
|
984
|
-
s.toolGroupCount = 0;
|
|
985
|
-
s.toolGroupCompletedCount = 0;
|
|
986
|
-
s.toolGroupRendered = 0;
|
|
987
|
-
s.toolGroupAllOk = true;
|
|
988
|
-
s.toolGroupSummaries = [];
|
|
989
|
-
return;
|
|
990
|
-
}
|
|
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
|
+
|
|
991
884
|
stopCurrentSpinner();
|
|
992
885
|
closeToolLine();
|
|
993
886
|
if (!s.renderer) startAgentResponse();
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
);
|
|
998
|
-
|
|
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
|
+
}
|
|
999
911
|
drain();
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
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
|
+
}
|
|
1006
929
|
}
|
|
1007
930
|
|
|
1008
931
|
function renderCommandLine(line: string): string {
|
package/src/utils/executor.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn, spawnSync, type ChildProcess } 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
|
|
|
5
6
|
// Node reports a missing cwd as `spawn <binary> ENOENT` — disambiguate.
|
|
@@ -106,25 +107,23 @@ export function executeCommand(opts: {
|
|
|
106
107
|
|
|
107
108
|
session.process = child;
|
|
108
109
|
|
|
109
|
-
const
|
|
110
|
-
|
|
110
|
+
const handleText = (raw: string) => {
|
|
111
|
+
if (!raw) return;
|
|
111
112
|
const clean = stripAnsi(raw);
|
|
112
|
-
|
|
113
|
-
// Accumulate cleaned output for the agent
|
|
114
113
|
session.output += clean;
|
|
115
|
-
|
|
116
|
-
// Enforce output cap — truncate from beginning, keep tail
|
|
117
114
|
if (session.output.length > maxOutput) {
|
|
118
115
|
session.output = session.output.slice(-maxOutput);
|
|
119
116
|
session.truncated = true;
|
|
120
117
|
}
|
|
121
|
-
|
|
122
|
-
// Real-time streaming callback
|
|
123
118
|
opts.onOutput?.(raw);
|
|
124
119
|
};
|
|
125
120
|
|
|
126
|
-
|
|
127
|
-
|
|
121
|
+
const outDecoder = new StringDecoder("utf-8");
|
|
122
|
+
const errDecoder = new StringDecoder("utf-8");
|
|
123
|
+
child.stdout?.on("data", (d: Buffer) => handleText(outDecoder.write(d)));
|
|
124
|
+
child.stderr?.on("data", (d: Buffer) => handleText(errDecoder.write(d)));
|
|
125
|
+
child.stdout?.on("end", () => handleText(outDecoder.end()));
|
|
126
|
+
child.stderr?.on("end", () => handleText(errDecoder.end()));
|
|
128
127
|
|
|
129
128
|
let cancelKill: (() => void) | undefined;
|
|
130
129
|
const timer = setTimeout(() => {
|
|
@@ -218,8 +217,8 @@ export function executeArgv(opts: {
|
|
|
218
217
|
|
|
219
218
|
session.process = child;
|
|
220
219
|
|
|
221
|
-
const
|
|
222
|
-
|
|
220
|
+
const handleText = (raw: string) => {
|
|
221
|
+
if (!raw) return;
|
|
223
222
|
const clean = stripAnsi(raw);
|
|
224
223
|
session.output += clean;
|
|
225
224
|
if (session.output.length > maxOutput) {
|
|
@@ -229,8 +228,12 @@ export function executeArgv(opts: {
|
|
|
229
228
|
opts.onOutput?.(raw);
|
|
230
229
|
};
|
|
231
230
|
|
|
232
|
-
|
|
233
|
-
|
|
231
|
+
const outDecoder = new StringDecoder("utf-8");
|
|
232
|
+
const errDecoder = new StringDecoder("utf-8");
|
|
233
|
+
child.stdout?.on("data", (d: Buffer) => handleText(outDecoder.write(d)));
|
|
234
|
+
child.stderr?.on("data", (d: Buffer) => handleText(errDecoder.write(d)));
|
|
235
|
+
child.stdout?.on("end", () => handleText(outDecoder.end()));
|
|
236
|
+
child.stderr?.on("end", () => handleText(errDecoder.end()));
|
|
234
237
|
|
|
235
238
|
const timer = setTimeout(() => {
|
|
236
239
|
if (!session.done && session.process) {
|
|
@@ -823,6 +823,13 @@ export class FloatingPanel {
|
|
|
823
823
|
this.autocompleteIndex = 0;
|
|
824
824
|
}
|
|
825
825
|
|
|
826
|
+
private moveAutocomplete(delta: number): void {
|
|
827
|
+
const n = this.autocompleteItems.length;
|
|
828
|
+
if (n === 0) return;
|
|
829
|
+
this.autocompleteIndex = (this.autocompleteIndex + delta + n) % n;
|
|
830
|
+
this.render();
|
|
831
|
+
}
|
|
832
|
+
|
|
826
833
|
// ── Input handling ──────────────────────────────────────────
|
|
827
834
|
|
|
828
835
|
private handleIntercept(payload: { data: string; consumed: boolean }): { data: string; consumed: boolean } {
|
|
@@ -913,6 +920,16 @@ export class FloatingPanel {
|
|
|
913
920
|
|
|
914
921
|
if (this.handleScroll(data, false)) return;
|
|
915
922
|
|
|
923
|
+
if (data === "\x10" || data === "\x0e") {
|
|
924
|
+
const forward = data === "\x0e";
|
|
925
|
+
if (this.autocompleteActive) {
|
|
926
|
+
this.moveAutocomplete(forward ? 1 : -1);
|
|
927
|
+
} else if (forward ? this.editor.historyForward() : this.editor.historyBack()) {
|
|
928
|
+
this.render();
|
|
929
|
+
}
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
|
|
916
933
|
const actions = this.editor.feed(data);
|
|
917
934
|
for (const action of actions) {
|
|
918
935
|
switch (action.action) {
|
|
@@ -924,6 +941,7 @@ export class FloatingPanel {
|
|
|
924
941
|
this.editor.pushHistory(query);
|
|
925
942
|
this.editor.clear();
|
|
926
943
|
this.clearAutocomplete();
|
|
944
|
+
this.userScrolled = false;
|
|
927
945
|
// Phase change is the submit handler's call — sync slash commands
|
|
928
946
|
// (e.g. /model, /help) keep the user in input mode.
|
|
929
947
|
this.handlers.call(`${this.prefix}:submit`, query);
|
|
@@ -945,30 +963,14 @@ export class FloatingPanel {
|
|
|
945
963
|
case "shift+tab":
|
|
946
964
|
this.render();
|
|
947
965
|
break;
|
|
948
|
-
case "arrow-up":
|
|
949
|
-
if (this.autocompleteActive)
|
|
950
|
-
|
|
951
|
-
? this.autocompleteItems.length - 1
|
|
952
|
-
: this.autocompleteIndex - 1;
|
|
953
|
-
this.render();
|
|
954
|
-
} else {
|
|
955
|
-
const hist = this.editor.historyBack();
|
|
956
|
-
if (hist) this.render();
|
|
957
|
-
}
|
|
966
|
+
case "arrow-up":
|
|
967
|
+
if (this.autocompleteActive) this.moveAutocomplete(-1);
|
|
968
|
+
else this.scrollUp(1);
|
|
958
969
|
break;
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
this.autocompleteIndex = this.autocompleteIndex === this.autocompleteItems.length - 1
|
|
963
|
-
? 0
|
|
964
|
-
: this.autocompleteIndex + 1;
|
|
965
|
-
this.render();
|
|
966
|
-
} else {
|
|
967
|
-
const hist = this.editor.historyForward();
|
|
968
|
-
if (hist) this.render();
|
|
969
|
-
}
|
|
970
|
+
case "arrow-down":
|
|
971
|
+
if (this.autocompleteActive) this.moveAutocomplete(1);
|
|
972
|
+
else this.scrollDown(1);
|
|
970
973
|
break;
|
|
971
|
-
}
|
|
972
974
|
case "changed":
|
|
973
975
|
case "delete-empty":
|
|
974
976
|
this.updateAutocomplete();
|