@ian-pascoe/pi-termctrl 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 +32 -19
- package/package.json +4 -3
- package/skills/pi-termctrl/SKILL.md +1 -1
- package/src/bash-replacement.ts +12 -8
- package/src/exit-notification.ts +55 -10
- package/src/pi-termctrl-extension.ts +27 -5
- package/src/pi-termctrl-settings.ts +66 -37
- package/src/ps-panel.ts +81 -35
- package/src/termctrl-registry.ts +7 -1
- package/src/terminal-render.ts +398 -0
- package/src/terminal-tools.ts +199 -45
package/src/terminal-tools.ts
CHANGED
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
formatSize,
|
|
8
8
|
truncateTail,
|
|
9
9
|
type ExtensionToolContext,
|
|
10
|
-
type TruncationResult,
|
|
11
10
|
} from "@earendil-works/pi-coding-agent";
|
|
12
11
|
import type { Key } from "@kitlangton/terminal-control";
|
|
13
12
|
import { Type, type Static } from "typebox";
|
|
@@ -19,8 +18,19 @@ import type {
|
|
|
19
18
|
} from "./termctrl-registry.js";
|
|
20
19
|
import { describeExitStatus, formatExitNotice, lastLines } from "./exit-notification.js";
|
|
21
20
|
import { termctrlTemporaryDirectory } from "./termctrl-driver.js";
|
|
22
|
-
import type { TerminalViewport } from "./pi-termctrl-settings.js";
|
|
21
|
+
import type { LineByteLimits, TerminalViewport } from "./pi-termctrl-settings.js";
|
|
23
22
|
import type { TerminalExit, TerminalSnapshot } from "./terminal-driver.js";
|
|
23
|
+
import {
|
|
24
|
+
renderListCall,
|
|
25
|
+
renderListResult,
|
|
26
|
+
renderSendCall,
|
|
27
|
+
renderStartCall,
|
|
28
|
+
renderStopCall,
|
|
29
|
+
renderStopResult,
|
|
30
|
+
renderTerminalResult,
|
|
31
|
+
renderWaitCall,
|
|
32
|
+
renderWaitResult,
|
|
33
|
+
} from "./terminal-render.js";
|
|
24
34
|
import { TROUBLESHOOTING_HINT } from "./troubleshooting-skill.js";
|
|
25
35
|
|
|
26
36
|
/** Screen quiet period that settles a Terminal. */
|
|
@@ -73,8 +83,13 @@ export interface TerminalToolRuntime {
|
|
|
73
83
|
readonly registry: TermctrlRegistry;
|
|
74
84
|
readonly shell: () => TerminalShell;
|
|
75
85
|
readonly viewport: () => TerminalViewport;
|
|
86
|
+
/** The `termctrl.scrollback` limits; undefined leaves only Pi's limits on the whole result. */
|
|
87
|
+
readonly scrollback: () => LineByteLimits | undefined;
|
|
76
88
|
}
|
|
77
89
|
|
|
90
|
+
/** What building a Terminal result reads; `terminal_stop` also stops Background jobs and needs only this. */
|
|
91
|
+
export type TerminalResultRuntime = Pick<TerminalToolRuntime, "registry" | "scrollback">;
|
|
92
|
+
|
|
78
93
|
const TerminalStateSchema = Type.Union([Type.Literal("running"), Type.Literal("exited")]);
|
|
79
94
|
|
|
80
95
|
const SettleReasonSchema = Type.Union(
|
|
@@ -108,7 +123,7 @@ const TerminalResultSchema = Type.Object({
|
|
|
108
123
|
}),
|
|
109
124
|
),
|
|
110
125
|
});
|
|
111
|
-
type TerminalResult = Static<typeof TerminalResultSchema>;
|
|
126
|
+
export type TerminalResult = Static<typeof TerminalResultSchema>;
|
|
112
127
|
|
|
113
128
|
const ListEntrySchema = Type.Object({
|
|
114
129
|
id: Type.String(),
|
|
@@ -124,7 +139,7 @@ const ListResultSchema = Type.Object({
|
|
|
124
139
|
Type.Object({ ...ListEntrySchema.properties, log_path: Type.String() }),
|
|
125
140
|
),
|
|
126
141
|
});
|
|
127
|
-
type ListResult = Static<typeof ListResultSchema>;
|
|
142
|
+
export type ListResult = Static<typeof ListResultSchema>;
|
|
128
143
|
|
|
129
144
|
const StopResultSchema = Type.Object({
|
|
130
145
|
id: Type.String(),
|
|
@@ -162,7 +177,7 @@ const StopResultSchema = Type.Object({
|
|
|
162
177
|
),
|
|
163
178
|
output: Type.Optional(Type.String({ description: "Background job only: its recent output" })),
|
|
164
179
|
});
|
|
165
|
-
type StopResult = Static<typeof StopResultSchema>;
|
|
180
|
+
export type StopResult = Static<typeof StopResultSchema>;
|
|
166
181
|
|
|
167
182
|
const WaitMsSchema = Type.Optional(
|
|
168
183
|
Type.Integer({ minimum: 0, description: "Maximum milliseconds to wait (clamped to 300000)" }),
|
|
@@ -264,7 +279,7 @@ const WaitResultSchema = Type.Object({
|
|
|
264
279
|
}),
|
|
265
280
|
),
|
|
266
281
|
});
|
|
267
|
-
type WaitResult = Static<typeof WaitResultSchema>;
|
|
282
|
+
export type WaitResult = Static<typeof WaitResultSchema>;
|
|
268
283
|
type WaitReason = WaitResult["reason"];
|
|
269
284
|
|
|
270
285
|
/** Clamp a requested wait to the fixed 0 to 5 minute range. */
|
|
@@ -405,10 +420,11 @@ function locateScreen(lines: readonly string[], screen: readonly string[], floor
|
|
|
405
420
|
|
|
406
421
|
/**
|
|
407
422
|
* Lines that left the screen since the previous result, advancing the Terminal's log cursor.
|
|
408
|
-
* Lines still on the result's screen stay unread,
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
*
|
|
423
|
+
* Lines still on the result's screen stay unread. Once they scroll off, a line the agent saw is
|
|
424
|
+
* skipped if it is unchanged, while a line rewritten since, such as a progress line or a prompt the
|
|
425
|
+
* agent typed at, is reported in its final form. When the cursor cannot be found again, because
|
|
426
|
+
* termctrl dropped lines the agent never received or the screen was cleared, every line termctrl
|
|
427
|
+
* still holds is reported and the result says that earlier output is missing.
|
|
412
428
|
*/
|
|
413
429
|
async function takeScrolledOff(entry: TerminalEntry, screen: string): Promise<ScrolledOff> {
|
|
414
430
|
let logs: string;
|
|
@@ -421,7 +437,10 @@ async function takeScrolledOff(entry: TerminalEntry, screen: string): Promise<Sc
|
|
|
421
437
|
const located = locateCursor(lines, entry.logCursor, entry.logAnchor);
|
|
422
438
|
const screenLines = screen === "" ? [] : screen.split("\n");
|
|
423
439
|
const boundary = locateScreen(lines, screenLines, located ?? 0);
|
|
424
|
-
const
|
|
440
|
+
const seen = located === undefined ? [] : entry.seenRows;
|
|
441
|
+
const scrolled = lines
|
|
442
|
+
.slice(Math.min(located ?? 0, boundary), boundary)
|
|
443
|
+
.filter((line, row) => line !== seen[row]);
|
|
425
444
|
entry.logCursor = boundary;
|
|
426
445
|
entry.logAnchor =
|
|
427
446
|
boundary > 0 ? lines.slice(Math.max(0, boundary - ANCHOR_LINES), boundary) : lines.slice(0, 1);
|
|
@@ -442,35 +461,135 @@ interface FittedOutput {
|
|
|
442
461
|
readonly notice?: string;
|
|
443
462
|
}
|
|
444
463
|
|
|
445
|
-
|
|
446
|
-
|
|
464
|
+
/** The scrolled-off lines a result shows. */
|
|
465
|
+
interface ShownScrolled {
|
|
466
|
+
readonly content: string;
|
|
467
|
+
readonly truncated: boolean;
|
|
468
|
+
readonly totalLines: number;
|
|
469
|
+
readonly outputLines: number;
|
|
470
|
+
/** Describes a cut to the `termctrl.scrollback` limits; absent for a cut to Pi's limits alone. */
|
|
471
|
+
readonly summary?: string;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function omissionMarker(count: number): string {
|
|
475
|
+
return `[… ${count} lines omitted …]`;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function describeLimits(limits: LineByteLimits): string {
|
|
479
|
+
return `(${formatSize(limits.maxBytes)} or ${limits.maxLines} line limit)`;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Keep the first and last scrolled-off lines within `limits`, splitting them evenly and marking
|
|
484
|
+
* the omitted lines between them on a line of its own. Returns `undefined` when the limits leave
|
|
485
|
+
* no room for a line beside the marker.
|
|
486
|
+
*/
|
|
487
|
+
function keepStartAndEnd(
|
|
488
|
+
lines: readonly string[],
|
|
489
|
+
limits: LineByteLimits,
|
|
490
|
+
): ShownScrolled | undefined {
|
|
491
|
+
// Each kept line is counted with the newline that joins it to the marker or its neighbour.
|
|
492
|
+
const bodyLines = limits.maxLines - 1;
|
|
493
|
+
const bodyBytes = limits.maxBytes - Buffer.byteLength(omissionMarker(lines.length), "utf8");
|
|
494
|
+
if (bodyLines < 1 || bodyBytes < 2) return undefined;
|
|
495
|
+
const head: string[] = [];
|
|
496
|
+
let headBytes = 0;
|
|
497
|
+
const headLines = Math.ceil(bodyLines / 2);
|
|
498
|
+
const headByteLimit = Math.floor(bodyBytes / 2);
|
|
499
|
+
for (const line of lines) {
|
|
500
|
+
const size = Buffer.byteLength(line, "utf8") + 1;
|
|
501
|
+
if (head.length >= headLines || headBytes + size > headByteLimit) break;
|
|
502
|
+
head.push(line);
|
|
503
|
+
headBytes += size;
|
|
504
|
+
}
|
|
505
|
+
const tail: string[] = [];
|
|
506
|
+
let tailBytes = 0;
|
|
507
|
+
let lastLineCut = false;
|
|
508
|
+
const tailByteLimit = bodyBytes - headBytes;
|
|
509
|
+
for (let index = lines.length - 1; index >= head.length; index--) {
|
|
510
|
+
if (tail.length >= bodyLines - head.length) break;
|
|
511
|
+
const line = lines[index] ?? "";
|
|
512
|
+
const size = Buffer.byteLength(line, "utf8") + 1;
|
|
513
|
+
if (tailBytes + size > tailByteLimit) {
|
|
514
|
+
// A last line too long for the limits on its own still shows its end.
|
|
515
|
+
if (head.length === 0 && tail.length === 0) {
|
|
516
|
+
tail.push(truncateTail(line, { maxLines: 1, maxBytes: tailByteLimit - 1 }).content);
|
|
517
|
+
lastLineCut = true;
|
|
518
|
+
}
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
tail.unshift(line);
|
|
522
|
+
tailBytes += size;
|
|
523
|
+
}
|
|
524
|
+
const omitted = lines.length - head.length - tail.length;
|
|
525
|
+
const kept = omitted > 0 ? [...head, omissionMarker(omitted), ...tail] : [...head, ...tail];
|
|
526
|
+
const shown = lastLineCut
|
|
527
|
+
? `the end of scrolled-off line ${lines.length} of ${lines.length}`
|
|
528
|
+
: `the first ${head.length} and last ${tail.length} of ${lines.length} scrolled-off lines`;
|
|
529
|
+
return {
|
|
530
|
+
content: kept.join("\n"),
|
|
531
|
+
truncated: true,
|
|
532
|
+
totalLines: lines.length,
|
|
533
|
+
outputLines: head.length + tail.length,
|
|
534
|
+
summary: `Showing ${shown} ${describeLimits(limits)}.`,
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/** Keep the newest scrolled-off lines within `limits`. */
|
|
539
|
+
function keepNewest(lines: readonly string[], limits: LineByteLimits): ShownScrolled {
|
|
540
|
+
const newest = truncateTail(lines.join("\n"), limits);
|
|
447
541
|
return {
|
|
448
|
-
content:
|
|
449
|
-
truncated:
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
542
|
+
content: newest.content,
|
|
543
|
+
truncated: newest.truncated,
|
|
544
|
+
totalLines: lines.length,
|
|
545
|
+
outputLines: newest.outputLines,
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Fit the scrolled-off lines into the lines and bytes the screen left. With `termctrl.scrollback`
|
|
551
|
+
* limits, a cut keeps their start and end; without them, their newest lines.
|
|
552
|
+
*/
|
|
553
|
+
function fitScrolled(
|
|
554
|
+
lines: readonly string[],
|
|
555
|
+
room: LineByteLimits,
|
|
556
|
+
scrollback: LineByteLimits | undefined,
|
|
557
|
+
): ShownScrolled {
|
|
558
|
+
if (room.maxLines <= 0 || room.maxBytes <= 0) {
|
|
559
|
+
return { content: "", truncated: lines.length > 0, totalLines: lines.length, outputLines: 0 };
|
|
560
|
+
}
|
|
561
|
+
if (scrollback === undefined) return keepNewest(lines, room);
|
|
562
|
+
const limits = {
|
|
563
|
+
maxLines: Math.min(scrollback.maxLines, room.maxLines),
|
|
564
|
+
maxBytes: Math.min(scrollback.maxBytes, room.maxBytes),
|
|
565
|
+
};
|
|
566
|
+
const text = lines.join("\n");
|
|
567
|
+
if (lines.length <= limits.maxLines && Buffer.byteLength(text, "utf8") <= limits.maxBytes) {
|
|
568
|
+
return { content: text, truncated: false, totalLines: lines.length, outputLines: lines.length };
|
|
569
|
+
}
|
|
570
|
+
const kept = keepStartAndEnd(lines, limits);
|
|
571
|
+
if (kept !== undefined) return kept;
|
|
572
|
+
const newest = keepNewest(lines, limits);
|
|
573
|
+
return {
|
|
574
|
+
...newest,
|
|
575
|
+
summary: `Showing the last ${newest.outputLines} of ${lines.length} scrolled-off lines ${describeLimits(limits)}.`,
|
|
459
576
|
};
|
|
460
577
|
}
|
|
461
578
|
|
|
462
579
|
/**
|
|
463
580
|
* Fit a result into Pi's 2000-line and 50 KB limits. The screen is kept first, from its bottom;
|
|
464
|
-
* the
|
|
465
|
-
* goes to a full output file that lasts until the owner's session
|
|
581
|
+
* the scrolled-off lines fill what is left, within the `termctrl.scrollback` limits. When anything
|
|
582
|
+
* is cut, every line of the result goes to a full output file that lasts until the owner's session
|
|
583
|
+
* shuts down.
|
|
466
584
|
*/
|
|
467
585
|
async function fitOutput(
|
|
468
586
|
registry: TermctrlRegistry,
|
|
469
587
|
entry: TerminalEntry,
|
|
470
588
|
screen: string,
|
|
471
589
|
scrolled: ScrolledOff,
|
|
590
|
+
scrollback: LineByteLimits | undefined,
|
|
472
591
|
): Promise<FittedOutput> {
|
|
473
|
-
const fitted = await truncateOutput(registry, entry, screen, scrolled.lines);
|
|
592
|
+
const fitted = await truncateOutput(registry, entry, screen, scrolled.lines, scrollback);
|
|
474
593
|
if (!scrolled.gap) return { ...fitted, gap: false };
|
|
475
594
|
const notice = fitted.notice === undefined ? GAP_NOTICE : `${GAP_NOTICE}\n${fitted.notice}`;
|
|
476
595
|
return { ...fitted, gap: true, notice };
|
|
@@ -481,24 +600,29 @@ async function truncateOutput(
|
|
|
481
600
|
entry: TerminalEntry,
|
|
482
601
|
screen: string,
|
|
483
602
|
scrolled: readonly string[],
|
|
603
|
+
scrollback: LineByteLimits | undefined,
|
|
484
604
|
): Promise<Omit<FittedOutput, "gap">> {
|
|
485
605
|
const limits = { maxLines: DEFAULT_MAX_LINES, maxBytes: DEFAULT_MAX_BYTES };
|
|
486
|
-
const scrolledText = scrolled.join("\n");
|
|
487
606
|
const shownScreen = truncateTail(screen, limits);
|
|
488
|
-
|
|
489
|
-
//
|
|
490
|
-
const
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
607
|
+
// A screen cut to fit leaves no room. One byte for the newline that joins the scrolled-off lines
|
|
608
|
+
// to the screen.
|
|
609
|
+
const room = shownScreen.truncated
|
|
610
|
+
? { maxLines: 0, maxBytes: 0 }
|
|
611
|
+
: {
|
|
612
|
+
maxLines: limits.maxLines - shownScreen.outputLines,
|
|
613
|
+
maxBytes: limits.maxBytes - shownScreen.outputBytes - 1,
|
|
614
|
+
};
|
|
615
|
+
const shownScrolled = fitScrolled(scrolled, room, scrollback);
|
|
616
|
+
const scrolledText = scrolled.join("\n");
|
|
495
617
|
if (!shownScreen.truncated && !shownScrolled.truncated)
|
|
496
618
|
return { screen, scrolledOff: scrolledText };
|
|
497
619
|
|
|
498
620
|
const total = shownScrolled.totalLines + shownScreen.totalLines;
|
|
499
621
|
const shown = shownScrolled.outputLines + shownScreen.outputLines;
|
|
500
622
|
const fitted = { screen: shownScreen.content, scrolledOff: shownScrolled.content };
|
|
501
|
-
const
|
|
623
|
+
const summary =
|
|
624
|
+
shownScrolled.summary ??
|
|
625
|
+
`Showing lines ${total - shown + 1}-${total} of ${total} (${formatSize(DEFAULT_MAX_BYTES)} or ${DEFAULT_MAX_LINES} line limit).`;
|
|
502
626
|
const directory = termctrlTemporaryDirectory();
|
|
503
627
|
let path: string;
|
|
504
628
|
try {
|
|
@@ -509,9 +633,21 @@ async function truncateOutput(
|
|
|
509
633
|
await writeFile(path, full, { encoding: "utf8", mode: 0o600 });
|
|
510
634
|
} catch (cause) {
|
|
511
635
|
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
512
|
-
return { ...fitted, notice:
|
|
636
|
+
return { ...fitted, notice: `[${summary} The full output could not be saved: ${reason}]` };
|
|
513
637
|
}
|
|
514
|
-
return { ...fitted, fullOutputPath: path, notice:
|
|
638
|
+
return { ...fitted, fullOutputPath: path, notice: `[${summary} Full output: ${path}]` };
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* The rows of `screen` that the agent received, aligned with `screen`: rows cut from the top of a
|
|
643
|
+
* screen too large for the result, or shortened to fit it, are `undefined`.
|
|
644
|
+
*/
|
|
645
|
+
function shownRows(screen: string, shown: string): (string | undefined)[] {
|
|
646
|
+
const rows = screen === "" ? [] : screen.split("\n");
|
|
647
|
+
if (shown === screen) return rows;
|
|
648
|
+
const kept = shown === "" ? [] : shown.split("\n");
|
|
649
|
+
const cut = rows.length - kept.length;
|
|
650
|
+
return rows.map((row, index) => (kept[index - cut] === row ? row : undefined));
|
|
515
651
|
}
|
|
516
652
|
|
|
517
653
|
/** Says that `wait_for_text` was not seen when its wait timed out. */
|
|
@@ -544,7 +680,7 @@ function formatTerminalText(
|
|
|
544
680
|
|
|
545
681
|
/** Build the agent's view of a Terminal after a wait, recording the exit as seen. */
|
|
546
682
|
async function terminalResult(
|
|
547
|
-
registry:
|
|
683
|
+
{ registry, scrollback }: TerminalResultRuntime,
|
|
548
684
|
entry: TerminalEntry,
|
|
549
685
|
snapshot: TerminalSnapshot | undefined,
|
|
550
686
|
settleReason: SettleReason,
|
|
@@ -555,7 +691,9 @@ async function terminalResult(
|
|
|
555
691
|
entry.state === "running" || snapshot !== undefined
|
|
556
692
|
? await takeScrolledOff(entry, screen)
|
|
557
693
|
: NOTHING_SCROLLED;
|
|
558
|
-
const output = await fitOutput(registry, entry, screen, scrolled);
|
|
694
|
+
const output = await fitOutput(registry, entry, screen, scrolled, scrollback());
|
|
695
|
+
// The screen now starts at the log cursor; when the cursor did not move, neither do the rows.
|
|
696
|
+
if (scrolled !== NOTHING_SCROLLED) entry.seenRows = shownRows(screen, output.screen);
|
|
559
697
|
if (snapshot?.state === "exited") {
|
|
560
698
|
registry.terminalExited(entry.id, snapshot.exit ?? { code: null, signal: null }, screen, true);
|
|
561
699
|
}
|
|
@@ -710,6 +848,8 @@ export function createTerminalStartTool(runtime: TerminalToolRuntime) {
|
|
|
710
848
|
openWorldHint: true,
|
|
711
849
|
},
|
|
712
850
|
outputSchema: TerminalResultSchema,
|
|
851
|
+
renderCall: renderStartCall,
|
|
852
|
+
renderResult: renderTerminalResult,
|
|
713
853
|
async execute(_toolCallId, params, signal, _onUpdate, context) {
|
|
714
854
|
const cwd = await resolveWorkingDirectory(context.cwd, params.cwd);
|
|
715
855
|
const entry = await runtime.registry.startTerminal(ownerOf(context), {
|
|
@@ -732,7 +872,7 @@ export function createTerminalStartTool(runtime: TerminalToolRuntime) {
|
|
|
732
872
|
baseline: undefined,
|
|
733
873
|
signal,
|
|
734
874
|
});
|
|
735
|
-
return terminalResult(runtime
|
|
875
|
+
return terminalResult(runtime, entry, snapshot, reason);
|
|
736
876
|
});
|
|
737
877
|
},
|
|
738
878
|
});
|
|
@@ -767,6 +907,8 @@ export function createTerminalSendTool(runtime: TerminalToolRuntime) {
|
|
|
767
907
|
openWorldHint: true,
|
|
768
908
|
},
|
|
769
909
|
outputSchema: TerminalResultSchema,
|
|
910
|
+
renderCall: renderSendCall,
|
|
911
|
+
renderResult: renderTerminalResult,
|
|
770
912
|
async execute(_toolCallId, params, signal, _onUpdate, context) {
|
|
771
913
|
const entry = runtime.registry.find(ownerOf(context), params.id);
|
|
772
914
|
if (entry === undefined) throw unknownId(params.id);
|
|
@@ -785,7 +927,7 @@ export function createTerminalSendTool(runtime: TerminalToolRuntime) {
|
|
|
785
927
|
const hasInput = (params.text ?? "") !== "" || keys.length > 0;
|
|
786
928
|
return driveTerminal(runtime.registry, entry, signal, async () => {
|
|
787
929
|
if (entry.state === "exited") {
|
|
788
|
-
if (!hasInput) return terminalResult(runtime
|
|
930
|
+
if (!hasInput) return terminalResult(runtime, entry, undefined, "exited");
|
|
789
931
|
// The error tells the agent about the exit, so a deferred Exit notification is redundant.
|
|
790
932
|
runtime.registry.markSeen(entry.id);
|
|
791
933
|
throw new InputToExitedError(entry.id, entry.exit);
|
|
@@ -814,7 +956,7 @@ export function createTerminalSendTool(runtime: TerminalToolRuntime) {
|
|
|
814
956
|
signal,
|
|
815
957
|
});
|
|
816
958
|
return terminalResult(
|
|
817
|
-
runtime
|
|
959
|
+
runtime,
|
|
818
960
|
entry,
|
|
819
961
|
snapshot,
|
|
820
962
|
reason,
|
|
@@ -862,7 +1004,7 @@ function stopTerminalInTurn(
|
|
|
862
1004
|
}
|
|
863
1005
|
|
|
864
1006
|
/** `terminal_stop`: stop a Terminal or Background job and forget it. */
|
|
865
|
-
export function createTerminalStopTool(registry:
|
|
1007
|
+
export function createTerminalStopTool({ registry, scrollback }: TerminalResultRuntime) {
|
|
866
1008
|
return defineTool<typeof StopParameters, StopResult>({
|
|
867
1009
|
name: "terminal_stop",
|
|
868
1010
|
label: "terminal_stop",
|
|
@@ -877,6 +1019,8 @@ export function createTerminalStopTool(registry: TermctrlRegistry) {
|
|
|
877
1019
|
openWorldHint: false,
|
|
878
1020
|
},
|
|
879
1021
|
outputSchema: StopResultSchema,
|
|
1022
|
+
renderCall: renderStopCall,
|
|
1023
|
+
renderResult: renderStopResult,
|
|
880
1024
|
async execute(_toolCallId, params, signal, _onUpdate, context) {
|
|
881
1025
|
const owner = ownerOf(context);
|
|
882
1026
|
const known = registry.find(owner, params.id);
|
|
@@ -906,7 +1050,13 @@ export function createTerminalStopTool(registry: TermctrlRegistry) {
|
|
|
906
1050
|
const changed = previousScreen !== screen;
|
|
907
1051
|
// A screen the agent already saw is not repeated, whether the Terminal was running or exited.
|
|
908
1052
|
const repeated = !changed;
|
|
909
|
-
const output = await fitOutput(
|
|
1053
|
+
const output = await fitOutput(
|
|
1054
|
+
registry,
|
|
1055
|
+
entry,
|
|
1056
|
+
repeated ? "" : screen,
|
|
1057
|
+
scrolled,
|
|
1058
|
+
scrollback(),
|
|
1059
|
+
);
|
|
910
1060
|
result.changed = changed;
|
|
911
1061
|
if (!repeated) result.screen = output.screen;
|
|
912
1062
|
if (!repeated || output.scrolledOff !== "") result.scrolled_off = output.scrolledOff;
|
|
@@ -1033,6 +1183,8 @@ export function createTerminalWaitTool(runtime: TerminalWaitRuntime) {
|
|
|
1033
1183
|
openWorldHint: false,
|
|
1034
1184
|
},
|
|
1035
1185
|
outputSchema: WaitResultSchema,
|
|
1186
|
+
renderCall: renderWaitCall,
|
|
1187
|
+
renderResult: renderWaitResult,
|
|
1036
1188
|
async execute(_toolCallId, params, signal, _onUpdate, context) {
|
|
1037
1189
|
const owner = ownerOf(context);
|
|
1038
1190
|
const targets = waitTargets(registry, owner, params.ids);
|
|
@@ -1105,6 +1257,8 @@ export function createTerminalListTool(registry: TermctrlRegistry) {
|
|
|
1105
1257
|
openWorldHint: false,
|
|
1106
1258
|
},
|
|
1107
1259
|
outputSchema: ListResultSchema,
|
|
1260
|
+
renderCall: renderListCall,
|
|
1261
|
+
renderResult: renderListResult,
|
|
1108
1262
|
async execute(_toolCallId, _params, _signal, _onUpdate, context) {
|
|
1109
1263
|
const now = Date.now();
|
|
1110
1264
|
const entries = registry.ownedEntries(ownerOf(context));
|