@oh-my-pi/pi-coding-agent 17.0.4 → 17.0.5
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 +86 -43
- package/dist/cli.js +3540 -3521
- package/dist/types/advisor/advise-tool.d.ts +3 -0
- package/dist/types/advisor/runtime.d.ts +7 -1
- package/dist/types/async/job-manager.d.ts +2 -0
- package/dist/types/config/model-registry.d.ts +7 -0
- package/dist/types/config/model-resolver.d.ts +8 -0
- package/dist/types/config/models-config-schema.d.ts +10 -0
- package/dist/types/config/models-config.d.ts +6 -0
- package/dist/types/dap/client.d.ts +10 -0
- package/dist/types/dap/types.d.ts +6 -5
- package/dist/types/extensibility/extensions/runner.d.ts +4 -2
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +16 -0
- package/dist/types/launch/spawn-options.d.ts +10 -0
- package/dist/types/launch/spawn-options.test.d.ts +1 -0
- package/dist/types/modes/components/status-line/component.d.ts +2 -3
- package/dist/types/modes/components/status-line/types.d.ts +3 -1
- package/dist/types/modes/components/tool-execution.d.ts +2 -0
- package/dist/types/modes/components/transcript-container.d.ts +4 -3
- package/dist/types/modes/components/tree-selector.d.ts +6 -2
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/print-mode.d.ts +4 -0
- package/dist/types/modes/rpc/rpc-input.d.ts +5 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +1 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +19 -1
- package/dist/types/session/messages.d.ts +6 -0
- package/dist/types/tools/browser/aria/aria-snapshot.d.ts +17 -7
- package/dist/types/tools/browser/cmux/socket-client.d.ts +2 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +2 -0
- package/dist/types/tools/browser/tab-worker.d.ts +2 -0
- package/dist/types/tools/gh.d.ts +5 -3
- package/dist/types/tools/hub/index.d.ts +1 -1
- package/dist/types/tools/hub/jobs.d.ts +1 -0
- package/dist/types/tools/hub/types.d.ts +2 -0
- package/dist/types/tools/tool-timeouts.d.ts +1 -1
- package/dist/types/utils/git.d.ts +33 -0
- package/dist/types/web/search/providers/codex.d.ts +1 -1
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +150 -0
- package/src/advisor/advise-tool.ts +4 -0
- package/src/advisor/runtime.ts +38 -14
- package/src/async/job-manager.ts +3 -0
- package/src/cli/bench-cli.ts +8 -2
- package/src/cli/dry-balance-cli.ts +1 -0
- package/src/config/model-registry.ts +89 -8
- package/src/config/model-resolver.ts +78 -14
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings.ts +3 -1
- package/src/dap/client.ts +168 -1
- package/src/dap/config.ts +51 -1
- package/src/dap/session.ts +575 -234
- package/src/dap/types.ts +6 -5
- package/src/discovery/agents.ts +2 -2
- package/src/extensibility/extensions/runner.ts +6 -4
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +26 -0
- package/src/launch/broker.ts +34 -31
- package/src/launch/client.ts +7 -1
- package/src/launch/spawn-options.test.ts +31 -0
- package/src/launch/spawn-options.ts +17 -0
- package/src/lsp/types.ts +5 -1
- package/src/main.ts +17 -4
- package/src/mcp/transports/stdio.test.ts +9 -1
- package/src/modes/components/ask-dialog.ts +137 -73
- package/src/modes/components/status-line/component.ts +2 -2
- package/src/modes/components/status-line/segments.ts +20 -3
- package/src/modes/components/status-line/types.ts +3 -1
- package/src/modes/components/tool-execution.ts +5 -0
- package/src/modes/components/transcript-container.ts +23 -122
- package/src/modes/components/tree-selector.ts +10 -4
- package/src/modes/controllers/event-controller.ts +1 -2
- package/src/modes/controllers/input-controller.ts +1 -1
- package/src/modes/controllers/selector-controller.ts +29 -8
- package/src/modes/interactive-mode.ts +40 -8
- package/src/modes/noninteractive-dispose.test.ts +12 -1
- package/src/modes/print-mode.ts +21 -9
- package/src/modes/rpc/rpc-input.ts +38 -0
- package/src/modes/rpc/rpc-mode.ts +7 -2
- package/src/modes/setup-wizard/scenes/sign-in.ts +3 -1
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/ui-helpers.ts +3 -2
- package/src/prompts/tools/browser.md +3 -2
- package/src/prompts/tools/debug.md +2 -7
- package/src/prompts/tools/github.md +6 -1
- package/src/prompts/tools/hub.md +4 -2
- package/src/prompts/tools/task-async-contract.md +1 -0
- package/src/prompts/tools/task.md +9 -2
- package/src/sdk.ts +38 -6
- package/src/session/agent-session.ts +395 -162
- package/src/session/messages.test.ts +91 -0
- package/src/session/messages.ts +248 -110
- package/src/slash-commands/builtin-registry.ts +1 -0
- package/src/task/executor.ts +59 -33
- package/src/task/index.ts +46 -9
- package/src/task/worktree.ts +10 -0
- package/src/tools/browser/aria/aria-snapshot.ts +36 -8
- package/src/tools/browser/cmux/cmux-tab.ts +2 -1
- package/src/tools/browser/cmux/socket-client.ts +139 -3
- package/src/tools/browser/run-cancellation.ts +4 -0
- package/src/tools/browser/tab-protocol.ts +2 -0
- package/src/tools/browser/tab-supervisor.ts +21 -11
- package/src/tools/browser/tab-worker.ts +199 -33
- package/src/tools/debug.ts +3 -0
- package/src/tools/gh.ts +40 -2
- package/src/tools/hub/index.ts +4 -1
- package/src/tools/hub/jobs.ts +42 -1
- package/src/tools/hub/types.ts +2 -0
- package/src/tools/tool-timeouts.ts +1 -1
- package/src/utils/git.ts +237 -0
- package/src/web/search/index.ts +9 -5
- package/src/web/search/providers/codex.ts +195 -99
|
@@ -138,6 +138,7 @@ const GRACE_MS = 750;
|
|
|
138
138
|
// vanished instead of a bare "not alive". Cleared when the name is opened again.
|
|
139
139
|
const killedTabs = new Map<string, string>();
|
|
140
140
|
const DEFAULT_TAB_CLOSE_TIMEOUT_MS = 5_000;
|
|
141
|
+
class RecoverableWorkerError extends ToolError {}
|
|
141
142
|
|
|
142
143
|
async function waitForTabCleanup<T>(
|
|
143
144
|
tab: TabSession,
|
|
@@ -488,16 +489,23 @@ async function runInTabWithSnapshot(
|
|
|
488
489
|
async reason => await forceKillTab(name, reason),
|
|
489
490
|
);
|
|
490
491
|
} catch (error) {
|
|
491
|
-
|
|
492
|
+
const runTimedOut =
|
|
493
|
+
error instanceof ToolError && error.message.startsWith("Browser code execution timed out after ");
|
|
494
|
+
if (runTimedOut || error instanceof RecoverableWorkerError) {
|
|
492
495
|
try {
|
|
493
|
-
if (tab.worker.mode === "inline")
|
|
494
|
-
|
|
495
|
-
|
|
496
|
+
if (tab.worker.mode === "inline") {
|
|
497
|
+
const reason = runTimedOut
|
|
498
|
+
? "Browser code execution timed out; tab killed"
|
|
499
|
+
: "Browser request interception cleanup failed; tab killed";
|
|
500
|
+
await forceKillTab(name, reason);
|
|
501
|
+
} else {
|
|
502
|
+
await recycleTimedOutWorkerTab(tab, opts.timeoutMs + GRACE_MS);
|
|
503
|
+
}
|
|
496
504
|
} catch (recycleError) {
|
|
497
|
-
logger.warn("Failed to recycle
|
|
505
|
+
logger.warn("Failed to recycle browser tab worker; killing tab", {
|
|
498
506
|
error: recycleError instanceof Error ? recycleError.message : String(recycleError),
|
|
499
507
|
});
|
|
500
|
-
await forceKillTab(name, "Browser
|
|
508
|
+
await forceKillTab(name, "Browser tab worker recovery failed; tab killed");
|
|
501
509
|
}
|
|
502
510
|
}
|
|
503
511
|
throw error;
|
|
@@ -873,11 +881,13 @@ async function targetIdForTarget(target: Target): Promise<string> {
|
|
|
873
881
|
}
|
|
874
882
|
|
|
875
883
|
function errorFromPayload(payload: RunErrorPayload): Error {
|
|
876
|
-
const error = payload.
|
|
877
|
-
? new
|
|
878
|
-
: payload.
|
|
879
|
-
? new
|
|
880
|
-
:
|
|
884
|
+
const error = payload.recoverTab
|
|
885
|
+
? new RecoverableWorkerError(payload.message)
|
|
886
|
+
: payload.isAbort
|
|
887
|
+
? new ToolAbortError()
|
|
888
|
+
: payload.isToolError
|
|
889
|
+
? new ToolError(payload.message)
|
|
890
|
+
: new Error(payload.message);
|
|
881
891
|
error.name = payload.name;
|
|
882
892
|
if (payload.stack) error.stack = payload.stack;
|
|
883
893
|
return error;
|
|
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
|
|
5
|
-
import { postmortem, Snowflake, untilAborted } from "@oh-my-pi/pi-utils";
|
|
5
|
+
import { postmortem, Snowflake, untilAborted, withTimeout } from "@oh-my-pi/pi-utils";
|
|
6
6
|
import type { HTMLElement } from "linkedom";
|
|
7
7
|
import type {
|
|
8
8
|
Browser,
|
|
@@ -24,6 +24,7 @@ import { formatScreenshot } from "../render-utils";
|
|
|
24
24
|
import { ToolAbortError, ToolError, throwIfAborted } from "../tool-errors";
|
|
25
25
|
import {
|
|
26
26
|
type AriaSnapshotOptions,
|
|
27
|
+
assertSelectorString,
|
|
27
28
|
captureAriaSnapshot,
|
|
28
29
|
parseAriaRefSelector,
|
|
29
30
|
resolveAriaRefHandle,
|
|
@@ -37,6 +38,7 @@ import {
|
|
|
37
38
|
} from "./launch";
|
|
38
39
|
import { extractReadableFromHtml, type ReadableFormat } from "./readable";
|
|
39
40
|
import {
|
|
41
|
+
bindBrowserRunFacade,
|
|
40
42
|
CELL_BUDGET_SLACK_MS,
|
|
41
43
|
markHandled,
|
|
42
44
|
resolvePredicateTimeout,
|
|
@@ -144,6 +146,8 @@ interface OpenDialogInfo {
|
|
|
144
146
|
*/
|
|
145
147
|
const QUICK_OP_TIMEOUT_MS = 20_000;
|
|
146
148
|
const ACTION_OP_TIMEOUT_MS = 8_000;
|
|
149
|
+
/** Maximum wait for a renderer acknowledgement after a wheel event is queued. */
|
|
150
|
+
const SCROLL_ACK_TIMEOUT_MS = 2_000;
|
|
147
151
|
/** Headroom subtracted from the cell budget so a per-op deadline fires before it. */
|
|
148
152
|
const OP_DEADLINE_SLACK_MS = CELL_BUDGET_SLACK_MS;
|
|
149
153
|
/**
|
|
@@ -155,6 +159,8 @@ const OP_DEADLINE_SLACK_MS = CELL_BUDGET_SLACK_MS;
|
|
|
155
159
|
const ZERO_MATCH_FAIL_FAST_MS = 2_000;
|
|
156
160
|
/** Poll cadence for the zero-match watchdog. */
|
|
157
161
|
const ZERO_MATCH_POLL_MS = 250;
|
|
162
|
+
/** Cleanup must settle inside the supervisor's 750ms post-run grace window. */
|
|
163
|
+
const REQUEST_INTERCEPTION_CLEANUP_TIMEOUT_MS = 500;
|
|
158
164
|
|
|
159
165
|
export interface OpTimeouts {
|
|
160
166
|
/** Largest per-op deadline allowed — strictly below the cell budget. */
|
|
@@ -175,6 +181,21 @@ export function resolveOpTimeouts(cellTimeoutMs: number): OpTimeouts {
|
|
|
175
181
|
};
|
|
176
182
|
}
|
|
177
183
|
|
|
184
|
+
/** Queue a wheel event without treating a delayed renderer acknowledgement as dispatch failure. */
|
|
185
|
+
export async function dispatchScroll(
|
|
186
|
+
dispatch: () => Promise<void>,
|
|
187
|
+
ackTimeoutMs = SCROLL_ACK_TIMEOUT_MS,
|
|
188
|
+
): Promise<void> {
|
|
189
|
+
const deadline = Promise.withResolvers<void>();
|
|
190
|
+
const timer = setTimeout(() => deadline.resolve(), ackTimeoutMs);
|
|
191
|
+
timer.unref();
|
|
192
|
+
try {
|
|
193
|
+
await Promise.race([dispatch(), deadline.promise]);
|
|
194
|
+
} finally {
|
|
195
|
+
clearTimeout(timer);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
178
199
|
/**
|
|
179
200
|
* Effective timeout for a wait helper (`waitFor*`). A positive explicit `{ timeout }` is
|
|
180
201
|
* honored but clamped to the cell budget so it still fails fast + named; raising the tool
|
|
@@ -246,6 +267,7 @@ interface TabApi {
|
|
|
246
267
|
}
|
|
247
268
|
|
|
248
269
|
export function normalizeSelector(selector: string): string {
|
|
270
|
+
assertSelectorString(selector);
|
|
249
271
|
if (!selector) return selector;
|
|
250
272
|
if (
|
|
251
273
|
!SELECTOR_HANDLER_PREFIXES.some(prefix => selector.startsWith(prefix)) &&
|
|
@@ -334,12 +356,125 @@ function redactUrlCredentials(url: string): string {
|
|
|
334
356
|
}
|
|
335
357
|
}
|
|
336
358
|
|
|
359
|
+
class RequestInterceptionCleanupError extends ToolError {}
|
|
360
|
+
|
|
361
|
+
interface RunPageScope {
|
|
362
|
+
page: Page;
|
|
363
|
+
cleanup(): Promise<void>;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Expose the tab page while retaining the request handlers created by this run.
|
|
368
|
+
* Puppeteer's Page wraps an internal emitter, so `removeAllListeners("request")`
|
|
369
|
+
* would also remove its forwarding listener; the facade removes only user handlers.
|
|
370
|
+
*/
|
|
371
|
+
function createRunPageScope(page: Page): RunPageScope {
|
|
372
|
+
const requestHandlers: unknown[] = [];
|
|
373
|
+
const on = page.on;
|
|
374
|
+
const off = page.off;
|
|
375
|
+
const once = page.once;
|
|
376
|
+
const removeAllListeners = page.removeAllListeners;
|
|
377
|
+
const onDescriptor = Object.getOwnPropertyDescriptor(page, "on");
|
|
378
|
+
const offDescriptor = Object.getOwnPropertyDescriptor(page, "off");
|
|
379
|
+
const onceDescriptor = Object.getOwnPropertyDescriptor(page, "once");
|
|
380
|
+
const removeAllDescriptor = Object.getOwnPropertyDescriptor(page, "removeAllListeners");
|
|
381
|
+
|
|
382
|
+
Object.defineProperties(page, {
|
|
383
|
+
on: {
|
|
384
|
+
configurable: true,
|
|
385
|
+
value: (type: unknown, handler: unknown): Page => {
|
|
386
|
+
Reflect.apply(on, page, [type, handler]);
|
|
387
|
+
if (type === "request") requestHandlers.push(handler);
|
|
388
|
+
return page;
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
once: {
|
|
392
|
+
configurable: true,
|
|
393
|
+
value: (type: unknown, handler: unknown): Page => {
|
|
394
|
+
if (type !== "request" || typeof handler !== "function") {
|
|
395
|
+
Reflect.apply(once, page, [type, handler]);
|
|
396
|
+
return page;
|
|
397
|
+
}
|
|
398
|
+
const wrapper = (event: unknown): void => {
|
|
399
|
+
const index = requestHandlers.lastIndexOf(wrapper);
|
|
400
|
+
if (index >= 0) requestHandlers.splice(index, 1);
|
|
401
|
+
Reflect.apply(off, page, ["request", wrapper]);
|
|
402
|
+
Reflect.apply(handler, page, [event]);
|
|
403
|
+
};
|
|
404
|
+
requestHandlers.push(wrapper);
|
|
405
|
+
Reflect.apply(on, page, [type, wrapper]);
|
|
406
|
+
return page;
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
off: {
|
|
410
|
+
configurable: true,
|
|
411
|
+
value: (type: unknown, handler?: unknown): Page => {
|
|
412
|
+
Reflect.apply(off, page, [type, handler]);
|
|
413
|
+
if (type === "request") {
|
|
414
|
+
if (handler === undefined) requestHandlers.length = 0;
|
|
415
|
+
else {
|
|
416
|
+
const index = requestHandlers.lastIndexOf(handler);
|
|
417
|
+
if (index >= 0) requestHandlers.splice(index, 1);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return page;
|
|
421
|
+
},
|
|
422
|
+
},
|
|
423
|
+
removeAllListeners: {
|
|
424
|
+
configurable: true,
|
|
425
|
+
value: (type?: unknown): Page => {
|
|
426
|
+
Reflect.apply(removeAllListeners, page, [type]);
|
|
427
|
+
if (type === undefined || type === "request") requestHandlers.length = 0;
|
|
428
|
+
return page;
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
return {
|
|
434
|
+
page,
|
|
435
|
+
async cleanup() {
|
|
436
|
+
if (onDescriptor) Object.defineProperty(page, "on", onDescriptor);
|
|
437
|
+
else Reflect.deleteProperty(page, "on");
|
|
438
|
+
if (offDescriptor) Object.defineProperty(page, "off", offDescriptor);
|
|
439
|
+
else Reflect.deleteProperty(page, "off");
|
|
440
|
+
if (onceDescriptor) Object.defineProperty(page, "once", onceDescriptor);
|
|
441
|
+
else Reflect.deleteProperty(page, "once");
|
|
442
|
+
if (removeAllDescriptor) Object.defineProperty(page, "removeAllListeners", removeAllDescriptor);
|
|
443
|
+
else Reflect.deleteProperty(page, "removeAllListeners");
|
|
444
|
+
for (const handler of requestHandlers) Reflect.apply(off, page, ["request", handler]);
|
|
445
|
+
requestHandlers.length = 0;
|
|
446
|
+
try {
|
|
447
|
+
await withTimeout(
|
|
448
|
+
page.setRequestInterception(false),
|
|
449
|
+
REQUEST_INTERCEPTION_CLEANUP_TIMEOUT_MS,
|
|
450
|
+
"Timed out clearing browser request interception",
|
|
451
|
+
);
|
|
452
|
+
} catch (error) {
|
|
453
|
+
throw new RequestInterceptionCleanupError(
|
|
454
|
+
"Failed to clear browser request interception after browser.run",
|
|
455
|
+
{
|
|
456
|
+
error: error instanceof Error ? error.message : String(error),
|
|
457
|
+
},
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
},
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
337
464
|
function errorPayload(error: unknown): RunErrorPayload {
|
|
465
|
+
const recoverTab = error instanceof RequestInterceptionCleanupError || undefined;
|
|
338
466
|
if (error instanceof ToolAbortError) {
|
|
339
467
|
return { name: error.name, message: error.message, stack: error.stack, isToolError: false, isAbort: true };
|
|
340
468
|
}
|
|
341
469
|
if (error instanceof ToolError) {
|
|
342
|
-
return {
|
|
470
|
+
return {
|
|
471
|
+
name: error.name,
|
|
472
|
+
message: error.message,
|
|
473
|
+
stack: error.stack,
|
|
474
|
+
isToolError: true,
|
|
475
|
+
isAbort: false,
|
|
476
|
+
recoverTab,
|
|
477
|
+
};
|
|
343
478
|
}
|
|
344
479
|
if (error instanceof Error) {
|
|
345
480
|
return { name: error.name, message: error.message, stack: error.stack, isToolError: false, isAbort: false };
|
|
@@ -720,6 +855,7 @@ export class WorkerCore {
|
|
|
720
855
|
await session.send("Page.enable").catch(() => undefined);
|
|
721
856
|
await session.send("Page.handleJavaScriptDialog", { accept: false }).catch(() => undefined);
|
|
722
857
|
await session.send("Page.stopLoading").catch(() => undefined);
|
|
858
|
+
await session.send("Fetch.disable").catch(() => undefined);
|
|
723
859
|
} catch (error) {
|
|
724
860
|
this.#log("debug", "Recovery CDP session failed; proceeding with attach", {
|
|
725
861
|
error: error instanceof Error ? error.message : String(error),
|
|
@@ -816,17 +952,21 @@ export class WorkerCore {
|
|
|
816
952
|
opCounter: 0,
|
|
817
953
|
};
|
|
818
954
|
this.#active = active;
|
|
955
|
+
let completed = false;
|
|
956
|
+
let returnValue: unknown;
|
|
957
|
+
let failure: { error: unknown } | undefined;
|
|
958
|
+
let runPage: RunPageScope | undefined;
|
|
819
959
|
try {
|
|
820
960
|
throwIfAborted(signal);
|
|
821
|
-
|
|
961
|
+
runPage = createRunPageScope(this.#requirePage());
|
|
822
962
|
const browser = this.#requireBrowser();
|
|
823
963
|
const tabApi = this.#createTabApi(msg.name, msg.timeoutMs, signal, msg.session, output, screenshots, active);
|
|
824
964
|
const runtime = this.#ensureRuntime(msg.session);
|
|
825
965
|
runtime.setCwd(msg.session.cwd);
|
|
826
966
|
runtime.setRunScope({
|
|
827
|
-
page,
|
|
828
|
-
browser,
|
|
829
|
-
tab: tabApi,
|
|
967
|
+
page: bindBrowserRunFacade(runPage.page, signal),
|
|
968
|
+
browser: bindBrowserRunFacade(browser, signal),
|
|
969
|
+
tab: bindBrowserRunFacade(tabApi, signal),
|
|
830
970
|
assert: (cond: unknown, text?: string): void => {
|
|
831
971
|
if (!cond) throw new ToolError(text ?? "Assertion failed");
|
|
832
972
|
},
|
|
@@ -879,25 +1019,37 @@ export class WorkerCore {
|
|
|
879
1019
|
try {
|
|
880
1020
|
const hooks = this.#hooksForActiveRun();
|
|
881
1021
|
if (!hooks) throw new ToolError("Browser runtime started without an active run");
|
|
882
|
-
|
|
1022
|
+
returnValue = await Promise.race([
|
|
883
1023
|
runtime.run(msg.code, `browser-run-${msg.id}.js`, hooks, { runId: msg.id, cwd: msg.session.cwd }),
|
|
884
1024
|
cancelRejection,
|
|
885
1025
|
]);
|
|
886
|
-
|
|
887
|
-
this.#transport.send({
|
|
888
|
-
type: "result",
|
|
889
|
-
id: msg.id,
|
|
890
|
-
ok: true,
|
|
891
|
-
payload: { displays: output.finish(), returnValue: cloneSafe(returnValue), screenshots },
|
|
892
|
-
});
|
|
1026
|
+
completed = true;
|
|
893
1027
|
} finally {
|
|
894
1028
|
signal.removeEventListener("abort", onCancel);
|
|
895
1029
|
}
|
|
896
1030
|
} catch (error) {
|
|
897
|
-
|
|
1031
|
+
failure = { error };
|
|
898
1032
|
} finally {
|
|
899
|
-
if (this.#active?.id === msg.id) this.#active = null;
|
|
900
1033
|
runAc.abort(postmortem.markExpectedCleanupError(new ToolAbortError("Browser run ended")));
|
|
1034
|
+
try {
|
|
1035
|
+
await runPage?.cleanup();
|
|
1036
|
+
} catch (error) {
|
|
1037
|
+
failure = { error };
|
|
1038
|
+
}
|
|
1039
|
+
if (this.#active?.id === msg.id) this.#active = null;
|
|
1040
|
+
}
|
|
1041
|
+
if (failure) {
|
|
1042
|
+
this.#transport.send({ type: "result", id: msg.id, ok: false, error: errorPayload(failure.error) });
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
if (completed) {
|
|
1046
|
+
await this.#postReadyInfo();
|
|
1047
|
+
this.#transport.send({
|
|
1048
|
+
type: "result",
|
|
1049
|
+
id: msg.id,
|
|
1050
|
+
ok: true,
|
|
1051
|
+
payload: { displays: output.finish(), returnValue: cloneSafe(returnValue), screenshots },
|
|
1052
|
+
});
|
|
901
1053
|
}
|
|
902
1054
|
}
|
|
903
1055
|
|
|
@@ -1212,11 +1364,22 @@ export class WorkerCore {
|
|
|
1212
1364
|
press: (key, opts) =>
|
|
1213
1365
|
op(`tab.press(${JSON.stringify(key)})`, actionOpMs, async sig => {
|
|
1214
1366
|
const selector = opts?.selector;
|
|
1215
|
-
if (selector)
|
|
1367
|
+
if (selector) {
|
|
1368
|
+
if (parseAriaRefSelector(selector) !== null) {
|
|
1369
|
+
const handle = await this.#resolveAriaRef(selector);
|
|
1370
|
+
try {
|
|
1371
|
+
await untilAborted(sig, () => handle.focus());
|
|
1372
|
+
} finally {
|
|
1373
|
+
await handle.dispose().catch(() => undefined);
|
|
1374
|
+
}
|
|
1375
|
+
} else await untilAborted(sig, () => page.focus(normalizeSelector(selector)));
|
|
1376
|
+
}
|
|
1216
1377
|
await untilAborted(sig, () => page.keyboard.press(key));
|
|
1217
1378
|
}),
|
|
1218
1379
|
scroll: (deltaX, deltaY) =>
|
|
1219
|
-
op("tab.scroll()", actionOpMs, sig =>
|
|
1380
|
+
op("tab.scroll()", actionOpMs, sig =>
|
|
1381
|
+
untilAborted(sig, () => dispatchScroll(() => page.mouse.wheel({ deltaX, deltaY }))),
|
|
1382
|
+
),
|
|
1220
1383
|
drag: (from, to) => op("tab.drag()", actionOpMs, sig => this.#drag(from, to, sig)),
|
|
1221
1384
|
waitFor: (selector, opts) => {
|
|
1222
1385
|
const w = waitMs(opts?.timeout);
|
|
@@ -1387,9 +1550,10 @@ export class WorkerCore {
|
|
|
1387
1550
|
const captureMime = `image/${captureType}` as const;
|
|
1388
1551
|
let buffer: Buffer;
|
|
1389
1552
|
if (opts.selector) {
|
|
1390
|
-
const handle =
|
|
1391
|
-
|
|
1392
|
-
|
|
1553
|
+
const handle =
|
|
1554
|
+
parseAriaRefSelector(opts.selector) !== null
|
|
1555
|
+
? await this.#resolveAriaRef(opts.selector)
|
|
1556
|
+
: asElementHandle(await untilAborted(signal, () => page.$(normalizeSelector(opts.selector!))));
|
|
1393
1557
|
if (!handle) throw new ToolError("Screenshot selector did not resolve to an element");
|
|
1394
1558
|
try {
|
|
1395
1559
|
// Bring the element into view with a single instant scroll instead of puppeteer's
|
|
@@ -1462,9 +1626,10 @@ export class WorkerCore {
|
|
|
1462
1626
|
role: "from" | "to",
|
|
1463
1627
|
): Promise<{ x: number; y: number; handle?: ElementHandle }> => {
|
|
1464
1628
|
if (typeof target === "string") {
|
|
1465
|
-
const handle =
|
|
1466
|
-
|
|
1467
|
-
|
|
1629
|
+
const handle =
|
|
1630
|
+
parseAriaRefSelector(target) !== null
|
|
1631
|
+
? await this.#resolveAriaRef(target)
|
|
1632
|
+
: asElementHandle(await untilAborted(signal, () => page.$(normalizeSelector(target))));
|
|
1468
1633
|
if (!handle) throw new ToolError(`Drag ${role} selector did not resolve: ${target}`);
|
|
1469
1634
|
const box = (await untilAborted(signal, () => handle.boundingBox())) as {
|
|
1470
1635
|
x: number;
|
|
@@ -1505,10 +1670,7 @@ export class WorkerCore {
|
|
|
1505
1670
|
}
|
|
1506
1671
|
|
|
1507
1672
|
async #select(selector: string, values: string[], timeoutMs: number, signal: AbortSignal): Promise<string[]> {
|
|
1508
|
-
const
|
|
1509
|
-
const handle = (await untilAborted(signal, () =>
|
|
1510
|
-
page.locator(normalizeSelector(selector)).setTimeout(timeoutMs).waitHandle({ signal }),
|
|
1511
|
-
)) as ElementHandle;
|
|
1673
|
+
const handle = await this.#resolveActionHandle(selector, timeoutMs, signal);
|
|
1512
1674
|
try {
|
|
1513
1675
|
return (await untilAborted(signal, () =>
|
|
1514
1676
|
handle.evaluate((el, vals) => {
|
|
@@ -1527,10 +1689,17 @@ export class WorkerCore {
|
|
|
1527
1689
|
globalThis as unknown as { Event: new (type: string, init?: { bubbles: boolean }) => unknown }
|
|
1528
1690
|
).Event;
|
|
1529
1691
|
const wanted = new Set(vals as string[]);
|
|
1530
|
-
|
|
1692
|
+
// Assign the full selection first, then read back: on a single
|
|
1693
|
+
// <select>, un-selecting the current option mid-loop leaves the
|
|
1694
|
+
// browser reporting it selected until another option takes over,
|
|
1695
|
+
// which double-counted the old value in the returned list.
|
|
1531
1696
|
for (let i = 0; i < select.options.length; i++) {
|
|
1532
1697
|
const opt = select.options[i] as SelectOption;
|
|
1533
1698
|
opt.selected = wanted.has(opt.value);
|
|
1699
|
+
}
|
|
1700
|
+
const selected: string[] = [];
|
|
1701
|
+
for (let i = 0; i < select.options.length; i++) {
|
|
1702
|
+
const opt = select.options[i] as SelectOption;
|
|
1534
1703
|
if (opt.selected) selected.push(opt.value);
|
|
1535
1704
|
}
|
|
1536
1705
|
select.dispatchEvent(new EventCtor("input", { bubbles: true }));
|
|
@@ -1551,10 +1720,7 @@ export class WorkerCore {
|
|
|
1551
1720
|
session: SessionSnapshot,
|
|
1552
1721
|
): Promise<void> {
|
|
1553
1722
|
if (!filePaths.length) throw new ToolError("tab.uploadFile() requires at least one file path");
|
|
1554
|
-
const
|
|
1555
|
-
const handle = (await untilAborted(signal, () =>
|
|
1556
|
-
page.locator(normalizeSelector(selector)).setTimeout(timeoutMs).waitHandle({ signal }),
|
|
1557
|
-
)) as ElementHandle;
|
|
1723
|
+
const handle = await this.#resolveActionHandle(selector, timeoutMs, signal);
|
|
1558
1724
|
try {
|
|
1559
1725
|
const absolute = filePaths.map(filePath => resolveToCwd(filePath, session.cwd));
|
|
1560
1726
|
const upload = handle as unknown as { uploadFile: (...paths: string[]) => Promise<void> };
|
package/src/tools/debug.ts
CHANGED
|
@@ -504,12 +504,15 @@ const ADAPTER_UNAVAILABLE_MESSAGES: Readonly<Record<string, string>> = {
|
|
|
504
504
|
debugpy: "adapter 'debugpy' is not available: python not found in PATH",
|
|
505
505
|
dlv: "adapter 'dlv' is not available: install with 'go install github.com/go-delve/delve/cmd/dlv@latest'",
|
|
506
506
|
rdbg: "adapter 'rdbg' is not available: install with 'gem install debug'",
|
|
507
|
+
"js-debug-adapter":
|
|
508
|
+
"adapter 'js-debug-adapter' is not available: install vscode-js-debug with Mason or set JS_DEBUG_DAP_SERVER to dapDebugServer.js",
|
|
507
509
|
};
|
|
508
510
|
|
|
509
511
|
const ADAPTER_CANONICAL_COMMANDS: Readonly<Record<string, string>> = {
|
|
510
512
|
debugpy: "python",
|
|
511
513
|
dlv: "dlv",
|
|
512
514
|
rdbg: "rdbg",
|
|
515
|
+
"js-debug-adapter": "js-debug-adapter",
|
|
513
516
|
};
|
|
514
517
|
|
|
515
518
|
function formatAdapterUnavailable(adapterName: string, command: string, cwd: string): string {
|
package/src/tools/gh.ts
CHANGED
|
@@ -249,6 +249,7 @@ const RUN_FAILURE_CONCLUSIONS = new Set(["failure", "timed_out", "cancelled", "a
|
|
|
249
249
|
const JOB_FAILURE_CONCLUSIONS = new Set(["failure", "timed_out", "cancelled", "action_required"]);
|
|
250
250
|
const GITHUB_READONLY_OPS: ReadonlySet<string> = new Set([
|
|
251
251
|
"repo_view",
|
|
252
|
+
"file_read",
|
|
252
253
|
"search_issues",
|
|
253
254
|
"search_prs",
|
|
254
255
|
"search_code",
|
|
@@ -259,10 +260,11 @@ const GITHUB_READONLY_OPS: ReadonlySet<string> = new Set([
|
|
|
259
260
|
|
|
260
261
|
const githubSchema = type({
|
|
261
262
|
op: type(
|
|
262
|
-
"'repo_view' | 'pr_create' | 'pr_checkout' | 'pr_push' | 'search_issues' | 'search_prs' | 'search_code' | 'search_commits' | 'search_repos' | 'run_watch'",
|
|
263
|
+
"'repo_view' | 'file_read' | 'pr_create' | 'pr_checkout' | 'pr_push' | 'search_issues' | 'search_prs' | 'search_code' | 'search_commits' | 'search_repos' | 'run_watch'",
|
|
263
264
|
).describe("github operation"),
|
|
264
265
|
"repo?": type("string").describe("owner/repo"),
|
|
265
266
|
"branch?": type("string").describe("branch"),
|
|
267
|
+
"path?": type("string").describe("repository-relative file path"),
|
|
266
268
|
"pr?": type("string | string[]").describe("pr number, url, or branch"),
|
|
267
269
|
"force?": type("boolean").describe("reset existing local branch"),
|
|
268
270
|
"forceWithLease?": type("boolean").describe("force-with-lease push"),
|
|
@@ -2455,7 +2457,7 @@ export class GithubTool implements AgentTool<typeof githubSchema, GhToolDetails>
|
|
|
2455
2457
|
const op = typeof rawOp === "string" ? rawOp : "";
|
|
2456
2458
|
return GITHUB_READONLY_OPS.has(op) ? "read" : "exec";
|
|
2457
2459
|
};
|
|
2458
|
-
readonly summary = "Interact with GitHub
|
|
2460
|
+
readonly summary = "Interact with GitHub repositories, files, pull requests, and Actions";
|
|
2459
2461
|
readonly loadMode = "discoverable";
|
|
2460
2462
|
readonly label = "GitHub";
|
|
2461
2463
|
readonly description = prompt.render(githubDescription);
|
|
@@ -2480,6 +2482,8 @@ export class GithubTool implements AgentTool<typeof githubSchema, GhToolDetails>
|
|
|
2480
2482
|
switch (params.op) {
|
|
2481
2483
|
case "repo_view":
|
|
2482
2484
|
return executeRepoView(this.session, params, signal);
|
|
2485
|
+
case "file_read":
|
|
2486
|
+
return executeFileRead(this.session, params, signal);
|
|
2483
2487
|
case "pr_create":
|
|
2484
2488
|
return executePrCreate(this.session, params, signal);
|
|
2485
2489
|
case "pr_checkout":
|
|
@@ -2525,6 +2529,40 @@ async function executeRepoView(
|
|
|
2525
2529
|
return buildTextResult(formatRepoView(data, { repo, branch }), data.url);
|
|
2526
2530
|
}
|
|
2527
2531
|
|
|
2532
|
+
async function executeFileRead(
|
|
2533
|
+
session: ToolSession,
|
|
2534
|
+
params: GithubInput,
|
|
2535
|
+
signal: AbortSignal | undefined,
|
|
2536
|
+
): Promise<AgentToolResult<GhToolDetails>> {
|
|
2537
|
+
const repo = await resolveGitHubRepo(session.cwd, normalizeOptionalString(params.repo), undefined, signal);
|
|
2538
|
+
const filePath = requireNonEmpty(normalizeOptionalString(params.path), "path");
|
|
2539
|
+
if (filePath.startsWith("/")) {
|
|
2540
|
+
throw new ToolError("path must be repository-relative");
|
|
2541
|
+
}
|
|
2542
|
+
const branch = normalizeOptionalString(params.branch);
|
|
2543
|
+
const endpointPath = filePath
|
|
2544
|
+
.split("/")
|
|
2545
|
+
.map(segment => encodeURIComponent(segment))
|
|
2546
|
+
.join("/");
|
|
2547
|
+
const args = [
|
|
2548
|
+
"api",
|
|
2549
|
+
`/repos/${repo}/contents/${endpointPath}`,
|
|
2550
|
+
"--method",
|
|
2551
|
+
"GET",
|
|
2552
|
+
"-H",
|
|
2553
|
+
"Accept: application/vnd.github.raw+json",
|
|
2554
|
+
];
|
|
2555
|
+
if (branch) {
|
|
2556
|
+
args.push("-f", `ref=${branch}`);
|
|
2557
|
+
}
|
|
2558
|
+
const text = await git.github.text(session.cwd, args, signal, {
|
|
2559
|
+
repoProvided: true,
|
|
2560
|
+
trimOutput: false,
|
|
2561
|
+
});
|
|
2562
|
+
const sourceUrl = `https://github.com/${repo}/blob/${encodeURIComponent(branch ?? "HEAD")}/${endpointPath}`;
|
|
2563
|
+
return buildTextResult(text, sourceUrl, { repo, branch });
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2528
2566
|
// ────────────────────────────────────────────────────────────────────────────
|
|
2529
2567
|
// Cached issue/PR view fetchers
|
|
2530
2568
|
//
|
package/src/tools/hub/index.ts
CHANGED
|
@@ -159,7 +159,10 @@ export class HubTool implements AgentTool<typeof hubSchema, HubDetails> {
|
|
|
159
159
|
readonly description: string;
|
|
160
160
|
readonly parameters = hubSchema;
|
|
161
161
|
readonly strict = true;
|
|
162
|
-
readonly interruptible =
|
|
162
|
+
readonly interruptible = (params: Partial<HubParams>): boolean => {
|
|
163
|
+
if (params.op === "wait") return true;
|
|
164
|
+
return params.op === "logs" && params.follow === true;
|
|
165
|
+
};
|
|
163
166
|
readonly loadMode = "essential";
|
|
164
167
|
|
|
165
168
|
readonly examples: readonly ToolExample<typeof hubSchema.infer>[] = [
|
package/src/tools/hub/jobs.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
|
|
8
8
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
9
9
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
10
10
|
import type { AsyncJob, AsyncJobManager } from "../../async";
|
|
11
|
+
import { settings } from "../../config/settings";
|
|
11
12
|
import type { RenderResultOptions } from "../../extensibility/custom-tools/types";
|
|
12
13
|
import { shimmerEnabled, shimmerText } from "../../modes/theme/shimmer";
|
|
13
14
|
import type { Theme } from "../../modes/theme/theme";
|
|
@@ -134,6 +135,7 @@ interface TrackedJobLike {
|
|
|
134
135
|
status: string;
|
|
135
136
|
label: string;
|
|
136
137
|
startTime: number;
|
|
138
|
+
latestDetails?: Record<string, unknown>;
|
|
137
139
|
resultText?: string;
|
|
138
140
|
errorText?: string;
|
|
139
141
|
}
|
|
@@ -143,12 +145,34 @@ export function snapshotJobs(session: ToolSession, jobs: TrackedJobLike[]): JobS
|
|
|
143
145
|
return jobs.map(j => {
|
|
144
146
|
const current = session.asyncJobManager?.getJob(j.id);
|
|
145
147
|
const latest = current ?? j;
|
|
148
|
+
let resolvedModel: string | undefined;
|
|
149
|
+
if (latest.type === "task") {
|
|
150
|
+
const progressValue = latest.latestDetails?.progress;
|
|
151
|
+
if (Array.isArray(progressValue)) {
|
|
152
|
+
let progressRecord: Record<string, unknown> | undefined;
|
|
153
|
+
for (const item of progressValue) {
|
|
154
|
+
if (!item || typeof item !== "object") continue;
|
|
155
|
+
const candidate = item as Record<string, unknown>;
|
|
156
|
+
if (!progressRecord) progressRecord = candidate;
|
|
157
|
+
if (candidate.id === latest.id) {
|
|
158
|
+
progressRecord = candidate;
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const modelValue = progressRecord?.resolvedModel;
|
|
163
|
+
if (typeof modelValue === "string") {
|
|
164
|
+
const trimmed = modelValue.trim();
|
|
165
|
+
if (trimmed) resolvedModel = trimmed;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
146
169
|
return {
|
|
147
170
|
id: latest.id,
|
|
148
171
|
type: latest.type,
|
|
149
172
|
status: latest.status as JobSnapshot["status"],
|
|
150
173
|
label: latest.label,
|
|
151
174
|
durationMs: Math.max(0, now - latest.startTime),
|
|
175
|
+
...(resolvedModel ? { resolvedModel } : {}),
|
|
152
176
|
...(latest.resultText ? { resultText: latest.resultText } : {}),
|
|
153
177
|
...(latest.errorText ? { errorText: latest.errorText } : {}),
|
|
154
178
|
};
|
|
@@ -354,6 +378,7 @@ const PREVIEW_LINES_EXPANDED = 4;
|
|
|
354
378
|
const LABEL_LINES_COLLAPSED = 1;
|
|
355
379
|
const LABEL_LINES_EXPANDED = 3;
|
|
356
380
|
const PREVIEW_LINE_WIDTH = 80;
|
|
381
|
+
const MODEL_BADGE_MAX_WIDTH = 48;
|
|
357
382
|
|
|
358
383
|
function statusToIcon(status: JobSnapshot["status"]): ToolUIStatus {
|
|
359
384
|
switch (status) {
|
|
@@ -545,6 +570,20 @@ export function jobsRenderResult(
|
|
|
545
570
|
visibleLabelLines[visibleLabelLines.length - 1] = `${last} …`;
|
|
546
571
|
}
|
|
547
572
|
const durationText = uiTheme.fg("dim", formatDuration(job.durationMs));
|
|
573
|
+
const modelText =
|
|
574
|
+
job.type === "task" &&
|
|
575
|
+
typeof job.resolvedModel === "string" &&
|
|
576
|
+
job.resolvedModel.trim() &&
|
|
577
|
+
settings.get("task.showResolvedModelBadge")
|
|
578
|
+
? `${uiTheme.sep.dot}${uiTheme.fg(
|
|
579
|
+
"dim",
|
|
580
|
+
truncateToWidth(
|
|
581
|
+
replaceTabs(job.resolvedModel.trim()),
|
|
582
|
+
MODEL_BADGE_MAX_WIDTH,
|
|
583
|
+
Ellipsis.Unicode,
|
|
584
|
+
),
|
|
585
|
+
)}`
|
|
586
|
+
: "";
|
|
548
587
|
// Running rows in a live block shimmer their label; once the block
|
|
549
588
|
// stops animating (sealed, or a settled snapshot — spinnerFrame
|
|
550
589
|
// cleared) they render static so scrollback never keeps a mid-sweep
|
|
@@ -556,7 +595,9 @@ export function jobsRenderResult(
|
|
|
556
595
|
? shimmerText(headRaw, uiTheme)
|
|
557
596
|
: uiTheme.fg("accent", headRaw)
|
|
558
597
|
: uiTheme.fg("toolOutput", headRaw);
|
|
559
|
-
lines.push(
|
|
598
|
+
lines.push(
|
|
599
|
+
`${icon}${idPart} ${typeBadge} ${headLabel}${modelText}${modelText ? uiTheme.sep.dot : " "}${durationText}`,
|
|
600
|
+
);
|
|
560
601
|
for (let i = 1; i < visibleLabelLines.length; i++) {
|
|
561
602
|
lines.push(` ${uiTheme.fg("toolOutput", visibleLabelLines[i]!)}`);
|
|
562
603
|
}
|
package/src/tools/hub/types.ts
CHANGED
|
@@ -46,6 +46,8 @@ export interface JobSnapshot {
|
|
|
46
46
|
status: "running" | "completed" | "failed" | "cancelled";
|
|
47
47
|
label: string;
|
|
48
48
|
durationMs: number;
|
|
49
|
+
/** Effective task model selector, including an explicit reasoning suffix when configured. */
|
|
50
|
+
resolvedModel?: string;
|
|
49
51
|
resultText?: string;
|
|
50
52
|
errorText?: string;
|
|
51
53
|
}
|
|
@@ -13,7 +13,7 @@ export const TOOL_TIMEOUTS = {
|
|
|
13
13
|
browser: { default: 30, min: 1, max: 300 },
|
|
14
14
|
ssh: { default: 60, min: 1, max: 3600 },
|
|
15
15
|
fetch: { default: 20, min: 1, max: 45 },
|
|
16
|
-
lsp: { default: 20, min: 5, max:
|
|
16
|
+
lsp: { default: 20, min: 5, max: 300 },
|
|
17
17
|
debug: { default: 30, min: 5, max: 300 },
|
|
18
18
|
} as const satisfies Record<string, ToolTimeoutConfig>;
|
|
19
19
|
|