@oh-my-pi/pi-coding-agent 15.10.4 → 15.10.6
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 +74 -0
- package/dist/types/capability/rule-buckets.d.ts +1 -1
- package/dist/types/capability/rule.d.ts +6 -1
- package/dist/types/cli/update-cli.d.ts +11 -1
- package/dist/types/config/model-registry.d.ts +18 -1
- package/dist/types/discovery/at-imports.d.ts +15 -0
- package/dist/types/edit/diff.d.ts +3 -2
- package/dist/types/eval/__tests__/helpers-local-roots.test.d.ts +1 -0
- package/dist/types/eval/backend.d.ts +7 -0
- package/dist/types/eval/js/context-manager.d.ts +1 -0
- package/dist/types/eval/js/executor.d.ts +2 -0
- package/dist/types/eval/js/index.d.ts +1 -1
- package/dist/types/eval/js/shared/helpers.d.ts +6 -0
- package/dist/types/eval/js/shared/runtime.d.ts +5 -0
- package/dist/types/eval/js/worker-protocol.d.ts +6 -0
- package/dist/types/eval/py/executor.d.ts +7 -0
- package/dist/types/eval/py/index.d.ts +1 -1
- package/dist/types/exa/index.d.ts +1 -19
- package/dist/types/exa/mcp-client.d.ts +10 -3
- package/dist/types/exa/types.d.ts +0 -83
- package/dist/types/export/ttsr.d.ts +14 -0
- package/dist/types/extensibility/extensions/types.d.ts +8 -1
- package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +1 -1
- package/dist/types/internal-urls/local-protocol.d.ts +10 -0
- package/dist/types/mcp/oauth-flow.d.ts +2 -2
- package/dist/types/modes/components/custom-editor.d.ts +3 -0
- package/dist/types/modes/components/{status-line.d.ts → status-line/component.d.ts} +2 -32
- package/dist/types/modes/components/status-line/index.d.ts +1 -0
- package/dist/types/modes/components/status-line/types.d.ts +31 -2
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +8 -0
- package/dist/types/modes/image-references.d.ts +8 -3
- package/dist/types/modes/interactive-mode.d.ts +9 -1
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +3 -1
- package/dist/types/modes/utils/ui-helpers.d.ts +2 -2
- package/dist/types/session/agent-session.d.ts +0 -2
- package/dist/types/task/render.d.ts +1 -0
- package/dist/types/tools/ask.d.ts +1 -0
- package/dist/types/tools/browser/tab-worker.d.ts +15 -0
- package/dist/types/tools/index.d.ts +17 -2
- package/dist/types/tools/render-utils.d.ts +1 -1
- package/dist/types/tools/tool-timeouts.d.ts +1 -1
- package/dist/types/utils/block-context.d.ts +35 -0
- package/dist/types/utils/git.d.ts +6 -0
- package/dist/types/utils/image-loading.d.ts +12 -0
- package/package.json +29 -9
- package/src/capability/rule-buckets.ts +4 -2
- package/src/capability/rule.ts +10 -1
- package/src/cli/auth-broker-cli.ts +6 -7
- package/src/cli/auth-gateway-cli.ts +4 -3
- package/src/cli/list-models.ts +5 -0
- package/src/cli/update-cli.ts +138 -16
- package/src/commit/agentic/tools/split-commit.ts +8 -1
- package/src/config/model-provider-priority.ts +1 -0
- package/src/config/model-registry.ts +81 -2
- package/src/debug/index.ts +4 -8
- package/src/discovery/at-imports.ts +273 -0
- package/src/discovery/builtin-rules/index.ts +4 -0
- package/src/discovery/builtin-rules/ts-no-test-timers.md +55 -0
- package/src/discovery/builtin-rules/ts-redundant-clear-guard.md +75 -0
- package/src/discovery/helpers.ts +2 -1
- package/src/edit/diff.ts +114 -4
- package/src/edit/hashline/diff.ts +1 -1
- package/src/edit/hashline/execute.ts +1 -1
- package/src/edit/modes/patch.ts +6 -2
- package/src/edit/modes/replace.ts +1 -1
- package/src/edit/renderer.ts +12 -2
- package/src/eval/__tests__/helpers-local-roots.test.ts +58 -0
- package/src/eval/backend.ts +15 -0
- package/src/eval/js/context-manager.ts +4 -2
- package/src/eval/js/executor.ts +3 -0
- package/src/eval/js/index.ts +7 -1
- package/src/eval/js/shared/helpers.ts +53 -6
- package/src/eval/js/shared/runtime.ts +8 -0
- package/src/eval/js/worker-core.ts +1 -0
- package/src/eval/js/worker-protocol.ts +6 -0
- package/src/eval/py/executor.ts +12 -0
- package/src/eval/py/index.ts +7 -1
- package/src/eval/py/prelude.py +43 -4
- package/src/eval/py/runner.py +1 -0
- package/src/exa/index.ts +1 -26
- package/src/exa/mcp-client.ts +10 -10
- package/src/exa/types.ts +0 -97
- package/src/export/ttsr.ts +122 -1
- package/src/extensibility/extensions/types.ts +8 -1
- package/src/extensibility/legacy-pi-ai-shim.ts +1 -1
- package/src/extensibility/plugins/doctor.ts +1 -1
- package/src/extensibility/plugins/legacy-pi-compat.ts +6 -5
- package/src/goals/tools/goal-tool.ts +1 -1
- package/src/internal-urls/docs-index.generated.ts +7 -6
- package/src/internal-urls/local-protocol.ts +13 -0
- package/src/lsp/render.ts +8 -6
- package/src/mcp/oauth-flow.ts +3 -3
- package/src/mcp/render.ts +7 -1
- package/src/modes/components/agent-dashboard.ts +6 -4
- package/src/modes/components/custom-editor.ts +12 -6
- package/src/modes/components/login-dialog.ts +1 -1
- package/src/modes/components/oauth-selector.ts +4 -4
- package/src/modes/components/read-tool-group.ts +10 -3
- package/src/modes/components/{status-line.ts → status-line/component.ts} +18 -40
- package/src/modes/components/status-line/index.ts +1 -0
- package/src/modes/components/status-line/types.ts +23 -8
- package/src/modes/components/tool-execution.ts +1 -1
- package/src/modes/components/transcript-container.ts +17 -10
- package/src/modes/components/user-message.ts +6 -3
- package/src/modes/components/welcome.ts +1 -1
- package/src/modes/controllers/event-controller.ts +8 -0
- package/src/modes/controllers/extension-ui-controller.ts +143 -127
- package/src/modes/controllers/input-controller.ts +60 -11
- package/src/modes/controllers/mcp-command-controller.ts +52 -17
- package/src/modes/controllers/selector-controller.ts +4 -11
- package/src/modes/controllers/ssh-command-controller.ts +2 -2
- package/src/modes/image-references.ts +13 -7
- package/src/modes/interactive-mode.ts +35 -3
- package/src/modes/rpc/rpc-mode.ts +1 -1
- package/src/modes/setup-wizard/scenes/sign-in.ts +3 -11
- package/src/modes/theme/theme.ts +95 -1
- package/src/modes/types.ts +3 -1
- package/src/modes/utils/ui-helpers.ts +14 -5
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/eval.md +4 -4
- package/src/sdk.ts +31 -14
- package/src/session/agent-session.ts +290 -196
- package/src/session/session-manager.ts +1 -1
- package/src/slash-commands/builtin-registry.ts +9 -1
- package/src/system-prompt.ts +15 -9
- package/src/task/index.ts +9 -1
- package/src/task/render.ts +36 -14
- package/src/tools/ask.ts +14 -5
- package/src/tools/bash-interactive.ts +1 -1
- package/src/tools/bash.ts +14 -2
- package/src/tools/browser/render.ts +5 -2
- package/src/tools/browser/tab-worker.ts +211 -91
- package/src/tools/debug.ts +5 -2
- package/src/tools/eval-render.ts +6 -3
- package/src/tools/eval.ts +1 -1
- package/src/tools/gh-renderer.ts +29 -15
- package/src/tools/index.ts +32 -4
- package/src/tools/inspect-image-renderer.ts +12 -5
- package/src/tools/job.ts +9 -6
- package/src/tools/memory-render.ts +19 -5
- package/src/tools/read.ts +165 -18
- package/src/tools/render-utils.ts +3 -1
- package/src/tools/resolve.ts +1 -1
- package/src/tools/review.ts +1 -1
- package/src/tools/ssh.ts +4 -1
- package/src/tools/todo.ts +8 -1
- package/src/tools/tool-timeouts.ts +1 -1
- package/src/tools/write.ts +1 -1
- package/src/tui/code-cell.ts +1 -1
- package/src/utils/block-context.ts +312 -0
- package/src/utils/git.ts +41 -0
- package/src/utils/image-loading.ts +31 -1
- package/src/web/search/providers/codex.ts +1 -1
- package/src/web/search/render.ts +14 -6
- package/dist/types/exa/factory.d.ts +0 -13
- package/dist/types/exa/render.d.ts +0 -19
- package/dist/types/exa/researcher.d.ts +0 -9
- package/dist/types/exa/search.d.ts +0 -9
- package/dist/types/exa/websets.d.ts +0 -9
- package/src/exa/factory.ts +0 -60
- package/src/exa/render.ts +0 -244
- package/src/exa/researcher.ts +0 -36
- package/src/exa/search.ts +0 -47
- package/src/exa/websets.ts +0 -248
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
Browser,
|
|
9
9
|
Dialog,
|
|
10
10
|
ElementHandle,
|
|
11
|
+
ElementScreenshotOptions,
|
|
11
12
|
HTTPResponse,
|
|
12
13
|
KeyInput,
|
|
13
14
|
Page,
|
|
@@ -78,6 +79,14 @@ type DialogPolicy = "accept" | "dismiss";
|
|
|
78
79
|
type DragTarget = string | { readonly x: number; readonly y: number };
|
|
79
80
|
type ActionabilityResult = { ok: true; x: number; y: number } | { ok: false; reason: string };
|
|
80
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Per-op ceiling for puppeteer-internal helpers that should resolve quickly
|
|
84
|
+
* (`observe`, `screenshot`, `extract`). Kept below the default 30s cell budget so a
|
|
85
|
+
* single stalled helper fails fast with a named error and leaves budget for the rest
|
|
86
|
+
* of the cell. Effective cap is `min(cellBudget, QUICK_OP_TIMEOUT_MS)`.
|
|
87
|
+
*/
|
|
88
|
+
const QUICK_OP_TIMEOUT_MS = 20_000;
|
|
89
|
+
|
|
81
90
|
interface ScreenshotOptions {
|
|
82
91
|
selector?: string;
|
|
83
92
|
fullPage?: boolean;
|
|
@@ -404,12 +413,36 @@ async function clickQueryHandlerText(
|
|
|
404
413
|
);
|
|
405
414
|
}
|
|
406
415
|
|
|
416
|
+
export interface InflightOp {
|
|
417
|
+
label: string;
|
|
418
|
+
startedAt: number;
|
|
419
|
+
}
|
|
420
|
+
|
|
407
421
|
interface ActiveRun {
|
|
408
422
|
id: string;
|
|
409
423
|
ac: AbortController;
|
|
410
424
|
displays: RunResultOk["displays"];
|
|
411
425
|
screenshots: ScreenshotResult[];
|
|
412
426
|
pendingTools: Map<string, { resolve(value: unknown): void; reject(error: Error): void }>;
|
|
427
|
+
/** Helper invocations currently awaiting the page/network, keyed by op id. */
|
|
428
|
+
inflight: Map<number, InflightOp>;
|
|
429
|
+
opCounter: number;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** Human-readable label for a screenshot op, used in op tracking + timeout errors. */
|
|
433
|
+
export function describeScreenshot(opts?: ScreenshotOptions): string {
|
|
434
|
+
if (opts?.selector) return `tab.screenshot({ selector: ${JSON.stringify(opts.selector)} })`;
|
|
435
|
+
if (opts?.fullPage) return "tab.screenshot({ fullPage: true })";
|
|
436
|
+
return "tab.screenshot()";
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/** Summarize still-running helpers (oldest first) so a cell timeout names what stalled. */
|
|
440
|
+
export function describeInflight(inflight: Map<number, InflightOp>): string {
|
|
441
|
+
const now = Date.now();
|
|
442
|
+
return [...inflight.values()]
|
|
443
|
+
.sort((a, b) => a.startedAt - b.startedAt)
|
|
444
|
+
.map(op => `${op.label} (${((now - op.startedAt) / 1000).toFixed(1)}s)`)
|
|
445
|
+
.join(", ");
|
|
413
446
|
}
|
|
414
447
|
|
|
415
448
|
export class WorkerCore {
|
|
@@ -560,13 +593,21 @@ export class WorkerCore {
|
|
|
560
593
|
const signal = AbortSignal.any([timeoutSignal, ac.signal]);
|
|
561
594
|
const displays: RunResultOk["displays"] = [];
|
|
562
595
|
const screenshots: ScreenshotResult[] = [];
|
|
563
|
-
const active: ActiveRun = {
|
|
596
|
+
const active: ActiveRun = {
|
|
597
|
+
id: msg.id,
|
|
598
|
+
ac,
|
|
599
|
+
displays,
|
|
600
|
+
screenshots,
|
|
601
|
+
pendingTools: new Map(),
|
|
602
|
+
inflight: new Map(),
|
|
603
|
+
opCounter: 0,
|
|
604
|
+
};
|
|
564
605
|
this.#active = active;
|
|
565
606
|
try {
|
|
566
607
|
throwIfAborted(signal);
|
|
567
608
|
const page = this.#requirePage();
|
|
568
609
|
const browser = this.#requireBrowser();
|
|
569
|
-
const tabApi = this.#createTabApi(msg.name, msg.timeoutMs, signal, msg.session, displays, screenshots);
|
|
610
|
+
const tabApi = this.#createTabApi(msg.name, msg.timeoutMs, signal, msg.session, displays, screenshots, active);
|
|
570
611
|
const runtime = this.#ensureRuntime(msg.session);
|
|
571
612
|
runtime.setCwd(msg.session.cwd);
|
|
572
613
|
runtime.setRunScope({
|
|
@@ -580,11 +621,16 @@ export class WorkerCore {
|
|
|
580
621
|
});
|
|
581
622
|
const { promise: cancelRejection, reject: rejectCancel } = Promise.withResolvers<never>();
|
|
582
623
|
const onCancel = (): void => {
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
624
|
+
if (timeoutSignal.aborted) {
|
|
625
|
+
const stalled = describeInflight(active.inflight);
|
|
626
|
+
rejectCancel(
|
|
627
|
+
new ToolError(
|
|
628
|
+
`Browser code execution timed out after ${msg.timeoutMs}ms${stalled ? ` (stalled on ${stalled})` : ""}`,
|
|
629
|
+
),
|
|
630
|
+
);
|
|
631
|
+
} else {
|
|
632
|
+
rejectCancel(new ToolAbortError());
|
|
633
|
+
}
|
|
588
634
|
// Cancel in-flight tool calls so user code's awaited proxies reject promptly.
|
|
589
635
|
for (const pending of active.pendingTools.values()) {
|
|
590
636
|
pending.reject(new ToolAbortError());
|
|
@@ -670,6 +716,39 @@ export class WorkerCore {
|
|
|
670
716
|
else pending.reject(replyError(reply.error));
|
|
671
717
|
}
|
|
672
718
|
|
|
719
|
+
/**
|
|
720
|
+
* Wrap a tab helper so it (a) registers in the active run's in-flight map for
|
|
721
|
+
* timeout diagnostics and (b) honors an optional per-op deadline that fails fast
|
|
722
|
+
* with a named error instead of silently consuming the whole cell budget. Pass
|
|
723
|
+
* `Number.POSITIVE_INFINITY` for `perOpTimeoutMs` to bound the op only by the cell
|
|
724
|
+
* budget (used for `evaluate` running user code and for locator helpers that already
|
|
725
|
+
* carry puppeteer's own `.setTimeout(timeoutMs)`).
|
|
726
|
+
*/
|
|
727
|
+
async #runOp<T>(
|
|
728
|
+
active: ActiveRun,
|
|
729
|
+
label: string,
|
|
730
|
+
cellSignal: AbortSignal,
|
|
731
|
+
perOpTimeoutMs: number,
|
|
732
|
+
fn: (signal: AbortSignal) => Promise<T>,
|
|
733
|
+
): Promise<T> {
|
|
734
|
+
const opId = active.opCounter++;
|
|
735
|
+
active.inflight.set(opId, { label, startedAt: Date.now() });
|
|
736
|
+
const capped = Number.isFinite(perOpTimeoutMs) && perOpTimeoutMs > 0;
|
|
737
|
+
const opTimeout = capped ? AbortSignal.timeout(perOpTimeoutMs) : undefined;
|
|
738
|
+
const opSignal = opTimeout ? AbortSignal.any([cellSignal, opTimeout]) : cellSignal;
|
|
739
|
+
try {
|
|
740
|
+
return await fn(opSignal);
|
|
741
|
+
} catch (err) {
|
|
742
|
+
// Per-op deadline fired (not the cell budget, not an explicit abort) → named, actionable error.
|
|
743
|
+
if (opTimeout?.aborted && !cellSignal.aborted) {
|
|
744
|
+
throw new ToolError(`${label} timed out after ${perOpTimeoutMs}ms`);
|
|
745
|
+
}
|
|
746
|
+
throw err;
|
|
747
|
+
} finally {
|
|
748
|
+
active.inflight.delete(opId);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
673
752
|
#createTabApi(
|
|
674
753
|
name: string,
|
|
675
754
|
timeoutMs: number,
|
|
@@ -677,98 +756,125 @@ export class WorkerCore {
|
|
|
677
756
|
session: SessionSnapshot,
|
|
678
757
|
displays: RunResultOk["displays"],
|
|
679
758
|
screenshots: ScreenshotResult[],
|
|
759
|
+
active: ActiveRun,
|
|
680
760
|
): TabApi {
|
|
681
761
|
const page = this.#requirePage();
|
|
762
|
+
const quickOpMs = Math.min(timeoutMs, QUICK_OP_TIMEOUT_MS);
|
|
763
|
+
const INF = Number.POSITIVE_INFINITY;
|
|
764
|
+
const op = <T>(label: string, perOpMs: number, fn: (sig: AbortSignal) => Promise<T>): Promise<T> =>
|
|
765
|
+
this.#runOp(active, label, signal, perOpMs, fn);
|
|
682
766
|
return {
|
|
683
767
|
name,
|
|
684
768
|
page,
|
|
685
769
|
signal,
|
|
686
770
|
url: () => page.url(),
|
|
687
|
-
title: () => page.title(),
|
|
688
|
-
goto:
|
|
689
|
-
|
|
690
|
-
|
|
771
|
+
title: () => op("tab.title()", INF, sig => untilAborted(sig, () => page.title())),
|
|
772
|
+
goto: (url, opts) =>
|
|
773
|
+
op(`tab.goto(${JSON.stringify(url)})`, INF, async sig => {
|
|
774
|
+
this.#clearElementCache();
|
|
691
775
|
// Default to "load" because dev servers with HMR/WS never reach networkidle.
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
},
|
|
695
|
-
observe: opts => this.#collectObservation({ ...opts, signal }),
|
|
696
|
-
screenshot: async opts => await this.#captureScreenshot(session, displays, screenshots, signal, opts),
|
|
697
|
-
extract: async (format = "markdown") => {
|
|
698
|
-
const html = (await untilAborted(signal, () => page.content())) as string;
|
|
699
|
-
const result = await extractReadableFromHtml(html, page.url(), format);
|
|
700
|
-
if (!result) {
|
|
701
|
-
throw new ToolError(`tab.extract(${JSON.stringify(format)}) found no readable content on ${page.url()}`);
|
|
702
|
-
}
|
|
703
|
-
const content = format === "markdown" ? result.markdown : result.text;
|
|
704
|
-
if (!content) {
|
|
705
|
-
throw new ToolError(
|
|
706
|
-
`tab.extract(${JSON.stringify(format)}) produced empty ${format} content for ${page.url()}`,
|
|
707
|
-
);
|
|
708
|
-
}
|
|
709
|
-
return content;
|
|
710
|
-
},
|
|
711
|
-
click: async selector => {
|
|
712
|
-
const resolved = normalizeSelector(selector);
|
|
713
|
-
if (resolved.startsWith("text/")) await clickQueryHandlerText(page, resolved, timeoutMs, signal);
|
|
714
|
-
else await untilAborted(signal, () => page.locator(resolved).setTimeout(timeoutMs).click());
|
|
715
|
-
},
|
|
716
|
-
type: async (selector, text) => {
|
|
717
|
-
const handle = (await untilAborted(signal, () =>
|
|
718
|
-
page.locator(normalizeSelector(selector)).setTimeout(timeoutMs).waitHandle(),
|
|
719
|
-
)) as ElementHandle;
|
|
720
|
-
try {
|
|
721
|
-
await untilAborted(signal, () => handle.type(text, { delay: 0 }));
|
|
722
|
-
} finally {
|
|
723
|
-
await handle.dispose();
|
|
724
|
-
}
|
|
725
|
-
},
|
|
726
|
-
fill: async (selector, value) => {
|
|
727
|
-
await untilAborted(signal, () =>
|
|
728
|
-
page.locator(normalizeSelector(selector)).setTimeout(timeoutMs).fill(value),
|
|
729
|
-
);
|
|
730
|
-
},
|
|
731
|
-
press: async (key, opts) => {
|
|
732
|
-
const selector = opts?.selector;
|
|
733
|
-
if (selector) await untilAborted(signal, () => page.focus(normalizeSelector(selector)));
|
|
734
|
-
await untilAborted(signal, () => page.keyboard.press(key));
|
|
735
|
-
},
|
|
736
|
-
scroll: async (deltaX, deltaY) => {
|
|
737
|
-
await untilAborted(signal, () => page.mouse.wheel({ deltaX, deltaY }));
|
|
738
|
-
},
|
|
739
|
-
drag: async (from, to) => await this.#drag(from, to, signal),
|
|
740
|
-
waitFor: async selector =>
|
|
741
|
-
(await untilAborted(signal, () =>
|
|
742
|
-
page.locator(normalizeSelector(selector)).setTimeout(timeoutMs).waitHandle(),
|
|
743
|
-
)) as ElementHandle,
|
|
744
|
-
evaluate: async (fn, ...args) =>
|
|
745
|
-
(await untilAborted(signal, () =>
|
|
746
|
-
typeof fn === "string" ? page.evaluate(fn) : page.evaluate(fn as (...a: unknown[]) => unknown, ...args),
|
|
747
|
-
)) as never,
|
|
748
|
-
scrollIntoView: async selector => {
|
|
749
|
-
const handle = (await untilAborted(signal, () =>
|
|
750
|
-
page.locator(normalizeSelector(selector)).setTimeout(timeoutMs).waitHandle(),
|
|
751
|
-
)) as ElementHandle;
|
|
752
|
-
try {
|
|
753
|
-
await untilAborted(signal, () =>
|
|
754
|
-
handle.evaluate(el => {
|
|
755
|
-
const target = el as unknown as {
|
|
756
|
-
scrollIntoView: (opts: { behavior: string; block: string; inline: string }) => void;
|
|
757
|
-
};
|
|
758
|
-
target.scrollIntoView({ behavior: "instant", block: "center", inline: "center" });
|
|
759
|
-
}),
|
|
776
|
+
await untilAborted(sig, () =>
|
|
777
|
+
page.goto(url, { waitUntil: opts?.waitUntil ?? "load", timeout: timeoutMs }),
|
|
760
778
|
);
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
779
|
+
}),
|
|
780
|
+
observe: opts => op("tab.observe()", quickOpMs, sig => this.#collectObservation({ ...opts, signal: sig })),
|
|
781
|
+
screenshot: opts =>
|
|
782
|
+
op(describeScreenshot(opts), quickOpMs, sig =>
|
|
783
|
+
this.#captureScreenshot(session, displays, screenshots, sig, opts),
|
|
784
|
+
),
|
|
785
|
+
extract: (format = "markdown") =>
|
|
786
|
+
op(`tab.extract(${JSON.stringify(format)})`, quickOpMs, async sig => {
|
|
787
|
+
const html = (await untilAborted(sig, () => page.content())) as string;
|
|
788
|
+
const result = await extractReadableFromHtml(html, page.url(), format);
|
|
789
|
+
if (!result) {
|
|
790
|
+
throw new ToolError(
|
|
791
|
+
`tab.extract(${JSON.stringify(format)}) found no readable content on ${page.url()}`,
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
const content = format === "markdown" ? result.markdown : result.text;
|
|
795
|
+
if (!content) {
|
|
796
|
+
throw new ToolError(
|
|
797
|
+
`tab.extract(${JSON.stringify(format)}) produced empty ${format} content for ${page.url()}`,
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
return content;
|
|
801
|
+
}),
|
|
802
|
+
click: selector =>
|
|
803
|
+
op(`tab.click(${JSON.stringify(selector)})`, INF, async sig => {
|
|
804
|
+
const resolved = normalizeSelector(selector);
|
|
805
|
+
if (resolved.startsWith("text/")) await clickQueryHandlerText(page, resolved, timeoutMs, sig);
|
|
806
|
+
else await untilAborted(sig, () => page.locator(resolved).setTimeout(timeoutMs).click());
|
|
807
|
+
}),
|
|
808
|
+
type: (selector, text) =>
|
|
809
|
+
op(`tab.type(${JSON.stringify(selector)})`, INF, async sig => {
|
|
810
|
+
const handle = (await untilAborted(sig, () =>
|
|
811
|
+
page.locator(normalizeSelector(selector)).setTimeout(timeoutMs).waitHandle(),
|
|
812
|
+
)) as ElementHandle;
|
|
813
|
+
try {
|
|
814
|
+
await untilAborted(sig, () => handle.type(text, { delay: 0 }));
|
|
815
|
+
} finally {
|
|
816
|
+
await handle.dispose();
|
|
817
|
+
}
|
|
818
|
+
}),
|
|
819
|
+
fill: (selector, value) =>
|
|
820
|
+
op(`tab.fill(${JSON.stringify(selector)})`, INF, sig =>
|
|
821
|
+
untilAborted(sig, () => page.locator(normalizeSelector(selector)).setTimeout(timeoutMs).fill(value)),
|
|
822
|
+
),
|
|
823
|
+
press: (key, opts) =>
|
|
824
|
+
op(`tab.press(${JSON.stringify(key)})`, INF, async sig => {
|
|
825
|
+
const selector = opts?.selector;
|
|
826
|
+
if (selector) await untilAborted(sig, () => page.focus(normalizeSelector(selector)));
|
|
827
|
+
await untilAborted(sig, () => page.keyboard.press(key));
|
|
828
|
+
}),
|
|
829
|
+
scroll: (deltaX, deltaY) =>
|
|
830
|
+
op("tab.scroll()", INF, sig => untilAborted(sig, () => page.mouse.wheel({ deltaX, deltaY }))),
|
|
831
|
+
drag: (from, to) => op("tab.drag()", INF, sig => this.#drag(from, to, sig)),
|
|
832
|
+
waitFor: selector =>
|
|
833
|
+
op(
|
|
834
|
+
`tab.waitFor(${JSON.stringify(selector)})`,
|
|
835
|
+
INF,
|
|
836
|
+
async sig =>
|
|
837
|
+
(await untilAborted(sig, () =>
|
|
838
|
+
page.locator(normalizeSelector(selector)).setTimeout(timeoutMs).waitHandle(),
|
|
839
|
+
)) as ElementHandle,
|
|
840
|
+
),
|
|
841
|
+
evaluate: (fn, ...args) =>
|
|
842
|
+
op("tab.evaluate()", INF, sig =>
|
|
843
|
+
untilAborted(sig, () =>
|
|
844
|
+
typeof fn === "string"
|
|
845
|
+
? page.evaluate(fn)
|
|
846
|
+
: page.evaluate(fn as (...a: unknown[]) => unknown, ...args),
|
|
847
|
+
),
|
|
848
|
+
) as never,
|
|
849
|
+
scrollIntoView: selector =>
|
|
850
|
+
op(`tab.scrollIntoView(${JSON.stringify(selector)})`, INF, async sig => {
|
|
851
|
+
const handle = (await untilAborted(sig, () =>
|
|
852
|
+
page.locator(normalizeSelector(selector)).setTimeout(timeoutMs).waitHandle(),
|
|
853
|
+
)) as ElementHandle;
|
|
854
|
+
try {
|
|
855
|
+
await untilAborted(sig, () =>
|
|
856
|
+
handle.evaluate(el => {
|
|
857
|
+
const target = el as unknown as {
|
|
858
|
+
scrollIntoView: (opts: { behavior: string; block: string; inline: string }) => void;
|
|
859
|
+
};
|
|
860
|
+
target.scrollIntoView({ behavior: "instant", block: "center", inline: "center" });
|
|
861
|
+
}),
|
|
862
|
+
);
|
|
863
|
+
} finally {
|
|
864
|
+
await handle.dispose().catch(() => undefined);
|
|
865
|
+
}
|
|
866
|
+
}),
|
|
867
|
+
select: (selector, ...values) =>
|
|
868
|
+
op(`tab.select(${JSON.stringify(selector)})`, INF, sig => this.#select(selector, values, timeoutMs, sig)),
|
|
869
|
+
uploadFile: (selector, ...filePaths) =>
|
|
870
|
+
op(`tab.uploadFile(${JSON.stringify(selector)})`, INF, sig =>
|
|
871
|
+
this.#uploadFile(selector, filePaths, timeoutMs, sig, session),
|
|
872
|
+
),
|
|
873
|
+
waitForUrl: (pattern, opts) =>
|
|
874
|
+
op("tab.waitForUrl()", INF, sig => this.#waitForUrl(pattern, opts?.timeout ?? timeoutMs, sig)),
|
|
875
|
+
waitForResponse: (pattern, opts) =>
|
|
876
|
+
op("tab.waitForResponse()", INF, sig => this.#waitForResponse(pattern, opts?.timeout ?? timeoutMs, sig)),
|
|
877
|
+
id: id => this.#resolveCachedHandle(id),
|
|
772
878
|
};
|
|
773
879
|
}
|
|
774
880
|
|
|
@@ -832,7 +938,21 @@ export class WorkerCore {
|
|
|
832
938
|
)) as ElementHandle | null;
|
|
833
939
|
if (!handle) throw new ToolError("Screenshot selector did not resolve to an element");
|
|
834
940
|
try {
|
|
835
|
-
|
|
941
|
+
// Bring the element into view with a single instant scroll instead of puppeteer's
|
|
942
|
+
// scrollIntoViewIfNeeded(), whose IntersectionObserver promise can stall indefinitely
|
|
943
|
+
// on continuously-animating pages (WebGL / backdrop-filter "glass" effects). Best-effort.
|
|
944
|
+
await untilAborted(signal, () =>
|
|
945
|
+
handle.evaluate(el => {
|
|
946
|
+
const target = el as unknown as {
|
|
947
|
+
scrollIntoView: (opts: { behavior: string; block: string; inline: string }) => void;
|
|
948
|
+
};
|
|
949
|
+
target.scrollIntoView({ behavior: "instant", block: "center", inline: "center" });
|
|
950
|
+
}),
|
|
951
|
+
).catch(() => undefined);
|
|
952
|
+
// scrollIntoView:false skips the same IntersectionObserver check inside screenshot();
|
|
953
|
+
// captureBeyondViewport (puppeteer's default) still renders the clipped region.
|
|
954
|
+
const shotOpts: ElementScreenshotOptions = { type: "png", scrollIntoView: false };
|
|
955
|
+
buffer = (await untilAborted(signal, () => handle.screenshot(shotOpts))) as Buffer;
|
|
836
956
|
} finally {
|
|
837
957
|
await handle.dispose().catch(() => undefined);
|
|
838
958
|
}
|
package/src/tools/debug.ts
CHANGED
|
@@ -594,8 +594,11 @@ export const debugToolRenderer = {
|
|
|
594
594
|
return markFramedBlockComponent({
|
|
595
595
|
render(width: number): string[] {
|
|
596
596
|
const action = (args?.action ?? result.details?.action ?? "debug").replaceAll("_", " ");
|
|
597
|
-
const
|
|
598
|
-
const
|
|
597
|
+
const success = !options.isPartial && !result.isError;
|
|
598
|
+
const statusIcon = success
|
|
599
|
+
? theme.styledSymbol("tool.debug", "accent")
|
|
600
|
+
: formatStatusIcon(options.isPartial ? "running" : "error", theme, options.spinnerFrame);
|
|
601
|
+
const header = `${statusIcon} Debug ${action}`;
|
|
599
602
|
const summaryLines = result.details?.snapshot
|
|
600
603
|
? formatSessionSnapshot(result.details.snapshot).map(line => replaceTabs(line))
|
|
601
604
|
: [];
|
package/src/tools/eval-render.ts
CHANGED
|
@@ -172,7 +172,7 @@ function renderAgentProgressEvents(events: EvalStatusEvent[], theme: Theme, spin
|
|
|
172
172
|
const status = agentEventStatus(event.status);
|
|
173
173
|
const iconStatus =
|
|
174
174
|
status === "completed"
|
|
175
|
-
? "
|
|
175
|
+
? "done"
|
|
176
176
|
: status === "failed"
|
|
177
177
|
? "error"
|
|
178
178
|
: status === "aborted"
|
|
@@ -182,10 +182,13 @@ function renderAgentProgressEvents(events: EvalStatusEvent[], theme: Theme, spin
|
|
|
182
182
|
: "running";
|
|
183
183
|
const iconColor =
|
|
184
184
|
status === "completed" ? "success" : status === "failed" || status === "aborted" ? "error" : "accent";
|
|
185
|
-
const icon =
|
|
185
|
+
const icon =
|
|
186
|
+
status === "completed"
|
|
187
|
+
? theme.styledSymbol("tool.eval", "accent")
|
|
188
|
+
: theme.fg(iconColor, formatStatusIcon(iconStatus, theme, status === "running" ? spinnerFrame : undefined));
|
|
186
189
|
|
|
187
190
|
const id = eventString(event.id) ?? "agent";
|
|
188
|
-
let line = `${prefix} ${
|
|
191
|
+
let line = `${prefix} ${icon} ${theme.fg("accent", theme.bold(id))}`;
|
|
189
192
|
|
|
190
193
|
if (status === "failed" || status === "aborted") {
|
|
191
194
|
line += ` ${formatBadge(status, iconColor, theme)}`;
|
package/src/tools/eval.ts
CHANGED
|
@@ -30,7 +30,7 @@ const evalCellSchema = z.object({
|
|
|
30
30
|
language: z.enum(["py", "js"]).describe('runtime: "py" for the IPython kernel, "js" for the persistent JS VM'),
|
|
31
31
|
code: z.string().describe("cell body, verbatim. Use top-level await freely."),
|
|
32
32
|
title: z.string().optional().describe('short label shown in transcript (e.g. "imports", "load config")'),
|
|
33
|
-
timeout: z.number().int().min(1).max(
|
|
33
|
+
timeout: z.number().int().min(1).max(3600).optional().describe("per-cell timeout in seconds (1-3600, default 30)"),
|
|
34
34
|
reset: z
|
|
35
35
|
.boolean()
|
|
36
36
|
.optional()
|
package/src/tools/gh-renderer.ts
CHANGED
|
@@ -163,8 +163,8 @@ function getJobStateVisual(
|
|
|
163
163
|
): { iconRaw: string; iconColor: ToolUIColor; textColor: ThemeColor } {
|
|
164
164
|
if (job.conclusion && SUCCESS_CONCLUSIONS.has(job.conclusion)) {
|
|
165
165
|
return {
|
|
166
|
-
iconRaw: theme.
|
|
167
|
-
iconColor: "
|
|
166
|
+
iconRaw: theme.symbol("tool.gh"),
|
|
167
|
+
iconColor: "accent",
|
|
168
168
|
textColor: "success",
|
|
169
169
|
};
|
|
170
170
|
}
|
|
@@ -327,14 +327,21 @@ function renderFallbackComponent(
|
|
|
327
327
|
const title = formatOpTitle(args.op);
|
|
328
328
|
const meta = buildOpMeta(args);
|
|
329
329
|
const isError = result.isError === true;
|
|
330
|
-
const
|
|
330
|
+
const success = !isError && Boolean(text);
|
|
331
331
|
const header = renderStatusLine(
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
332
|
+
success
|
|
333
|
+
? {
|
|
334
|
+
iconOverride: theme.styledSymbol("tool.gh", "accent"),
|
|
335
|
+
title,
|
|
336
|
+
titleColor: "accent",
|
|
337
|
+
meta,
|
|
338
|
+
}
|
|
339
|
+
: {
|
|
340
|
+
icon: isError ? "error" : "warning",
|
|
341
|
+
title,
|
|
342
|
+
titleColor: isError ? "error" : "accent",
|
|
343
|
+
meta,
|
|
344
|
+
},
|
|
338
345
|
theme,
|
|
339
346
|
);
|
|
340
347
|
|
|
@@ -438,12 +445,19 @@ export const githubToolRenderer = {
|
|
|
438
445
|
if (watch) {
|
|
439
446
|
const isError = result.isError === true;
|
|
440
447
|
const header = renderStatusLine(
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
448
|
+
isError
|
|
449
|
+
? {
|
|
450
|
+
icon: "error",
|
|
451
|
+
title: "GitHub Run Watch",
|
|
452
|
+
titleColor: "error",
|
|
453
|
+
meta: [getWatchHeader(watch)],
|
|
454
|
+
}
|
|
455
|
+
: {
|
|
456
|
+
iconOverride: uiTheme.styledSymbol("tool.gh", "accent"),
|
|
457
|
+
title: "GitHub Run Watch",
|
|
458
|
+
titleColor: "accent",
|
|
459
|
+
meta: [getWatchHeader(watch)],
|
|
460
|
+
},
|
|
447
461
|
uiTheme,
|
|
448
462
|
);
|
|
449
463
|
return framedBlock(uiTheme, width => {
|
package/src/tools/index.ts
CHANGED
|
@@ -59,11 +59,7 @@ import { type TodoPhase, TodoTool } from "./todo";
|
|
|
59
59
|
import { WriteTool } from "./write";
|
|
60
60
|
import { YieldTool } from "./yield";
|
|
61
61
|
|
|
62
|
-
// Exa MCP tools (22 tools)
|
|
63
|
-
|
|
64
62
|
export * from "../edit";
|
|
65
|
-
export * from "../exa";
|
|
66
|
-
export type * from "../exa/types";
|
|
67
63
|
export * from "../goals";
|
|
68
64
|
export * from "../lsp";
|
|
69
65
|
export * from "../session/streaming-output";
|
|
@@ -342,6 +338,38 @@ export function computeEssentialBuiltinNames(settings: Settings): string[] {
|
|
|
342
338
|
return [...DEFAULT_ESSENTIAL_TOOL_NAMES];
|
|
343
339
|
}
|
|
344
340
|
|
|
341
|
+
/**
|
|
342
|
+
* Filter the initial active tool set when `tools.discoveryMode === "all"`.
|
|
343
|
+
*
|
|
344
|
+
* Non-essential discoverable built-ins are hidden — the model rediscovers them
|
|
345
|
+
* via `search_tool_bm25` and activates them on demand. A tool survives hiding
|
|
346
|
+
* when it is essential, explicitly requested, restored from a prior selection,
|
|
347
|
+
* or required by a forced tool_choice feature (`forceActive`). The last case is
|
|
348
|
+
* load-bearing: a named tool_choice (e.g. the eager `todo` prelude) must
|
|
349
|
+
* reference a tool present in the request, or the provider rejects it with 400.
|
|
350
|
+
*/
|
|
351
|
+
export function filterInitialToolsForDiscoveryAll(
|
|
352
|
+
initialToolNames: string[],
|
|
353
|
+
opts: {
|
|
354
|
+
loadModeOf: (name: string) => BuiltinToolLoadMode | undefined;
|
|
355
|
+
essentialNames: ReadonlySet<string>;
|
|
356
|
+
explicitlyRequested: ReadonlySet<string>;
|
|
357
|
+
restored: ReadonlySet<string>;
|
|
358
|
+
forceActive: ReadonlySet<string>;
|
|
359
|
+
},
|
|
360
|
+
): string[] {
|
|
361
|
+
return initialToolNames.filter(name => {
|
|
362
|
+
const loadMode = opts.loadModeOf(name);
|
|
363
|
+
if (!loadMode) return true; // not a built-in — leave MCP/custom/extension to existing logic
|
|
364
|
+
if (loadMode === "essential") return true;
|
|
365
|
+
if (opts.essentialNames.has(name)) return true;
|
|
366
|
+
if (opts.explicitlyRequested.has(name)) return true;
|
|
367
|
+
if (opts.restored.has(name)) return true;
|
|
368
|
+
if (opts.forceActive.has(name)) return true;
|
|
369
|
+
return false;
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
345
373
|
/**
|
|
346
374
|
* Public callable factory map. External callers may invoke `BUILTIN_TOOLS.read(session)` or
|
|
347
375
|
* `BUILTIN_TOOLS[name](session)` to construct a tool directly.
|
|
@@ -53,12 +53,19 @@ export const inspectImageToolRenderer = {
|
|
|
53
53
|
const details = result.details;
|
|
54
54
|
const rawPath = details?.imagePath ?? args?.path ?? "";
|
|
55
55
|
const pathDisplay = rawPath ? shortenPath(rawPath) : "image";
|
|
56
|
+
const success = !result.isError;
|
|
56
57
|
const header = renderStatusLine(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
success
|
|
59
|
+
? {
|
|
60
|
+
iconOverride: uiTheme.styledSymbol("tool.inspectImage", "accent"),
|
|
61
|
+
title: "Inspect",
|
|
62
|
+
description: pathDisplay,
|
|
63
|
+
}
|
|
64
|
+
: {
|
|
65
|
+
icon: "error",
|
|
66
|
+
title: "Inspect",
|
|
67
|
+
description: pathDisplay,
|
|
68
|
+
},
|
|
62
69
|
uiTheme,
|
|
63
70
|
);
|
|
64
71
|
|
package/src/tools/job.ts
CHANGED
|
@@ -355,7 +355,7 @@ const PREVIEW_LINE_WIDTH = 80;
|
|
|
355
355
|
function statusToIcon(status: JobSnapshot["status"]): ToolUIStatus {
|
|
356
356
|
switch (status) {
|
|
357
357
|
case "completed":
|
|
358
|
-
return "
|
|
358
|
+
return "done";
|
|
359
359
|
case "failed":
|
|
360
360
|
return "error";
|
|
361
361
|
case "cancelled":
|
|
@@ -468,11 +468,14 @@ export const jobToolRenderer = {
|
|
|
468
468
|
itemType: "job",
|
|
469
469
|
renderItem: job => {
|
|
470
470
|
const lines: string[] = [];
|
|
471
|
-
const icon =
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
471
|
+
const icon =
|
|
472
|
+
job.status === "completed"
|
|
473
|
+
? uiTheme.styledSymbol("tool.job", "accent")
|
|
474
|
+
: formatStatusIcon(
|
|
475
|
+
statusToIcon(job.status),
|
|
476
|
+
uiTheme,
|
|
477
|
+
job.status === "running" ? options.spinnerFrame : undefined,
|
|
478
|
+
);
|
|
476
479
|
const typeBadge = formatBadge(job.type, statusToColor(job.status), uiTheme);
|
|
477
480
|
const idText = uiTheme.fg("muted", job.id);
|
|
478
481
|
const rawLabelLines = (job.label || "(no label)").split(/\r?\n/);
|
|
@@ -49,10 +49,11 @@ function queryHeader(
|
|
|
49
49
|
icon: ToolUIStatus,
|
|
50
50
|
theme: Theme,
|
|
51
51
|
meta?: string[],
|
|
52
|
+
iconOverride?: string,
|
|
52
53
|
): string {
|
|
53
54
|
const trimmed = replaceTabs((query ?? "").trim());
|
|
54
55
|
const description = trimmed ? truncateToWidth(trimmed, 80, Ellipsis.Unicode) : undefined;
|
|
55
|
-
return renderStatusLine({ icon, title, description, meta }, theme);
|
|
56
|
+
return renderStatusLine({ icon, iconOverride, title, description, meta }, theme);
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
function retainComponent(contents: string[], header: string, getExpanded: () => boolean, theme: Theme): Component {
|
|
@@ -96,7 +97,11 @@ export const retainToolRenderer = {
|
|
|
96
97
|
// trailing period so it reads cleanly as a status meta segment.
|
|
97
98
|
const summary = resultText(result).replace(/\.$/, "");
|
|
98
99
|
const header = renderStatusLine(
|
|
99
|
-
{
|
|
100
|
+
{
|
|
101
|
+
iconOverride: theme.styledSymbol("tool.memory", "accent"),
|
|
102
|
+
title: "Retain",
|
|
103
|
+
meta: summary ? [summary] : undefined,
|
|
104
|
+
},
|
|
100
105
|
theme,
|
|
101
106
|
);
|
|
102
107
|
return retainComponent(contents, header, () => options.expanded, theme);
|
|
@@ -121,9 +126,11 @@ export const recallToolRenderer = {
|
|
|
121
126
|
const text = resultText(result);
|
|
122
127
|
const match = text.match(/^Found (\d+) relevant/);
|
|
123
128
|
const found = match ? Number(match[1]) : 0;
|
|
124
|
-
const icon: ToolUIStatus = found > 0 ? "success" : "warning";
|
|
125
129
|
const meta = [found > 0 ? `${found} found` : "no matches"];
|
|
126
|
-
const header =
|
|
130
|
+
const header =
|
|
131
|
+
found > 0
|
|
132
|
+
? queryHeader("Recall", args?.query, "success", theme, meta, theme.styledSymbol("tool.memory", "accent"))
|
|
133
|
+
: queryHeader("Recall", args?.query, "warning", theme, meta);
|
|
127
134
|
if (found === 0) {
|
|
128
135
|
return new Text(header, 0, 0);
|
|
129
136
|
}
|
|
@@ -163,7 +170,14 @@ export const reflectToolRenderer = {
|
|
|
163
170
|
if (result.isError) {
|
|
164
171
|
return new Text(formatErrorMessage(resultText(result) || "Reflect failed", theme), 0, 0);
|
|
165
172
|
}
|
|
166
|
-
const header = queryHeader(
|
|
173
|
+
const header = queryHeader(
|
|
174
|
+
"Reflect",
|
|
175
|
+
args?.query,
|
|
176
|
+
"success",
|
|
177
|
+
theme,
|
|
178
|
+
undefined,
|
|
179
|
+
theme.styledSymbol("tool.memory", "accent"),
|
|
180
|
+
);
|
|
167
181
|
const answer = resultText(result);
|
|
168
182
|
const answerLines = answer.split("\n").filter(line => line.trim().length > 0);
|
|
169
183
|
return createCachedComponent(
|