shariq-pi-extensions 0.1.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/LICENSE +21 -0
- package/README.md +94 -0
- package/THIRD_PARTY_NOTICES.md +11 -0
- package/docs/ARCHITECTURE.md +54 -0
- package/docs/DEVELOPMENT.md +88 -0
- package/docs/EXTENSIONS.md +89 -0
- package/extensions/antigravity-provider/LICENSE +21 -0
- package/extensions/antigravity-provider/README.md +23 -0
- package/extensions/antigravity-provider/antigravity/cloud-code-assist.ts +519 -0
- package/extensions/antigravity-provider/antigravity/index.ts +67 -0
- package/extensions/antigravity-provider/antigravity/models.ts +184 -0
- package/extensions/antigravity-provider/antigravity/oauth.ts +499 -0
- package/extensions/antigravity-provider/index.ts +3 -0
- package/extensions/ask-user/README.md +21 -0
- package/extensions/ask-user/index.ts +417 -0
- package/extensions/background-terminals/README.md +43 -0
- package/extensions/background-terminals/index.ts +400 -0
- package/extensions/background-terminals/src/manager.ts +480 -0
- package/extensions/background-terminals/src/output-buffer.ts +110 -0
- package/extensions/background-terminals/src/presentation.ts +74 -0
- package/extensions/background-terminals/src/types.ts +47 -0
- package/extensions/background-terminals/src/ui.ts +478 -0
- package/extensions/context-usage/LICENSE +21 -0
- package/extensions/context-usage/README.md +20 -0
- package/extensions/context-usage/index.ts +2006 -0
- package/extensions/context-usage/lib/ansi.ts +60 -0
- package/extensions/context-usage/lib/boundary.ts +27 -0
- package/extensions/context-usage/lib/chat.ts +190 -0
- package/extensions/context-usage/lib/config.ts +30 -0
- package/extensions/context-usage/lib/fmt.ts +59 -0
- package/extensions/context-usage/lib/style.ts +260 -0
- package/extensions/copy-all/README.md +15 -0
- package/extensions/copy-all/index.ts +72 -0
- package/extensions/factory-provider/API_KEYS.md +17 -0
- package/extensions/factory-provider/AUTH.md +15 -0
- package/extensions/factory-provider/README.md +90 -0
- package/extensions/factory-provider/REFRESH_MODELS.md +185 -0
- package/extensions/factory-provider/UPDATING.md +97 -0
- package/extensions/factory-provider/factory/api-keys.ts +407 -0
- package/extensions/factory-provider/factory/auth.ts +132 -0
- package/extensions/factory-provider/factory/constants.ts +15 -0
- package/extensions/factory-provider/factory/dashboard.ts +363 -0
- package/extensions/factory-provider/factory/droid.ts +84 -0
- package/extensions/factory-provider/factory/limits.ts +342 -0
- package/extensions/factory-provider/factory/models.ts +283 -0
- package/extensions/factory-provider/factory/responses.ts +331 -0
- package/extensions/factory-provider/factory/warm-theme.ts +53 -0
- package/extensions/factory-provider/factory/websocket.ts +130 -0
- package/extensions/factory-provider/index.ts +204 -0
- package/extensions/factory-provider/scripts/audit-reasoning-efforts.py +84 -0
- package/extensions/firecrawl-web/README.md +31 -0
- package/extensions/firecrawl-web/auth.ts +92 -0
- package/extensions/firecrawl-web/client.ts +238 -0
- package/extensions/firecrawl-web/index.ts +96 -0
- package/extensions/firecrawl-web/output.ts +125 -0
- package/extensions/git-info/README.md +25 -0
- package/extensions/git-info/index.ts +162 -0
- package/extensions/git-info/src/git.ts +206 -0
- package/extensions/git-info/src/ui.ts +291 -0
- package/extensions/goal/README.md +44 -0
- package/extensions/goal/constants.ts +4 -0
- package/extensions/goal/extension.ts +1064 -0
- package/extensions/goal/index.ts +2 -0
- package/extensions/goal/types.ts +56 -0
- package/extensions/goal/ui.ts +334 -0
- package/extensions/orchestration/README.md +58 -0
- package/extensions/orchestration/dashboard.ts +192 -0
- package/extensions/orchestration/engine.ts +706 -0
- package/extensions/orchestration/index.ts +243 -0
- package/extensions/orchestration/prompts.ts +124 -0
- package/extensions/orchestration/protocol.ts +116 -0
- package/extensions/orchestration/settings-ui.ts +41 -0
- package/extensions/orchestration/settings.ts +59 -0
- package/extensions/orchestration/storage.ts +49 -0
- package/extensions/orchestration/types.ts +105 -0
- package/extensions/performance-status/README.md +5 -0
- package/extensions/performance-status/index.ts +171 -0
- package/extensions/pi-memory/README.md +53 -0
- package/extensions/pi-memory/index.ts +326 -0
- package/extensions/pi-memory/src/config.ts +75 -0
- package/extensions/pi-memory/src/database.ts +617 -0
- package/extensions/pi-memory/src/extraction.ts +178 -0
- package/extensions/pi-memory/src/project.ts +80 -0
- package/extensions/pi-memory/src/projection.ts +59 -0
- package/extensions/pi-memory/src/redaction.ts +60 -0
- package/extensions/pi-memory/src/retrieval.ts +30 -0
- package/extensions/pi-memory/src/service.ts +194 -0
- package/extensions/pi-memory/src/session.ts +146 -0
- package/extensions/pi-memory/src/types.ts +82 -0
- package/extensions/shared/README.md +18 -0
- package/extensions/shared/activity-status.ts +31 -0
- package/extensions/shared/child-session.ts +150 -0
- package/extensions/shared/context-utilization.ts +47 -0
- package/extensions/shared/dashboard-state.ts +99 -0
- package/extensions/shared/tool-call-timeout.ts +104 -0
- package/extensions/shared/tui-dashboard.ts +139 -0
- package/extensions/shell-shortcuts/README.md +17 -0
- package/extensions/shell-shortcuts/features/exit-alias.ts +10 -0
- package/extensions/shell-shortcuts/index.ts +6 -0
- package/extensions/subagents/README.md +89 -0
- package/extensions/subagents/index.ts +1386 -0
- package/extensions/subagents/src/backend.ts +69 -0
- package/extensions/subagents/src/backends/pi.ts +781 -0
- package/extensions/subagents/src/catalog.ts +85 -0
- package/extensions/subagents/src/config.ts +241 -0
- package/extensions/subagents/src/context.ts +57 -0
- package/extensions/subagents/src/coordinator.ts +64 -0
- package/extensions/subagents/src/domain.ts +294 -0
- package/extensions/subagents/src/format.ts +74 -0
- package/extensions/subagents/src/manager.ts +813 -0
- package/extensions/subagents/src/prompt.ts +94 -0
- package/extensions/subagents/src/result-delivery.ts +20 -0
- package/extensions/subagents/src/runtime.ts +48 -0
- package/extensions/subagents/src/ui/takeover.ts +695 -0
- package/extensions/subagents/src/ui/transcript.ts +182 -0
- package/extensions/subagents/src/worktree.ts +175 -0
- package/extensions/web-fetch/README.md +13 -0
- package/extensions/web-fetch/index.ts +441 -0
- package/package.json +117 -0
- package/skills/background-terminals/SKILL.md +42 -0
- package/skills/orchestration/SKILL.md +33 -0
- package/skills/subagents/SKILL.md +89 -0
- package/themes/ember-warm-dark.json +85 -0
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type {
|
|
4
|
+
ExtensionAPI,
|
|
5
|
+
ExtensionCommandContext,
|
|
6
|
+
ExtensionContext,
|
|
7
|
+
ExtensionUIContext,
|
|
8
|
+
} from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import { Markdown, Text } from "@earendil-works/pi-tui";
|
|
11
|
+
import { Type } from "typebox";
|
|
12
|
+
import { oneLine, sanitizeTerminalText, stateLabel } from "../shared/tui-dashboard.ts";
|
|
13
|
+
import { TerminalManager, MAX_RUNNING_TERMINALS } from "./src/manager.ts";
|
|
14
|
+
import {
|
|
15
|
+
formatCompletion,
|
|
16
|
+
formatReadResult,
|
|
17
|
+
formatTerminal,
|
|
18
|
+
} from "./src/presentation.ts";
|
|
19
|
+
import type { TerminalReadResult, TerminalSnapshot } from "./src/types.ts";
|
|
20
|
+
import { openTerminalDashboard } from "./src/ui.ts";
|
|
21
|
+
|
|
22
|
+
const STATUS_KEY = "background-terminals";
|
|
23
|
+
|
|
24
|
+
function titleFrom(command: string, title?: string): string {
|
|
25
|
+
const requested = oneLine(title ?? "").slice(0, 100);
|
|
26
|
+
if (requested) return requested;
|
|
27
|
+
return oneLine(command).slice(0, 100) || "terminal";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function resolveCwd(base: string, requested?: string): string {
|
|
31
|
+
const cwd = path.resolve(base, requested ?? ".");
|
|
32
|
+
if (!fs.existsSync(cwd) || !fs.statSync(cwd).isDirectory()) {
|
|
33
|
+
throw new Error(`working_dir is not a directory: ${cwd}`);
|
|
34
|
+
}
|
|
35
|
+
return cwd;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default function backgroundTerminals(pi: ExtensionAPI) {
|
|
39
|
+
let manager: TerminalManager | undefined;
|
|
40
|
+
let sessionContext: ExtensionContext | undefined;
|
|
41
|
+
let ui: ExtensionUIContext | undefined;
|
|
42
|
+
let unsubscribe: (() => void) | undefined;
|
|
43
|
+
let lastStatus = "";
|
|
44
|
+
const pendingResults = new Map<string, TerminalSnapshot>();
|
|
45
|
+
const modelOwned = new Set<string>();
|
|
46
|
+
|
|
47
|
+
const getManager = (): TerminalManager => {
|
|
48
|
+
if (manager) return manager;
|
|
49
|
+
manager = new TerminalManager();
|
|
50
|
+
manager.setOnSettled((snapshot) => {
|
|
51
|
+
if (!modelOwned.delete(snapshot.id)) {
|
|
52
|
+
ui?.notify(
|
|
53
|
+
`${snapshot.id} "${oneLine(snapshot.title)}" ${snapshot.status} (exit ${snapshot.exitCode ?? "?"})`,
|
|
54
|
+
snapshot.status === "failed" ? "error" : "info",
|
|
55
|
+
);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
pendingResults.set(snapshot.id, snapshot);
|
|
59
|
+
if (sessionContext?.isIdle()) flushResults();
|
|
60
|
+
});
|
|
61
|
+
unsubscribe = manager.view.subscribe(updateStatus);
|
|
62
|
+
updateStatus();
|
|
63
|
+
return manager;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const consume = (id: string): void => {
|
|
67
|
+
pendingResults.delete(id);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
function updateStatus(): void {
|
|
71
|
+
if (!ui || !manager) return;
|
|
72
|
+
const terminals = manager.list();
|
|
73
|
+
const running = terminals.filter((terminal) => terminal.status === "running").length;
|
|
74
|
+
const next = running > 0
|
|
75
|
+
? `${running} background terminal${running === 1 ? "" : "s"} · /term`
|
|
76
|
+
: "";
|
|
77
|
+
if (next === lastStatus) return;
|
|
78
|
+
lastStatus = next;
|
|
79
|
+
if (!next) ui.setStatus(STATUS_KEY, undefined);
|
|
80
|
+
else {
|
|
81
|
+
ui.setStatus(
|
|
82
|
+
STATUS_KEY,
|
|
83
|
+
stateLabel(ui.theme, "active", next),
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function deliver(snapshot: TerminalSnapshot): void {
|
|
89
|
+
pi.sendMessage(
|
|
90
|
+
{
|
|
91
|
+
customType: "background-terminal-result",
|
|
92
|
+
content: formatCompletion(snapshot),
|
|
93
|
+
display: true,
|
|
94
|
+
details: {
|
|
95
|
+
id: snapshot.id,
|
|
96
|
+
title: snapshot.title,
|
|
97
|
+
status: snapshot.status,
|
|
98
|
+
exitCode: snapshot.exitCode,
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
{ deliverAs: "followUp", triggerTurn: true },
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function flushResults(): void {
|
|
106
|
+
const results = [...pendingResults.values()];
|
|
107
|
+
pendingResults.clear();
|
|
108
|
+
for (const snapshot of results) deliver(snapshot);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
pi.on("session_start", (_event, ctx) => {
|
|
112
|
+
sessionContext = ctx;
|
|
113
|
+
if (ctx.hasUI) ui = ctx.ui;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
pi.on("agent_settled", flushResults);
|
|
117
|
+
|
|
118
|
+
pi.on("session_shutdown", async () => {
|
|
119
|
+
sessionContext = undefined;
|
|
120
|
+
pendingResults.clear();
|
|
121
|
+
modelOwned.clear();
|
|
122
|
+
unsubscribe?.();
|
|
123
|
+
unsubscribe = undefined;
|
|
124
|
+
ui?.setStatus(STATUS_KEY, undefined);
|
|
125
|
+
ui = undefined;
|
|
126
|
+
lastStatus = "";
|
|
127
|
+
const closing = manager;
|
|
128
|
+
manager = undefined;
|
|
129
|
+
await closing?.dispose();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
pi.registerTool({
|
|
133
|
+
name: "start_terminal",
|
|
134
|
+
label: "Start Background Terminal",
|
|
135
|
+
description:
|
|
136
|
+
"Start a command in a real background pseudo-terminal (PTY) and return its terminal id. " +
|
|
137
|
+
"Use for development servers, watchers, long builds, and commands that need terminal semantics. " +
|
|
138
|
+
"The PTY accepts later input through write_terminal, captures a bounded live tail in memory, writes a size-limited private temporary log, and is stopped at session shutdown. " +
|
|
139
|
+
`At most ${MAX_RUNNING_TERMINALS} terminals may run concurrently.`,
|
|
140
|
+
promptSnippet: "Start an interactive or long-running command in a managed background PTY.",
|
|
141
|
+
promptGuidelines: [
|
|
142
|
+
"Use start_terminal by default for servers, watchers, downloads, long or uncertain builds and tests, interactive shells, and any command that should not occupy the main turn; reserve bash for short commands whose result is needed immediately. Never use a large bash timeout merely to wait for long work.",
|
|
143
|
+
"After start_terminal returns, continue only genuinely independent work. If none remains, end the turn immediately. The terminal completion automatically sends a follow-up and starts the next parent turn; do not call read_terminal, list_terminals, or start a timer merely to check whether it finished.",
|
|
144
|
+
"Use stop_terminal when a managed process is no longer needed. Background terminals are session-scoped and are stopped during session shutdown or reload.",
|
|
145
|
+
],
|
|
146
|
+
parameters: Type.Object({
|
|
147
|
+
command: Type.String({ minLength: 1, maxLength: 32_000, description: "Shell command to run in the PTY." }),
|
|
148
|
+
title: Type.Optional(Type.String({ maxLength: 100, description: "Short human-readable terminal name; defaults to the command." })),
|
|
149
|
+
working_dir: Type.Optional(Type.String({ description: "Working directory relative to the current workspace, or an absolute directory." })),
|
|
150
|
+
wait_ms: Type.Optional(Type.Integer({ minimum: 0, maximum: 5_000, description: "Optional initial wait for startup output; default 300ms." })),
|
|
151
|
+
}),
|
|
152
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
153
|
+
const command = params.command.trim();
|
|
154
|
+
if (!command) throw new Error("command must not be empty.");
|
|
155
|
+
const terminal = getManager().start({
|
|
156
|
+
command,
|
|
157
|
+
title: titleFrom(command, params.title),
|
|
158
|
+
cwd: resolveCwd(ctx.cwd, params.working_dir),
|
|
159
|
+
});
|
|
160
|
+
modelOwned.add(terminal.id);
|
|
161
|
+
let result: TerminalReadResult;
|
|
162
|
+
try {
|
|
163
|
+
result = await getManager().read(terminal.id, {
|
|
164
|
+
cursor: 0,
|
|
165
|
+
waitMs: params.wait_ms ?? 300,
|
|
166
|
+
signal,
|
|
167
|
+
});
|
|
168
|
+
} catch (error) {
|
|
169
|
+
await getManager().kill([terminal.id]).catch(() => undefined);
|
|
170
|
+
modelOwned.delete(terminal.id);
|
|
171
|
+
consume(terminal.id);
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
if (result.snapshot.status !== "running") consume(terminal.id);
|
|
175
|
+
let text = `Started background terminal ${terminal.id} "${oneLine(terminal.title)}" (pid ${terminal.pid}). Use cursor ${result.cursor} for incremental reads.`;
|
|
176
|
+
if (result.text || result.snapshot.status !== "running") {
|
|
177
|
+
text += `\n\n${formatReadResult(result)}`;
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
content: [{ type: "text", text }],
|
|
181
|
+
details: {
|
|
182
|
+
id: terminal.id,
|
|
183
|
+
pid: terminal.pid,
|
|
184
|
+
status: result.snapshot.status,
|
|
185
|
+
cursor: result.cursor,
|
|
186
|
+
logPath: terminal.output.spillPath,
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
},
|
|
190
|
+
renderCall(args, theme) {
|
|
191
|
+
return new Text(
|
|
192
|
+
`${theme.fg("toolTitle", theme.bold("terminal"))} ${theme.fg("accent", oneLine(args.title ?? args.command))}\n${theme.fg("dim", `$ ${oneLine(args.command)}`)}`,
|
|
193
|
+
0,
|
|
194
|
+
0,
|
|
195
|
+
);
|
|
196
|
+
},
|
|
197
|
+
renderResult(result, { isPartial }, theme) {
|
|
198
|
+
if (isPartial) return new Text(theme.fg("warning", "Starting PTY…"), 0, 0);
|
|
199
|
+
const details = result.details as { id?: string; pid?: number; status?: string } | undefined;
|
|
200
|
+
return new Text(
|
|
201
|
+
`${stateLabel(theme, details?.status === "running" ? "active" : "success", details?.status ?? "started")} ${theme.fg("muted", `${details?.id ?? "?"} · pid ${details?.pid ?? "?"} · /term to inspect`)}`,
|
|
202
|
+
0,
|
|
203
|
+
0,
|
|
204
|
+
);
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
pi.registerTool({
|
|
209
|
+
name: "read_terminal",
|
|
210
|
+
label: "Read Background Terminal",
|
|
211
|
+
description:
|
|
212
|
+
"Read output from a managed background terminal only when the user asks for progress or current output is required for immediate interaction. " +
|
|
213
|
+
"Do not use it to wait for completion: terminal settlement automatically sends a follow-up that starts the next parent turn. " +
|
|
214
|
+
"Pass the cursor from the previous terminal result to receive only newer output. Output is tail-truncated for model context; the private log path is reported when available.",
|
|
215
|
+
promptSnippet: "Read new output from a managed background terminal by id and cursor.",
|
|
216
|
+
parameters: Type.Object({
|
|
217
|
+
id: Type.String({ description: 'Terminal id, for example "term-1".' }),
|
|
218
|
+
cursor: Type.Optional(Type.Integer({ minimum: 0, description: "Cursor returned by the previous terminal operation; omit for the retained tail." })),
|
|
219
|
+
wait_ms: Type.Optional(Type.Integer({ minimum: 0, maximum: 10_000, description: "Long-poll duration when no newer output exists; default 0." })),
|
|
220
|
+
}),
|
|
221
|
+
async execute(_toolCallId, params, signal) {
|
|
222
|
+
const result = await getManager().read(params.id, {
|
|
223
|
+
cursor: params.cursor,
|
|
224
|
+
waitMs: params.wait_ms,
|
|
225
|
+
signal,
|
|
226
|
+
});
|
|
227
|
+
return {
|
|
228
|
+
content: [{ type: "text", text: formatReadResult(result) }],
|
|
229
|
+
details: {
|
|
230
|
+
id: params.id,
|
|
231
|
+
status: result.snapshot.status,
|
|
232
|
+
cursor: result.cursor,
|
|
233
|
+
exitCode: result.snapshot.exitCode,
|
|
234
|
+
omittedBytes: result.omittedBytes,
|
|
235
|
+
logPath: result.snapshot.output.spillPath,
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
pi.registerTool({
|
|
242
|
+
name: "write_terminal",
|
|
243
|
+
label: "Write Background Terminal",
|
|
244
|
+
description:
|
|
245
|
+
"Send input to a running background PTY, optionally press Enter, wait briefly, and return output produced since the supplied cursor. " +
|
|
246
|
+
"Use input \\u0003 with press_enter=false to send Ctrl+C inside the PTY; use stop_terminal to terminate the managed process tree.",
|
|
247
|
+
promptSnippet: "Send input to a running background PTY and collect its response.",
|
|
248
|
+
parameters: Type.Object({
|
|
249
|
+
id: Type.String({ description: "Terminal id." }),
|
|
250
|
+
input: Type.String({ maxLength: 65_536, description: "Text or control character to write." }),
|
|
251
|
+
press_enter: Type.Optional(Type.Boolean({ description: "Append Enter after the input; default true." })),
|
|
252
|
+
cursor: Type.Optional(Type.Integer({ minimum: 0, description: "Previous output cursor for incremental output." })),
|
|
253
|
+
wait_ms: Type.Optional(Type.Integer({ minimum: 0, maximum: 5_000, description: "Wait for resulting output; default 250ms." })),
|
|
254
|
+
}),
|
|
255
|
+
async execute(_toolCallId, params, signal) {
|
|
256
|
+
const current = getManager().get(params.id);
|
|
257
|
+
if (!current) throw new Error(`Unknown background terminal "${params.id}".`);
|
|
258
|
+
const cursor = params.cursor ?? current.output.cursor;
|
|
259
|
+
const input = params.press_enter === false ? params.input : `${params.input}\r`;
|
|
260
|
+
getManager().write(params.id, input);
|
|
261
|
+
const result = await getManager().read(params.id, {
|
|
262
|
+
cursor,
|
|
263
|
+
waitMs: params.wait_ms ?? 250,
|
|
264
|
+
signal,
|
|
265
|
+
});
|
|
266
|
+
return {
|
|
267
|
+
content: [{ type: "text", text: formatReadResult(result) }],
|
|
268
|
+
details: {
|
|
269
|
+
id: params.id,
|
|
270
|
+
status: result.snapshot.status,
|
|
271
|
+
cursor: result.cursor,
|
|
272
|
+
exitCode: result.snapshot.exitCode,
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
pi.registerTool({
|
|
279
|
+
name: "list_terminals",
|
|
280
|
+
label: "List Background Terminals",
|
|
281
|
+
description: "List managed background terminals with status, pid, elapsed time, output size, and working directory.",
|
|
282
|
+
parameters: Type.Object({}),
|
|
283
|
+
async execute() {
|
|
284
|
+
const terminals = getManager().list();
|
|
285
|
+
return {
|
|
286
|
+
content: [{ type: "text", text: terminals.length ? terminals.map(formatTerminal).join("\n") : "No background terminals." }],
|
|
287
|
+
details: { terminals: terminals.map((terminal) => ({ id: terminal.id, title: terminal.title, status: terminal.status, pid: terminal.pid })) },
|
|
288
|
+
};
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
pi.registerTool({
|
|
293
|
+
name: "stop_terminal",
|
|
294
|
+
label: "Stop Background Terminals",
|
|
295
|
+
description: "Stop one or more managed background terminal process groups, escalating from SIGTERM to SIGKILL when needed, and return their final states.",
|
|
296
|
+
promptSnippet: "Stop managed background terminals and their process trees.",
|
|
297
|
+
parameters: Type.Object({
|
|
298
|
+
ids: Type.Array(Type.String(), { minItems: 1, maxItems: 32, description: "Terminal ids to stop." }),
|
|
299
|
+
}),
|
|
300
|
+
async execute(_toolCallId, params) {
|
|
301
|
+
const terminals = await getManager().kill(params.ids);
|
|
302
|
+
for (const terminal of terminals) consume(terminal.id);
|
|
303
|
+
return {
|
|
304
|
+
content: [{ type: "text", text: terminals.map((terminal) => `${terminal.id} "${oneLine(terminal.title)}" is ${terminal.status} (exit ${terminal.exitCode ?? "?"}).`).join("\n") }],
|
|
305
|
+
details: { terminals: terminals.map((terminal) => ({ id: terminal.id, status: terminal.status, exitCode: terminal.exitCode })) },
|
|
306
|
+
};
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
pi.registerMessageRenderer(
|
|
311
|
+
"background-terminal-result",
|
|
312
|
+
(message, { expanded }, theme) => {
|
|
313
|
+
const details = (message.details ?? {}) as { id?: string; title?: string; status?: string; exitCode?: number };
|
|
314
|
+
const state = details.status === "done" ? "success" : details.status === "killed" ? "muted" : "error";
|
|
315
|
+
const header = `${stateLabel(theme, state, details.status ?? "settled")} ${theme.fg("accent", theme.bold(`${details.id ?? "?"} · ${oneLine(details.title ?? "terminal")}`))}${theme.fg("muted", ` · exit ${details.exitCode ?? "?"}`)}`;
|
|
316
|
+
const body = sanitizeTerminalText(typeof message.content === "string" ? message.content : "").split("\n").slice(1).join("\n").trim();
|
|
317
|
+
if (expanded) {
|
|
318
|
+
const markdown = new Markdown(body, 0, 0, getMarkdownTheme());
|
|
319
|
+
const title = new Text(header, 0, 0);
|
|
320
|
+
return {
|
|
321
|
+
render: (width: number) => [...title.render(width), ...markdown.render(width)],
|
|
322
|
+
invalidate: () => { title.invalidate(); markdown.invalidate(); },
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
const preview = body.split("\n").slice(0, 8);
|
|
326
|
+
return new Text(
|
|
327
|
+
[header, ...preview.map((line) => theme.fg("toolOutput", line)), ...(body.split("\n").length > 8 ? [theme.fg("dim", "… ctrl+o to expand")] : [])].join("\n"),
|
|
328
|
+
0,
|
|
329
|
+
0,
|
|
330
|
+
);
|
|
331
|
+
},
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
const openCommand = async (ctx: ExtensionCommandContext) => {
|
|
335
|
+
const terminals = getManager().list();
|
|
336
|
+
if (ctx.mode !== "tui") {
|
|
337
|
+
if (ctx.hasUI) ctx.ui.notify(terminals.length ? terminals.map(formatTerminal).join("\n") : "No background terminals.", "info");
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (terminals.length === 0) {
|
|
341
|
+
ctx.ui.notify("No background terminals yet. The agent starts them with start_terminal.", "info");
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
await openTerminalDashboard(ctx, getManager().view);
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
pi.registerCommand("term", {
|
|
348
|
+
description: "Start, stop, list, or inspect background terminals",
|
|
349
|
+
getArgumentCompletions: (prefix) => {
|
|
350
|
+
const values = ["start", "stop", "list"];
|
|
351
|
+
const filtered = values.filter((value) => value.startsWith(prefix.trim()));
|
|
352
|
+
return filtered.length ? filtered.map((value) => ({ value, label: value })) : null;
|
|
353
|
+
},
|
|
354
|
+
handler: async (args, ctx) => {
|
|
355
|
+
const trimmed = args.trim();
|
|
356
|
+
if (trimmed.startsWith("start ")) {
|
|
357
|
+
const command = trimmed.slice("start ".length).trim();
|
|
358
|
+
if (!command) {
|
|
359
|
+
ctx.ui.notify("Usage: /term start <command>", "error");
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
try {
|
|
363
|
+
const terminal = getManager().start({
|
|
364
|
+
command,
|
|
365
|
+
title: titleFrom(command),
|
|
366
|
+
cwd: ctx.cwd,
|
|
367
|
+
});
|
|
368
|
+
ctx.ui.notify(`Started ${terminal.id} "${terminal.title}" · use /term to inspect`, "info");
|
|
369
|
+
} catch (error) {
|
|
370
|
+
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
371
|
+
}
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (trimmed.startsWith("stop ")) {
|
|
375
|
+
const ids = [...new Set(trimmed.slice("stop ".length).split(/[\\s,]+/).filter(Boolean))];
|
|
376
|
+
if (ids.length === 0) {
|
|
377
|
+
ctx.ui.notify("Usage: /term stop <id> [id…]", "error");
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
try {
|
|
381
|
+
const stopped = await getManager().kill(ids);
|
|
382
|
+
for (const terminal of stopped) consume(terminal.id);
|
|
383
|
+
ctx.ui.notify(stopped.map((terminal) => `${terminal.id}: ${terminal.status}`).join(" · "), "info");
|
|
384
|
+
} catch (error) {
|
|
385
|
+
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
386
|
+
}
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
if (trimmed && trimmed !== "list") {
|
|
390
|
+
ctx.ui.notify("Use /term, /term list, /term start <command>, or /term stop <id>.", "error");
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
await openCommand(ctx);
|
|
394
|
+
},
|
|
395
|
+
});
|
|
396
|
+
pi.registerCommand("ps", {
|
|
397
|
+
description: "Open the background terminal control center",
|
|
398
|
+
handler: async (_args, ctx) => openCommand(ctx),
|
|
399
|
+
});
|
|
400
|
+
}
|