@oh-my-pi/pi-coding-agent 17.1.0 → 17.1.1

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.
Files changed (112) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/cli.js +3686 -4013
  3. package/dist/types/config/settings-schema.d.ts +58 -0
  4. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +3 -0
  5. package/dist/types/live/attestation.d.ts +2 -0
  6. package/dist/types/live/controller.d.ts +10 -2
  7. package/dist/types/live/protocol.d.ts +1 -1
  8. package/dist/types/live/transport.d.ts +6 -19
  9. package/dist/types/live/visualizer.d.ts +8 -11
  10. package/dist/types/modes/components/assistant-message.d.ts +1 -0
  11. package/dist/types/modes/components/custom-message.d.ts +1 -1
  12. package/dist/types/modes/components/message-frame.d.ts +8 -4
  13. package/dist/types/modes/components/session-account-selector.d.ts +11 -0
  14. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  15. package/dist/types/modes/interactive-mode.d.ts +1 -0
  16. package/dist/types/modes/types.d.ts +1 -0
  17. package/dist/types/session/agent-session-types.d.ts +8 -1
  18. package/dist/types/session/agent-session.d.ts +20 -1
  19. package/dist/types/session/auth-storage.d.ts +1 -1
  20. package/dist/types/session/eval-runner.d.ts +2 -0
  21. package/dist/types/session/messages.d.ts +2 -0
  22. package/dist/types/session/session-tools.d.ts +15 -0
  23. package/dist/types/session/streaming-output.d.ts +8 -0
  24. package/dist/types/slash-commands/helpers/session-pin.d.ts +9 -0
  25. package/dist/types/stt/index.d.ts +0 -2
  26. package/dist/types/stt/stt-controller.d.ts +7 -0
  27. package/dist/types/tiny/title-client.d.ts +10 -0
  28. package/dist/types/tools/builtin-names.d.ts +1 -1
  29. package/dist/types/tools/computer/protocol.d.ts +43 -0
  30. package/dist/types/tools/computer/supervisor.d.ts +32 -0
  31. package/dist/types/tools/computer/worker-entry.d.ts +1 -0
  32. package/dist/types/tools/computer/worker.d.ts +15 -0
  33. package/dist/types/tools/computer-renderer.d.ts +22 -0
  34. package/dist/types/tools/computer.d.ts +71 -0
  35. package/dist/types/tools/context.d.ts +2 -0
  36. package/dist/types/tools/default-renderer.d.ts +21 -0
  37. package/dist/types/tools/index.d.ts +2 -0
  38. package/dist/types/tts/streaming-player.d.ts +10 -43
  39. package/dist/types/utils/tools-manager.d.ts +1 -2
  40. package/package.json +12 -12
  41. package/src/cli/args.ts +1 -0
  42. package/src/cli/setup-cli.ts +2 -14
  43. package/src/cli.ts +8 -0
  44. package/src/config/model-registry.ts +6 -0
  45. package/src/config/settings-schema.ts +61 -0
  46. package/src/eval/executor-base.ts +1 -0
  47. package/src/eval/js/executor.ts +2 -0
  48. package/src/exec/bash-executor.ts +1 -0
  49. package/src/extensibility/extensions/wrapper.ts +68 -12
  50. package/src/extensibility/legacy-pi-coding-agent-shim.ts +7 -1
  51. package/src/live/attestation.ts +91 -0
  52. package/src/live/controller.ts +76 -23
  53. package/src/live/protocol.test.ts +3 -3
  54. package/src/live/protocol.ts +1 -1
  55. package/src/live/transport.ts +72 -140
  56. package/src/live/visualizer.ts +114 -134
  57. package/src/modes/components/assistant-message.ts +7 -2
  58. package/src/modes/components/custom-message.ts +4 -1
  59. package/src/modes/components/message-frame.ts +14 -8
  60. package/src/modes/components/session-account-selector.ts +62 -0
  61. package/src/modes/components/tool-execution.ts +17 -110
  62. package/src/modes/controllers/input-controller.ts +47 -39
  63. package/src/modes/controllers/live-command-controller.ts +82 -5
  64. package/src/modes/controllers/selector-controller.ts +62 -0
  65. package/src/modes/interactive-mode.ts +19 -0
  66. package/src/modes/types.ts +1 -0
  67. package/src/prompts/system/computer-safety.md +14 -0
  68. package/src/prompts/tools/computer.md +26 -0
  69. package/src/sdk.ts +5 -0
  70. package/src/session/agent-session-types.ts +9 -0
  71. package/src/session/agent-session.ts +56 -0
  72. package/src/session/auth-storage.ts +1 -0
  73. package/src/session/eval-runner.ts +5 -0
  74. package/src/session/messages.ts +3 -0
  75. package/src/session/session-tools.ts +37 -0
  76. package/src/session/streaming-output.ts +52 -5
  77. package/src/slash-commands/builtin-registry.ts +165 -9
  78. package/src/slash-commands/helpers/session-pin.ts +44 -0
  79. package/src/stt/downloader.ts +0 -2
  80. package/src/stt/index.ts +0 -2
  81. package/src/stt/stt-controller.ts +57 -146
  82. package/src/system-prompt.ts +4 -0
  83. package/src/tiny/title-client.ts +22 -0
  84. package/src/tools/bash-interactive.ts +90 -86
  85. package/src/tools/builtin-names.ts +1 -0
  86. package/src/tools/computer/protocol.ts +28 -0
  87. package/src/tools/computer/supervisor.ts +258 -0
  88. package/src/tools/computer/worker-entry.ts +25 -0
  89. package/src/tools/computer/worker.ts +135 -0
  90. package/src/tools/computer-renderer.ts +108 -0
  91. package/src/tools/computer.ts +433 -0
  92. package/src/tools/context.ts +2 -0
  93. package/src/tools/default-renderer.ts +139 -0
  94. package/src/tools/essential-tools.ts +1 -0
  95. package/src/tools/index.ts +5 -0
  96. package/src/tools/renderers.ts +2 -0
  97. package/src/tools/xdev.ts +54 -26
  98. package/src/tts/streaming-player.ts +81 -340
  99. package/src/utils/clipboard.ts +1 -30
  100. package/src/utils/mac-file-urls.applescript +37 -0
  101. package/src/utils/tool-choice.ts +14 -0
  102. package/src/utils/tools-manager.ts +1 -19
  103. package/dist/types/stt/recorder.d.ts +0 -30
  104. package/dist/types/stt/transcriber.d.ts +0 -14
  105. package/dist/types/stt/wav.d.ts +0 -29
  106. package/dist/types/tts/player.d.ts +0 -32
  107. package/src/live/audio-worklet.txt +0 -59
  108. package/src/live/browser-runtime.txt +0 -221
  109. package/src/stt/recorder.ts +0 -551
  110. package/src/stt/transcriber.ts +0 -60
  111. package/src/stt/wav.ts +0 -173
  112. package/src/tts/player.ts +0 -137
@@ -0,0 +1,258 @@
1
+ import type { DesktopAction, DesktopCapabilities, DesktopCapture, DesktopSessionOptions } from "@oh-my-pi/pi-natives";
2
+ import { withTimeout, workerHostEntry } from "@oh-my-pi/pi-utils";
3
+ import { ToolAbortError, ToolError } from "../tool-errors";
4
+ import {
5
+ COMPUTER_WORKER_ARG,
6
+ type ComputerWorkerError,
7
+ type ComputerWorkerInbound,
8
+ type ComputerWorkerOutbound,
9
+ } from "./protocol";
10
+
11
+ const START_TIMEOUT_MS = 10_000;
12
+ const CLOSE_TIMEOUT_MS = 1_500;
13
+ const SMOKE_TIMEOUT_MS = 5_000;
14
+
15
+ export interface ComputerController {
16
+ readonly capabilities: DesktopCapabilities | undefined;
17
+ execute(actions: DesktopAction[], signal?: AbortSignal): Promise<DesktopCapture>;
18
+ close(): Promise<void>;
19
+ }
20
+
21
+ export interface ComputerWorkerHandle {
22
+ send(message: ComputerWorkerInbound): void;
23
+ onMessage(handler: (message: ComputerWorkerOutbound) => void): () => void;
24
+ onError(handler: (error: Error) => void): () => void;
25
+ terminate(): Promise<void>;
26
+ }
27
+
28
+ export interface ComputerSupervisorTimeouts {
29
+ startMs: number;
30
+ closeMs: number;
31
+ }
32
+
33
+ const DEFAULT_TIMEOUTS: ComputerSupervisorTimeouts = {
34
+ startMs: START_TIMEOUT_MS,
35
+ closeMs: CLOSE_TIMEOUT_MS,
36
+ };
37
+ export type ComputerWorkerFactory = () => ComputerWorkerHandle;
38
+
39
+ function workerError(error: ComputerWorkerError): Error {
40
+ const result = new ToolError(error.message);
41
+ result.name = error.name;
42
+ if (error.stack) result.stack = error.stack;
43
+ return result;
44
+ }
45
+
46
+ function wrapWorker(worker: Worker): ComputerWorkerHandle {
47
+ return {
48
+ send(message) {
49
+ worker.postMessage(message);
50
+ },
51
+ onMessage(handler) {
52
+ const listener = (event: MessageEvent): void => handler(event.data as ComputerWorkerOutbound);
53
+ worker.addEventListener("message", listener);
54
+ return () => worker.removeEventListener("message", listener);
55
+ },
56
+ onError(handler) {
57
+ const listener = (event: ErrorEvent): void =>
58
+ handler(event.error instanceof Error ? event.error : new Error(event.message));
59
+ worker.addEventListener("error", listener);
60
+ return () => worker.removeEventListener("error", listener);
61
+ },
62
+ async terminate() {
63
+ worker.terminate();
64
+ },
65
+ };
66
+ }
67
+
68
+ export function spawnComputerWorker(): ComputerWorkerHandle {
69
+ const hostEntry = workerHostEntry();
70
+ const worker = hostEntry
71
+ ? new Worker(hostEntry, { type: "module", argv: [COMPUTER_WORKER_ARG] })
72
+ : new Worker(new URL("./worker-entry.ts", import.meta.url).href, { type: "module" });
73
+ return wrapWorker(worker);
74
+ }
75
+
76
+ interface PendingRequest {
77
+ resolve(capture: DesktopCapture): void;
78
+ reject(error: unknown): void;
79
+ }
80
+
81
+ export class ComputerSupervisor implements ComputerController {
82
+ #worker?: ComputerWorkerHandle;
83
+ #startPromise?: Promise<void>;
84
+ #capabilities?: DesktopCapabilities;
85
+ #pending = new Map<string, PendingRequest>();
86
+ #nextId = 0;
87
+ #serial: Promise<void> = Promise.resolve();
88
+ #closed = false;
89
+ #unsubscribeMessage?: () => void;
90
+ #unsubscribeError?: () => void;
91
+
92
+ constructor(
93
+ private readonly options: DesktopSessionOptions,
94
+ private readonly createWorker: ComputerWorkerFactory = spawnComputerWorker,
95
+ private readonly timeouts: ComputerSupervisorTimeouts = DEFAULT_TIMEOUTS,
96
+ ) {}
97
+
98
+ get capabilities(): DesktopCapabilities | undefined {
99
+ return this.#capabilities;
100
+ }
101
+
102
+ execute(actions: DesktopAction[], signal?: AbortSignal): Promise<DesktopCapture> {
103
+ if (this.#closed) return Promise.reject(new ToolError("Computer session is closed"));
104
+ const run = (): Promise<DesktopCapture> => this.#execute(actions, signal);
105
+ const result = this.#serial.then(run, run);
106
+ this.#serial = result.then(
107
+ () => undefined,
108
+ () => undefined,
109
+ );
110
+ return result;
111
+ }
112
+
113
+ async #execute(actions: DesktopAction[], signal?: AbortSignal): Promise<DesktopCapture> {
114
+ if (this.#closed) throw new ToolError("Computer session is closed");
115
+ if (signal?.aborted) throw new ToolAbortError();
116
+ await this.#start();
117
+ if (signal?.aborted) throw new ToolAbortError();
118
+ const id = `computer-${++this.#nextId}`;
119
+ const request = Promise.withResolvers<DesktopCapture>();
120
+ this.#pending.set(id, request);
121
+ this.#worker!.send({ type: "execute", id, actions });
122
+ if (!signal) return request.promise;
123
+
124
+ const aborted = Promise.withResolvers<never>();
125
+ const onAbort = (): void => aborted.reject(new ToolAbortError());
126
+ signal.addEventListener("abort", onAbort, { once: true });
127
+ try {
128
+ return await Promise.race([request.promise, aborted.promise]);
129
+ } catch (error) {
130
+ if (error instanceof ToolAbortError) await this.#terminate(error);
131
+ throw error;
132
+ } finally {
133
+ signal.removeEventListener("abort", onAbort);
134
+ }
135
+ }
136
+
137
+ #start(): Promise<void> {
138
+ if (this.#startPromise) return this.#startPromise;
139
+ const ready = Promise.withResolvers<void>();
140
+ try {
141
+ const worker = this.createWorker();
142
+ this.#worker = worker;
143
+ this.#unsubscribeMessage = worker.onMessage(message => {
144
+ if (message.type === "ready") {
145
+ this.#capabilities = message.capabilities;
146
+ ready.resolve();
147
+ return;
148
+ }
149
+ if (message.type === "result") {
150
+ this.#capabilities = message.capabilities;
151
+ const pending = this.#pending.get(message.id);
152
+ this.#pending.delete(message.id);
153
+ pending?.resolve(message.capture);
154
+ return;
155
+ }
156
+ if (message.type === "error") {
157
+ const error = workerError(message.error);
158
+ if (message.id) {
159
+ const pending = this.#pending.get(message.id);
160
+ this.#pending.delete(message.id);
161
+ pending?.reject(error);
162
+ } else {
163
+ ready.reject(error);
164
+ }
165
+ }
166
+ });
167
+ this.#unsubscribeError = worker.onError(error => {
168
+ ready.reject(error);
169
+ void this.#terminate(error);
170
+ });
171
+ worker.send({ type: "init", options: this.options });
172
+ } catch (error) {
173
+ ready.reject(error);
174
+ }
175
+ this.#startPromise = withTimeout(
176
+ ready.promise,
177
+ this.timeouts.startMs,
178
+ "Timed out starting native computer worker",
179
+ ).catch(async error => {
180
+ await this.#terminate(error);
181
+ throw error;
182
+ });
183
+ return this.#startPromise;
184
+ }
185
+
186
+ async #terminate(reason: unknown): Promise<void> {
187
+ const worker = this.#worker;
188
+ this.#worker = undefined;
189
+ this.#startPromise = undefined;
190
+ this.#capabilities = undefined;
191
+ this.#unsubscribeMessage?.();
192
+ this.#unsubscribeMessage = undefined;
193
+ this.#unsubscribeError?.();
194
+ this.#unsubscribeError = undefined;
195
+ for (const pending of this.#pending.values()) pending.reject(reason);
196
+ this.#pending.clear();
197
+ await worker?.terminate();
198
+ }
199
+
200
+ async close(): Promise<void> {
201
+ if (this.#closed) return;
202
+ this.#closed = true;
203
+ const worker = this.#worker;
204
+ if (!worker) return;
205
+ const closed = Promise.withResolvers<void>();
206
+ const unsubscribe = worker.onMessage(message => {
207
+ if (message.type === "closed") closed.resolve();
208
+ });
209
+ try {
210
+ worker.send({ type: "close" });
211
+ await withTimeout(closed.promise, this.timeouts.closeMs, "Timed out closing native computer worker");
212
+ } catch {
213
+ // Forced termination below is the bounded close fallback.
214
+ } finally {
215
+ unsubscribe();
216
+ await this.#terminate(new ToolError("Computer session closed"));
217
+ }
218
+ }
219
+ }
220
+
221
+ const ownedSupervisors = new Map<string, Set<ComputerController>>();
222
+
223
+ export function registerComputerController(ownerId: string | undefined, controller: ComputerController): () => void {
224
+ if (!ownerId) return () => {};
225
+ const controllers = ownedSupervisors.get(ownerId) ?? new Set<ComputerController>();
226
+ controllers.add(controller);
227
+ ownedSupervisors.set(ownerId, controllers);
228
+ return () => {
229
+ controllers.delete(controller);
230
+ if (controllers.size === 0) ownedSupervisors.delete(ownerId);
231
+ };
232
+ }
233
+
234
+ export async function releaseComputerSessionsForOwner(ownerId: string | undefined): Promise<void> {
235
+ if (!ownerId) return;
236
+ const controllers = ownedSupervisors.get(ownerId);
237
+ if (!controllers) return;
238
+ ownedSupervisors.delete(ownerId);
239
+ await Promise.allSettled(Array.from(controllers, controller => controller.close()));
240
+ }
241
+
242
+ export async function smokeTestComputerWorker(timeoutMs = SMOKE_TIMEOUT_MS): Promise<void> {
243
+ const worker = spawnComputerWorker();
244
+ const id = "computer-smoke";
245
+ const pong = Promise.withResolvers<void>();
246
+ const unsubscribeMessage = worker.onMessage(message => {
247
+ if (message.type === "pong" && message.id === id) pong.resolve();
248
+ });
249
+ const unsubscribeError = worker.onError(error => pong.reject(error));
250
+ try {
251
+ worker.send({ type: "ping", id });
252
+ await withTimeout(pong.promise, timeoutMs, "Computer worker smoke ping timed out");
253
+ } finally {
254
+ unsubscribeMessage();
255
+ unsubscribeError();
256
+ await worker.terminate();
257
+ }
258
+ }
@@ -0,0 +1,25 @@
1
+ import { parentPort } from "node:worker_threads";
2
+ import { consumeWorkerInbox } from "@oh-my-pi/pi-utils/worker-host";
3
+ import type { ComputerWorkerInbound, ComputerWorkerTransport } from "./protocol";
4
+ import { ComputerWorkerCore } from "./worker";
5
+
6
+ if (!parentPort) throw new Error("computer-worker-entry: missing parentPort");
7
+
8
+ const port = parentPort;
9
+ const inbox = consumeWorkerInbox();
10
+ const transport: ComputerWorkerTransport = {
11
+ send(message, transfer) {
12
+ port.postMessage(message, transfer ?? []);
13
+ },
14
+ onMessage(handler) {
15
+ if (inbox) return inbox.bind(message => handler(message as ComputerWorkerInbound));
16
+ const listener = (message: unknown): void => handler(message as ComputerWorkerInbound);
17
+ port.on("message", listener);
18
+ return () => port.off("message", listener);
19
+ },
20
+ close() {
21
+ port.close();
22
+ },
23
+ };
24
+
25
+ new ComputerWorkerCore(transport);
@@ -0,0 +1,135 @@
1
+ import {
2
+ type DesktopAction,
3
+ type DesktopCapture,
4
+ DesktopSession,
5
+ type DesktopSessionOptions,
6
+ } from "@oh-my-pi/pi-natives";
7
+ import type { ComputerWorkerError, ComputerWorkerInbound, ComputerWorkerTransport } from "./protocol";
8
+
9
+ export interface NativeDesktopSession {
10
+ readonly capabilities: DesktopSession["capabilities"];
11
+ capture(): Promise<DesktopCapture>;
12
+ execute(actions: DesktopAction[]): Promise<DesktopCapture>;
13
+ close(): Promise<void>;
14
+ }
15
+
16
+ export type NativeDesktopSessionFactory = (options: DesktopSessionOptions) => NativeDesktopSession;
17
+
18
+ const COORDINATE_ACTIONS: ReadonlySet<DesktopAction["type"]> = new Set([
19
+ "click",
20
+ "double_click",
21
+ "drag",
22
+ "move",
23
+ "scroll",
24
+ ]);
25
+
26
+ function serializeError(error: unknown): ComputerWorkerError {
27
+ if (error instanceof Error) {
28
+ return { name: error.name, message: error.message, ...(error.stack ? { stack: error.stack } : {}) };
29
+ }
30
+ return { name: "Error", message: String(error) };
31
+ }
32
+
33
+ function captureTransfer(capture: DesktopCapture): Bun.Transferable[] {
34
+ const buffer = capture.data.buffer;
35
+ return buffer instanceof ArrayBuffer ? [buffer] : [];
36
+ }
37
+
38
+ export class ComputerWorkerCore {
39
+ #session?: NativeDesktopSession;
40
+ #hasReturnedFrame = false;
41
+ #closed = false;
42
+ #tail: Promise<void> = Promise.resolve();
43
+ readonly #unsubscribe: () => void;
44
+
45
+ constructor(
46
+ private readonly transport: ComputerWorkerTransport,
47
+ private readonly createSession: NativeDesktopSessionFactory = options => new DesktopSession(options),
48
+ ) {
49
+ this.#unsubscribe = transport.onMessage(message => this.#onMessage(message));
50
+ }
51
+
52
+ #onMessage(message: ComputerWorkerInbound): void {
53
+ if (message.type === "ping") {
54
+ this.transport.send({ type: "pong", id: message.id });
55
+ return;
56
+ }
57
+ if (message.type === "close") {
58
+ this.#tail = this.#tail.then(() => this.#close());
59
+ return;
60
+ }
61
+ if (message.type === "init") {
62
+ this.#tail = this.#tail.then(() => this.#init(message.options));
63
+ return;
64
+ }
65
+ this.#tail = this.#tail.then(() => this.#execute(message.id, message.actions));
66
+ }
67
+
68
+ async #init(options: DesktopSessionOptions): Promise<void> {
69
+ if (this.#closed) return;
70
+ if (this.#session) {
71
+ this.transport.send({
72
+ type: "error",
73
+ error: { name: "Error", message: "Computer worker already initialized" },
74
+ });
75
+ return;
76
+ }
77
+ try {
78
+ this.#session = this.createSession(options);
79
+ this.#hasReturnedFrame = false;
80
+ this.transport.send({ type: "ready", capabilities: this.#session.capabilities });
81
+ } catch (error) {
82
+ this.transport.send({ type: "error", error: serializeError(error) });
83
+ }
84
+ }
85
+
86
+ async #execute(id: string, actions: DesktopAction[]): Promise<void> {
87
+ const session = this.#session;
88
+ if (!session) {
89
+ this.transport.send({
90
+ type: "error",
91
+ id,
92
+ error: { name: "Error", message: "Computer worker is not initialized" },
93
+ });
94
+ return;
95
+ }
96
+ try {
97
+ if (!this.#hasReturnedFrame && actions.some(action => COORDINATE_ACTIONS.has(action.type))) {
98
+ this.transport.send({
99
+ type: "error",
100
+ id,
101
+ error: {
102
+ name: "Error",
103
+ message:
104
+ "Coordinate computer actions require a screenshot returned to the provider; request a screenshot first",
105
+ },
106
+ });
107
+ return;
108
+ }
109
+ const capture = await session.execute(actions);
110
+ this.transport.send(
111
+ { type: "result", id, capture, capabilities: session.capabilities },
112
+ captureTransfer(capture),
113
+ );
114
+ this.#hasReturnedFrame = true;
115
+ } catch (error) {
116
+ this.transport.send({ type: "error", id, error: serializeError(error) });
117
+ }
118
+ }
119
+
120
+ async #close(): Promise<void> {
121
+ if (this.#closed) return;
122
+ this.#closed = true;
123
+ try {
124
+ await this.#session?.close();
125
+ } catch (error) {
126
+ this.transport.send({ type: "error", error: serializeError(error) });
127
+ } finally {
128
+ this.#session = undefined;
129
+ this.#hasReturnedFrame = false;
130
+ this.#unsubscribe();
131
+ this.transport.send({ type: "closed" });
132
+ this.transport.close();
133
+ }
134
+ }
135
+ }
@@ -0,0 +1,108 @@
1
+ import type { Component } from "@oh-my-pi/pi-tui";
2
+ import { Text } from "@oh-my-pi/pi-tui";
3
+ import { sanitizeText } from "@oh-my-pi/pi-utils";
4
+ import type { RenderResultOptions } from "../extensibility/custom-tools/types";
5
+ import type { Theme } from "../modes/theme/theme";
6
+ import { framedBlock, renderStatusLine } from "../tui";
7
+ import type { ComputerToolDetails } from "./computer";
8
+ import { replaceTabs, truncateToWidth } from "./render-utils";
9
+
10
+ interface ComputerRenderArgs {
11
+ actions?: Array<{ type?: unknown }>;
12
+ }
13
+
14
+ interface ComputerRenderResult {
15
+ content: Array<{ type: string; text?: string }>;
16
+ details?: unknown;
17
+ isError?: boolean;
18
+ }
19
+
20
+ function clean(value: unknown, width = 100): string {
21
+ const text = typeof value === "string" ? value : JSON.stringify(value) || String(value);
22
+ return truncateToWidth(replaceTabs(sanitizeText(text)).replace(/[\r\n]+/g, " "), width);
23
+ }
24
+
25
+ function isComputerToolDetails(value: unknown): value is ComputerToolDetails {
26
+ if (!value || typeof value !== "object") return false;
27
+ const details = value as Partial<ComputerToolDetails>;
28
+ return (
29
+ Array.isArray(details.actions) &&
30
+ Array.isArray(details.displays) &&
31
+ typeof details.width === "number" &&
32
+ typeof details.height === "number"
33
+ );
34
+ }
35
+
36
+ function actionDescription(args: ComputerRenderArgs | undefined): string | undefined {
37
+ if (!Array.isArray(args?.actions) || args.actions.length === 0) return undefined;
38
+ return clean(args.actions.map(action => (typeof action?.type === "string" ? action.type : "action")).join(" → "));
39
+ }
40
+
41
+ function resultDescription(details: ComputerToolDetails): string {
42
+ return clean(`${details.actions.join(" → ")} · ${details.width}×${details.height}`, 120);
43
+ }
44
+
45
+ function errorDescription(result: ComputerRenderResult, args: ComputerRenderArgs | undefined): string | undefined {
46
+ const text = result.content.find(item => item.type === "text" && typeof item.text === "string")?.text;
47
+ return text ? clean(text, 120) : actionDescription(args);
48
+ }
49
+
50
+ export const computerToolRenderer = {
51
+ mergeCallAndResult: true,
52
+ renderCall(args: ComputerRenderArgs, _options: RenderResultOptions, theme: Theme): Component {
53
+ return new Text(
54
+ renderStatusLine({ icon: "pending", title: "Computer", description: actionDescription(args) }, theme),
55
+ 0,
56
+ 0,
57
+ );
58
+ },
59
+ renderResult(
60
+ result: ComputerRenderResult,
61
+ options: RenderResultOptions,
62
+ theme: Theme,
63
+ args?: ComputerRenderArgs,
64
+ ): Component {
65
+ const details = isComputerToolDetails(result.details) ? result.details : undefined;
66
+ const header = renderStatusLine(
67
+ result.isError
68
+ ? { icon: "error", title: "Computer", description: errorDescription(result, args) }
69
+ : { icon: "success", title: "Computer", description: details ? resultDescription(details) : undefined },
70
+ theme,
71
+ );
72
+ if (!details) return new Text(header, 0, 0);
73
+ return framedBlock(theme, width => {
74
+ const body: string[] = [
75
+ theme.fg(
76
+ "dim",
77
+ `backend ${clean(details.backend)}${details.displayServer ? ` · server ${clean(details.displayServer)}` : ""} · capture ${clean(details.capturePermission)} · input ${clean(details.inputPermission)} · ${details.displays.length} display(s)`,
78
+ ),
79
+ ];
80
+ const displayLimit = options.expanded ? details.displays.length : Math.min(details.displays.length, 3);
81
+ for (const display of details.displays.slice(0, displayLimit)) {
82
+ body.push(
83
+ theme.fg(
84
+ "toolOutput",
85
+ clean(
86
+ `${display.id}${display.name ? ` ${display.name}` : ""}: logical ${display.x},${display.y} ${display.width}×${display.height}; pixels ${display.pixelX},${display.pixelY} ${display.pixelWidth}×${display.pixelHeight}; scale ${display.scale}${display.isPrimary ? "; primary" : ""}`,
87
+ 160,
88
+ ),
89
+ ),
90
+ );
91
+ }
92
+ if (displayLimit < details.displays.length) {
93
+ body.push(theme.fg("dim", `… ${details.displays.length - displayLimit} more display(s)`));
94
+ }
95
+ if (details.capabilities) {
96
+ body.push(theme.fg("dim", `capabilities ${clean(details.capabilities, 160)}`));
97
+ }
98
+ return {
99
+ header,
100
+ sections: [{ lines: body }],
101
+ state: result.isError ? "error" : "success",
102
+ borderColor: result.isError ? "error" : "borderMuted",
103
+ applyBg: false,
104
+ width,
105
+ };
106
+ });
107
+ },
108
+ };