@oh-my-pi/pi-coding-agent 15.12.3 → 15.12.4
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 +43 -1
- package/dist/cli.js +1120 -870
- package/dist/types/autoresearch/tools/init-experiment.d.ts +1 -1
- package/dist/types/autoresearch/tools/log-experiment.d.ts +1 -1
- package/dist/types/autoresearch/tools/run-experiment.d.ts +1 -1
- package/dist/types/autoresearch/tools/update-notes.d.ts +1 -1
- package/dist/types/cli/args.d.ts +0 -1
- package/dist/types/cli/models-cli.d.ts +49 -0
- package/dist/types/commands/launch.d.ts +0 -3
- package/dist/types/commands/models.d.ts +33 -0
- package/dist/types/commands/token.d.ts +25 -0
- package/dist/types/commit/agentic/tools/analyze-file.d.ts +1 -1
- package/dist/types/commit/agentic/tools/git-file-diff.d.ts +1 -1
- package/dist/types/commit/agentic/tools/git-hunk.d.ts +1 -1
- package/dist/types/commit/agentic/tools/git-overview.d.ts +1 -1
- package/dist/types/commit/agentic/tools/propose-changelog.d.ts +1 -1
- package/dist/types/commit/agentic/tools/propose-commit.d.ts +1 -1
- package/dist/types/commit/agentic/tools/recent-commits.d.ts +1 -1
- package/dist/types/commit/agentic/tools/schemas.d.ts +1 -1
- package/dist/types/commit/agentic/tools/split-commit.d.ts +1 -1
- package/dist/types/commit/changelog/generate.d.ts +1 -1
- package/dist/types/commit/shared-llm.d.ts +1 -1
- package/dist/types/config/model-registry.d.ts +7 -0
- package/dist/types/config/models-config-schema.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +20 -0
- package/dist/types/edit/hashline/params.d.ts +1 -1
- package/dist/types/edit/modes/apply-patch.d.ts +1 -1
- package/dist/types/edit/modes/patch.d.ts +1 -1
- package/dist/types/edit/modes/replace.d.ts +1 -1
- package/dist/types/extensibility/custom-commands/types.d.ts +2 -2
- package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
- package/dist/types/extensibility/extensions/types.d.ts +2 -2
- package/dist/types/extensibility/hooks/types.d.ts +2 -2
- package/dist/types/goals/tools/goal-tool.d.ts +1 -1
- package/dist/types/lsp/types.d.ts +1 -1
- package/dist/types/mcp/manager.d.ts +8 -0
- package/dist/types/mnemopi/config.d.ts +28 -0
- package/dist/types/modes/acp/acp-agent.d.ts +1 -2
- package/dist/types/modes/components/index.d.ts +1 -0
- package/dist/types/modes/components/logout-account-selector.d.ts +8 -0
- package/dist/types/modes/components/status-line/component.d.ts +9 -5
- package/dist/types/modes/components/status-line/types.d.ts +2 -1
- package/dist/types/modes/controllers/event-controller.d.ts +0 -17
- package/dist/types/modes/interactive-mode.d.ts +0 -3
- package/dist/types/modes/types.d.ts +0 -5
- package/dist/types/session/agent-session.d.ts +14 -33
- package/dist/types/session/agent-storage.d.ts +2 -1
- package/dist/types/session/indexed-session-storage.d.ts +1 -0
- package/dist/types/session/messages.d.ts +8 -10
- package/dist/types/session/session-manager.d.ts +15 -0
- package/dist/types/session/session-storage.d.ts +5 -0
- package/dist/types/slash-commands/helpers/logout.d.ts +15 -0
- package/dist/types/task/types.d.ts +1 -1
- package/dist/types/tools/ask.d.ts +1 -1
- package/dist/types/tools/ast-edit.d.ts +1 -1
- package/dist/types/tools/ast-grep.d.ts +1 -1
- package/dist/types/tools/bash.d.ts +1 -1
- package/dist/types/tools/browser/cmux/cmux-tab.d.ts +202 -0
- package/dist/types/tools/browser/cmux/rpc.d.ts +70 -0
- package/dist/types/tools/browser/cmux/socket-client.d.ts +19 -0
- package/dist/types/tools/browser/registry.d.ts +16 -3
- package/dist/types/tools/browser/render.d.ts +2 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +2 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +16 -4
- package/dist/types/tools/browser.d.ts +3 -1
- package/dist/types/tools/checkpoint.d.ts +1 -1
- package/dist/types/tools/debug.d.ts +1 -1
- package/dist/types/tools/eval.d.ts +1 -1
- package/dist/types/tools/find.d.ts +1 -1
- package/dist/types/tools/gh.d.ts +1 -1
- package/dist/types/tools/image-gen.d.ts +1 -1
- package/dist/types/tools/index.d.ts +3 -1
- package/dist/types/tools/inspect-image.d.ts +1 -1
- package/dist/types/tools/irc.d.ts +1 -1
- package/dist/types/tools/job.d.ts +1 -1
- package/dist/types/tools/memory-edit.d.ts +1 -1
- package/dist/types/tools/memory-recall.d.ts +1 -1
- package/dist/types/tools/memory-reflect.d.ts +1 -1
- package/dist/types/tools/memory-retain.d.ts +1 -1
- package/dist/types/tools/read.d.ts +1 -1
- package/dist/types/tools/render-mermaid.d.ts +1 -1
- package/dist/types/tools/resolve.d.ts +1 -1
- package/dist/types/tools/review.d.ts +1 -1
- package/dist/types/tools/search-tool-bm25.d.ts +1 -1
- package/dist/types/tools/search.d.ts +1 -1
- package/dist/types/tools/ssh.d.ts +1 -1
- package/dist/types/tools/todo.d.ts +1 -1
- package/dist/types/tools/tts.d.ts +1 -1
- package/dist/types/tools/write.d.ts +1 -1
- package/dist/types/utils/clipboard.d.ts +4 -3
- package/dist/types/utils/image-loading.d.ts +18 -1
- package/dist/types/utils/thinking-display.d.ts +17 -0
- package/dist/types/web/search/index.d.ts +1 -1
- package/package.json +14 -14
- package/src/autoresearch/storage.ts +2 -1
- package/src/autoresearch/tools/init-experiment.ts +1 -1
- package/src/autoresearch/tools/log-experiment.ts +1 -1
- package/src/autoresearch/tools/run-experiment.ts +1 -1
- package/src/autoresearch/tools/update-notes.ts +1 -1
- package/src/cli/args.ts +0 -8
- package/src/cli/auth-gateway-cli.ts +1 -1
- package/src/cli/bench-cli.ts +1 -1
- package/src/cli/dry-balance-cli.ts +1 -1
- package/src/cli/models-cli.ts +427 -0
- package/src/cli-commands.ts +2 -0
- package/src/collab/host.ts +9 -12
- package/src/commands/launch.ts +0 -3
- package/src/commands/models.ts +61 -0
- package/src/commands/token.ts +89 -0
- package/src/commit/agentic/tools/analyze-file.ts +1 -1
- package/src/commit/agentic/tools/git-file-diff.ts +1 -1
- package/src/commit/agentic/tools/git-hunk.ts +1 -1
- package/src/commit/agentic/tools/git-overview.ts +1 -1
- package/src/commit/agentic/tools/propose-changelog.ts +1 -1
- package/src/commit/agentic/tools/propose-commit.ts +1 -1
- package/src/commit/agentic/tools/recent-commits.ts +1 -1
- package/src/commit/agentic/tools/schemas.ts +1 -1
- package/src/commit/agentic/tools/split-commit.ts +1 -1
- package/src/commit/analysis/summary.ts +1 -1
- package/src/commit/changelog/generate.ts +1 -1
- package/src/commit/shared-llm.ts +1 -1
- package/src/config/model-registry.ts +15 -12
- package/src/config/model-resolver.ts +2 -2
- package/src/config/models-config-schema.ts +1 -1
- package/src/config/settings-schema.ts +18 -0
- package/src/edit/hashline/params.ts +1 -1
- package/src/edit/modes/apply-patch.ts +1 -1
- package/src/edit/modes/patch.ts +1 -1
- package/src/edit/modes/replace.ts +1 -1
- package/src/eval/agent-bridge.ts +1 -1
- package/src/eval/completion-bridge.ts +1 -1
- package/src/export/html/template.js +24 -2
- package/src/export/html/tool-views.generated.js +2 -2
- package/src/extensibility/custom-commands/loader.ts +1 -1
- package/src/extensibility/custom-commands/types.ts +2 -2
- package/src/extensibility/custom-tools/loader.ts +1 -1
- package/src/extensibility/custom-tools/types.ts +2 -2
- package/src/extensibility/extensions/loader.ts +2 -2
- package/src/extensibility/extensions/types.ts +2 -2
- package/src/extensibility/hooks/loader.ts +1 -1
- package/src/extensibility/hooks/types.ts +2 -2
- package/src/extensibility/skills.ts +18 -3
- package/src/goals/tools/goal-tool.ts +1 -1
- package/src/internal-urls/docs-index.generated.ts +5 -2
- package/src/lsp/types.ts +1 -1
- package/src/main.ts +0 -25
- package/src/mcp/config-writer.ts +7 -3
- package/src/mcp/manager.ts +11 -0
- package/src/memories/index.ts +3 -1
- package/src/memories/storage.ts +2 -1
- package/src/mnemopi/config.ts +95 -11
- package/src/modes/acp/acp-agent.ts +5 -48
- package/src/modes/acp/acp-event-mapper.ts +5 -1
- package/src/modes/components/agent-hub.ts +2 -1
- package/src/modes/components/assistant-message.ts +8 -7
- package/src/modes/components/index.ts +1 -0
- package/src/modes/components/logout-account-selector.ts +130 -0
- package/src/modes/components/mcp-add-wizard.ts +1 -1
- package/src/modes/components/model-selector.ts +2 -2
- package/src/modes/components/status-line/component.ts +54 -157
- package/src/modes/components/status-line/segments.ts +1 -1
- package/src/modes/components/status-line/types.ts +2 -1
- package/src/modes/controllers/command-controller.ts +0 -12
- package/src/modes/controllers/event-controller.ts +23 -62
- package/src/modes/controllers/input-controller.ts +53 -30
- package/src/modes/controllers/mcp-command-controller.ts +44 -3
- package/src/modes/controllers/selector-controller.ts +56 -10
- package/src/modes/controllers/streaming-reveal.ts +4 -3
- package/src/modes/interactive-mode.ts +2 -8
- package/src/modes/theme/theme.ts +1 -1
- package/src/modes/types.ts +0 -5
- package/src/modes/utils/ui-helpers.ts +2 -1
- package/src/prompts/system/empty-stop-retry.md +4 -6
- package/src/sdk.ts +15 -19
- package/src/session/agent-session.ts +125 -234
- package/src/session/agent-storage.ts +18 -9
- package/src/session/history-storage.ts +2 -1
- package/src/session/indexed-session-storage.ts +7 -0
- package/src/session/messages.ts +9 -11
- package/src/session/session-dump-format.ts +4 -2
- package/src/session/session-manager.ts +116 -0
- package/src/session/session-storage.ts +20 -0
- package/src/slash-commands/builtin-registry.ts +15 -1
- package/src/slash-commands/helpers/logout.ts +88 -0
- package/src/task/types.ts +1 -1
- package/src/tools/ask.ts +1 -1
- package/src/tools/ast-edit.ts +1 -1
- package/src/tools/ast-grep.ts +1 -1
- package/src/tools/bash.ts +1 -1
- package/src/tools/browser/cmux/cmux-tab.ts +1264 -0
- package/src/tools/browser/cmux/rpc.ts +156 -0
- package/src/tools/browser/cmux/socket-client.ts +309 -0
- package/src/tools/browser/registry.ts +37 -3
- package/src/tools/browser/render.ts +6 -1
- package/src/tools/browser/tab-protocol.ts +2 -0
- package/src/tools/browser/tab-supervisor.ts +189 -18
- package/src/tools/browser/tab-worker.ts +1 -1
- package/src/tools/browser.ts +16 -1
- package/src/tools/checkpoint.ts +1 -1
- package/src/tools/debug.ts +1 -1
- package/src/tools/eval.ts +11 -6
- package/src/tools/fetch.ts +13 -2
- package/src/tools/find.ts +1 -1
- package/src/tools/gh.ts +1 -1
- package/src/tools/github-cache.ts +2 -1
- package/src/tools/image-gen.ts +1 -1
- package/src/tools/index.ts +3 -1
- package/src/tools/inspect-image.ts +3 -1
- package/src/tools/irc.ts +1 -1
- package/src/tools/job.ts +1 -1
- package/src/tools/memory-edit.ts +1 -1
- package/src/tools/memory-recall.ts +1 -1
- package/src/tools/memory-reflect.ts +1 -1
- package/src/tools/memory-retain.ts +1 -1
- package/src/tools/read.ts +8 -2
- package/src/tools/render-mermaid.ts +1 -1
- package/src/tools/report-tool-issue.ts +3 -2
- package/src/tools/resolve.ts +1 -1
- package/src/tools/review.ts +1 -1
- package/src/tools/search-tool-bm25.ts +1 -1
- package/src/tools/search.ts +1 -1
- package/src/tools/ssh.ts +1 -1
- package/src/tools/todo.ts +1 -1
- package/src/tools/tts.ts +1 -1
- package/src/tools/write.ts +1 -1
- package/src/utils/clipboard.ts +35 -18
- package/src/utils/image-loading.ts +35 -4
- package/src/utils/thinking-display.ts +37 -0
- package/src/web/search/index.ts +1 -1
- package/dist/types/cli/list-models.d.ts +0 -30
- package/src/cli/list-models.ts +0 -194
|
@@ -0,0 +1,1264 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { logger, Snowflake } from "@oh-my-pi/pi-utils";
|
|
5
|
+
import { JsRuntime, type RuntimeHooks } from "../../../eval/js/shared/runtime";
|
|
6
|
+
import type { JsDisplayOutput } from "../../../eval/js/shared/types";
|
|
7
|
+
import { callSessionTool } from "../../../eval/js/tool-bridge";
|
|
8
|
+
import type { ToolSession } from "../../../sdk";
|
|
9
|
+
import { resizeImage } from "../../../utils/image-resize";
|
|
10
|
+
import { resolveToCwd } from "../../path-utils";
|
|
11
|
+
import { formatScreenshot } from "../../render-utils";
|
|
12
|
+
import { ToolAbortError, ToolError } from "../../tool-errors";
|
|
13
|
+
import { DEFAULT_VIEWPORT } from "../launch";
|
|
14
|
+
import { extractReadableFromHtml, type ReadableFormat } from "../readable";
|
|
15
|
+
import type { Observation, ReadyInfo, RunResultOk, ScreenshotResult, SessionSnapshot } from "../tab-protocol";
|
|
16
|
+
import {
|
|
17
|
+
type CmuxEvalResult,
|
|
18
|
+
type CmuxGeometry,
|
|
19
|
+
type CmuxScreenshotResult,
|
|
20
|
+
type CmuxSnapshotResult,
|
|
21
|
+
type CmuxUrlGetResult,
|
|
22
|
+
cmuxSnapshotToObservation,
|
|
23
|
+
GEOMETRY_SCRIPT,
|
|
24
|
+
mapWaitUntil,
|
|
25
|
+
serializeEval,
|
|
26
|
+
} from "./rpc";
|
|
27
|
+
import type { CmuxSocketClient } from "./socket-client";
|
|
28
|
+
|
|
29
|
+
interface ScreenshotOptions {
|
|
30
|
+
selector?: string;
|
|
31
|
+
fullPage?: boolean;
|
|
32
|
+
save?: string;
|
|
33
|
+
silent?: boolean;
|
|
34
|
+
encoding?: "base64" | "binary";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface ObserveOptions {
|
|
38
|
+
includeAll?: boolean;
|
|
39
|
+
viewportOnly?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface RunContext {
|
|
43
|
+
session: SessionSnapshot;
|
|
44
|
+
displays: RunResultOk["displays"];
|
|
45
|
+
screenshots: ScreenshotResult[];
|
|
46
|
+
signal?: AbortSignal;
|
|
47
|
+
timeoutMs: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type WaitUntil = "load" | "domcontentloaded" | "networkidle0" | "networkidle2";
|
|
51
|
+
type DragTarget = string | { readonly x: number; readonly y: number };
|
|
52
|
+
type SelectorKind = "css" | "ref" | "text" | "aria" | "xpath" | "pierce" | "ax";
|
|
53
|
+
|
|
54
|
+
interface SelectorSpec {
|
|
55
|
+
kind: SelectorKind;
|
|
56
|
+
value: string;
|
|
57
|
+
raw: string;
|
|
58
|
+
ref?: string;
|
|
59
|
+
name?: string;
|
|
60
|
+
role?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface CachedElementRef {
|
|
64
|
+
ref: string;
|
|
65
|
+
name?: string;
|
|
66
|
+
role?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface BoundingBox {
|
|
70
|
+
x: number;
|
|
71
|
+
y: number;
|
|
72
|
+
width: number;
|
|
73
|
+
height: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface FilePayload {
|
|
77
|
+
name: string;
|
|
78
|
+
type: string;
|
|
79
|
+
data: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interface CmuxResponseRecord {
|
|
83
|
+
id: number;
|
|
84
|
+
url: string;
|
|
85
|
+
status: number;
|
|
86
|
+
statusText: string;
|
|
87
|
+
headers: Record<string, string>;
|
|
88
|
+
body: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface ViewportOptions {
|
|
92
|
+
width: number;
|
|
93
|
+
height: number;
|
|
94
|
+
deviceScaleFactor?: number;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const PAGE_SELECTOR_HELPERS = `
|
|
98
|
+
const isVisible = element => {
|
|
99
|
+
const style = getComputedStyle(element);
|
|
100
|
+
const rect = element.getBoundingClientRect();
|
|
101
|
+
return style.visibility !== "hidden" && style.display !== "none" && rect.width > 0 && rect.height > 0;
|
|
102
|
+
};
|
|
103
|
+
const textOf = element => (element.innerText || element.textContent || "").trim();
|
|
104
|
+
const allElements = () => Array.from(document.querySelectorAll("body *"));
|
|
105
|
+
const pierceQuery = (root, selector) => {
|
|
106
|
+
const direct = root.querySelector?.(selector);
|
|
107
|
+
if (direct) return direct;
|
|
108
|
+
const nodes = root.querySelectorAll ? Array.from(root.querySelectorAll("*")) : [];
|
|
109
|
+
for (const node of nodes) {
|
|
110
|
+
if (node.shadowRoot) {
|
|
111
|
+
const found = pierceQuery(node.shadowRoot, selector);
|
|
112
|
+
if (found) return found;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
};
|
|
117
|
+
const accessibleName = element =>
|
|
118
|
+
(
|
|
119
|
+
element.getAttribute("aria-label") ||
|
|
120
|
+
element.getAttribute("alt") ||
|
|
121
|
+
element.getAttribute("title") ||
|
|
122
|
+
textOf(element)
|
|
123
|
+
).trim();
|
|
124
|
+
const findElement = spec => {
|
|
125
|
+
if (spec.kind === "css") return document.querySelector(spec.value);
|
|
126
|
+
if (spec.kind === "pierce") return pierceQuery(document, spec.value);
|
|
127
|
+
if (spec.kind === "xpath") {
|
|
128
|
+
const result = document.evaluate(spec.value, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
129
|
+
return result.singleNodeValue instanceof Element ? result.singleNodeValue : null;
|
|
130
|
+
}
|
|
131
|
+
if (spec.kind === "text") {
|
|
132
|
+
const wanted = spec.value.trim();
|
|
133
|
+
return allElements().find(element => isVisible(element) && textOf(element).includes(wanted)) || null;
|
|
134
|
+
}
|
|
135
|
+
if (spec.kind === "aria" || spec.kind === "ax") {
|
|
136
|
+
const wanted = (spec.name || spec.value).trim();
|
|
137
|
+
const role = spec.role || "";
|
|
138
|
+
return (
|
|
139
|
+
allElements().find(element => {
|
|
140
|
+
if (!isVisible(element)) return false;
|
|
141
|
+
if (role && element.getAttribute("role") !== role) return false;
|
|
142
|
+
const name = accessibleName(element);
|
|
143
|
+
return name === wanted || name.includes(wanted);
|
|
144
|
+
}) || null
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
};
|
|
149
|
+
const event = (target, type, init = {}) =>
|
|
150
|
+
target.dispatchEvent(new Event(type, { bubbles: true, cancelable: true, ...init }));
|
|
151
|
+
const mouseEvent = (target, type, init = {}) =>
|
|
152
|
+
target.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true, view: window, ...init }));
|
|
153
|
+
const inputEvent = target => {
|
|
154
|
+
event(target, "input");
|
|
155
|
+
event(target, "change");
|
|
156
|
+
};
|
|
157
|
+
const setValue = (target, value, append = false) => {
|
|
158
|
+
if ("value" in target) {
|
|
159
|
+
target.value = append ? String(target.value || "") + value : value;
|
|
160
|
+
inputEvent(target);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (target.isContentEditable) {
|
|
164
|
+
target.textContent = append ? String(target.textContent || "") + value : value;
|
|
165
|
+
inputEvent(target);
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
`;
|
|
169
|
+
|
|
170
|
+
const RESPONSE_OBSERVER_SCRIPT = String.raw`
|
|
171
|
+
(() => {
|
|
172
|
+
const key = "__ompCmuxResponses";
|
|
173
|
+
if (globalThis[key]) return true;
|
|
174
|
+
const state = { nextId: 1, records: [] };
|
|
175
|
+
Object.defineProperty(globalThis, key, { value: state, configurable: true });
|
|
176
|
+
const headersObject = headers => {
|
|
177
|
+
const out = {};
|
|
178
|
+
if (headers && typeof headers.forEach === "function") headers.forEach((value, name) => (out[name] = value));
|
|
179
|
+
return out;
|
|
180
|
+
};
|
|
181
|
+
const remember = async response => {
|
|
182
|
+
try {
|
|
183
|
+
const clone = response.clone();
|
|
184
|
+
const body = await clone.text().catch(() => "");
|
|
185
|
+
state.records.push({
|
|
186
|
+
id: state.nextId++,
|
|
187
|
+
url: response.url,
|
|
188
|
+
status: response.status,
|
|
189
|
+
statusText: response.statusText,
|
|
190
|
+
headers: headersObject(response.headers),
|
|
191
|
+
body,
|
|
192
|
+
});
|
|
193
|
+
if (state.records.length > 200) state.records.splice(0, state.records.length - 200);
|
|
194
|
+
} catch {
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
const originalFetch = globalThis.fetch;
|
|
198
|
+
if (typeof originalFetch === "function") {
|
|
199
|
+
globalThis.fetch = async (...args) => {
|
|
200
|
+
const response = await originalFetch(...args);
|
|
201
|
+
void remember(response);
|
|
202
|
+
return response;
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
const OriginalXHR = globalThis.XMLHttpRequest;
|
|
206
|
+
if (typeof OriginalXHR === "function") {
|
|
207
|
+
globalThis.XMLHttpRequest = function XMLHttpRequestProxy() {
|
|
208
|
+
const xhr = new OriginalXHR();
|
|
209
|
+
xhr.addEventListener("loadend", () => {
|
|
210
|
+
const rawHeaders = xhr.getAllResponseHeaders();
|
|
211
|
+
const headers = {};
|
|
212
|
+
for (const line of rawHeaders.trim().split(/[\r\n]+/)) {
|
|
213
|
+
const index = line.indexOf(":");
|
|
214
|
+
if (index > 0) headers[line.slice(0, index).trim().toLowerCase()] = line.slice(index + 1).trim();
|
|
215
|
+
}
|
|
216
|
+
state.records.push({
|
|
217
|
+
id: state.nextId++,
|
|
218
|
+
url: xhr.responseURL || "",
|
|
219
|
+
status: xhr.status,
|
|
220
|
+
statusText: xhr.statusText,
|
|
221
|
+
headers,
|
|
222
|
+
body: typeof xhr.responseText === "string" ? xhr.responseText : "",
|
|
223
|
+
});
|
|
224
|
+
if (state.records.length > 200) state.records.splice(0, state.records.length - 200);
|
|
225
|
+
});
|
|
226
|
+
return xhr;
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
return true;
|
|
230
|
+
})()
|
|
231
|
+
`;
|
|
232
|
+
|
|
233
|
+
export interface RunCmuxCodeOptions {
|
|
234
|
+
code: string;
|
|
235
|
+
timeoutMs: number;
|
|
236
|
+
signal?: AbortSignal;
|
|
237
|
+
session: ToolSession;
|
|
238
|
+
snapshot: SessionSnapshot;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export class CmuxTab {
|
|
242
|
+
readonly #client: CmuxSocketClient;
|
|
243
|
+
readonly #surfaceId: string;
|
|
244
|
+
#lastUrl = "about:blank";
|
|
245
|
+
#lastTitle: string | undefined;
|
|
246
|
+
#lastViewport: ReadyInfo["viewport"] = DEFAULT_VIEWPORT;
|
|
247
|
+
#runContext: RunContext | undefined;
|
|
248
|
+
#runtime: JsRuntime | undefined;
|
|
249
|
+
readonly #elementRefs = new Map<number, CachedElementRef>();
|
|
250
|
+
#pageFacade: CmuxPageFacade | undefined;
|
|
251
|
+
#browserFacade: CmuxBrowserFacade | undefined;
|
|
252
|
+
constructor(opts: { client: CmuxSocketClient; surfaceId: string; url?: string; title?: string }) {
|
|
253
|
+
this.#client = opts.client;
|
|
254
|
+
this.#surfaceId = opts.surfaceId;
|
|
255
|
+
if (opts.url) this.#lastUrl = opts.url;
|
|
256
|
+
this.#lastTitle = opts.title;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
get surfaceId(): string {
|
|
260
|
+
return this.#surfaceId;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
get page(): CmuxPageFacade {
|
|
264
|
+
this.#pageFacade ??= new CmuxPageFacade(this);
|
|
265
|
+
return this.#pageFacade;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
get browser(): CmuxBrowserFacade {
|
|
269
|
+
this.#browserFacade ??= new CmuxBrowserFacade(this);
|
|
270
|
+
return this.#browserFacade;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
viewport(): ReadyInfo["viewport"] {
|
|
274
|
+
return this.#lastViewport;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async setViewport(viewport: ViewportOptions): Promise<void> {
|
|
278
|
+
this.#lastViewport = {
|
|
279
|
+
width: viewport.width,
|
|
280
|
+
height: viewport.height,
|
|
281
|
+
deviceScaleFactor: viewport.deviceScaleFactor,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
url(): string {
|
|
286
|
+
return this.#lastUrl;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async title(): Promise<string> {
|
|
290
|
+
const result = (await this.#request("browser.eval", { script: "document.title" })) as CmuxEvalResult;
|
|
291
|
+
this.#lastTitle = String(result.value ?? "");
|
|
292
|
+
return this.#lastTitle;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async readyInfo(viewport: ReadyInfo["viewport"] = DEFAULT_VIEWPORT): Promise<ReadyInfo> {
|
|
296
|
+
const urlResult = (await this.#request("browser.url.get", {})) as CmuxUrlGetResult;
|
|
297
|
+
if (typeof urlResult.url === "string" && urlResult.url.length > 0) {
|
|
298
|
+
this.#lastUrl = urlResult.url;
|
|
299
|
+
}
|
|
300
|
+
const geometry = await this.#readGeometry().catch(() => undefined);
|
|
301
|
+
this.#lastViewport = geometry
|
|
302
|
+
? { width: geometry.innerWidth, height: geometry.innerHeight, deviceScaleFactor: geometry.dpr }
|
|
303
|
+
: viewport;
|
|
304
|
+
await this.title().catch(() => "");
|
|
305
|
+
return {
|
|
306
|
+
url: this.#lastUrl,
|
|
307
|
+
title: this.#lastTitle,
|
|
308
|
+
viewport: this.#lastViewport,
|
|
309
|
+
targetId: this.#surfaceId,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
setRunContext(context: RunContext): void {
|
|
314
|
+
this.#runContext = context;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
clearRunContext(): void {
|
|
318
|
+
this.#runContext = undefined;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async goto(url: string, opts?: { waitUntil?: WaitUntil; timeoutMs?: number }): Promise<void> {
|
|
322
|
+
const timeoutMs = opts?.timeoutMs ?? this.#runContext?.timeoutMs ?? 30_000;
|
|
323
|
+
const result = await this.#request("browser.navigate", { url }, timeoutMs);
|
|
324
|
+
const navigatedUrl = result.url;
|
|
325
|
+
this.#lastUrl = typeof navigatedUrl === "string" && navigatedUrl.length > 0 ? navigatedUrl : url;
|
|
326
|
+
if (opts?.waitUntil) {
|
|
327
|
+
await this.#request(
|
|
328
|
+
"browser.wait",
|
|
329
|
+
{ load_state: mapWaitUntil(opts.waitUntil), timeout_ms: timeoutMs },
|
|
330
|
+
timeoutMs,
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async observe(opts?: ObserveOptions): Promise<Observation> {
|
|
336
|
+
void opts?.viewportOnly;
|
|
337
|
+
const timeoutMs = Math.min(this.#runContext?.timeoutMs ?? 30_000, 30_000);
|
|
338
|
+
const [snapshot, geometry] = await Promise.all([
|
|
339
|
+
this.#request("browser.snapshot", { interactive: !opts?.includeAll, max_depth: 12 }, timeoutMs),
|
|
340
|
+
this.#readGeometry(timeoutMs),
|
|
341
|
+
]);
|
|
342
|
+
const viewport = {
|
|
343
|
+
width: geometry.innerWidth,
|
|
344
|
+
height: geometry.innerHeight,
|
|
345
|
+
deviceScaleFactor: geometry.dpr,
|
|
346
|
+
};
|
|
347
|
+
this.#lastViewport = viewport;
|
|
348
|
+
const observation = cmuxSnapshotToObservation(snapshot as CmuxSnapshotResult, viewport, geometry);
|
|
349
|
+
this.#lastUrl = observation.url;
|
|
350
|
+
this.#lastTitle = observation.title;
|
|
351
|
+
this.#rememberObservedElements(observation);
|
|
352
|
+
return observation;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async click(selector: string): Promise<void> {
|
|
356
|
+
await this.#selectorAction(selector, "click");
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async dblclick(selector: string): Promise<void> {
|
|
360
|
+
await this.#selectorAction(selector, "dblclick");
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async hover(selector: string): Promise<void> {
|
|
364
|
+
await this.#selectorAction(selector, "hover");
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async focus(selector: string): Promise<void> {
|
|
368
|
+
await this.#selectorAction(selector, "focus");
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async check(selector: string): Promise<void> {
|
|
372
|
+
await this.#selectorAction(selector, "check");
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async uncheck(selector: string): Promise<void> {
|
|
376
|
+
await this.#selectorAction(selector, "uncheck");
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async type(selector: string, text: string): Promise<void> {
|
|
380
|
+
await this.#selectorAction(selector, "type", { text });
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async fill(selector: string, value: string): Promise<void> {
|
|
384
|
+
await this.#selectorAction(selector, "fill", { value });
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async press(key: string, opts?: { selector?: string }): Promise<void> {
|
|
388
|
+
if (opts?.selector) {
|
|
389
|
+
await this.focus(opts.selector);
|
|
390
|
+
}
|
|
391
|
+
await this.#request("browser.press", { key });
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async scroll(dx: number, dy: number): Promise<void> {
|
|
395
|
+
await this.#request("browser.scroll", { dx, dy });
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async waitFor(selector: string, opts?: { timeout?: number }): Promise<CmuxElementHandle> {
|
|
399
|
+
const timeoutMs = opts?.timeout ?? this.#runContext?.timeoutMs ?? 30_000;
|
|
400
|
+
await this.#waitForSelector(selector, timeoutMs);
|
|
401
|
+
return new CmuxElementHandle(this, selector);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
async evaluate<TResult, TArgs extends unknown[]>(
|
|
405
|
+
fn: string | ((...args: TArgs) => TResult | Promise<TResult>),
|
|
406
|
+
...args: TArgs
|
|
407
|
+
): Promise<TResult> {
|
|
408
|
+
const result = (await this.#request("browser.eval", {
|
|
409
|
+
script: serializeEval(fn as string | ((...args: unknown[]) => unknown), args),
|
|
410
|
+
})) as CmuxEvalResult;
|
|
411
|
+
return result.value as TResult;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async scrollIntoView(selector: string): Promise<void> {
|
|
415
|
+
await this.#selectorAction(selector, "scrollIntoView");
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
async select(selector: string, ...values: string[]): Promise<string[]> {
|
|
419
|
+
return await this.#selectorAction<string[]>(selector, "select", { values });
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
async extract(format: ReadableFormat = "markdown"): Promise<string> {
|
|
423
|
+
const result = (await this.#request("browser.snapshot", { interactive: false })) as CmuxSnapshotResult;
|
|
424
|
+
const html = typeof result.page?.html === "string" ? result.page.html : "";
|
|
425
|
+
const url =
|
|
426
|
+
(typeof result.url === "string" && result.url.length > 0 ? result.url : undefined) ??
|
|
427
|
+
(typeof result.page?.url === "string" && result.page.url.length > 0 ? result.page.url : undefined) ??
|
|
428
|
+
this.#lastUrl;
|
|
429
|
+
const readable = await extractReadableFromHtml(html, url, format);
|
|
430
|
+
if (!readable) {
|
|
431
|
+
throw new ToolError(`tab.extract(${JSON.stringify(format)}) found no readable content on ${url}`);
|
|
432
|
+
}
|
|
433
|
+
const content = format === "markdown" ? readable.markdown : readable.text;
|
|
434
|
+
if (!content) {
|
|
435
|
+
throw new ToolError(`tab.extract(${JSON.stringify(format)}) produced empty ${format} content for ${url}`);
|
|
436
|
+
}
|
|
437
|
+
return content;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async screenshot(opts: ScreenshotOptions = {}): Promise<ScreenshotResult> {
|
|
441
|
+
const context = this.#requireRunContext("tab.screenshot()");
|
|
442
|
+
if (opts.selector) {
|
|
443
|
+
await this.scrollIntoView(opts.selector);
|
|
444
|
+
}
|
|
445
|
+
void opts.fullPage;
|
|
446
|
+
const result = await this.#captureScreenshotPng(context.timeoutMs);
|
|
447
|
+
const buffer = Buffer.from(result.png_base64, "base64");
|
|
448
|
+
const captureMime = "image/png";
|
|
449
|
+
const resized = await resizeImage(
|
|
450
|
+
{ type: "image", data: result.png_base64, mimeType: captureMime },
|
|
451
|
+
{
|
|
452
|
+
maxWidth: 1024,
|
|
453
|
+
maxHeight: 1024,
|
|
454
|
+
maxBytes: 150 * 1024,
|
|
455
|
+
jpegQuality: 70,
|
|
456
|
+
excludeWebP: context.session.excludeWebP,
|
|
457
|
+
},
|
|
458
|
+
);
|
|
459
|
+
const explicitPath = opts.save ? resolveToCwd(opts.save, context.session.cwd) : undefined;
|
|
460
|
+
const returnedPath = typeof result.path === "string" && result.path.length > 0 ? result.path : undefined;
|
|
461
|
+
const saveFullRes = !!(explicitPath || context.session.browserScreenshotDir || returnedPath);
|
|
462
|
+
const savedBuffer = saveFullRes ? buffer : Buffer.from(resized.buffer);
|
|
463
|
+
const savedMimeType = saveFullRes ? captureMime : resized.mimeType;
|
|
464
|
+
const ext = savedMimeType === "image/webp" ? "webp" : savedMimeType === "image/jpeg" ? "jpg" : "png";
|
|
465
|
+
const dest =
|
|
466
|
+
explicitPath ??
|
|
467
|
+
(context.session.browserScreenshotDir
|
|
468
|
+
? path.join(
|
|
469
|
+
context.session.browserScreenshotDir,
|
|
470
|
+
`screenshot-${new Date().toISOString().replace(/[:.]/g, "-").slice(0, -1)}.${ext}`,
|
|
471
|
+
)
|
|
472
|
+
: (returnedPath ?? path.join(os.tmpdir(), `omp-sshots-${Snowflake.next()}.${ext}`)));
|
|
473
|
+
await fs.promises.mkdir(path.dirname(dest), { recursive: true });
|
|
474
|
+
await Bun.write(dest, savedBuffer);
|
|
475
|
+
const info: ScreenshotResult = {
|
|
476
|
+
dest,
|
|
477
|
+
mimeType: savedMimeType,
|
|
478
|
+
bytes: savedBuffer.length,
|
|
479
|
+
width: resized.width,
|
|
480
|
+
height: resized.height,
|
|
481
|
+
};
|
|
482
|
+
context.screenshots.push(info);
|
|
483
|
+
if (!opts.silent) {
|
|
484
|
+
const lines = formatScreenshot({
|
|
485
|
+
saveFullRes,
|
|
486
|
+
savedMimeType,
|
|
487
|
+
savedByteLength: savedBuffer.length,
|
|
488
|
+
dest,
|
|
489
|
+
resized,
|
|
490
|
+
});
|
|
491
|
+
context.displays.push({ type: "text", text: lines.join("\n") });
|
|
492
|
+
context.displays.push({ type: "image", data: resized.data, mimeType: resized.mimeType });
|
|
493
|
+
}
|
|
494
|
+
return info;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
async waitForUrl(pattern: string | RegExp, opts?: { timeout?: number }): Promise<string> {
|
|
498
|
+
const timeoutMs = opts?.timeout ?? this.#runContext?.timeoutMs ?? 30_000;
|
|
499
|
+
if (typeof pattern === "string") {
|
|
500
|
+
await this.#request("browser.wait", { url_contains: pattern, timeout_ms: timeoutMs }, timeoutMs);
|
|
501
|
+
const result = (await this.#request("browser.url.get", {}, timeoutMs)) as CmuxUrlGetResult;
|
|
502
|
+
if (typeof result.url === "string" && result.url.length > 0) {
|
|
503
|
+
this.#lastUrl = result.url;
|
|
504
|
+
}
|
|
505
|
+
return this.#lastUrl;
|
|
506
|
+
}
|
|
507
|
+
const deadline = Date.now() + timeoutMs;
|
|
508
|
+
while (Date.now() <= deadline) {
|
|
509
|
+
const result = (await this.#request("browser.url.get", {}, Math.min(timeoutMs, 5_000))) as CmuxUrlGetResult;
|
|
510
|
+
if (typeof result.url === "string" && result.url.length > 0) {
|
|
511
|
+
this.#lastUrl = result.url;
|
|
512
|
+
if (pattern.test(result.url)) return result.url;
|
|
513
|
+
}
|
|
514
|
+
await Bun.sleep(200);
|
|
515
|
+
}
|
|
516
|
+
throw new ToolError(`tab.waitForUrl() timed out after ${timeoutMs}ms`);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
async drag(from: DragTarget, to: DragTarget): Promise<void> {
|
|
520
|
+
const start = await this.#dragPoint(from);
|
|
521
|
+
const end = await this.#dragPoint(to);
|
|
522
|
+
await this.#evalScript(
|
|
523
|
+
`(() => {
|
|
524
|
+
const points = ${JSON.stringify({ start, end })};
|
|
525
|
+
const target = document.elementFromPoint(points.start.x, points.start.y) || document.body;
|
|
526
|
+
const dispatch = (type, point) => target.dispatchEvent(new MouseEvent(type, {
|
|
527
|
+
bubbles: true,
|
|
528
|
+
cancelable: true,
|
|
529
|
+
view: window,
|
|
530
|
+
clientX: point.x,
|
|
531
|
+
clientY: point.y,
|
|
532
|
+
buttons: type === "mouseup" ? 0 : 1,
|
|
533
|
+
}));
|
|
534
|
+
dispatch("mousemove", points.start);
|
|
535
|
+
dispatch("mousedown", points.start);
|
|
536
|
+
dispatch("mousemove", points.end);
|
|
537
|
+
dispatch("mouseup", points.end);
|
|
538
|
+
return true;
|
|
539
|
+
})()`,
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
async uploadFile(selector: string, ...filePaths: string[]): Promise<void> {
|
|
544
|
+
if (!filePaths.length) throw new ToolError("tab.uploadFile() requires at least one file path");
|
|
545
|
+
const files: FilePayload[] = [];
|
|
546
|
+
for (const filePath of filePaths) {
|
|
547
|
+
const absolute = resolveToCwd(filePath, this.#requireRunContext("tab.uploadFile()").session.cwd);
|
|
548
|
+
const file = Bun.file(absolute);
|
|
549
|
+
const data = Buffer.from(await file.arrayBuffer()).toString("base64");
|
|
550
|
+
files.push({ name: path.basename(absolute), type: file.type || "application/octet-stream", data });
|
|
551
|
+
}
|
|
552
|
+
await this.#selectorAction(selector, "uploadFile", { files });
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
async waitForResponse(
|
|
556
|
+
pattern: string | RegExp | ((response: CmuxResponse) => boolean | Promise<boolean>),
|
|
557
|
+
opts?: { timeout?: number },
|
|
558
|
+
): Promise<CmuxResponse> {
|
|
559
|
+
const timeoutMs = opts?.timeout ?? this.#runContext?.timeoutMs ?? 30_000;
|
|
560
|
+
await this.#installResponseObserver();
|
|
561
|
+
const startId = await this.#responseCursor();
|
|
562
|
+
const deadline = Date.now() + timeoutMs;
|
|
563
|
+
while (Date.now() <= deadline) {
|
|
564
|
+
const records = await this.#responseRecordsAfter(startId);
|
|
565
|
+
for (const record of records) {
|
|
566
|
+
const response = new CmuxResponse(record);
|
|
567
|
+
if (typeof pattern === "function") {
|
|
568
|
+
if (await pattern(response)) return response;
|
|
569
|
+
} else if (pattern instanceof RegExp ? pattern.test(record.url) : record.url.includes(pattern)) {
|
|
570
|
+
return response;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
await Bun.sleep(100);
|
|
574
|
+
}
|
|
575
|
+
throw new ToolError(`tab.waitForResponse() timed out after ${timeoutMs}ms`);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
async id(id: number): Promise<CmuxElementHandle> {
|
|
579
|
+
const ref = this.#elementRefs.get(id)?.ref ?? `@e${id}`;
|
|
580
|
+
await this.#waitForSelector(ref, this.#runContext?.timeoutMs ?? 30_000);
|
|
581
|
+
return new CmuxElementHandle(this, ref);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
ensureRuntime(session: SessionSnapshot): JsRuntime {
|
|
585
|
+
if (!this.#runtime) {
|
|
586
|
+
this.#runtime = new JsRuntime({
|
|
587
|
+
initialCwd: session.cwd,
|
|
588
|
+
sessionId: `cmux-tab-${this.#surfaceId}`,
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
return this.#runtime;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
async #request(
|
|
595
|
+
method: string,
|
|
596
|
+
params: Record<string, unknown>,
|
|
597
|
+
timeoutMs?: number,
|
|
598
|
+
): Promise<Record<string, unknown>> {
|
|
599
|
+
return await this.#client.request(method, { surface_id: this.#surfaceId, ...params }, { timeoutMs });
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
async #readGeometry(timeoutMs?: number): Promise<CmuxGeometry> {
|
|
603
|
+
const result = (await this.#request("browser.eval", { script: GEOMETRY_SCRIPT }, timeoutMs)) as CmuxEvalResult;
|
|
604
|
+
return this.#normalizeGeometry(result.value);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
elementHandle(selector: string): CmuxElementHandle {
|
|
608
|
+
return new CmuxElementHandle(this, selector);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
async elementExists(selector: string): Promise<boolean> {
|
|
612
|
+
return await this.#selectorExists(this.#selectorSpec(selector));
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
async elementBox(selector: string): Promise<BoundingBox | null> {
|
|
616
|
+
return await this.#selectorBox(this.#selectorSpec(selector));
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
async evaluateOnSelector<TResult>(selector: string, source: string, args: unknown[]): Promise<TResult> {
|
|
620
|
+
const spec = this.#selectorSpec(selector);
|
|
621
|
+
const script = `(() => {
|
|
622
|
+
const spec = ${JSON.stringify(spec)};
|
|
623
|
+
const source = ${JSON.stringify(source)};
|
|
624
|
+
const args = ${JSON.stringify(args)};
|
|
625
|
+
${PAGE_SELECTOR_HELPERS}
|
|
626
|
+
const element = findElement(spec);
|
|
627
|
+
if (!element) throw new Error("Element handle selector no longer resolves");
|
|
628
|
+
const callable = (0, eval)("(" + source + ")");
|
|
629
|
+
return callable(element, ...args);
|
|
630
|
+
})()`;
|
|
631
|
+
return await this.#evalScript<TResult>(script);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
async pageContent(): Promise<string> {
|
|
635
|
+
return await this.#evalScript<string>("document.documentElement.outerHTML");
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
async pageScreenshot(opts: ScreenshotOptions = {}): Promise<Buffer | string> {
|
|
639
|
+
if (opts.selector) await this.scrollIntoView(opts.selector);
|
|
640
|
+
const result = await this.#captureScreenshotPng(this.#runContext?.timeoutMs ?? 30_000);
|
|
641
|
+
return opts.encoding === "base64" ? result.png_base64 : Buffer.from(result.png_base64, "base64");
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
async waitForFunction(
|
|
645
|
+
fn: string | ((...args: unknown[]) => unknown | Promise<unknown>),
|
|
646
|
+
opts: { timeout?: number; polling?: number } | undefined,
|
|
647
|
+
...args: unknown[]
|
|
648
|
+
): Promise<unknown> {
|
|
649
|
+
const timeoutMs = opts?.timeout ?? this.#runContext?.timeoutMs ?? 30_000;
|
|
650
|
+
const pollingMs = typeof opts?.polling === "number" ? opts.polling : 200;
|
|
651
|
+
const deadline = Date.now() + timeoutMs;
|
|
652
|
+
while (Date.now() <= deadline) {
|
|
653
|
+
const value = typeof fn === "string" ? await this.#evalScript<unknown>(fn) : await this.evaluate(fn, ...args);
|
|
654
|
+
if (value) return value;
|
|
655
|
+
await Bun.sleep(pollingMs);
|
|
656
|
+
}
|
|
657
|
+
throw new ToolError(`page.waitForFunction() timed out after ${timeoutMs}ms`);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
async #evalScript<TResult>(script: string, timeoutMs?: number): Promise<TResult> {
|
|
661
|
+
const result = (await this.#request("browser.eval", { script }, timeoutMs)) as CmuxEvalResult;
|
|
662
|
+
return result.value as TResult;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
async #captureScreenshotPng(timeoutMs: number): Promise<CmuxScreenshotResult & { png_base64: string }> {
|
|
666
|
+
const result = (await this.#request("browser.screenshot", {}, timeoutMs)) as CmuxScreenshotResult;
|
|
667
|
+
if (typeof result.png_base64 !== "string" || result.png_base64.length === 0) {
|
|
668
|
+
throw new ToolError("cmux browser screenshot response did not include png_base64");
|
|
669
|
+
}
|
|
670
|
+
return result as CmuxScreenshotResult & { png_base64: string };
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
async #selectorAction<TResult = void>(
|
|
674
|
+
selector: string,
|
|
675
|
+
action: string,
|
|
676
|
+
args: Record<string, unknown> = {},
|
|
677
|
+
): Promise<TResult> {
|
|
678
|
+
const spec = this.#selectorSpec(selector);
|
|
679
|
+
const nativeSelector = this.#nativeSelector(spec);
|
|
680
|
+
if (nativeSelector && action !== "select" && action !== "uploadFile") {
|
|
681
|
+
switch (action) {
|
|
682
|
+
case "click":
|
|
683
|
+
await this.#request("browser.click", { selector: nativeSelector });
|
|
684
|
+
return undefined as TResult;
|
|
685
|
+
case "dblclick":
|
|
686
|
+
await this.#request("browser.dblclick", { selector: nativeSelector });
|
|
687
|
+
return undefined as TResult;
|
|
688
|
+
case "hover":
|
|
689
|
+
await this.#request("browser.hover", { selector: nativeSelector });
|
|
690
|
+
return undefined as TResult;
|
|
691
|
+
case "focus":
|
|
692
|
+
await this.#request("browser.focus", { selector: nativeSelector });
|
|
693
|
+
return undefined as TResult;
|
|
694
|
+
case "check":
|
|
695
|
+
await this.#request("browser.check", { selector: nativeSelector });
|
|
696
|
+
return undefined as TResult;
|
|
697
|
+
case "uncheck":
|
|
698
|
+
await this.#request("browser.uncheck", { selector: nativeSelector });
|
|
699
|
+
return undefined as TResult;
|
|
700
|
+
case "type":
|
|
701
|
+
await this.#request("browser.type", { selector: nativeSelector, text: String(args.text ?? "") });
|
|
702
|
+
return undefined as TResult;
|
|
703
|
+
case "fill":
|
|
704
|
+
await this.#request("browser.fill", { selector: nativeSelector, text: String(args.value ?? "") });
|
|
705
|
+
return undefined as TResult;
|
|
706
|
+
case "scrollIntoView":
|
|
707
|
+
await this.#request("browser.scroll_into_view", { selector: nativeSelector });
|
|
708
|
+
return undefined as TResult;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
return await this.#evalSelectorAction<TResult>(spec, action, args);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
async #evalSelectorAction<TResult>(
|
|
715
|
+
spec: SelectorSpec,
|
|
716
|
+
action: string,
|
|
717
|
+
args: Record<string, unknown>,
|
|
718
|
+
): Promise<TResult> {
|
|
719
|
+
const script = `(() => {
|
|
720
|
+
const spec = ${JSON.stringify(spec)};
|
|
721
|
+
const action = ${JSON.stringify(action)};
|
|
722
|
+
const args = ${JSON.stringify(args)};
|
|
723
|
+
${PAGE_SELECTOR_HELPERS}
|
|
724
|
+
const element = findElement(spec);
|
|
725
|
+
if (!element) throw new Error("No element matched " + spec.raw);
|
|
726
|
+
if (action !== "exists") element.scrollIntoView({ block: "center", inline: "center" });
|
|
727
|
+
switch (action) {
|
|
728
|
+
case "click":
|
|
729
|
+
mouseEvent(element, "mousedown");
|
|
730
|
+
mouseEvent(element, "mouseup");
|
|
731
|
+
if (typeof element.click === "function") element.click();
|
|
732
|
+
else mouseEvent(element, "click");
|
|
733
|
+
return true;
|
|
734
|
+
case "dblclick":
|
|
735
|
+
mouseEvent(element, "dblclick");
|
|
736
|
+
return true;
|
|
737
|
+
case "hover":
|
|
738
|
+
mouseEvent(element, "mouseover");
|
|
739
|
+
mouseEvent(element, "mouseenter");
|
|
740
|
+
mouseEvent(element, "mousemove");
|
|
741
|
+
return true;
|
|
742
|
+
case "focus":
|
|
743
|
+
if (typeof element.focus === "function") element.focus();
|
|
744
|
+
return true;
|
|
745
|
+
case "check":
|
|
746
|
+
element.checked = true;
|
|
747
|
+
inputEvent(element);
|
|
748
|
+
return true;
|
|
749
|
+
case "uncheck":
|
|
750
|
+
element.checked = false;
|
|
751
|
+
inputEvent(element);
|
|
752
|
+
return true;
|
|
753
|
+
case "type":
|
|
754
|
+
if (typeof element.focus === "function") element.focus();
|
|
755
|
+
setValue(element, String(args.text || ""), true);
|
|
756
|
+
return true;
|
|
757
|
+
case "fill":
|
|
758
|
+
if (typeof element.focus === "function") element.focus();
|
|
759
|
+
setValue(element, String(args.value || ""), false);
|
|
760
|
+
return true;
|
|
761
|
+
case "scrollIntoView":
|
|
762
|
+
return true;
|
|
763
|
+
case "select": {
|
|
764
|
+
const values = Array.isArray(args.values) ? args.values.map(String) : [String(args.value || "")];
|
|
765
|
+
if (element.tagName !== "SELECT") throw new Error("tab.select() requires a <select> element");
|
|
766
|
+
const wanted = new Set(values);
|
|
767
|
+
const selected = [];
|
|
768
|
+
for (const option of Array.from(element.options)) {
|
|
769
|
+
option.selected = wanted.has(option.value);
|
|
770
|
+
if (option.selected) selected.push(option.value);
|
|
771
|
+
}
|
|
772
|
+
inputEvent(element);
|
|
773
|
+
return selected;
|
|
774
|
+
}
|
|
775
|
+
case "uploadFile": {
|
|
776
|
+
if (element.tagName !== "INPUT" || element.type !== "file") {
|
|
777
|
+
throw new Error("tab.uploadFile() requires an <input type=file> element");
|
|
778
|
+
}
|
|
779
|
+
const transfer = new DataTransfer();
|
|
780
|
+
for (const file of args.files || []) {
|
|
781
|
+
const bytes = Uint8Array.from(atob(file.data), char => char.charCodeAt(0));
|
|
782
|
+
transfer.items.add(new File([bytes], file.name, { type: file.type || "application/octet-stream" }));
|
|
783
|
+
}
|
|
784
|
+
element.files = transfer.files;
|
|
785
|
+
inputEvent(element);
|
|
786
|
+
return true;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
throw new Error("Unsupported selector action " + action);
|
|
790
|
+
})()`;
|
|
791
|
+
const result = (await this.#request("browser.eval", { script }, this.#runContext?.timeoutMs)) as CmuxEvalResult;
|
|
792
|
+
return result.value as TResult;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
async #waitForSelector(selector: string, timeoutMs: number): Promise<void> {
|
|
796
|
+
const spec = this.#selectorSpec(selector);
|
|
797
|
+
const nativeSelector = this.#nativeSelector(spec);
|
|
798
|
+
if (nativeSelector) {
|
|
799
|
+
await this.#request("browser.wait", { selector: nativeSelector, timeout_ms: timeoutMs }, timeoutMs);
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
const deadline = Date.now() + timeoutMs;
|
|
803
|
+
while (Date.now() <= deadline) {
|
|
804
|
+
if (await this.#selectorExists(spec)) return;
|
|
805
|
+
await Bun.sleep(100);
|
|
806
|
+
}
|
|
807
|
+
throw new ToolError(`tab.waitFor(${JSON.stringify(selector)}) timed out after ${timeoutMs}ms`);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
async #selectorExists(spec: SelectorSpec): Promise<boolean> {
|
|
811
|
+
if (spec.kind === "ref") return this.#elementRefs.has(Number(spec.value));
|
|
812
|
+
const script = `(() => {
|
|
813
|
+
const spec = ${JSON.stringify(spec)};
|
|
814
|
+
${PAGE_SELECTOR_HELPERS}
|
|
815
|
+
return !!findElement(spec);
|
|
816
|
+
})()`;
|
|
817
|
+
return !!(await this.#evalScript<unknown>(script));
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
async #selectorBox(spec: SelectorSpec): Promise<BoundingBox | null> {
|
|
821
|
+
if (spec.kind === "ref") return null;
|
|
822
|
+
const script = `(() => {
|
|
823
|
+
const spec = ${JSON.stringify(spec)};
|
|
824
|
+
${PAGE_SELECTOR_HELPERS}
|
|
825
|
+
const element = findElement(spec);
|
|
826
|
+
if (!element) return null;
|
|
827
|
+
const rect = element.getBoundingClientRect();
|
|
828
|
+
if (rect.width <= 0 || rect.height <= 0) return null;
|
|
829
|
+
return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
|
|
830
|
+
})()`;
|
|
831
|
+
const value = await this.#evalScript<unknown>(script);
|
|
832
|
+
if (!value || typeof value !== "object") return null;
|
|
833
|
+
const object = value as Record<string, unknown>;
|
|
834
|
+
return {
|
|
835
|
+
x: numberFrom(object.x, 0),
|
|
836
|
+
y: numberFrom(object.y, 0),
|
|
837
|
+
width: numberFrom(object.width, 0),
|
|
838
|
+
height: numberFrom(object.height, 0),
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
async #dragPoint(target: DragTarget): Promise<{ x: number; y: number }> {
|
|
843
|
+
if (typeof target === "string") {
|
|
844
|
+
const box = await this.#selectorBox(this.#selectorSpec(target));
|
|
845
|
+
if (!box) throw new ToolError(`Drag selector did not resolve to a visible element: ${target}`);
|
|
846
|
+
return { x: box.x + box.width / 2, y: box.y + box.height / 2 };
|
|
847
|
+
}
|
|
848
|
+
if (Number.isFinite(target.x) && Number.isFinite(target.y)) {
|
|
849
|
+
return { x: target.x, y: target.y };
|
|
850
|
+
}
|
|
851
|
+
throw new ToolError("Drag target must be a selector string or { x: number, y: number } point");
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
async #installResponseObserver(): Promise<void> {
|
|
855
|
+
await this.#evalScript<boolean>(RESPONSE_OBSERVER_SCRIPT);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
async #responseCursor(): Promise<number> {
|
|
859
|
+
const value = await this.#evalScript<unknown>(
|
|
860
|
+
"(() => Math.max(0, ((globalThis.__ompCmuxResponses && globalThis.__ompCmuxResponses.nextId) || 1) - 1))()",
|
|
861
|
+
);
|
|
862
|
+
return numberFrom(value, 0);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
async #responseRecordsAfter(id: number): Promise<CmuxResponseRecord[]> {
|
|
866
|
+
const value = await this.#evalScript<unknown>(
|
|
867
|
+
`(() => ((globalThis.__ompCmuxResponses && globalThis.__ompCmuxResponses.records) || []).filter(record => record.id > ${JSON.stringify(id)}))()`,
|
|
868
|
+
);
|
|
869
|
+
if (!Array.isArray(value)) return [];
|
|
870
|
+
const records: CmuxResponseRecord[] = [];
|
|
871
|
+
for (const item of value) {
|
|
872
|
+
if (!item || typeof item !== "object") continue;
|
|
873
|
+
const object = item as Record<string, unknown>;
|
|
874
|
+
const headers = object.headers && typeof object.headers === "object" ? object.headers : {};
|
|
875
|
+
records.push({
|
|
876
|
+
id: numberFrom(object.id, 0),
|
|
877
|
+
url: typeof object.url === "string" ? object.url : "",
|
|
878
|
+
status: numberFrom(object.status, 0),
|
|
879
|
+
statusText: typeof object.statusText === "string" ? object.statusText : "",
|
|
880
|
+
headers: Object.fromEntries(
|
|
881
|
+
Object.entries(headers as Record<string, unknown>).map(([key, value]) => [key, String(value)]),
|
|
882
|
+
),
|
|
883
|
+
body: typeof object.body === "string" ? object.body : "",
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
return records;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
#selectorSpec(selector: string): SelectorSpec {
|
|
890
|
+
const raw = selector;
|
|
891
|
+
let normalized = selector;
|
|
892
|
+
if (normalized.startsWith("p-text/")) normalized = `text/${normalized.slice("p-text/".length)}`;
|
|
893
|
+
else if (normalized.startsWith("p-aria/")) normalized = `aria/${normalized.slice("p-aria/".length)}`;
|
|
894
|
+
else if (normalized.startsWith("p-xpath/")) normalized = `xpath/${normalized.slice("p-xpath/".length)}`;
|
|
895
|
+
else if (normalized.startsWith("p-pierce/")) normalized = `pierce/${normalized.slice("p-pierce/".length)}`;
|
|
896
|
+
const ref = /^@?e(\d+)$/.exec(normalized);
|
|
897
|
+
if (ref) return { kind: "ref", value: ref[1]!, raw, ref: `@e${ref[1]}` };
|
|
898
|
+
const slash = normalized.indexOf("/");
|
|
899
|
+
if (slash > 0) {
|
|
900
|
+
const prefix = normalized.slice(0, slash);
|
|
901
|
+
const value = normalized.slice(slash + 1);
|
|
902
|
+
if (prefix === "text" || prefix === "aria" || prefix === "xpath" || prefix === "pierce") {
|
|
903
|
+
return { kind: prefix, value, raw, name: prefix === "aria" ? value : undefined };
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
return { kind: "css", value: normalized, raw };
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
#nativeSelector(spec: SelectorSpec): string | undefined {
|
|
910
|
+
if (spec.kind === "css") return spec.value;
|
|
911
|
+
if (spec.kind === "ref") return spec.ref;
|
|
912
|
+
return undefined;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
#rememberObservedElements(observation: Observation): void {
|
|
916
|
+
this.#elementRefs.clear();
|
|
917
|
+
for (const element of observation.elements) {
|
|
918
|
+
this.#elementRefs.set(element.id, {
|
|
919
|
+
ref: `@e${element.id}`,
|
|
920
|
+
name: element.name,
|
|
921
|
+
role: element.role,
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
#normalizeGeometry(value: unknown): CmuxGeometry {
|
|
927
|
+
const object = value && typeof value === "object" ? (value as Record<string, unknown>) : {};
|
|
928
|
+
return {
|
|
929
|
+
innerWidth: numberFrom(object.innerWidth, DEFAULT_VIEWPORT.width),
|
|
930
|
+
innerHeight: numberFrom(object.innerHeight, DEFAULT_VIEWPORT.height),
|
|
931
|
+
dpr: numberFrom(object.dpr, DEFAULT_VIEWPORT.deviceScaleFactor ?? 1),
|
|
932
|
+
scrollX: numberFrom(object.scrollX, 0),
|
|
933
|
+
scrollY: numberFrom(object.scrollY, 0),
|
|
934
|
+
scrollWidth: numberFrom(object.scrollWidth, DEFAULT_VIEWPORT.width),
|
|
935
|
+
scrollHeight: numberFrom(object.scrollHeight, DEFAULT_VIEWPORT.height),
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
#requireRunContext(operation: string): RunContext {
|
|
940
|
+
if (!this.#runContext) {
|
|
941
|
+
throw new ToolError(`${operation} requires an active cmux browser run`);
|
|
942
|
+
}
|
|
943
|
+
return this.#runContext;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
class CmuxResponse {
|
|
948
|
+
readonly #record: CmuxResponseRecord;
|
|
949
|
+
|
|
950
|
+
constructor(record: CmuxResponseRecord) {
|
|
951
|
+
this.#record = record;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
url(): string {
|
|
955
|
+
return this.#record.url;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
status(): number {
|
|
959
|
+
return this.#record.status;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
statusText(): string {
|
|
963
|
+
return this.#record.statusText;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
headers(): Record<string, string> {
|
|
967
|
+
return { ...this.#record.headers };
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
async text(): Promise<string> {
|
|
971
|
+
return this.#record.body;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
async json(): Promise<unknown> {
|
|
975
|
+
return JSON.parse(this.#record.body);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
class CmuxElementHandle {
|
|
980
|
+
readonly #tab: CmuxTab;
|
|
981
|
+
readonly #selector: string;
|
|
982
|
+
|
|
983
|
+
constructor(tab: CmuxTab, selector: string) {
|
|
984
|
+
this.#tab = tab;
|
|
985
|
+
this.#selector = selector;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
async click(): Promise<void> {
|
|
989
|
+
await this.#tab.click(this.#selector);
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
async type(text: string): Promise<void> {
|
|
993
|
+
await this.#tab.type(this.#selector, text);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
async fill(value: string): Promise<void> {
|
|
997
|
+
await this.#tab.fill(this.#selector, value);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
async focus(): Promise<void> {
|
|
1001
|
+
await this.#tab.focus(this.#selector);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
async hover(): Promise<void> {
|
|
1005
|
+
await this.#tab.hover(this.#selector);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
async evaluate<TResult, TArgs extends unknown[]>(
|
|
1009
|
+
fn: (element: unknown, ...args: TArgs) => TResult | Promise<TResult>,
|
|
1010
|
+
...args: TArgs
|
|
1011
|
+
): Promise<TResult> {
|
|
1012
|
+
return await this.#tab.evaluateOnSelector<TResult>(this.#selector, fn.toString(), args);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
async boundingBox(): Promise<BoundingBox | null> {
|
|
1016
|
+
return await this.#tab.elementBox(this.#selector);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
async uploadFile(...paths: string[]): Promise<void> {
|
|
1020
|
+
await this.#tab.uploadFile(this.#selector, ...paths);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
async dispose(): Promise<void> {}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
class CmuxLocator {
|
|
1027
|
+
readonly #tab: CmuxTab;
|
|
1028
|
+
readonly #selector: string;
|
|
1029
|
+
#timeoutMs: number | undefined;
|
|
1030
|
+
|
|
1031
|
+
constructor(tab: CmuxTab, selector: string) {
|
|
1032
|
+
this.#tab = tab;
|
|
1033
|
+
this.#selector = selector;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
setTimeout(timeoutMs: number): this {
|
|
1037
|
+
this.#timeoutMs = timeoutMs;
|
|
1038
|
+
return this;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
async click(): Promise<void> {
|
|
1042
|
+
await this.#tab.waitFor(this.#selector, { timeout: this.#timeoutMs });
|
|
1043
|
+
await this.#tab.click(this.#selector);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
async fill(value: string): Promise<void> {
|
|
1047
|
+
await this.#tab.waitFor(this.#selector, { timeout: this.#timeoutMs });
|
|
1048
|
+
await this.#tab.fill(this.#selector, value);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
async waitHandle(): Promise<CmuxElementHandle> {
|
|
1052
|
+
return await this.#tab.waitFor(this.#selector, { timeout: this.#timeoutMs });
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
class CmuxPageFacade {
|
|
1057
|
+
readonly #tab: CmuxTab;
|
|
1058
|
+
readonly keyboard: { press: (key: string) => Promise<void> };
|
|
1059
|
+
readonly mouse: {
|
|
1060
|
+
wheel: (delta: { deltaX?: number; deltaY?: number }) => Promise<void>;
|
|
1061
|
+
move: (x: number, y: number) => Promise<void>;
|
|
1062
|
+
down: () => Promise<void>;
|
|
1063
|
+
up: () => Promise<void>;
|
|
1064
|
+
};
|
|
1065
|
+
|
|
1066
|
+
constructor(tab: CmuxTab) {
|
|
1067
|
+
this.#tab = tab;
|
|
1068
|
+
this.keyboard = { press: key => this.#tab.press(key) };
|
|
1069
|
+
let lastPoint = { x: 0, y: 0 };
|
|
1070
|
+
let dragStart: { x: number; y: number } | undefined;
|
|
1071
|
+
this.mouse = {
|
|
1072
|
+
wheel: delta => this.#tab.scroll(delta.deltaX ?? 0, delta.deltaY ?? 0),
|
|
1073
|
+
move: (x, y) => {
|
|
1074
|
+
lastPoint = { x, y };
|
|
1075
|
+
return Promise.resolve();
|
|
1076
|
+
},
|
|
1077
|
+
down: () => {
|
|
1078
|
+
dragStart = lastPoint;
|
|
1079
|
+
return Promise.resolve();
|
|
1080
|
+
},
|
|
1081
|
+
up: async () => {
|
|
1082
|
+
if (dragStart) await this.#tab.drag(dragStart, lastPoint);
|
|
1083
|
+
dragStart = undefined;
|
|
1084
|
+
},
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
url(): string {
|
|
1089
|
+
return this.#tab.url();
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
async title(): Promise<string> {
|
|
1093
|
+
return await this.#tab.title();
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
viewport(): ReadyInfo["viewport"] {
|
|
1097
|
+
return this.#tab.viewport();
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
async setViewport(viewport: ViewportOptions): Promise<void> {
|
|
1101
|
+
await this.#tab.setViewport(viewport);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
async goto(url: string, opts?: { waitUntil?: WaitUntil; timeout?: number }): Promise<{ url: string }> {
|
|
1105
|
+
await this.#tab.goto(url, { waitUntil: opts?.waitUntil, timeoutMs: opts?.timeout });
|
|
1106
|
+
return { url: this.#tab.url() };
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
async evaluate<TResult, TArgs extends unknown[]>(
|
|
1110
|
+
fn: string | ((...args: TArgs) => TResult | Promise<TResult>),
|
|
1111
|
+
...args: TArgs
|
|
1112
|
+
): Promise<TResult> {
|
|
1113
|
+
return await this.#tab.evaluate(fn, ...args);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
async content(): Promise<string> {
|
|
1117
|
+
return await this.#tab.pageContent();
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
locator(selector: string): CmuxLocator {
|
|
1121
|
+
return new CmuxLocator(this.#tab, selector);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
async $(selector: string): Promise<CmuxElementHandle | null> {
|
|
1125
|
+
return (await this.#tab.elementExists(selector)) ? this.#tab.elementHandle(selector) : null;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
async waitForSelector(selector: string, opts?: { timeout?: number }): Promise<CmuxElementHandle> {
|
|
1129
|
+
return await this.#tab.waitFor(selector, opts);
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
async waitForFunction(
|
|
1133
|
+
fn: string | ((...args: unknown[]) => unknown | Promise<unknown>),
|
|
1134
|
+
opts?: { timeout?: number; polling?: number },
|
|
1135
|
+
...args: unknown[]
|
|
1136
|
+
): Promise<unknown> {
|
|
1137
|
+
return await this.#tab.waitForFunction(fn, opts, ...args);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
async waitForResponse(
|
|
1141
|
+
pattern: string | RegExp | ((response: CmuxResponse) => boolean | Promise<boolean>),
|
|
1142
|
+
opts?: { timeout?: number },
|
|
1143
|
+
): Promise<CmuxResponse> {
|
|
1144
|
+
return await this.#tab.waitForResponse(pattern, opts);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
async screenshot(opts: ScreenshotOptions = {}): Promise<Buffer | string> {
|
|
1148
|
+
return await this.#tab.pageScreenshot(opts);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
class CmuxBrowserFacade {
|
|
1153
|
+
readonly #tab: CmuxTab;
|
|
1154
|
+
connected = true;
|
|
1155
|
+
|
|
1156
|
+
constructor(tab: CmuxTab) {
|
|
1157
|
+
this.#tab = tab;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
async pages(): Promise<CmuxPageFacade[]> {
|
|
1161
|
+
return [this.#tab.page];
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
async version(): Promise<string> {
|
|
1165
|
+
return "cmux";
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
wsEndpoint(): string {
|
|
1169
|
+
return `cmux://${this.#tab.surfaceId}`;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
disconnect(): void {
|
|
1173
|
+
this.connected = false;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
async close(): Promise<void> {
|
|
1177
|
+
this.connected = false;
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
export async function runCmuxCode(tab: CmuxTab, opts: RunCmuxCodeOptions): Promise<RunResultOk> {
|
|
1182
|
+
const timeoutSignal = AbortSignal.timeout(opts.timeoutMs);
|
|
1183
|
+
const signal = opts.signal ? AbortSignal.any([timeoutSignal, opts.signal]) : timeoutSignal;
|
|
1184
|
+
const displays: RunResultOk["displays"] = [];
|
|
1185
|
+
const screenshots: ScreenshotResult[] = [];
|
|
1186
|
+
const runId = crypto.randomUUID();
|
|
1187
|
+
tab.setRunContext({ session: opts.snapshot, displays, screenshots, signal, timeoutMs: opts.timeoutMs });
|
|
1188
|
+
const runtime = tab.ensureRuntime(opts.snapshot);
|
|
1189
|
+
runtime.setCwd(opts.snapshot.cwd);
|
|
1190
|
+
runtime.setRunScope({
|
|
1191
|
+
page: tab.page,
|
|
1192
|
+
browser: tab.browser,
|
|
1193
|
+
tab,
|
|
1194
|
+
assert: (cond: unknown, text?: string): void => {
|
|
1195
|
+
if (!cond) throw new ToolError(text ?? "Assertion failed");
|
|
1196
|
+
},
|
|
1197
|
+
wait: (ms: number): Promise<void> => Bun.sleep(ms),
|
|
1198
|
+
});
|
|
1199
|
+
|
|
1200
|
+
const { promise: cancelRejection, reject } = Promise.withResolvers<never>();
|
|
1201
|
+
const onAbort = (): void => {
|
|
1202
|
+
if (timeoutSignal.aborted) {
|
|
1203
|
+
reject(new ToolError(`Browser code execution timed out after ${opts.timeoutMs}ms`));
|
|
1204
|
+
} else {
|
|
1205
|
+
reject(new ToolAbortError());
|
|
1206
|
+
}
|
|
1207
|
+
};
|
|
1208
|
+
if (signal.aborted) onAbort();
|
|
1209
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
1210
|
+
|
|
1211
|
+
try {
|
|
1212
|
+
const hooks: RuntimeHooks = {
|
|
1213
|
+
onText: chunk => logger.debug(chunk.replace(/\n$/, "")),
|
|
1214
|
+
onDisplay: output => pushDisplay(displays, output),
|
|
1215
|
+
callTool: (name, args) => callSessionTool(name, args, { session: opts.session, signal }),
|
|
1216
|
+
};
|
|
1217
|
+
// Like the inline worker fallback, cmux runs user JS in-process: awaited cmux/tool calls
|
|
1218
|
+
// observe this abort signal, but a synchronous infinite loop cannot be interrupted here.
|
|
1219
|
+
const returnValue = await Promise.race([
|
|
1220
|
+
runtime.run(opts.code, `cmux-run-${runId}.js`, hooks, { runId, cwd: opts.snapshot.cwd }),
|
|
1221
|
+
cancelRejection,
|
|
1222
|
+
]);
|
|
1223
|
+
return { displays, returnValue: cloneSafe(returnValue), screenshots };
|
|
1224
|
+
} finally {
|
|
1225
|
+
signal.removeEventListener("abort", onAbort);
|
|
1226
|
+
tab.clearRunContext();
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
function pushDisplay(displays: RunResultOk["displays"], output: JsDisplayOutput): void {
|
|
1231
|
+
if (output.type === "image") {
|
|
1232
|
+
displays.push({ type: "image", data: output.data, mimeType: output.mimeType });
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
if (output.type === "json") {
|
|
1236
|
+
displays.push({ type: "text", text: safeJsonStringify(output.data) });
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
displays.push({ type: "text", text: safeJsonStringify(output.event) });
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
function safeJsonStringify(value: unknown): string {
|
|
1243
|
+
try {
|
|
1244
|
+
return JSON.stringify(value, null, 2);
|
|
1245
|
+
} catch {
|
|
1246
|
+
return String(value);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
function cloneSafe(value: unknown): unknown {
|
|
1251
|
+
if (value === undefined) return undefined;
|
|
1252
|
+
try {
|
|
1253
|
+
structuredClone(value);
|
|
1254
|
+
return value;
|
|
1255
|
+
} catch {}
|
|
1256
|
+
try {
|
|
1257
|
+
return JSON.parse(JSON.stringify(value)) as unknown;
|
|
1258
|
+
} catch {}
|
|
1259
|
+
return String(value);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
function numberFrom(value: unknown, fallback: number): number {
|
|
1263
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
1264
|
+
}
|