@ian-pascoe/pi-termctrl 0.6.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 +10 -3
- package/package.json +4 -3
- package/src/bash-replacement.ts +5 -1
- package/src/exit-notification.ts +55 -10
- package/src/pi-termctrl-extension.ts +13 -3
- package/src/ps-panel.ts +81 -35
- package/src/terminal-render.ts +398 -0
- package/src/terminal-tools.ts +25 -4
package/README.md
CHANGED
|
@@ -189,9 +189,16 @@ the session that started the entry, and wait across `/reload`.
|
|
|
189
189
|
`/ps` opens an overlay listing every entry in the Pi process, including those of
|
|
190
190
|
in-process child agents, with kind, id, owner (`root` or `child`), state and
|
|
191
191
|
age, plus a live preview of the selected Terminal's screen or Background job's
|
|
192
|
-
log tail.
|
|
193
|
-
|
|
194
|
-
|
|
192
|
+
log tail. It uses Pi's selector frame, with a Status Mark before each entry
|
|
193
|
+
(`●` running, `✓` exited cleanly, `✗` failed, `■` ended by a signal). Keys:
|
|
194
|
+
`↑`/`↓` select, `k` stops without confirmation (the agent gets an Exit
|
|
195
|
+
notification), `x` removes an exited entry, `Esc` closes. The footer shows
|
|
196
|
+
`● termctrl N running` while anything is live.
|
|
197
|
+
|
|
198
|
+
The `terminal_*` tools render like `bash` in the transcript: a header with the
|
|
199
|
+
tool name and target, the last 5 screen lines (`ctrl+o` expands), and a `Took`
|
|
200
|
+
footer. Exit notifications show one row per exit, marked `✓` for a clean exit and
|
|
201
|
+
`■` otherwise. In RPC mode `/ps` sends a one-line summary;
|
|
195
202
|
print mode has no UI but keeps the tools and notifications.
|
|
196
203
|
|
|
197
204
|
## Lifecycle
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ian-pascoe/pi-termctrl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Interactive Terminals, Background jobs, and a /ps panel for Pi.",
|
|
6
6
|
"keywords": [
|
|
@@ -34,11 +34,12 @@
|
|
|
34
34
|
"provenance": true
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
+
"@ian-pascoe/pi-utils": "^0.5.0",
|
|
37
38
|
"@kitlangton/terminal-control": "^1.2.1"
|
|
38
39
|
},
|
|
39
40
|
"peerDependencies": {
|
|
40
|
-
"@earendil-works/pi-coding-agent": ">=
|
|
41
|
-
"@earendil-works/pi-tui": ">=
|
|
41
|
+
"@earendil-works/pi-coding-agent": ">=1.1.0",
|
|
42
|
+
"@earendil-works/pi-tui": ">=1.1.0",
|
|
42
43
|
"typebox": "*"
|
|
43
44
|
},
|
|
44
45
|
"engines": {
|
package/src/bash-replacement.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
type TruncationResult,
|
|
20
20
|
} from "@earendil-works/pi-coding-agent";
|
|
21
21
|
import { matchesKey } from "@earendil-works/pi-tui";
|
|
22
|
+
import { noticeText } from "@ian-pascoe/pi-utils/ui";
|
|
22
23
|
import { Type, type TSchema } from "typebox";
|
|
23
24
|
import { KILLED_EXIT, TermctrlRegistry } from "./termctrl-registry.js";
|
|
24
25
|
import type { LineByteLimits } from "./pi-termctrl-settings.js";
|
|
@@ -264,7 +265,10 @@ export class RunningBashCalls {
|
|
|
264
265
|
try {
|
|
265
266
|
call.background(this.registry());
|
|
266
267
|
} catch (error) {
|
|
267
|
-
ui.notify(
|
|
268
|
+
ui.notify(
|
|
269
|
+
noticeText("Termctrl", error instanceof Error ? error.message : String(error)),
|
|
270
|
+
"warning",
|
|
271
|
+
);
|
|
268
272
|
}
|
|
269
273
|
}
|
|
270
274
|
}
|
package/src/exit-notification.ts
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import type { MessageRenderer } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Text, type Component } from "@earendil-works/pi-tui";
|
|
3
|
+
import {
|
|
4
|
+
COLLAPSED_LINES,
|
|
5
|
+
customMessageBox,
|
|
6
|
+
expandHint,
|
|
7
|
+
joinInline,
|
|
8
|
+
previewBody,
|
|
9
|
+
statusMark,
|
|
10
|
+
summaryExpandHint,
|
|
11
|
+
} from "@ian-pascoe/pi-utils/ui";
|
|
3
12
|
import { Type, type Static } from "typebox";
|
|
4
13
|
import { Value } from "typebox/value";
|
|
5
14
|
import type { ExitNotice } from "./termctrl-registry.js";
|
|
@@ -110,19 +119,55 @@ function messageText(content: RenderedMessage["content"]): string {
|
|
|
110
119
|
return content.map((part) => (part.type === "text" ? part.text : "")).join("");
|
|
111
120
|
}
|
|
112
121
|
|
|
113
|
-
|
|
122
|
+
type MessageTheme = Parameters<ExitNotificationRenderer>[2];
|
|
123
|
+
|
|
124
|
+
function exitRow(theme: MessageTheme, exit: ExitNotificationDetails["exits"][number]): string {
|
|
125
|
+
const done = exit.exit_code === 0 && exit.signal === null;
|
|
126
|
+
const body = (text: string) => theme.fg("customMessageText", text);
|
|
127
|
+
const status = describeExitStatus({ code: exit.exit_code, signal: exit.signal });
|
|
128
|
+
return `${statusMark(theme, done ? "done" : "stopped")} ${body(theme.bold(exit.id))} ${joinInline(
|
|
129
|
+
theme,
|
|
130
|
+
[body(status), body(formatDuration(exit.duration_ms)), theme.fg("muted", exit.command)],
|
|
131
|
+
)}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** The Collapsed View's rows: at most 10, then the Expand Hint for the rest or for the full text. */
|
|
135
|
+
function collapsedRows(theme: MessageTheme, rows: readonly string[]): string[] {
|
|
136
|
+
const hidden = rows.length - COLLAPSED_LINES.fallback;
|
|
137
|
+
if (hidden > 0) return [...rows.slice(0, COLLAPSED_LINES.fallback), expandHint(theme, hidden)];
|
|
138
|
+
const last = rows.length - 1;
|
|
139
|
+
return rows.map((row, index) => (index === last ? `${row}${summaryExpandHint(theme)}` : row));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** `exit`, or `N exits` when several Terminals or Background jobs ended together. */
|
|
143
|
+
function exitHeading(details: ExitNotificationDetails | undefined): string {
|
|
144
|
+
const count = Value.Check(ExitNotificationDetailsSchema, details) ? details.exits.length : 1;
|
|
145
|
+
return count === 1 ? "exit" : `${count} exits`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Pi's custom-message look: a `customMessageBg` box under a bold `[termctrl] exit` label.
|
|
150
|
+
* Collapsed, it shows one Status Mark row per exit; expanded, the full text.
|
|
151
|
+
*/
|
|
114
152
|
export const renderExitNotification: ExitNotificationRenderer = (
|
|
115
153
|
message,
|
|
116
154
|
options,
|
|
117
155
|
theme,
|
|
118
156
|
): Component => {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
157
|
+
const details = message.details;
|
|
158
|
+
const rows =
|
|
159
|
+
!options.expanded && Value.Check(ExitNotificationDetailsSchema, details)
|
|
160
|
+
? collapsedRows(
|
|
161
|
+
theme,
|
|
162
|
+
details.exits.map((exit) => exitRow(theme, exit)),
|
|
163
|
+
)
|
|
164
|
+
: previewBody(theme, messageText(message.content).replace(/\n+$/u, "").split("\n"), {
|
|
165
|
+
limit: COLLAPSED_LINES.fallback,
|
|
166
|
+
expanded: options.expanded,
|
|
167
|
+
color: "customMessageText",
|
|
168
|
+
});
|
|
169
|
+
const heading = theme.fg("customMessageText", exitHeading(details));
|
|
170
|
+
return customMessageBox(theme, { outputPad: options.outputPad, source: "termctrl", heading }, [
|
|
171
|
+
new Text(rows.join("\n"), 0, 0),
|
|
172
|
+
]);
|
|
128
173
|
};
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
type ExtensionFactory,
|
|
8
8
|
type SessionShutdownEvent,
|
|
9
9
|
} from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import { expandMessageOnClick, noticeText } from "@ian-pascoe/pi-utils/ui";
|
|
10
11
|
import { createBashReplacement, RunningBashCalls } from "./bash-replacement.js";
|
|
11
12
|
import {
|
|
12
13
|
EXIT_NOTIFICATION_TYPE,
|
|
@@ -84,7 +85,10 @@ class PiTermctrlController {
|
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
register(): void {
|
|
87
|
-
this.pi.registerMessageRenderer(
|
|
88
|
+
this.pi.registerMessageRenderer(
|
|
89
|
+
EXIT_NOTIFICATION_TYPE,
|
|
90
|
+
expandMessageOnClick(renderExitNotification),
|
|
91
|
+
);
|
|
88
92
|
this.pi.registerCommand("ps", {
|
|
89
93
|
description: "Show and stop Terminals and Background jobs",
|
|
90
94
|
handler: async () => {
|
|
@@ -154,7 +158,10 @@ class PiTermctrlController {
|
|
|
154
158
|
);
|
|
155
159
|
const settings = resolveTermctrlSettings(settingsManager);
|
|
156
160
|
if (settings.warnings.length > 0) {
|
|
157
|
-
context.ui.notify(
|
|
161
|
+
context.ui.notify(
|
|
162
|
+
noticeText("Termctrl", `Settings:\n- ${settings.warnings.join("\n- ")}`),
|
|
163
|
+
"warning",
|
|
164
|
+
);
|
|
158
165
|
}
|
|
159
166
|
const registry = this.registry;
|
|
160
167
|
const owner = context.sessionManager.getSessionId();
|
|
@@ -178,7 +185,10 @@ class PiTermctrlController {
|
|
|
178
185
|
const commandPrefix = settingsManager.getShellCommandPrefix();
|
|
179
186
|
if (this.binary.kind === "missing") {
|
|
180
187
|
context.ui.notify(
|
|
181
|
-
|
|
188
|
+
noticeText(
|
|
189
|
+
"Termctrl",
|
|
190
|
+
`Terminal tools are unavailable: ${this.binary.reason}\nRun /skill:pi-termctrl to diagnose.`,
|
|
191
|
+
),
|
|
182
192
|
"warning",
|
|
183
193
|
);
|
|
184
194
|
if (settings.replaceBash) this.registerManagementTools();
|
package/src/ps-panel.ts
CHANGED
|
@@ -1,12 +1,27 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
DynamicBorder,
|
|
3
|
+
keyText,
|
|
4
|
+
type ExtensionContext,
|
|
5
|
+
type KeybindingsManager,
|
|
6
|
+
type Theme,
|
|
7
|
+
} from "@earendil-works/pi-coding-agent";
|
|
2
8
|
import {
|
|
3
9
|
matchesKey,
|
|
4
|
-
truncateToWidth,
|
|
5
10
|
visibleWidth,
|
|
11
|
+
wrapTextWithAnsi,
|
|
6
12
|
type Component,
|
|
7
13
|
type OverlayHandle,
|
|
8
14
|
type TUI,
|
|
9
15
|
} from "@earendil-works/pi-tui";
|
|
16
|
+
import {
|
|
17
|
+
clipPlain,
|
|
18
|
+
footerStatus,
|
|
19
|
+
hintLine,
|
|
20
|
+
joinInline,
|
|
21
|
+
noticeText,
|
|
22
|
+
statusMark,
|
|
23
|
+
type StatusKind,
|
|
24
|
+
} from "@ian-pascoe/pi-utils/ui";
|
|
10
25
|
import { formatDuration } from "./exit-notification.js";
|
|
11
26
|
import type { TermctrlEntry, TermctrlRegistry } from "./termctrl-registry.js";
|
|
12
27
|
|
|
@@ -40,6 +55,13 @@ function singleLine(text: string): string {
|
|
|
40
55
|
return text.replaceAll(/\s+/gu, " ").trim();
|
|
41
56
|
}
|
|
42
57
|
|
|
58
|
+
/** The Status Mark of an entry: running, ended cleanly, ended by a signal, or failed. */
|
|
59
|
+
function markKind(entry: TermctrlEntry): StatusKind {
|
|
60
|
+
if (entry.state === "running") return "active";
|
|
61
|
+
if (entry.exit?.signal !== null && entry.exit?.signal !== undefined) return "stopped";
|
|
62
|
+
return entry.exit?.code === 0 ? "done" : "failed";
|
|
63
|
+
}
|
|
64
|
+
|
|
43
65
|
/** The live `/ps` overlay: every entry in the process, a preview of the selected one, and stop keys. */
|
|
44
66
|
export class TermctrlPsPanel implements Component {
|
|
45
67
|
private entries: readonly TermctrlEntry[] = [];
|
|
@@ -85,40 +107,54 @@ export class TermctrlPsPanel implements Component {
|
|
|
85
107
|
1,
|
|
86
108
|
Math.min(Math.floor(this.tui.terminal.rows * 0.9), this.tui.terminal.rows - 2 * MARGIN),
|
|
87
109
|
);
|
|
88
|
-
|
|
89
|
-
|
|
110
|
+
const inner = width - 2;
|
|
111
|
+
const help = wrapTextWithAnsi(this.hints(), inner);
|
|
112
|
+
if (width < 8 || height < 8 + help.length) {
|
|
113
|
+
return wrapTextWithAnsi(this.theme.fg("muted", "Enlarge terminal"), width);
|
|
90
114
|
}
|
|
91
|
-
const inner = width - 4;
|
|
92
115
|
const now = Date.now();
|
|
93
116
|
const running = this.entries.filter((entry) => entry.state === "running").length;
|
|
117
|
+
const title = joinInline(this.theme, [
|
|
118
|
+
this.theme.fg("accent", this.theme.bold("Terminals and Background jobs")),
|
|
119
|
+
this.theme.fg("muted", `${running} running`),
|
|
120
|
+
]);
|
|
94
121
|
const header = [
|
|
95
|
-
|
|
96
|
-
this.theme.fg("warning", this.notice),
|
|
122
|
+
...wrapTextWithAnsi(title, inner),
|
|
123
|
+
...(this.notice === "" ? [] : wrapTextWithAnsi(this.theme.fg("warning", this.notice), inner)),
|
|
97
124
|
];
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
Math.min(this.entries.length || 1, Math.floor((height - 6) / 2)),
|
|
102
|
-
);
|
|
125
|
+
// Two borders and the blank lines around the title, the list, and the hints.
|
|
126
|
+
const room = height - 6 - header.length - help.length;
|
|
127
|
+
const listHeight = Math.max(1, Math.min(this.entries.length || 1, Math.floor(room / 2)));
|
|
103
128
|
const rows =
|
|
104
129
|
this.entries.length === 0
|
|
105
130
|
? [this.theme.fg("muted", "Nothing is running.")]
|
|
106
131
|
: this.visibleRows(listHeight).map((entry) => this.row(entry, inner, now));
|
|
107
|
-
const
|
|
108
|
-
const
|
|
132
|
+
const previewLines = this.previewLines(room - rows.length - 1, inner);
|
|
133
|
+
const border = new DynamicBorder((text) => this.theme.fg("border", text)).render(width);
|
|
109
134
|
const body = [
|
|
135
|
+
"",
|
|
110
136
|
...header,
|
|
137
|
+
"",
|
|
111
138
|
...rows,
|
|
112
|
-
|
|
113
|
-
|
|
139
|
+
...(previewLines.length === 0 ? [] : ["", ...previewLines]),
|
|
140
|
+
"",
|
|
114
141
|
...help,
|
|
142
|
+
"",
|
|
115
143
|
];
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
144
|
+
const framed = body.map(
|
|
145
|
+
(line) => ` ${line}${" ".repeat(Math.max(0, inner - visibleWidth(line)))} `,
|
|
146
|
+
);
|
|
147
|
+
return [...border, ...framed, ...border];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** The footer hints, drawn from the injected theme as Pi's selector does. */
|
|
151
|
+
private hints(): string {
|
|
152
|
+
return hintLine(this.theme, [
|
|
153
|
+
{ key: "\u2191\u2193", description: "navigate" },
|
|
154
|
+
{ key: "k", description: "stop" },
|
|
155
|
+
{ key: "x", description: "remove exited" },
|
|
156
|
+
{ key: keyText("tui.select.cancel"), description: "close" },
|
|
157
|
+
]);
|
|
122
158
|
}
|
|
123
159
|
|
|
124
160
|
invalidate(): void {}
|
|
@@ -163,17 +199,15 @@ export class TermctrlPsPanel implements Component {
|
|
|
163
199
|
ageOf(entry, now).padStart(6),
|
|
164
200
|
]
|
|
165
201
|
: [entry.id.padEnd(4), stateLabel(entry).padEnd(9)];
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
: line;
|
|
172
|
-
return truncateToWidth(styled, width, "…");
|
|
202
|
+
// Pi's selector marks the selected row with `→ `; the Status Mark follows it.
|
|
203
|
+
const text = clipPlain(`${columns.join(" ")} ${singleLine(entry.command)}`, width - 4);
|
|
204
|
+
const color = selected ? "accent" : entry.state === "exited" ? "muted" : "text";
|
|
205
|
+
const marker = selected ? this.theme.fg("accent", "→ ") : " ";
|
|
206
|
+
return `${marker}${statusMark(this.theme, markKind(entry))} ${this.theme.fg(color, text)}`;
|
|
173
207
|
}
|
|
174
208
|
|
|
175
|
-
private previewLines(height: number): string[] {
|
|
176
|
-
if (height
|
|
209
|
+
private previewLines(height: number, width: number): string[] {
|
|
210
|
+
if (height < 2) return [];
|
|
177
211
|
const selected = this.entries.find((entry) => entry.id === this.selectedId);
|
|
178
212
|
if (selected === undefined) return [];
|
|
179
213
|
const title = this.theme.fg(
|
|
@@ -181,7 +215,10 @@ export class TermctrlPsPanel implements Component {
|
|
|
181
215
|
selected.kind === "terminal" ? `${selected.id} screen` : `${selected.id} log tail`,
|
|
182
216
|
);
|
|
183
217
|
const text = this.preview.replace(/\n+$/u, "");
|
|
184
|
-
const lines =
|
|
218
|
+
const lines =
|
|
219
|
+
text === ""
|
|
220
|
+
? [this.theme.fg("muted", "(no output yet)")]
|
|
221
|
+
: text.split("\n").map((line) => this.theme.fg("text", clipPlain(line, width)));
|
|
185
222
|
return [title, ...lines.slice(-(height - 1))];
|
|
186
223
|
}
|
|
187
224
|
|
|
@@ -235,7 +272,7 @@ export class TermctrlPsPanel implements Component {
|
|
|
235
272
|
private stopSelected(): void {
|
|
236
273
|
const entry = this.entries.find((candidate) => candidate.id === this.selectedId);
|
|
237
274
|
if (entry === undefined || entry.state !== "running") return;
|
|
238
|
-
this.notice = `Stopping ${entry.id}
|
|
275
|
+
this.notice = `Stopping ${entry.id}...`;
|
|
239
276
|
void this.registry.stopByUser(entry.id).then(() => {
|
|
240
277
|
if (this.disposed) return;
|
|
241
278
|
this.notice = `Stopped ${entry.id}.`;
|
|
@@ -274,7 +311,16 @@ export class TermctrlPsController {
|
|
|
274
311
|
) {
|
|
275
312
|
const update = () => {
|
|
276
313
|
const running = registry.runningCount();
|
|
277
|
-
context.ui.setStatus(
|
|
314
|
+
context.ui.setStatus(
|
|
315
|
+
STATUS_KEY,
|
|
316
|
+
running > 0
|
|
317
|
+
? footerStatus(context.ui.theme, {
|
|
318
|
+
mark: "active",
|
|
319
|
+
name: "termctrl",
|
|
320
|
+
value: `${running} running`,
|
|
321
|
+
})
|
|
322
|
+
: undefined,
|
|
323
|
+
);
|
|
278
324
|
};
|
|
279
325
|
this.stopFooter = context.hasUI ? registry.onChange(update) : () => {};
|
|
280
326
|
if (context.hasUI) update();
|
|
@@ -322,7 +368,7 @@ export class TermctrlPsController {
|
|
|
322
368
|
},
|
|
323
369
|
)
|
|
324
370
|
.catch(() => {
|
|
325
|
-
this.context.ui.notify("The /ps view failed.", "error");
|
|
371
|
+
this.context.ui.notify(noticeText("Termctrl", "The /ps view failed."), "error");
|
|
326
372
|
})
|
|
327
373
|
.finally(() => {
|
|
328
374
|
this.panel?.dispose();
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import { Container, Spacer, Text, type Component } from "@earendil-works/pi-tui";
|
|
2
|
+
import {
|
|
3
|
+
appendDurationFooter,
|
|
4
|
+
callDurationFooter,
|
|
5
|
+
COLLAPSED_LINES,
|
|
6
|
+
expandHint,
|
|
7
|
+
joinInline,
|
|
8
|
+
previewBody,
|
|
9
|
+
toolHeader,
|
|
10
|
+
type DurationContext,
|
|
11
|
+
type UiTheme,
|
|
12
|
+
} from "@ian-pascoe/pi-utils/ui";
|
|
13
|
+
import { describeExitStatus, formatDuration } from "./exit-notification.js";
|
|
14
|
+
import type { ListResult, StopResult, TerminalResult, WaitResult } from "./terminal-tools.js";
|
|
15
|
+
|
|
16
|
+
type RenderTheme = Pick<UiTheme, "fg" | "bold">;
|
|
17
|
+
|
|
18
|
+
/** What a result renderer reads from Pi's render context. */
|
|
19
|
+
export interface ResultContext extends DurationContext {
|
|
20
|
+
readonly isError: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** What a result renderer reads from Pi's render options. */
|
|
24
|
+
export interface ResultOptions {
|
|
25
|
+
readonly expanded: boolean;
|
|
26
|
+
readonly isPartial: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface RenderedResult<TDetails> {
|
|
30
|
+
readonly content: readonly { readonly type: string; readonly text?: string }[];
|
|
31
|
+
readonly details: TDetails | undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The widest a typed or sent value gets in a call header before it is cut. */
|
|
35
|
+
const ARGUMENT_WIDTH = 60;
|
|
36
|
+
|
|
37
|
+
function column(...children: Component[]): Container {
|
|
38
|
+
const container = new Container();
|
|
39
|
+
for (const child of children) container.addChild(child);
|
|
40
|
+
return container;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function textBlock(lines: readonly string[]): Component {
|
|
44
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The call row: Pi's header, with the live `Elapsed` footer until a result row takes over. */
|
|
48
|
+
function callRow(
|
|
49
|
+
theme: RenderTheme & Pick<UiTheme, "fg">,
|
|
50
|
+
context: DurationContext,
|
|
51
|
+
header: string,
|
|
52
|
+
): Component {
|
|
53
|
+
return column(textBlock([header]), callDurationFooter(theme, context));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function quoted(value: string): string {
|
|
57
|
+
const characters = Array.from(JSON.stringify(value));
|
|
58
|
+
return characters.length <= ARGUMENT_WIDTH
|
|
59
|
+
? characters.join("")
|
|
60
|
+
: `${characters.slice(0, ARGUMENT_WIDTH - 3).join("")}...`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function when(condition: boolean, text: string): string | undefined {
|
|
64
|
+
return condition ? text : undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function joinArguments(parts: readonly (string | undefined)[]): string | undefined {
|
|
68
|
+
const present = parts.filter((part): part is string => part !== undefined);
|
|
69
|
+
return present.length === 0 ? undefined : present.join(" ");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Arguments of a `terminal_start` call as they stream in, so every one may be absent. */
|
|
73
|
+
export interface StartCallArguments {
|
|
74
|
+
readonly command?: string;
|
|
75
|
+
readonly cwd?: string;
|
|
76
|
+
readonly wait_ms?: number;
|
|
77
|
+
readonly notify?: boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** `terminal_start` call row: `terminal_start $ command`, as Pi's bash shows its command. */
|
|
81
|
+
export function renderStartCall(
|
|
82
|
+
args: StartCallArguments,
|
|
83
|
+
theme: RenderTheme,
|
|
84
|
+
context: DurationContext,
|
|
85
|
+
): Component {
|
|
86
|
+
const command = args.command === undefined ? "..." : args.command;
|
|
87
|
+
const extra = joinArguments([
|
|
88
|
+
args.cwd === undefined ? undefined : `in ${args.cwd}`,
|
|
89
|
+
args.wait_ms === undefined ? undefined : `wait ${args.wait_ms}ms`,
|
|
90
|
+
when(args.notify === false, "no notify"),
|
|
91
|
+
]);
|
|
92
|
+
return callRow(theme, context, toolHeader(theme, "terminal_start", `$ ${command}`, extra));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Arguments of a `terminal_send` call as they stream in. */
|
|
96
|
+
export interface SendCallArguments {
|
|
97
|
+
readonly id?: string;
|
|
98
|
+
readonly text?: string;
|
|
99
|
+
readonly keys?: readonly string[];
|
|
100
|
+
readonly wait_for_text?: string;
|
|
101
|
+
readonly wait_ms?: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** `terminal_send` call row: the Terminal id, then what is typed, pressed and waited for. */
|
|
105
|
+
export function renderSendCall(
|
|
106
|
+
args: SendCallArguments,
|
|
107
|
+
theme: RenderTheme,
|
|
108
|
+
context: DurationContext,
|
|
109
|
+
): Component {
|
|
110
|
+
const text = args.text ?? "";
|
|
111
|
+
const keys = args.keys ?? [];
|
|
112
|
+
const extra = joinArguments([
|
|
113
|
+
text === "" ? undefined : `type ${quoted(text)}`,
|
|
114
|
+
keys.length === 0 ? undefined : `press ${keys.join(" ")}`,
|
|
115
|
+
args.wait_for_text === undefined ? undefined : `wait_for ${quoted(args.wait_for_text)}`,
|
|
116
|
+
args.wait_ms === undefined ? undefined : `${args.wait_ms}ms`,
|
|
117
|
+
when(text === "" && keys.length === 0 && args.wait_for_text === undefined, "poll"),
|
|
118
|
+
]);
|
|
119
|
+
return callRow(theme, context, toolHeader(theme, "terminal_send", args.id ?? "...", extra));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** `terminal_stop` call row. */
|
|
123
|
+
export function renderStopCall(
|
|
124
|
+
args: { readonly id?: string },
|
|
125
|
+
theme: RenderTheme,
|
|
126
|
+
context: DurationContext,
|
|
127
|
+
): Component {
|
|
128
|
+
return callRow(theme, context, toolHeader(theme, "terminal_stop", args.id ?? "..."));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** `terminal_wait` call row: the ids it waits for, or every running entry. */
|
|
132
|
+
export function renderWaitCall(
|
|
133
|
+
args: { readonly ids?: readonly string[]; readonly wait_ms?: number },
|
|
134
|
+
theme: RenderTheme,
|
|
135
|
+
context: DurationContext,
|
|
136
|
+
): Component {
|
|
137
|
+
const ids = args.ids === undefined || args.ids.length === 0 ? undefined : args.ids.join(" ");
|
|
138
|
+
const extra = args.wait_ms === undefined ? undefined : `${args.wait_ms}ms`;
|
|
139
|
+
return callRow(theme, context, toolHeader(theme, "terminal_wait", ids, extra));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** `terminal_list` takes no arguments. */
|
|
143
|
+
export type ListCallArguments = Readonly<Record<never, never>>;
|
|
144
|
+
|
|
145
|
+
/** `terminal_list` call row. */
|
|
146
|
+
export function renderListCall(
|
|
147
|
+
_args: ListCallArguments,
|
|
148
|
+
theme: RenderTheme,
|
|
149
|
+
context: DurationContext,
|
|
150
|
+
): Component {
|
|
151
|
+
return callRow(theme, context, toolHeader(theme, "terminal_list"));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function contentText(content: RenderedResult<unknown>["content"]): string {
|
|
155
|
+
return content.map((part) => (part.type === "text" ? (part.text ?? "") : "")).join("");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Pi's result layout, as its bash renderer builds it: a blank line, the body, then the footer. */
|
|
159
|
+
function resultRow(
|
|
160
|
+
theme: RenderTheme,
|
|
161
|
+
context: ResultContext,
|
|
162
|
+
options: ResultOptions,
|
|
163
|
+
body: readonly string[],
|
|
164
|
+
): Component {
|
|
165
|
+
const container = new Container();
|
|
166
|
+
if (body.length > 0) {
|
|
167
|
+
container.addChild(new Spacer(1));
|
|
168
|
+
container.addChild(new Text(body.join("\n"), 0, 0));
|
|
169
|
+
}
|
|
170
|
+
appendDurationFooter(container, theme, context, { isPartial: options.isPartial });
|
|
171
|
+
return container;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function errorRow(
|
|
175
|
+
theme: RenderTheme,
|
|
176
|
+
context: ResultContext,
|
|
177
|
+
options: ResultOptions,
|
|
178
|
+
result: RenderedResult<unknown>,
|
|
179
|
+
): Component {
|
|
180
|
+
const lines = contentText(result.content).trim().split("\n");
|
|
181
|
+
return resultRow(
|
|
182
|
+
theme,
|
|
183
|
+
context,
|
|
184
|
+
options,
|
|
185
|
+
previewBody(theme, lines, {
|
|
186
|
+
limit: COLLAPSED_LINES.fallback,
|
|
187
|
+
expanded: options.expanded,
|
|
188
|
+
color: "error",
|
|
189
|
+
}),
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function warning(theme: RenderTheme, text: string): string {
|
|
194
|
+
return theme.fg("warning", `[${text}]`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function splitLines(text: string | undefined): string[] {
|
|
198
|
+
if (text === undefined || text === "") return [];
|
|
199
|
+
return text.replace(/\n+$/u, "").split("\n");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function screenLines(screen: string): string[] {
|
|
203
|
+
const lines = splitLines(screen);
|
|
204
|
+
return lines.length === 0 ? ["(blank)"] : lines;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* A Terminal's lines in bash's Collapsed View, the last 5 with the earlier lines counted in the
|
|
209
|
+
* Expand Hint. The Expanded View labels the scrolled-off lines apart from the screen.
|
|
210
|
+
*/
|
|
211
|
+
function terminalBody(
|
|
212
|
+
theme: RenderTheme,
|
|
213
|
+
expanded: boolean,
|
|
214
|
+
scrolledOff: readonly string[],
|
|
215
|
+
screen: readonly string[],
|
|
216
|
+
): string[] {
|
|
217
|
+
const styled = (lines: readonly string[]) => lines.map((line) => theme.fg("toolOutput", line));
|
|
218
|
+
if (!expanded) {
|
|
219
|
+
return previewBody(theme, [...scrolledOff, ...screen], {
|
|
220
|
+
limit: COLLAPSED_LINES.terminal,
|
|
221
|
+
expanded,
|
|
222
|
+
keep: "end",
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
if (scrolledOff.length === 0) return styled(screen);
|
|
226
|
+
return [
|
|
227
|
+
theme.fg("muted", "--- scrolled off ---"),
|
|
228
|
+
...styled(scrolledOff),
|
|
229
|
+
theme.fg("muted", "--- screen ---"),
|
|
230
|
+
...styled(screen),
|
|
231
|
+
];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function terminalWarnings(
|
|
235
|
+
theme: RenderTheme,
|
|
236
|
+
details: Pick<TerminalResult, "output_missing" | "full_output_path">,
|
|
237
|
+
): string[] {
|
|
238
|
+
const warnings: string[] = [];
|
|
239
|
+
if (details.output_missing === true) warnings.push(warning(theme, "Earlier output is missing"));
|
|
240
|
+
if (details.full_output_path !== undefined) {
|
|
241
|
+
warnings.push(warning(theme, `Full output: ${details.full_output_path}`));
|
|
242
|
+
}
|
|
243
|
+
return warnings;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function exitText(details: Pick<TerminalResult, "state" | "exit_code" | "signal">): string {
|
|
247
|
+
return details.state === "running"
|
|
248
|
+
? "running"
|
|
249
|
+
: describeExitStatus({ code: details.exit_code ?? null, signal: details.signal ?? null });
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function isFailedExit(details: Pick<TerminalResult, "state" | "exit_code" | "signal">): boolean {
|
|
253
|
+
return details.state === "exited" && (details.exit_code !== 0 || details.signal !== undefined);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** `terminal_start` and `terminal_send` result: a status line, then the screen like bash output. */
|
|
257
|
+
export function renderTerminalResult(
|
|
258
|
+
result: RenderedResult<TerminalResult>,
|
|
259
|
+
options: ResultOptions,
|
|
260
|
+
theme: RenderTheme,
|
|
261
|
+
context: ResultContext,
|
|
262
|
+
): Component {
|
|
263
|
+
const details = result.details;
|
|
264
|
+
if (context.isError || details === undefined) return errorRow(theme, context, options, result);
|
|
265
|
+
const status = joinInline(theme, [
|
|
266
|
+
theme.fg(isFailedExit(details) ? "warning" : "muted", `${details.id} ${exitText(details)}`),
|
|
267
|
+
theme.fg("muted", `settled: ${details.settle_reason}`),
|
|
268
|
+
details.changed ? undefined : theme.fg("muted", "screen unchanged"),
|
|
269
|
+
]);
|
|
270
|
+
const body = terminalBody(
|
|
271
|
+
theme,
|
|
272
|
+
options.expanded,
|
|
273
|
+
splitLines(details.scrolled_off),
|
|
274
|
+
screenLines(details.screen),
|
|
275
|
+
);
|
|
276
|
+
return resultRow(theme, context, options, [status, ...body, ...terminalWarnings(theme, details)]);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** `terminal_stop` result: the stop line, then the final screen or the job's recent output. */
|
|
280
|
+
export function renderStopResult(
|
|
281
|
+
result: RenderedResult<StopResult>,
|
|
282
|
+
options: ResultOptions,
|
|
283
|
+
theme: RenderTheme,
|
|
284
|
+
context: ResultContext,
|
|
285
|
+
): Component {
|
|
286
|
+
const details = result.details;
|
|
287
|
+
if (context.isError || details === undefined) return errorRow(theme, context, options, result);
|
|
288
|
+
const status = theme.fg("muted", contentText(result.content).split("\n")[0] ?? "");
|
|
289
|
+
const body =
|
|
290
|
+
details.kind === "terminal"
|
|
291
|
+
? terminalBody(
|
|
292
|
+
theme,
|
|
293
|
+
options.expanded,
|
|
294
|
+
splitLines(details.scrolled_off),
|
|
295
|
+
splitLines(details.screen),
|
|
296
|
+
)
|
|
297
|
+
: terminalBody(theme, options.expanded, [], splitLines(details.output));
|
|
298
|
+
return resultRow(theme, context, options, [status, ...body, ...terminalWarnings(theme, details)]);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** A list in ls's Collapsed View: its first 20 rows and the Expand Hint for the rest. */
|
|
302
|
+
function listRows(theme: RenderTheme, rows: readonly string[], expanded: boolean): string[] {
|
|
303
|
+
const hidden = rows.length - COLLAPSED_LINES.list;
|
|
304
|
+
if (expanded || hidden <= 0) return [...rows];
|
|
305
|
+
return [...rows.slice(0, COLLAPSED_LINES.list), expandHint(theme, hidden)];
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function entryRow(
|
|
309
|
+
theme: RenderTheme,
|
|
310
|
+
entry: {
|
|
311
|
+
id: string;
|
|
312
|
+
command: string;
|
|
313
|
+
state: string;
|
|
314
|
+
exit_code?: number | undefined;
|
|
315
|
+
signal?: string | undefined;
|
|
316
|
+
},
|
|
317
|
+
ageSeconds: number,
|
|
318
|
+
): string {
|
|
319
|
+
const state =
|
|
320
|
+
entry.state === "running"
|
|
321
|
+
? "running"
|
|
322
|
+
: describeExitStatus({ code: entry.exit_code ?? null, signal: entry.signal ?? null });
|
|
323
|
+
return `${theme.fg("accent", entry.id)} ${joinInline(theme, [
|
|
324
|
+
theme.fg("toolOutput", state),
|
|
325
|
+
theme.fg("muted", formatDuration(ageSeconds * 1000)),
|
|
326
|
+
theme.fg("toolOutput", entry.command),
|
|
327
|
+
])}`;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** `terminal_list` result: one row per Terminal and Background job, like ls. */
|
|
331
|
+
export function renderListResult(
|
|
332
|
+
result: RenderedResult<ListResult>,
|
|
333
|
+
options: ResultOptions,
|
|
334
|
+
theme: RenderTheme,
|
|
335
|
+
context: ResultContext,
|
|
336
|
+
): Component {
|
|
337
|
+
const details = result.details;
|
|
338
|
+
if (context.isError || details === undefined) return errorRow(theme, context, options, result);
|
|
339
|
+
const rows = [
|
|
340
|
+
...details.terminals.map((entry) => entryRow(theme, entry, entry.age_seconds)),
|
|
341
|
+
...details.background_jobs.map((entry) => {
|
|
342
|
+
const row = entryRow(theme, entry, entry.age_seconds);
|
|
343
|
+
return options.expanded ? `${row} ${theme.fg("muted", `(log ${entry.log_path})`)}` : row;
|
|
344
|
+
}),
|
|
345
|
+
];
|
|
346
|
+
const body =
|
|
347
|
+
rows.length === 0 ? [theme.fg("muted", "(none)")] : listRows(theme, rows, options.expanded);
|
|
348
|
+
return resultRow(theme, context, options, body);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const WAIT_REASON_SUMMARY = {
|
|
352
|
+
exited: undefined,
|
|
353
|
+
timeout: "Nothing exited before the wait ended",
|
|
354
|
+
message: "Returned early: a message is waiting",
|
|
355
|
+
aborted: "Wait cancelled",
|
|
356
|
+
nothing_running: "Nothing to wait for",
|
|
357
|
+
} as const satisfies Record<WaitResult["reason"], string | undefined>;
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* `terminal_wait` result: what exited and what still runs, one row each. The Expanded View adds
|
|
361
|
+
* each exit's last lines.
|
|
362
|
+
*/
|
|
363
|
+
export function renderWaitResult(
|
|
364
|
+
result: RenderedResult<WaitResult>,
|
|
365
|
+
options: ResultOptions,
|
|
366
|
+
theme: RenderTheme,
|
|
367
|
+
context: ResultContext,
|
|
368
|
+
): Component {
|
|
369
|
+
const details = result.details;
|
|
370
|
+
if (context.isError || details === undefined) return errorRow(theme, context, options, result);
|
|
371
|
+
const summary = WAIT_REASON_SUMMARY[details.reason];
|
|
372
|
+
const rows: string[] = [];
|
|
373
|
+
for (const exit of details.exited) {
|
|
374
|
+
const status = describeExitStatus({
|
|
375
|
+
code: exit.exit_code ?? null,
|
|
376
|
+
signal: exit.signal ?? null,
|
|
377
|
+
});
|
|
378
|
+
rows.push(
|
|
379
|
+
`${theme.fg("accent", exit.id)} ${joinInline(theme, [
|
|
380
|
+
theme.fg("toolOutput", status),
|
|
381
|
+
theme.fg("muted", formatDuration(exit.duration_ms)),
|
|
382
|
+
theme.fg("toolOutput", exit.command),
|
|
383
|
+
])}`,
|
|
384
|
+
);
|
|
385
|
+
if (options.expanded) {
|
|
386
|
+
for (const line of splitLines(exit.output)) rows.push(` ${theme.fg("toolOutput", line)}`);
|
|
387
|
+
if (exit.log_path !== undefined) rows.push(theme.fg("muted", ` Log: ${exit.log_path}`));
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
for (const entry of details.running) {
|
|
391
|
+
rows.push(entryRow(theme, { ...entry, state: "running" }, entry.age_seconds));
|
|
392
|
+
}
|
|
393
|
+
const body = [
|
|
394
|
+
...(summary === undefined ? [] : [theme.fg("muted", summary)]),
|
|
395
|
+
...listRows(theme, rows, options.expanded),
|
|
396
|
+
];
|
|
397
|
+
return resultRow(theme, context, options, body);
|
|
398
|
+
}
|
package/src/terminal-tools.ts
CHANGED
|
@@ -20,6 +20,17 @@ import { describeExitStatus, formatExitNotice, lastLines } from "./exit-notifica
|
|
|
20
20
|
import { termctrlTemporaryDirectory } from "./termctrl-driver.js";
|
|
21
21
|
import type { LineByteLimits, TerminalViewport } from "./pi-termctrl-settings.js";
|
|
22
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";
|
|
23
34
|
import { TROUBLESHOOTING_HINT } from "./troubleshooting-skill.js";
|
|
24
35
|
|
|
25
36
|
/** Screen quiet period that settles a Terminal. */
|
|
@@ -112,7 +123,7 @@ const TerminalResultSchema = Type.Object({
|
|
|
112
123
|
}),
|
|
113
124
|
),
|
|
114
125
|
});
|
|
115
|
-
type TerminalResult = Static<typeof TerminalResultSchema>;
|
|
126
|
+
export type TerminalResult = Static<typeof TerminalResultSchema>;
|
|
116
127
|
|
|
117
128
|
const ListEntrySchema = Type.Object({
|
|
118
129
|
id: Type.String(),
|
|
@@ -128,7 +139,7 @@ const ListResultSchema = Type.Object({
|
|
|
128
139
|
Type.Object({ ...ListEntrySchema.properties, log_path: Type.String() }),
|
|
129
140
|
),
|
|
130
141
|
});
|
|
131
|
-
type ListResult = Static<typeof ListResultSchema>;
|
|
142
|
+
export type ListResult = Static<typeof ListResultSchema>;
|
|
132
143
|
|
|
133
144
|
const StopResultSchema = Type.Object({
|
|
134
145
|
id: Type.String(),
|
|
@@ -166,7 +177,7 @@ const StopResultSchema = Type.Object({
|
|
|
166
177
|
),
|
|
167
178
|
output: Type.Optional(Type.String({ description: "Background job only: its recent output" })),
|
|
168
179
|
});
|
|
169
|
-
type StopResult = Static<typeof StopResultSchema>;
|
|
180
|
+
export type StopResult = Static<typeof StopResultSchema>;
|
|
170
181
|
|
|
171
182
|
const WaitMsSchema = Type.Optional(
|
|
172
183
|
Type.Integer({ minimum: 0, description: "Maximum milliseconds to wait (clamped to 300000)" }),
|
|
@@ -268,7 +279,7 @@ const WaitResultSchema = Type.Object({
|
|
|
268
279
|
}),
|
|
269
280
|
),
|
|
270
281
|
});
|
|
271
|
-
type WaitResult = Static<typeof WaitResultSchema>;
|
|
282
|
+
export type WaitResult = Static<typeof WaitResultSchema>;
|
|
272
283
|
type WaitReason = WaitResult["reason"];
|
|
273
284
|
|
|
274
285
|
/** Clamp a requested wait to the fixed 0 to 5 minute range. */
|
|
@@ -837,6 +848,8 @@ export function createTerminalStartTool(runtime: TerminalToolRuntime) {
|
|
|
837
848
|
openWorldHint: true,
|
|
838
849
|
},
|
|
839
850
|
outputSchema: TerminalResultSchema,
|
|
851
|
+
renderCall: renderStartCall,
|
|
852
|
+
renderResult: renderTerminalResult,
|
|
840
853
|
async execute(_toolCallId, params, signal, _onUpdate, context) {
|
|
841
854
|
const cwd = await resolveWorkingDirectory(context.cwd, params.cwd);
|
|
842
855
|
const entry = await runtime.registry.startTerminal(ownerOf(context), {
|
|
@@ -894,6 +907,8 @@ export function createTerminalSendTool(runtime: TerminalToolRuntime) {
|
|
|
894
907
|
openWorldHint: true,
|
|
895
908
|
},
|
|
896
909
|
outputSchema: TerminalResultSchema,
|
|
910
|
+
renderCall: renderSendCall,
|
|
911
|
+
renderResult: renderTerminalResult,
|
|
897
912
|
async execute(_toolCallId, params, signal, _onUpdate, context) {
|
|
898
913
|
const entry = runtime.registry.find(ownerOf(context), params.id);
|
|
899
914
|
if (entry === undefined) throw unknownId(params.id);
|
|
@@ -1004,6 +1019,8 @@ export function createTerminalStopTool({ registry, scrollback }: TerminalResultR
|
|
|
1004
1019
|
openWorldHint: false,
|
|
1005
1020
|
},
|
|
1006
1021
|
outputSchema: StopResultSchema,
|
|
1022
|
+
renderCall: renderStopCall,
|
|
1023
|
+
renderResult: renderStopResult,
|
|
1007
1024
|
async execute(_toolCallId, params, signal, _onUpdate, context) {
|
|
1008
1025
|
const owner = ownerOf(context);
|
|
1009
1026
|
const known = registry.find(owner, params.id);
|
|
@@ -1166,6 +1183,8 @@ export function createTerminalWaitTool(runtime: TerminalWaitRuntime) {
|
|
|
1166
1183
|
openWorldHint: false,
|
|
1167
1184
|
},
|
|
1168
1185
|
outputSchema: WaitResultSchema,
|
|
1186
|
+
renderCall: renderWaitCall,
|
|
1187
|
+
renderResult: renderWaitResult,
|
|
1169
1188
|
async execute(_toolCallId, params, signal, _onUpdate, context) {
|
|
1170
1189
|
const owner = ownerOf(context);
|
|
1171
1190
|
const targets = waitTargets(registry, owner, params.ids);
|
|
@@ -1238,6 +1257,8 @@ export function createTerminalListTool(registry: TermctrlRegistry) {
|
|
|
1238
1257
|
openWorldHint: false,
|
|
1239
1258
|
},
|
|
1240
1259
|
outputSchema: ListResultSchema,
|
|
1260
|
+
renderCall: renderListCall,
|
|
1261
|
+
renderResult: renderListResult,
|
|
1241
1262
|
async execute(_toolCallId, _params, _signal, _onUpdate, context) {
|
|
1242
1263
|
const now = Date.now();
|
|
1243
1264
|
const entries = registry.ownedEntries(ownerOf(context));
|