@oh-my-pi/pi-coding-agent 16.4.6 → 16.5.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/CHANGELOG.md +88 -0
- package/dist/cli.js +3312 -3254
- package/dist/types/cli/args.d.ts +5 -0
- package/dist/types/cli/gallery-fixtures/shell.d.ts +1 -1
- package/dist/types/commands/launch.d.ts +15 -0
- package/dist/types/config/model-resolver.d.ts +4 -3
- package/dist/types/config/model-roles.d.ts +8 -0
- package/dist/types/config/settings-schema.d.ts +46 -14
- package/dist/types/extensibility/extensions/types.d.ts +1 -1
- package/dist/types/launch/broker.d.ts +2 -0
- package/dist/types/launch/client.d.ts +22 -0
- package/dist/types/launch/paths.d.ts +4 -0
- package/dist/types/launch/presence.d.ts +8 -0
- package/dist/types/launch/protocol.d.ts +170 -0
- package/dist/types/launch/terminal-output.d.ts +7 -0
- package/dist/types/modes/components/agent-hub.d.ts +1 -1
- package/dist/types/modes/components/index.d.ts +1 -0
- package/dist/types/modes/components/model-browser.d.ts +28 -3
- package/dist/types/modes/components/model-hub.d.ts +0 -10
- package/dist/types/modes/components/model-picker.d.ts +44 -0
- package/dist/types/modes/components/plan-review-overlay.d.ts +2 -0
- package/dist/types/modes/components/status-line/types.d.ts +3 -0
- package/dist/types/modes/components/welcome.d.ts +4 -0
- package/dist/types/modes/print-mode.d.ts +14 -8
- package/dist/types/modes/print-mode.test.d.ts +1 -0
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/sdk.d.ts +5 -1
- package/dist/types/session/agent-session.d.ts +42 -0
- package/dist/types/session/session-context.d.ts +9 -0
- package/dist/types/session/session-entries.d.ts +6 -0
- package/dist/types/thinking.d.ts +2 -2
- package/dist/types/tiny/models.d.ts +1 -1
- package/dist/types/tools/browser/launch.d.ts +1 -0
- package/dist/types/tools/browser/run-cancellation.d.ts +28 -2
- package/dist/types/tools/browser/tab-protocol.d.ts +6 -0
- package/dist/types/tools/browser/tab-worker.d.ts +6 -0
- package/dist/types/tools/builtin-names.d.ts +1 -1
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/launch.d.ts +121 -0
- package/dist/types/tools/render-utils.d.ts +2 -0
- package/dist/types/tools/terminal-output.d.ts +5 -0
- package/dist/types/vibe/runtime.d.ts +2 -2
- package/dist/types/web/search/types.d.ts +0 -8
- package/package.json +20 -20
- package/src/cli/args.ts +11 -0
- package/src/cli/flag-tables.ts +9 -0
- package/src/cli/gallery-fixtures/shell.ts +82 -1
- package/src/cli.ts +10 -0
- package/src/commands/launch.ts +17 -0
- package/src/config/model-resolver.ts +110 -31
- package/src/config/model-roles.ts +14 -0
- package/src/config/settings-schema.ts +71 -7
- package/src/edit/renderer.ts +13 -10
- package/src/eval/__tests__/agent-bridge.test.ts +2 -2
- package/src/eval/__tests__/completion-bridge.test.ts +1 -1
- package/src/eval/completion-bridge.ts +4 -4
- package/src/eval/js/shared/rewrite-imports.ts +31 -13
- package/src/export/ttsr.ts +0 -3
- package/src/extensibility/extensions/model-api.ts +1 -1
- package/src/extensibility/extensions/types.ts +1 -1
- package/src/internal-urls/docs-index.ts +4 -4
- package/src/launch/broker.ts +1017 -0
- package/src/launch/client.ts +344 -0
- package/src/launch/paths.ts +17 -0
- package/src/launch/presence.ts +82 -0
- package/src/launch/protocol.ts +386 -0
- package/src/launch/terminal-output.ts +46 -0
- package/src/main.ts +50 -1
- package/src/modes/acp/acp-agent.ts +8 -1
- package/src/modes/components/agent-hub.ts +101 -31
- package/src/modes/components/compaction-summary-message.ts +8 -2
- package/src/modes/components/index.ts +1 -0
- package/src/modes/components/model-browser.ts +152 -49
- package/src/modes/components/model-hub.ts +55 -142
- package/src/modes/components/model-picker.ts +233 -0
- package/src/modes/components/plan-review-overlay.ts +7 -0
- package/src/modes/components/snapcompact-shape-preview-doc.md +7 -11
- package/src/modes/components/status-line/component.test.ts +41 -2
- package/src/modes/components/status-line/component.ts +4 -0
- package/src/modes/components/status-line/segments.ts +6 -0
- package/src/modes/components/status-line/types.ts +3 -0
- package/src/modes/components/tips.txt +2 -1
- package/src/modes/components/welcome.ts +13 -14
- package/src/modes/controllers/command-controller.ts +9 -1
- package/src/modes/controllers/event-controller.ts +31 -32
- package/src/modes/controllers/selector-controller.ts +96 -22
- package/src/modes/controllers/tan-command-controller.ts +40 -1
- package/src/modes/interactive-mode.ts +20 -3
- package/src/modes/print-mode.test.ts +71 -0
- package/src/modes/print-mode.ts +51 -2
- package/src/modes/theme/theme.ts +9 -0
- package/src/modes/utils/ui-helpers.ts +25 -4
- package/src/prompts/agents/designer.md +1 -1
- package/src/prompts/agents/librarian.md +1 -1
- package/src/prompts/agents/reviewer.md +1 -1
- package/src/prompts/agents/scout.md +1 -1
- package/src/prompts/system/plan-yolo-handoff.md +5 -0
- package/src/prompts/system/prewalk-checklist.md +7 -0
- package/src/prompts/system/prewalk-continue.md +1 -0
- package/src/prompts/system/prewalk-plan.md +13 -0
- package/src/prompts/system/system-prompt.md +9 -7
- package/src/prompts/system/tan-context-switch.md +17 -0
- package/src/prompts/tools/bash.md +6 -4
- package/src/prompts/tools/browser.md +4 -4
- package/src/prompts/tools/launch.md +25 -0
- package/src/sdk.ts +8 -2
- package/src/session/agent-session.ts +550 -87
- package/src/session/session-context.test.ts +10 -5
- package/src/session/session-context.ts +25 -8
- package/src/session/session-entries.ts +6 -0
- package/src/slash-commands/builtin-registry.ts +25 -0
- package/src/task/agents.ts +2 -2
- package/src/thinking.ts +10 -3
- package/src/tiny/models.ts +7 -7
- package/src/tools/bash-interactive.ts +5 -8
- package/src/tools/bash.ts +38 -24
- package/src/tools/browser/cmux/cmux-tab.ts +17 -2
- package/src/tools/browser/launch.ts +8 -4
- package/src/tools/browser/run-cancellation.ts +66 -6
- package/src/tools/browser/tab-protocol.ts +6 -0
- package/src/tools/browser/tab-supervisor.ts +17 -1
- package/src/tools/browser/tab-worker.ts +140 -21
- package/src/tools/builtin-names.ts +1 -0
- package/src/tools/eval-render.ts +16 -14
- package/src/tools/index.ts +5 -0
- package/src/tools/inspect-image.ts +2 -2
- package/src/tools/launch.ts +643 -0
- package/src/tools/render-utils.ts +3 -0
- package/src/tools/renderers.ts +2 -0
- package/src/tools/terminal-output.ts +141 -0
- package/src/tts/speech-enhancer.ts +2 -2
- package/src/utils/image-vision-fallback.ts +3 -3
- package/src/vibe/runtime.ts +2 -2
- package/src/web/search/provider.ts +0 -10
- package/src/web/search/providers/perplexity.ts +18 -2
- package/src/web/search/providers/public.ts +2 -4
- package/src/web/search/types.ts +0 -10
- package/dist/types/web/search/providers/bing.d.ts +0 -14
- package/dist/types/web/search/providers/yahoo.d.ts +0 -14
- package/src/web/search/providers/bing.ts +0 -197
- package/src/web/search/providers/yahoo.ts +0 -179
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentTool,
|
|
3
|
+
AgentToolContext,
|
|
4
|
+
AgentToolResult,
|
|
5
|
+
AgentToolUpdateCallback,
|
|
6
|
+
ToolApprovalDecision,
|
|
7
|
+
} from "@oh-my-pi/pi-agent-core";
|
|
8
|
+
import type { ToolExample } from "@oh-my-pi/pi-ai";
|
|
9
|
+
import type { Component } from "@oh-my-pi/pi-tui";
|
|
10
|
+
import { Text } from "@oh-my-pi/pi-tui";
|
|
11
|
+
import { prompt, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
12
|
+
import { type } from "arktype";
|
|
13
|
+
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
14
|
+
import { daemonClientForProject } from "../launch/client";
|
|
15
|
+
import type { DaemonOperation, DaemonRpcResult, DaemonSnapshot, DaemonSpec, DaemonState } from "../launch/protocol";
|
|
16
|
+
import { renderTerminalOutput } from "../launch/terminal-output";
|
|
17
|
+
import type { Theme, ThemeColor } from "../modes/theme/theme";
|
|
18
|
+
import launchDescription from "../prompts/tools/launch.md" with { type: "text" };
|
|
19
|
+
import { framedBlock, outputBlockContentWidth, renderStatusLine } from "../tui";
|
|
20
|
+
import type { ToolSession } from ".";
|
|
21
|
+
import { resolveToCwd } from "./path-utils";
|
|
22
|
+
import {
|
|
23
|
+
capPreviewLines,
|
|
24
|
+
createCachedComponent,
|
|
25
|
+
DEFAULT_TERMINAL_PREVIEW_LINES,
|
|
26
|
+
formatDuration,
|
|
27
|
+
formatExpandHint,
|
|
28
|
+
formatMoreItems,
|
|
29
|
+
PREVIEW_LIMITS,
|
|
30
|
+
pluralize,
|
|
31
|
+
previewLine,
|
|
32
|
+
replaceTabs,
|
|
33
|
+
shortenPath,
|
|
34
|
+
TRUNCATE_LENGTHS,
|
|
35
|
+
truncateToWidth,
|
|
36
|
+
} from "./render-utils";
|
|
37
|
+
import { styleTerminalRow } from "./terminal-output";
|
|
38
|
+
import { ToolError } from "./tool-errors";
|
|
39
|
+
|
|
40
|
+
const launchSchema = type({
|
|
41
|
+
op: type("'start' | 'list' | 'logs' | 'wait' | 'send' | 'stop' | 'restart' | 'describe'").describe(
|
|
42
|
+
"launch operation",
|
|
43
|
+
),
|
|
44
|
+
"name?": type("string <= 48").describe("stable project-scoped launch name"),
|
|
45
|
+
"application?": type("string > 0").describe("start: executable or application path"),
|
|
46
|
+
"args?": type("string[]").describe("start: argv passed directly to the application"),
|
|
47
|
+
"env?": type({ "[string]": "string" }).describe("start: extra environment variables"),
|
|
48
|
+
"cwd?": type("string").describe("start: working directory; defaults to the session directory"),
|
|
49
|
+
"pty?": type("boolean").describe("start: allocate an interactive PTY; default true"),
|
|
50
|
+
"ready?": type({
|
|
51
|
+
"log?": type("string > 0").describe("regex matched against output"),
|
|
52
|
+
"port?": type("number").describe("TCP port that must accept connections"),
|
|
53
|
+
"host?": type("string > 0").describe("TCP readiness host; default 127.0.0.1"),
|
|
54
|
+
"timeout?": type("number > 0").describe("seconds to wait; default 30"),
|
|
55
|
+
}).describe("start: readiness conditions; all supplied conditions must pass"),
|
|
56
|
+
"restart?": type("'no' | 'on-failure' | 'always'").describe("start: restart policy; default no"),
|
|
57
|
+
"persist?": type("boolean").describe("start: survive the last omp client exiting; default false"),
|
|
58
|
+
"detached?": type("boolean").describe(
|
|
59
|
+
"start: survive every omp and broker exit; implies persist and disables PTY input",
|
|
60
|
+
),
|
|
61
|
+
"lines?": type("number > 0").describe("logs: output lines; default 100, max 1000"),
|
|
62
|
+
"head?": type("boolean").describe("logs: read from the beginning instead of the tail"),
|
|
63
|
+
"grep?": type("string > 0").describe("logs: regex filter"),
|
|
64
|
+
"follow?": type("boolean").describe("logs: wait for output newer than cursor"),
|
|
65
|
+
"cursor?": type("number >= 0").describe("logs: output cursor returned by an earlier call"),
|
|
66
|
+
"for?": type("'ready' | 'exit'").describe("wait: lifecycle condition; default exit"),
|
|
67
|
+
"pattern?": type("string > 0").describe("wait: output regex; takes precedence over for"),
|
|
68
|
+
"text?": type("string > 0").describe("send: stdin text"),
|
|
69
|
+
"enter?": type("boolean").describe("send: append Enter after text; default true"),
|
|
70
|
+
"keys?": type("string[]").describe("send: terminal keys after text"),
|
|
71
|
+
"signal?": type("'SIGINT' | 'SIGTERM' | 'SIGHUP' | 'SIGQUIT' | 'SIGKILL'").describe("send: process-tree signal"),
|
|
72
|
+
"timeout?": type("number > 0").describe("logs/wait/stop: max seconds; default 30 (stop: 5)"),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
type LaunchParams = typeof launchSchema.infer;
|
|
76
|
+
|
|
77
|
+
const KEY_INPUT: Record<string, string> = {
|
|
78
|
+
ENTER: "\r",
|
|
79
|
+
TAB: "\t",
|
|
80
|
+
ESCAPE: "\u001b",
|
|
81
|
+
CTRL_C: "\u0003",
|
|
82
|
+
CTRL_D: "\u0004",
|
|
83
|
+
UP: "\u001b[A",
|
|
84
|
+
DOWN: "\u001b[B",
|
|
85
|
+
RIGHT: "\u001b[C",
|
|
86
|
+
LEFT: "\u001b[D",
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** Structured launch state retained for compact TUI rendering. */
|
|
90
|
+
export interface LaunchToolDetails {
|
|
91
|
+
op: LaunchParams["op"];
|
|
92
|
+
daemon?: DaemonSnapshot;
|
|
93
|
+
daemons?: DaemonSnapshot[];
|
|
94
|
+
cursor?: number;
|
|
95
|
+
timedOut?: boolean;
|
|
96
|
+
/** logs: daemon lifecycle state at read time. */
|
|
97
|
+
state?: DaemonState;
|
|
98
|
+
/** logs: virtual terminal rows for display; model-facing content remains sanitized text. */
|
|
99
|
+
terminalRows?: string[];
|
|
100
|
+
/** wait: output line that satisfied the pattern. */
|
|
101
|
+
matched?: string;
|
|
102
|
+
/** describe: immutable launch spec backing the command/cwd detail lines. */
|
|
103
|
+
spec?: DaemonSpec;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function requiredName(params: LaunchParams): string {
|
|
107
|
+
if (!params.name) throw new ToolError(`${params.op} requires name`);
|
|
108
|
+
return params.name;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function timeoutMs(value: number | undefined, fallbackSeconds: number): number {
|
|
112
|
+
const seconds = Math.max(0.05, Math.min(3_600, value ?? fallbackSeconds));
|
|
113
|
+
return Math.round(seconds * 1_000);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function commandSpec(params: LaunchParams, session: ToolSession): DaemonSpec {
|
|
117
|
+
const name = requiredName(params);
|
|
118
|
+
if (!params.application) throw new ToolError("start requires application");
|
|
119
|
+
const ready = params.ready;
|
|
120
|
+
const detached = params.detached ?? false;
|
|
121
|
+
if (ready?.port !== undefined && (!Number.isInteger(ready.port) || ready.port < 1 || ready.port > 65_535)) {
|
|
122
|
+
throw new ToolError("ready.port must be an integer from 1 to 65535");
|
|
123
|
+
}
|
|
124
|
+
if (ready && !ready.log && ready.port === undefined) throw new ToolError("ready requires log or port");
|
|
125
|
+
return {
|
|
126
|
+
name,
|
|
127
|
+
application: params.application,
|
|
128
|
+
args: params.args ?? [],
|
|
129
|
+
env: params.env ?? {},
|
|
130
|
+
cwd: resolveToCwd(params.cwd ?? session.cwd, session.cwd),
|
|
131
|
+
pty: detached ? false : (params.pty ?? true),
|
|
132
|
+
ready: ready
|
|
133
|
+
? {
|
|
134
|
+
log: ready.log,
|
|
135
|
+
port: ready.port,
|
|
136
|
+
host: ready.host,
|
|
137
|
+
timeoutMs: timeoutMs(ready.timeout, 30),
|
|
138
|
+
}
|
|
139
|
+
: undefined,
|
|
140
|
+
restart: params.restart ?? "no",
|
|
141
|
+
persist: (params.persist ?? false) || detached,
|
|
142
|
+
detached,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function sendData(params: LaunchParams): string | undefined {
|
|
147
|
+
let data = params.text ?? "";
|
|
148
|
+
if (params.text && (params.enter ?? true)) data += KEY_INPUT.ENTER;
|
|
149
|
+
for (const rawKey of params.keys ?? []) {
|
|
150
|
+
const key = rawKey.trim().toUpperCase();
|
|
151
|
+
const input = KEY_INPUT[key];
|
|
152
|
+
if (input === undefined) throw new ToolError(`Unsupported launch key ${rawKey}`);
|
|
153
|
+
data += input;
|
|
154
|
+
}
|
|
155
|
+
return data || undefined;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function operationFor(params: LaunchParams, session: ToolSession): DaemonOperation {
|
|
159
|
+
switch (params.op) {
|
|
160
|
+
case "start":
|
|
161
|
+
return { op: "start", spec: commandSpec(params, session), owner: session.getSessionId?.() ?? undefined };
|
|
162
|
+
case "list":
|
|
163
|
+
return { op: "list" };
|
|
164
|
+
case "logs":
|
|
165
|
+
return {
|
|
166
|
+
op: "logs",
|
|
167
|
+
name: requiredName(params),
|
|
168
|
+
lines: Math.min(1_000, Math.floor(params.lines ?? 100)),
|
|
169
|
+
head: params.head ?? false,
|
|
170
|
+
grep: params.grep,
|
|
171
|
+
follow: params.follow ?? false,
|
|
172
|
+
cursor: params.cursor,
|
|
173
|
+
timeoutMs: timeoutMs(params.timeout, 30),
|
|
174
|
+
};
|
|
175
|
+
case "wait":
|
|
176
|
+
return {
|
|
177
|
+
op: "wait",
|
|
178
|
+
name: requiredName(params),
|
|
179
|
+
for: params.for ?? "exit",
|
|
180
|
+
pattern: params.pattern,
|
|
181
|
+
timeoutMs: timeoutMs(params.timeout, 30),
|
|
182
|
+
};
|
|
183
|
+
case "send":
|
|
184
|
+
return {
|
|
185
|
+
op: "send",
|
|
186
|
+
name: requiredName(params),
|
|
187
|
+
data: sendData(params),
|
|
188
|
+
signal: params.signal,
|
|
189
|
+
};
|
|
190
|
+
case "stop":
|
|
191
|
+
return { op: "stop", name: requiredName(params), timeoutMs: timeoutMs(params.timeout, 5) };
|
|
192
|
+
case "restart":
|
|
193
|
+
return { op: "restart", name: requiredName(params) };
|
|
194
|
+
case "describe":
|
|
195
|
+
return { op: "describe", name: requiredName(params) };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function daemonLabel(daemon: DaemonSnapshot): string {
|
|
200
|
+
const pid = daemon.pid === undefined ? "" : ` pid=${daemon.pid}`;
|
|
201
|
+
const exit = daemon.exitCode === undefined ? "" : ` exit=${daemon.exitCode}`;
|
|
202
|
+
return `${daemon.name}: ${daemon.state}${pid}${exit} uptime=${formatDuration(
|
|
203
|
+
(daemon.exitedAt ?? Date.now()) - daemon.startedAt,
|
|
204
|
+
)} restarts=${daemon.restartCount}${daemon.detached ? " detached" : daemon.persist ? " persistent" : ""}`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Human sentences for the readiness conditions still unmet, e.g.
|
|
209
|
+
* `port 5173 on 127.0.0.1 never accepted connections`. `ready` (from the start
|
|
210
|
+
* params) adds the concrete pattern/port; absent it falls back to generic labels.
|
|
211
|
+
*/
|
|
212
|
+
function readyPendingSummary(daemon: DaemonSnapshot, ready?: LaunchParams["ready"]): string[] {
|
|
213
|
+
const parts: string[] = [];
|
|
214
|
+
for (const condition of daemon.readyPending ?? []) {
|
|
215
|
+
if (condition === "log") {
|
|
216
|
+
parts.push(ready?.log ? `log pattern /${ready.log}/ never matched` : "the log pattern never matched");
|
|
217
|
+
} else {
|
|
218
|
+
parts.push(
|
|
219
|
+
ready?.port !== undefined
|
|
220
|
+
? `port ${ready.port} on ${ready.host ?? "127.0.0.1"} never accepted connections`
|
|
221
|
+
: "the port never accepted connections",
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return parts;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function toolContent(result: DaemonRpcResult, params: LaunchParams): string {
|
|
229
|
+
switch (result.op) {
|
|
230
|
+
case "ping":
|
|
231
|
+
case "shutdown":
|
|
232
|
+
throw new ToolError(`Internal daemon result ${result.op} is not tool-visible`);
|
|
233
|
+
case "start": {
|
|
234
|
+
const daemon = result.daemon;
|
|
235
|
+
const lines = [`${daemon.state === "failed" ? "Failed to launch" : "Started"} ${daemonLabel(daemon)}`];
|
|
236
|
+
if (daemon.state === "failed" && daemon.exitReason) lines.push(`Reason: ${daemon.exitReason}`);
|
|
237
|
+
if (daemon.readyMatch) lines.push(`Ready log matched: ${daemon.readyMatch}`);
|
|
238
|
+
if (result.readyTimedOut) {
|
|
239
|
+
const pending = readyPendingSummary(daemon, params.ready);
|
|
240
|
+
const cause = pending.length > 0 ? `: ${pending.join("; ")}` : "";
|
|
241
|
+
lines.push(
|
|
242
|
+
`NOT ready — readiness timed out after ${params.ready?.timeout ?? 30}s${cause}. The process is still running (state: ${daemon.state}); follow its logs or stop it.`,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
return lines.join("\n");
|
|
246
|
+
}
|
|
247
|
+
case "list":
|
|
248
|
+
return result.daemons.length
|
|
249
|
+
? result.daemons.map(daemon => `- ${daemonLabel(daemon)}`).join("\n")
|
|
250
|
+
: "No daemons.";
|
|
251
|
+
case "logs": {
|
|
252
|
+
const text = sanitizeText(result.text);
|
|
253
|
+
return `${text}${text && !text.endsWith("\n") ? "\n" : ""}[${result.name}: ${result.state}; cursor=${result.cursor}${result.timedOut ? "; follow timed out" : ""}]`;
|
|
254
|
+
}
|
|
255
|
+
case "wait": {
|
|
256
|
+
const lines = [daemonLabel(result.daemon)];
|
|
257
|
+
if (result.matched) lines.push(`Matched: ${result.matched}`);
|
|
258
|
+
if (result.timedOut) {
|
|
259
|
+
const pending = readyPendingSummary(result.daemon);
|
|
260
|
+
lines.push(`Wait timed out${pending.length > 0 ? ` (still waiting on: ${pending.join("; ")})` : ""}.`);
|
|
261
|
+
}
|
|
262
|
+
return lines.join("\n");
|
|
263
|
+
}
|
|
264
|
+
case "send":
|
|
265
|
+
return `Sent input to ${daemonLabel(result.daemon)}`;
|
|
266
|
+
case "stop":
|
|
267
|
+
return `Stopped ${daemonLabel(result.daemon)}`;
|
|
268
|
+
case "restart":
|
|
269
|
+
return `Restarted ${daemonLabel(result.daemon)}`;
|
|
270
|
+
case "describe":
|
|
271
|
+
return [
|
|
272
|
+
daemonLabel(result.daemon),
|
|
273
|
+
`Command: ${[result.spec.application, ...result.spec.args].join(" ")}`,
|
|
274
|
+
`Cwd: ${shortenPath(result.spec.cwd)}`,
|
|
275
|
+
`PTY: ${result.spec.pty}; restart=${result.spec.restart}; persist=${result.spec.persist}; detached=${result.spec.detached}`,
|
|
276
|
+
].join("\n");
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function toolDetails(result: DaemonRpcResult, params: LaunchParams): Promise<LaunchToolDetails> {
|
|
281
|
+
switch (result.op) {
|
|
282
|
+
case "start":
|
|
283
|
+
return { op: "start", daemon: result.daemon, timedOut: result.readyTimedOut };
|
|
284
|
+
case "list":
|
|
285
|
+
return { op: "list", daemons: result.daemons };
|
|
286
|
+
case "logs": {
|
|
287
|
+
const terminalRows =
|
|
288
|
+
result.terminalText === undefined
|
|
289
|
+
? undefined
|
|
290
|
+
: await renderTerminalOutput(result.terminalText, {
|
|
291
|
+
head: params.head ?? false,
|
|
292
|
+
maxRows: Math.min(1_000, Math.floor(params.lines ?? 100)),
|
|
293
|
+
});
|
|
294
|
+
return {
|
|
295
|
+
op: "logs",
|
|
296
|
+
cursor: result.cursor,
|
|
297
|
+
timedOut: result.timedOut,
|
|
298
|
+
state: result.state,
|
|
299
|
+
terminalRows,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
case "wait":
|
|
303
|
+
return { op: "wait", daemon: result.daemon, timedOut: result.timedOut, matched: result.matched };
|
|
304
|
+
case "send":
|
|
305
|
+
return { op: "send", daemon: result.daemon };
|
|
306
|
+
case "stop":
|
|
307
|
+
return { op: "stop", daemon: result.daemon };
|
|
308
|
+
case "restart":
|
|
309
|
+
return { op: "restart", daemon: result.daemon };
|
|
310
|
+
case "describe":
|
|
311
|
+
return { op: "describe", daemon: result.daemon, spec: result.spec };
|
|
312
|
+
case "ping":
|
|
313
|
+
case "shutdown":
|
|
314
|
+
throw new ToolError(`Internal daemon result ${result.op} is not tool-visible`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
function approvalFor(params: unknown): ToolApprovalDecision {
|
|
318
|
+
if (typeof params !== "object" || params === null || !("op" in params)) return "exec";
|
|
319
|
+
switch (params.op) {
|
|
320
|
+
case "list":
|
|
321
|
+
case "logs":
|
|
322
|
+
case "wait":
|
|
323
|
+
case "describe":
|
|
324
|
+
return "read";
|
|
325
|
+
default:
|
|
326
|
+
return "exec";
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** Project-scoped launch tool for supervising processes in every coding-agent session. */
|
|
331
|
+
export class LaunchTool implements AgentTool<typeof launchSchema, LaunchToolDetails, Theme> {
|
|
332
|
+
readonly name = "launch";
|
|
333
|
+
readonly label = "Launch";
|
|
334
|
+
readonly loadMode = "essential";
|
|
335
|
+
readonly summary = "Launch and control shared long-running project processes";
|
|
336
|
+
readonly description = prompt.render(launchDescription);
|
|
337
|
+
readonly parameters = launchSchema;
|
|
338
|
+
readonly strict = true;
|
|
339
|
+
readonly examples: readonly ToolExample<LaunchParams>[] = [
|
|
340
|
+
{
|
|
341
|
+
caption: "Start a dev server and wait for its log banner and port",
|
|
342
|
+
call: {
|
|
343
|
+
op: "start",
|
|
344
|
+
name: "web",
|
|
345
|
+
application: "bun",
|
|
346
|
+
args: ["run", "dev"],
|
|
347
|
+
ready: { log: "Local:.*http", port: 5173, timeout: 30 },
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
caption: "Run a noninteractive service beyond broker lifetime",
|
|
352
|
+
call: {
|
|
353
|
+
op: "start",
|
|
354
|
+
name: "worker",
|
|
355
|
+
application: "worker",
|
|
356
|
+
args: ["serve"],
|
|
357
|
+
detached: true,
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
caption: "Inspect recent output",
|
|
362
|
+
call: { op: "logs", name: "web", lines: 100 },
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
caption: "Follow output after a cursor",
|
|
366
|
+
call: { op: "logs", name: "web", follow: true, cursor: 1842, timeout: 30 },
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
caption: "Set a debugger breakpoint",
|
|
370
|
+
call: { op: "send", name: "debugger", text: "breakpoint set --name main" },
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
caption: "Run a debugger command",
|
|
374
|
+
call: { op: "send", name: "debugger", text: "run" },
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
caption: "Interrupt a debugger",
|
|
378
|
+
call: { op: "send", name: "debugger", keys: ["CTRL_C"] },
|
|
379
|
+
},
|
|
380
|
+
];
|
|
381
|
+
readonly approval = approvalFor;
|
|
382
|
+
|
|
383
|
+
constructor(private readonly session: ToolSession) {}
|
|
384
|
+
|
|
385
|
+
async execute(
|
|
386
|
+
_toolCallId: string,
|
|
387
|
+
params: LaunchParams,
|
|
388
|
+
signal?: AbortSignal,
|
|
389
|
+
_onUpdate?: AgentToolUpdateCallback<LaunchToolDetails, typeof launchSchema>,
|
|
390
|
+
_context?: AgentToolContext,
|
|
391
|
+
): Promise<AgentToolResult<LaunchToolDetails>> {
|
|
392
|
+
const client = await daemonClientForProject(this.session.cwd);
|
|
393
|
+
const result = await client.request(operationFor(params, this.session), signal);
|
|
394
|
+
return {
|
|
395
|
+
content: [{ type: "text", text: replaceTabs(toolContent(result, params)) }],
|
|
396
|
+
details: await toolDetails(result, params),
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** Args shape visible to the renderer, possibly mid-stream (every field optional). */
|
|
402
|
+
type LaunchRenderArgs = Partial<LaunchParams>;
|
|
403
|
+
|
|
404
|
+
function stateColor(state: DaemonState): ThemeColor {
|
|
405
|
+
switch (state) {
|
|
406
|
+
case "running":
|
|
407
|
+
case "ready":
|
|
408
|
+
return "success";
|
|
409
|
+
case "failed":
|
|
410
|
+
return "error";
|
|
411
|
+
case "exited":
|
|
412
|
+
return "muted";
|
|
413
|
+
default:
|
|
414
|
+
return "warning";
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** Compact `state · pid · uptime` fragments for the status-line meta slot. */
|
|
419
|
+
function daemonMeta(daemon: DaemonSnapshot, theme: Theme): string[] {
|
|
420
|
+
const meta = [theme.fg(stateColor(daemon.state), daemon.state)];
|
|
421
|
+
if (daemon.readyPending?.length) meta.push(theme.fg("warning", `waiting on ${daemon.readyPending.join("+")}`));
|
|
422
|
+
if (daemon.exitCode !== undefined) {
|
|
423
|
+
meta.push(theme.fg(daemon.exitCode === 0 ? "muted" : "error", `exit ${daemon.exitCode}`));
|
|
424
|
+
} else if (daemon.pid !== undefined) {
|
|
425
|
+
meta.push(`pid ${daemon.pid}`);
|
|
426
|
+
}
|
|
427
|
+
const lifespan = formatDuration((daemon.exitedAt ?? Date.now()) - daemon.startedAt);
|
|
428
|
+
meta.push(daemon.exitedAt === undefined ? `up ${lifespan}` : `ran ${lifespan}`);
|
|
429
|
+
if (daemon.restartCount > 0) meta.push(`restarts ${daemon.restartCount}`);
|
|
430
|
+
if (daemon.detached) meta.push("detached");
|
|
431
|
+
else if (daemon.persist) meta.push("persistent");
|
|
432
|
+
return meta;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** Op-specific call context (command line, log filters, wait condition, send payload). */
|
|
436
|
+
function callMeta(args: LaunchRenderArgs): string[] {
|
|
437
|
+
const meta: string[] = [];
|
|
438
|
+
switch (args.op) {
|
|
439
|
+
case "start":
|
|
440
|
+
if (args.application) meta.push([args.application, ...(args.args ?? [])].join(" "));
|
|
441
|
+
break;
|
|
442
|
+
case "logs":
|
|
443
|
+
if (args.follow) meta.push("follow");
|
|
444
|
+
if (args.grep) meta.push(`grep /${args.grep}/`);
|
|
445
|
+
break;
|
|
446
|
+
case "wait":
|
|
447
|
+
meta.push(args.pattern ? `for /${args.pattern}/` : `for ${args.for ?? "exit"}`);
|
|
448
|
+
break;
|
|
449
|
+
case "send":
|
|
450
|
+
if (args.signal) meta.push(args.signal);
|
|
451
|
+
else if (args.text) meta.push(args.text);
|
|
452
|
+
if (args.keys?.length) meta.push(args.keys.join(" "));
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
return meta.map(entry => previewLine(replaceTabs(entry), TRUNCATE_LENGTHS.SHORT));
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** TUI renderer: one status header per op, meta from structured details, capped body lines. */
|
|
459
|
+
export const launchToolRenderer = {
|
|
460
|
+
inline: true,
|
|
461
|
+
mergeCallAndResult: true,
|
|
462
|
+
animatedPendingPreview: true,
|
|
463
|
+
|
|
464
|
+
renderCall(args: LaunchRenderArgs, options: RenderResultOptions, theme: Theme): Component {
|
|
465
|
+
const target = args.name ?? args.application;
|
|
466
|
+
const header = renderStatusLine(
|
|
467
|
+
{
|
|
468
|
+
icon: options.spinnerFrame !== undefined ? "running" : "pending",
|
|
469
|
+
spinnerFrame: options.spinnerFrame,
|
|
470
|
+
title: `Launch ${args.op ?? "…"}`,
|
|
471
|
+
description: target ? replaceTabs(target) : undefined,
|
|
472
|
+
meta: callMeta(args),
|
|
473
|
+
},
|
|
474
|
+
theme,
|
|
475
|
+
);
|
|
476
|
+
return new Text(header, 0, 0);
|
|
477
|
+
},
|
|
478
|
+
|
|
479
|
+
renderResult(
|
|
480
|
+
result: { content: Array<{ type: string; text?: string }>; details?: LaunchToolDetails; isError?: boolean },
|
|
481
|
+
options: RenderResultOptions,
|
|
482
|
+
theme: Theme,
|
|
483
|
+
args?: LaunchRenderArgs,
|
|
484
|
+
): Component {
|
|
485
|
+
const details = result.details;
|
|
486
|
+
const params = args ?? {};
|
|
487
|
+
const op = details?.op ?? params.op;
|
|
488
|
+
const isError = result.isError === true;
|
|
489
|
+
const daemon = details?.daemon;
|
|
490
|
+
const failed = isError || daemon?.state === "failed";
|
|
491
|
+
const text =
|
|
492
|
+
result.content
|
|
493
|
+
?.filter(item => item.type === "text")
|
|
494
|
+
.map(item => item.text ?? "")
|
|
495
|
+
.join("\n") ?? "";
|
|
496
|
+
|
|
497
|
+
const meta: string[] = [];
|
|
498
|
+
const body: string[] = [];
|
|
499
|
+
let description = params.name ?? daemon?.name;
|
|
500
|
+
|
|
501
|
+
if (isError) {
|
|
502
|
+
for (const line of replaceTabs(text.trimEnd()).split("\n")) body.push(theme.fg("error", line));
|
|
503
|
+
} else {
|
|
504
|
+
switch (op) {
|
|
505
|
+
case "start": {
|
|
506
|
+
meta.push(...callMeta(params));
|
|
507
|
+
if (daemon) meta.push(...daemonMeta(daemon, theme));
|
|
508
|
+
if (daemon?.readyMatch) body.push(theme.fg("dim", `log matched: ${replaceTabs(daemon.readyMatch)}`));
|
|
509
|
+
if (daemon?.state === "failed" && daemon.exitReason)
|
|
510
|
+
body.push(theme.fg("error", replaceTabs(daemon.exitReason)));
|
|
511
|
+
if (details?.timedOut) {
|
|
512
|
+
const pending = daemon ? readyPendingSummary(daemon, params.ready) : [];
|
|
513
|
+
body.push(
|
|
514
|
+
theme.fg(
|
|
515
|
+
"warning",
|
|
516
|
+
pending.length > 0
|
|
517
|
+
? `Not ready — ${pending.join("; ")}. Still running.`
|
|
518
|
+
: "Readiness timed out; the process is still running.",
|
|
519
|
+
),
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
break;
|
|
523
|
+
}
|
|
524
|
+
case "send":
|
|
525
|
+
meta.push(...callMeta(params));
|
|
526
|
+
if (daemon) meta.push(...daemonMeta(daemon, theme));
|
|
527
|
+
break;
|
|
528
|
+
case "stop":
|
|
529
|
+
case "restart":
|
|
530
|
+
if (daemon) meta.push(...daemonMeta(daemon, theme));
|
|
531
|
+
break;
|
|
532
|
+
case "wait": {
|
|
533
|
+
meta.push(...callMeta(params));
|
|
534
|
+
if (daemon) meta.push(...daemonMeta(daemon, theme));
|
|
535
|
+
if (details?.matched) body.push(theme.fg("dim", `matched: ${replaceTabs(details.matched)}`));
|
|
536
|
+
if (details?.timedOut) {
|
|
537
|
+
const pending = daemon ? readyPendingSummary(daemon) : [];
|
|
538
|
+
body.push(
|
|
539
|
+
theme.fg(
|
|
540
|
+
"warning",
|
|
541
|
+
pending.length > 0
|
|
542
|
+
? `Wait timed out — still waiting on ${pending.join("; ")}.`
|
|
543
|
+
: "Wait timed out.",
|
|
544
|
+
),
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
break;
|
|
548
|
+
}
|
|
549
|
+
case "list": {
|
|
550
|
+
const daemons = details?.daemons ?? [];
|
|
551
|
+
description = `${daemons.length || "no"} ${pluralize("process", daemons.length)}`;
|
|
552
|
+
for (const item of daemons) {
|
|
553
|
+
body.push(
|
|
554
|
+
`${theme.fg("accent", replaceTabs(item.name))} ${theme.fg("dim", daemonMeta(item, theme).join(theme.sep.dot))}`,
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
break;
|
|
558
|
+
}
|
|
559
|
+
case "logs": {
|
|
560
|
+
if (details?.state) meta.push(theme.fg(stateColor(details.state), details.state));
|
|
561
|
+
if (details?.cursor !== undefined) meta.push(`cursor ${details.cursor}`);
|
|
562
|
+
if (details?.timedOut) meta.push(theme.fg("warning", "follow timed out"));
|
|
563
|
+
// Strip the trailing `[name: state; cursor=N]` status suffix `toolContent` appends.
|
|
564
|
+
const logText = text.replace(/\n?\[[^\n]*\]$/, "").trimEnd();
|
|
565
|
+
const terminalRows = details?.terminalRows;
|
|
566
|
+
if (terminalRows) {
|
|
567
|
+
for (const row of terminalRows) body.push(styleTerminalRow(row, theme.getFgAnsi("toolOutput")));
|
|
568
|
+
} else if (logText) {
|
|
569
|
+
for (const line of logText.split("\n")) body.push(theme.fg("toolOutput", replaceTabs(line)));
|
|
570
|
+
}
|
|
571
|
+
break;
|
|
572
|
+
}
|
|
573
|
+
case "describe": {
|
|
574
|
+
if (daemon) meta.push(...daemonMeta(daemon, theme));
|
|
575
|
+
const spec = details?.spec;
|
|
576
|
+
if (spec) {
|
|
577
|
+
body.push(theme.fg("toolOutput", replaceTabs([spec.application, ...spec.args].join(" "))));
|
|
578
|
+
body.push(theme.fg("dim", `cwd ${shortenPath(spec.cwd)}`));
|
|
579
|
+
const flags = [`pty ${spec.pty}`, `restart ${spec.restart}`];
|
|
580
|
+
if (spec.detached) flags.push("detached");
|
|
581
|
+
else if (spec.persist) flags.push("persistent");
|
|
582
|
+
body.push(theme.fg("dim", flags.join(theme.sep.dot)));
|
|
583
|
+
}
|
|
584
|
+
break;
|
|
585
|
+
}
|
|
586
|
+
default:
|
|
587
|
+
if (text.trim()) {
|
|
588
|
+
for (const line of replaceTabs(text.trimEnd()).split("\n")) body.push(theme.fg("toolOutput", line));
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const header = renderStatusLine(
|
|
594
|
+
{
|
|
595
|
+
...(failed
|
|
596
|
+
? { icon: "error" as const }
|
|
597
|
+
: options.isPartial
|
|
598
|
+
? { icon: "pending" as const }
|
|
599
|
+
: { iconOverride: theme.styledSymbol("tool.launch", "accent") }),
|
|
600
|
+
title: `Launch ${op ?? ""}`.trimEnd(),
|
|
601
|
+
description: description ? replaceTabs(description) : undefined,
|
|
602
|
+
meta,
|
|
603
|
+
},
|
|
604
|
+
theme,
|
|
605
|
+
);
|
|
606
|
+
|
|
607
|
+
if (op === "logs") {
|
|
608
|
+
return framedBlock(theme, width => {
|
|
609
|
+
const innerWidth = outputBlockContentWidth(width);
|
|
610
|
+
const rows = body.map(line => truncateToWidth(line, innerWidth));
|
|
611
|
+
return {
|
|
612
|
+
header,
|
|
613
|
+
state: options.isPartial ? "pending" : failed ? "error" : "success",
|
|
614
|
+
sections: [
|
|
615
|
+
{
|
|
616
|
+
label: theme.fg("toolTitle", "Output"),
|
|
617
|
+
lines: capPreviewLines(rows, theme, {
|
|
618
|
+
expanded: options.expanded,
|
|
619
|
+
max: DEFAULT_TERMINAL_PREVIEW_LINES,
|
|
620
|
+
}),
|
|
621
|
+
},
|
|
622
|
+
],
|
|
623
|
+
width,
|
|
624
|
+
};
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return createCachedComponent(
|
|
629
|
+
() => options.expanded,
|
|
630
|
+
(width, expanded) => {
|
|
631
|
+
let visible = body;
|
|
632
|
+
if (!expanded && op === "list" && body.length > PREVIEW_LIMITS.COLLAPSED_ITEMS) {
|
|
633
|
+
const remaining = body.length - PREVIEW_LIMITS.COLLAPSED_ITEMS;
|
|
634
|
+
visible = [
|
|
635
|
+
...body.slice(0, PREVIEW_LIMITS.COLLAPSED_ITEMS),
|
|
636
|
+
theme.fg("dim", `${formatMoreItems(remaining, "process")} ${formatExpandHint(theme, false, true)}`),
|
|
637
|
+
];
|
|
638
|
+
}
|
|
639
|
+
return [header, ...visible].map(line => truncateToWidth(line, width));
|
|
640
|
+
},
|
|
641
|
+
);
|
|
642
|
+
},
|
|
643
|
+
};
|
|
@@ -62,6 +62,9 @@ export const PREVIEW_LIMITS = {
|
|
|
62
62
|
DIFF_COLLAPSED_LINES: 40,
|
|
63
63
|
} as const;
|
|
64
64
|
|
|
65
|
+
/** Default number of terminal output rows shown before expansion. */
|
|
66
|
+
export const DEFAULT_TERMINAL_PREVIEW_LINES = 10;
|
|
67
|
+
|
|
65
68
|
/** Truncation lengths for different content types */
|
|
66
69
|
export const TRUNCATE_LENGTHS = {
|
|
67
70
|
/** Short titles, labels */
|
package/src/tools/renderers.ts
CHANGED
|
@@ -24,6 +24,7 @@ import { grepToolRenderer } from "./grep";
|
|
|
24
24
|
import { inspectImageToolRenderer } from "./inspect-image-renderer";
|
|
25
25
|
import { ircToolRenderer } from "./irc";
|
|
26
26
|
import { jobToolRenderer } from "./job";
|
|
27
|
+
import { launchToolRenderer } from "./launch";
|
|
27
28
|
import { recallToolRenderer, reflectToolRenderer, retainToolRenderer } from "./memory-render";
|
|
28
29
|
import { readToolRenderer } from "./read";
|
|
29
30
|
import { resolveToolRenderer } from "./resolve";
|
|
@@ -93,6 +94,7 @@ export const toolRenderers: Record<string, ToolRenderer> = {
|
|
|
93
94
|
lsp: lspToolRenderer as ToolRenderer,
|
|
94
95
|
inspect_image: inspectImageToolRenderer as ToolRenderer,
|
|
95
96
|
irc: ircToolRenderer as ToolRenderer,
|
|
97
|
+
launch: launchToolRenderer as ToolRenderer,
|
|
96
98
|
read: readToolRenderer as ToolRenderer,
|
|
97
99
|
job: jobToolRenderer as ToolRenderer,
|
|
98
100
|
resolve: resolveToolRenderer as ToolRenderer,
|