headlesscode 1.0.2
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/ATTRIBUTION.md +53 -0
- package/CODE_OF_CONDUCT.md +130 -0
- package/CONTRIBUTING.md +107 -0
- package/LICENSE +202 -0
- package/README.md +486 -0
- package/SECURITY.md +211 -0
- package/bin/headlesscode.mjs +83 -0
- package/package.json +63 -0
- package/shared/prompts/review-mode-prompt-short.md +93 -0
- package/shared/prompts/review-mode-prompt.md +281 -0
- package/shared/rules-code/rules.md +22 -0
- package/shared/stacks/cpp/rules.md +30 -0
- package/shared/stacks/fastapi/rules.md +30 -0
- package/shared/stacks/javascript/rules.md +37 -0
- package/shared/stacks/postgresql/rules.md +31 -0
- package/shared/stacks/python/rules.md +35 -0
- package/shared/stacks/react/rules.md +11 -0
- package/shared/stacks/typescript/rules.md +10 -0
- package/src/budget/budget.ts +221 -0
- package/src/budget/concurrency.ts +126 -0
- package/src/budget/cost.ts +309 -0
- package/src/budget/index.ts +8 -0
- package/src/checkpoints/cli.ts +256 -0
- package/src/checkpoints/service.ts +227 -0
- package/src/cli.ts +1535 -0
- package/src/cloud/docker-provider.ts +334 -0
- package/src/cloud/provider.ts +300 -0
- package/src/codeintel/call-graph.ts +78 -0
- package/src/codeintel/find-references.ts +123 -0
- package/src/codeintel/go-to-definition.ts +193 -0
- package/src/codeintel/handlers.ts +190 -0
- package/src/codeintel/import-graph.ts +173 -0
- package/src/codeintel/outline.ts +180 -0
- package/src/codeintel/position.ts +77 -0
- package/src/codeintel/program.ts +350 -0
- package/src/codeintel/rename-symbol.ts +213 -0
- package/src/codeintel/tools.ts +280 -0
- package/src/codemap/build.ts +135 -0
- package/src/codemap/cli.ts +190 -0
- package/src/codemap/extract.ts +339 -0
- package/src/codemap/files.ts +236 -0
- package/src/codemap/fingerprint.ts +65 -0
- package/src/codemap/flows.ts +62 -0
- package/src/codemap/html.ts +451 -0
- package/src/codemap/lock.ts +80 -0
- package/src/codemap/types.ts +101 -0
- package/src/codesearch/airunner-embedder.ts +185 -0
- package/src/codesearch/chunk.ts +339 -0
- package/src/codesearch/cli.ts +223 -0
- package/src/codesearch/embedder.ts +332 -0
- package/src/codesearch/files.ts +280 -0
- package/src/codesearch/index.ts +469 -0
- package/src/codesearch/ollama-embedder.ts +205 -0
- package/src/codesearch/search.ts +141 -0
- package/src/codesearch/types.ts +100 -0
- package/src/config/mode-models.ts +218 -0
- package/src/dashboard/aggregate.ts +364 -0
- package/src/dashboard/chat-thread.ts +141 -0
- package/src/dashboard/checkpoints.ts +124 -0
- package/src/dashboard/cli.ts +193 -0
- package/src/dashboard/codemap.ts +44 -0
- package/src/dashboard/files.ts +121 -0
- package/src/dashboard/page.ts +2803 -0
- package/src/dashboard/self-improvement-metrics.ts +282 -0
- package/src/dashboard/server.ts +1103 -0
- package/src/dashboard/session-launch.ts +310 -0
- package/src/dashboard/timeline.ts +273 -0
- package/src/dashboard/tool-exec.ts +107 -0
- package/src/dashboard/trend-cli.ts +141 -0
- package/src/dashboard/trend.ts +413 -0
- package/src/decision-proxy/cli.ts +261 -0
- package/src/decision-proxy/proxy.ts +569 -0
- package/src/deploy/gate-cli.ts +147 -0
- package/src/deploy/gate.ts +254 -0
- package/src/engine/condense.ts +512 -0
- package/src/engine/events.ts +428 -0
- package/src/engine/handoff.ts +71 -0
- package/src/engine/lazy-tools.ts +160 -0
- package/src/engine/local-explore.ts +653 -0
- package/src/engine/logger.ts +96 -0
- package/src/engine/loop.ts +5517 -0
- package/src/engine/parser.ts +347 -0
- package/src/engine/prompt.ts +860 -0
- package/src/engine/reports.ts +47 -0
- package/src/engine/stacks.ts +448 -0
- package/src/engine/types.ts +291 -0
- package/src/engine/usage.ts +186 -0
- package/src/github/app-auth.ts +161 -0
- package/src/github/cli.ts +448 -0
- package/src/github/installations.ts +133 -0
- package/src/github/pr.ts +321 -0
- package/src/github/provision.ts +118 -0
- package/src/github/push.ts +122 -0
- package/src/index-util.ts +50 -0
- package/src/index.ts +81 -0
- package/src/init/cli.ts +248 -0
- package/src/init/gitignore.ts +74 -0
- package/src/llm/ollama.ts +308 -0
- package/src/llm/openrouter.ts +868 -0
- package/src/llm/preflight.ts +367 -0
- package/src/llm/transcript-capture.ts +84 -0
- package/src/memory/embed.ts +110 -0
- package/src/memory/index.ts +22 -0
- package/src/memory/local.ts +259 -0
- package/src/memory/summarizer.ts +283 -0
- package/src/memory/types.ts +153 -0
- package/src/memory/uwuchat.ts +157 -0
- package/src/migrate/cli.ts +115 -0
- package/src/orchestrator/analyze-cli.ts +104 -0
- package/src/orchestrator/auto-split.ts +206 -0
- package/src/orchestrator/cleanup.ts +1003 -0
- package/src/orchestrator/cli.ts +3571 -0
- package/src/orchestrator/cost-estimate.ts +564 -0
- package/src/orchestrator/cost-history-cli.ts +242 -0
- package/src/orchestrator/cost-history.ts +397 -0
- package/src/orchestrator/git-sync.ts +250 -0
- package/src/orchestrator/index.ts +153 -0
- package/src/orchestrator/log-analysis.ts +0 -0
- package/src/orchestrator/merge-check.ts +108 -0
- package/src/orchestrator/pipeline.ts +411 -0
- package/src/orchestrator/resume.ts +1940 -0
- package/src/orchestrator/reviewer.ts +503 -0
- package/src/orchestrator/split.ts +296 -0
- package/src/orchestrator/state.ts +542 -0
- package/src/orchestrator/status.ts +697 -0
- package/src/orchestrator/verification-gate.ts +134 -0
- package/src/orchestrator/watch.ts +898 -0
- package/src/permissions/commands.ts +1083 -0
- package/src/permissions/config.ts +241 -0
- package/src/permissions/index.ts +12 -0
- package/src/permissions/protected-files.ts +96 -0
- package/src/permissions/store-protection.ts +272 -0
- package/src/project-store.ts +648 -0
- package/src/projects/cli.ts +382 -0
- package/src/qa/qa.ts +487 -0
- package/src/tools/browser/handler.ts +346 -0
- package/src/tools/browser/service.ts +406 -0
- package/src/tools/browser/smoke.ts +78 -0
- package/src/tools/browser/tool.ts +99 -0
- package/src/tools/executor.ts +2575 -0
- package/src/tools/language-detect.ts +183 -0
- package/src/tools/output-summarizer.ts +369 -0
- package/src/tools/run-tests.ts +302 -0
- package/src/tools/set-indentation-tool.ts +49 -0
- package/src/tools/test-selection.ts +160 -0
- package/src/vendor/tests/smoke.ts +103 -0
- package/src/vendor/zoo-code/VENDOR-NOTES.md +213 -0
- package/src/vendor/zoo-code/shim/anthropic.ts +71 -0
- package/src/vendor/zoo-code/shim/openai.d.ts +60 -0
- package/src/vendor/zoo-code/shim/os-name.ts +18 -0
- package/src/vendor/zoo-code/shim/strip-bom.ts +14 -0
- package/src/vendor/zoo-code/shim/vscode.ts +76 -0
- package/src/vendor/zoo-code/src/core/config/CustomModesManager.ts +1015 -0
- package/src/vendor/zoo-code/src/core/diff/strategies/multi-search-replace.ts +670 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/capabilities.ts +46 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/custom-instructions.ts +559 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/index.ts +10 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/markdown-formatting.ts +7 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/modes.ts +35 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/objective.ts +13 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/rules.ts +95 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/skills.ts +105 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/system-info.ts +30 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/tool-use-guidelines.ts +9 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/tool-use.ts +7 -0
- package/src/vendor/zoo-code/src/core/prompts/system.ts +176 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/access_mcp_resource.ts +41 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/apply_diff.ts +40 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/apply_patch.ts +61 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/ask_followup_question.ts +62 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/attempt_completion.ts +33 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/codebase_search.ts +43 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/converters.ts +109 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/edit.ts +48 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/edit_file.ts +72 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/execute_command.ts +54 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/generate_image.ts +51 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/index.ts +75 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/list_files.ts +41 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/mcp_server.ts +75 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/new_task.ts +39 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/read_command_output.ts +81 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/read_file.ts +169 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/run_slash_command.ts +31 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/search_files.ts +50 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/search_replace.ts +51 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/skill.ts +33 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/switch_mode.ts +31 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/update_todo_list.ts +54 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/write_to_file.ts +40 -0
- package/src/vendor/zoo-code/src/core/prompts/types.ts +12 -0
- package/src/vendor/zoo-code/src/i18n/index.ts +19 -0
- package/src/vendor/zoo-code/src/integrations/misc/extract-text.ts +81 -0
- package/src/vendor/zoo-code/src/services/checkpoints/RepoPerTaskCheckpointService.ts +15 -0
- package/src/vendor/zoo-code/src/services/checkpoints/ShadowCheckpointService.ts +553 -0
- package/src/vendor/zoo-code/src/services/checkpoints/excludes.ts +212 -0
- package/src/vendor/zoo-code/src/services/checkpoints/index.ts +3 -0
- package/src/vendor/zoo-code/src/services/checkpoints/types.ts +35 -0
- package/src/vendor/zoo-code/src/services/code-index/manager.ts +19 -0
- package/src/vendor/zoo-code/src/services/mcp/McpHub.ts +36 -0
- package/src/vendor/zoo-code/src/services/roo-config/index.ts +441 -0
- package/src/vendor/zoo-code/src/services/search/file-search.ts +143 -0
- package/src/vendor/zoo-code/src/services/skills/SkillsManager.ts +20 -0
- package/src/vendor/zoo-code/src/shared/globalFileNames.ts +9 -0
- package/src/vendor/zoo-code/src/shared/language.ts +43 -0
- package/src/vendor/zoo-code/src/shared/modes.ts +257 -0
- package/src/vendor/zoo-code/src/shared/tools.ts +385 -0
- package/src/vendor/zoo-code/src/utils/fs.ts +39 -0
- package/src/vendor/zoo-code/src/utils/globalContext.ts +22 -0
- package/src/vendor/zoo-code/src/utils/json-schema.ts +16 -0
- package/src/vendor/zoo-code/src/utils/logging.ts +21 -0
- package/src/vendor/zoo-code/src/utils/mcp-name.ts +190 -0
- package/src/vendor/zoo-code/src/utils/object.ts +18 -0
- package/src/vendor/zoo-code/src/utils/path.ts +94 -0
- package/src/vendor/zoo-code/src/utils/shell.ts +376 -0
- package/src/vendor/zoo-code/src/utils/text-normalization.ts +99 -0
- package/src/vendor/zoo-code/types/global-settings.ts +19 -0
- package/src/vendor/zoo-code/types/index.ts +22 -0
- package/src/vendor/zoo-code/types/message.ts +375 -0
- package/src/vendor/zoo-code/types/mode.ts +241 -0
- package/src/vendor/zoo-code/types/todo.ts +19 -0
- package/src/vendor/zoo-code/types/tool-params.ts +116 -0
- package/src/vendor/zoo-code/types/tool.ts +67 -0
- package/src/vendor/zoo-code/types/vscode.ts +84 -0
- package/src/vision/describe.ts +242 -0
- package/src/vision/tool.ts +91 -0
- package/src/watcher/cli.ts +369 -0
- package/src/watcher/github.ts +304 -0
- package/src/watcher/index.ts +59 -0
- package/src/watcher/state.ts +254 -0
- package/src/watcher/watch.ts +562 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playwright-backed browser session service for the `browser_action` native
|
|
3
|
+
* tool (see src/tools/browser/tool.ts for the tool-facing contract).
|
|
4
|
+
*
|
|
5
|
+
* This is NEW design work — a browser-automation tool for the coding agent to
|
|
6
|
+
* inspect a running dev server mid-session. It is NOT a port of any upstream
|
|
7
|
+
* Zoo Code feature (verified: no browser tool exists in the vendored core or
|
|
8
|
+
* the reference clone; `docs/gap-audit.md`'s "No browser tool" line is a real
|
|
9
|
+
* absence). The QA-side Playwright runner in a sibling project is a different,
|
|
10
|
+
* report-generation concern; only the Playwright dependency choice is shared.
|
|
11
|
+
*
|
|
12
|
+
* Design decisions, matching the harness's own idioms:
|
|
13
|
+
*
|
|
14
|
+
* - One BrowserSession lives for the whole session, created lazily on the
|
|
15
|
+
* first `launch` action. Playwright's Chromium launches ~1s; it is the one
|
|
16
|
+
* browser that is guaranteed present in this repo's dev environment (the
|
|
17
|
+
* sibling project's QA runner already uses it, and its browsers are
|
|
18
|
+
* cached), so it needs no per-machine install guesswork.
|
|
19
|
+
* - `getConsoleLogs`/`getNetworkErrors` are polled by the executor: the
|
|
20
|
+
* session keeps a rolling buffer of page console entries / failed requests
|
|
21
|
+
* (console and requestfailed are capture-time events; Playwright exposes no
|
|
22
|
+
* "past events" API), and each call drains the buffer so repeated calls
|
|
23
|
+
* return only NEW entries. Network-error collection deliberately does NOT
|
|
24
|
+
* use Playwright's request interception (route()), which would abort real
|
|
25
|
+
* requests and change page behavior — see collectNetworkErrors.
|
|
26
|
+
* - Each browser action runs under an overall wall-clock timeout
|
|
27
|
+
* (DEFAULT_BROWSER_ACTION_TIMEOUT_MS) that also covers the launch/browser
|
|
28
|
+
* teardown. A stuck browser action must surface a clear tool error, never
|
|
29
|
+
* hang the session.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { chromium, type Browser, type BrowserContext, type Page } from "playwright"
|
|
33
|
+
|
|
34
|
+
/** Cap on the number of console/network entries returned per call. */
|
|
35
|
+
export const MAX_BROWSER_EVENT_ENTRIES = 200
|
|
36
|
+
|
|
37
|
+
/** Overall per-action timeout (ms) — see the file header. */
|
|
38
|
+
export const DEFAULT_BROWSER_ACTION_TIMEOUT_MS = 20_000
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Timeout for a single click/type actionability wait (ms). Kept BELOW the
|
|
42
|
+
* overall action timeout so a missing selector surfaces Playwright's precise
|
|
43
|
+
* "waiting for locator" error instead of the generic action-timeout message.
|
|
44
|
+
*/
|
|
45
|
+
export const BROWSER_INTERACTION_TIMEOUT_MS = 15_000
|
|
46
|
+
|
|
47
|
+
/** Cap on screenshot bytes so tool results stay bounded (see tool.ts). */
|
|
48
|
+
export const MAX_SCREENSHOT_BYTES = 2 * 1024 * 1024
|
|
49
|
+
|
|
50
|
+
/** A single captured console entry (page console, incl. errors). */
|
|
51
|
+
export interface ConsoleEntry {
|
|
52
|
+
type: "log" | "info" | "warn" | "error" | "debug" | "trace" | string
|
|
53
|
+
text: string
|
|
54
|
+
ts: number
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** A single captured network failure (page requestfailed events). */
|
|
58
|
+
export interface NetworkErrorEntry {
|
|
59
|
+
url: string
|
|
60
|
+
failure: string
|
|
61
|
+
method: string
|
|
62
|
+
ts: number
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** A single captured page navigation (page.on("framenavigated")). */
|
|
66
|
+
export interface NavigationEntry {
|
|
67
|
+
url: string
|
|
68
|
+
ts: number
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Lifecycle + per-page instrumentation state. A fresh BrowserSession is a
|
|
73
|
+
* null browser — the browser process is spawned lazily on the first `launch`
|
|
74
|
+
* and must be torn down on session end even if the model never calls close.
|
|
75
|
+
*/
|
|
76
|
+
export class BrowserSession {
|
|
77
|
+
private browser: Browser | null = null
|
|
78
|
+
private context: BrowserContext | null = null
|
|
79
|
+
private page: Page | null = null
|
|
80
|
+
private consoleEntries: ConsoleEntry[] = []
|
|
81
|
+
private networkErrors: NetworkErrorEntry[] = []
|
|
82
|
+
private navigations: NavigationEntry[] = []
|
|
83
|
+
private pageClosed = false
|
|
84
|
+
private lastUrl: string | null = null
|
|
85
|
+
|
|
86
|
+
/** True once launch() created the browser; false before that / after close(). */
|
|
87
|
+
get isLaunched(): boolean {
|
|
88
|
+
return this.browser !== null
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The current page URL (null before launch or after the page was closed). */
|
|
92
|
+
get currentUrl(): string | null {
|
|
93
|
+
return this.lastUrl
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Total console entries captured since launch (drained per getConsoleLogs call). */
|
|
97
|
+
get consoleCount(): number {
|
|
98
|
+
return this.consoleEntries.length
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Total network errors captured since launch (drained per getNetworkErrors call). */
|
|
102
|
+
get networkErrorCount(): number {
|
|
103
|
+
return this.networkErrors.length
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Launch a headless browser and navigate to `url`. Multiple launch calls
|
|
108
|
+
* are allowed: a subsequent launch closes the previous browser (with all
|
|
109
|
+
* its state) and starts fresh — the model's explicit reset path.
|
|
110
|
+
*/
|
|
111
|
+
async launch(url: string): Promise<void> {
|
|
112
|
+
// Fresh browser: reset the closed flag (close()/dispose() set it, and a
|
|
113
|
+
// re-launch must be able to open a new page).
|
|
114
|
+
this.pageClosed = false
|
|
115
|
+
this.browser = await chromium.launch({ headless: true })
|
|
116
|
+
this.context = await this.browser.newContext()
|
|
117
|
+
this.page = await this.context.newPage()
|
|
118
|
+
this.attachListeners()
|
|
119
|
+
await this.goto(url)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Navigate to `url` and wait for the page to load (default 30s). */
|
|
123
|
+
async goto(url: string): Promise<void> {
|
|
124
|
+
if (this.page === null || this.pageClosed) {
|
|
125
|
+
throw new Error("browser_action: no open page — call launch(url) first")
|
|
126
|
+
}
|
|
127
|
+
this.lastUrl = url
|
|
128
|
+
try {
|
|
129
|
+
// waitUntil: "load" matches the QA runner's default and the vendored
|
|
130
|
+
// ChromeDevTools "navigate" semantics (page fully loaded). Network
|
|
131
|
+
// failures (DNS, refused) reject with an ERR_* error.
|
|
132
|
+
await this.page.goto(url, { waitUntil: "load", timeout: 30_000 })
|
|
133
|
+
} catch (error) {
|
|
134
|
+
// Capture-time navigation (see the "page navigated to X" result) is
|
|
135
|
+
// only pushed on success; a failed navigation must not mislead the
|
|
136
|
+
// model into thinking the page is showing. Let the error propagate —
|
|
137
|
+
// the executor wraps it in a clear tool error.
|
|
138
|
+
throw error
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Screenshot the current page as base64 PNG. The executor converts this to
|
|
144
|
+
* a PNG file written inside the workspace, because tool results here are
|
|
145
|
+
* text-only (ToolResult.content is a string — no image content part exists
|
|
146
|
+
* in this harness's ChatMessage plumbing; see the report).
|
|
147
|
+
*/
|
|
148
|
+
async screenshot(): Promise<string> {
|
|
149
|
+
if (this.page === null || this.pageClosed) {
|
|
150
|
+
throw new Error("browser_action: no open page — call launch(url) first")
|
|
151
|
+
}
|
|
152
|
+
const buffer = await this.page.screenshot({ type: "png", fullPage: true })
|
|
153
|
+
if (buffer.length > MAX_SCREENSHOT_BYTES) {
|
|
154
|
+
throw new Error(`browser_action: screenshot is ${buffer.length} bytes — above the ${MAX_SCREENSHOT_BYTES}-byte cap`)
|
|
155
|
+
}
|
|
156
|
+
return buffer.toString("base64")
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Click the first element matching `selector`. The candidate is queued for
|
|
161
|
+
* up to 30 seconds (the Playwright default actionability wait): a dev
|
|
162
|
+
* server may still be mounting when the model clicks. If no match appears,
|
|
163
|
+
* this rejects with a clear "selector did not resolve" error.
|
|
164
|
+
*/
|
|
165
|
+
async click(selector: string): Promise<void> {
|
|
166
|
+
await this.withPage(async (page) => {
|
|
167
|
+
const locator = page.locator(selector).first()
|
|
168
|
+
await locator.click({ timeout: BROWSER_INTERACTION_TIMEOUT_MS })
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Type `text` into the first element matching `selector` (must be focusable). */
|
|
173
|
+
async type(selector: string, text: string): Promise<void> {
|
|
174
|
+
await this.withPage(async (page) => {
|
|
175
|
+
const locator = page.locator(selector).first()
|
|
176
|
+
await locator.fill(text, { timeout: BROWSER_INTERACTION_TIMEOUT_MS })
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Drain captured console entries since the last call (capture-time events —
|
|
182
|
+
* there is no "past events" API in Playwright). Returns the NEW entries.
|
|
183
|
+
*/
|
|
184
|
+
async getConsoleLogs(): Promise<ConsoleEntry[]> {
|
|
185
|
+
const entries = this.consoleEntries
|
|
186
|
+
this.consoleEntries = []
|
|
187
|
+
return entries
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Drain captured network failures since the last call (see file header). */
|
|
191
|
+
async getNetworkErrors(): Promise<NetworkErrorEntry[]> {
|
|
192
|
+
const errors = this.networkErrors
|
|
193
|
+
this.networkErrors = []
|
|
194
|
+
return errors
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Drain captured navigations since the last call. Used to surface the
|
|
199
|
+
* landing URL / link navigation results to the model.
|
|
200
|
+
*/
|
|
201
|
+
async getNavigations(): Promise<NavigationEntry[]> {
|
|
202
|
+
const navigations = this.navigations
|
|
203
|
+
this.navigations = []
|
|
204
|
+
return navigations
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Current page state summary — what the model sees after every action. */
|
|
208
|
+
async describe(): Promise<string> {
|
|
209
|
+
if (this.page === null || this.pageClosed) {
|
|
210
|
+
return "browser: no open page"
|
|
211
|
+
}
|
|
212
|
+
const title = await this.page.title()
|
|
213
|
+
const url = this.page.url()
|
|
214
|
+
this.lastUrl = url
|
|
215
|
+
return `browser: ${title} — ${url}`
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Wait for the current page to be quiet (no pending network requests for
|
|
220
|
+
* 300ms). Used by navigation/screenshot/click/type to surface the settled
|
|
221
|
+
* URL after the action, and by the dev-server smoke test.
|
|
222
|
+
*/
|
|
223
|
+
async waitForIdle(timeoutMs = 5_000): Promise<void> {
|
|
224
|
+
if (this.page === null || this.pageClosed) {
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
const idleTimeout = 300
|
|
228
|
+
try {
|
|
229
|
+
await this.page.waitForLoadState("networkidle", { timeout: timeoutMs })
|
|
230
|
+
} catch {
|
|
231
|
+
// Timeout: treat as "still some polling traffic", NOT a failure —
|
|
232
|
+
// a live dev server may legitimately keep a websocket open.
|
|
233
|
+
}
|
|
234
|
+
// Extra settle beat so capture-time events (console, failures) land
|
|
235
|
+
// before the caller drains them.
|
|
236
|
+
await new Promise((r) => setTimeout(r, idleTimeout))
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Hard-close the browser (the model's explicit cleanup). Playwright's
|
|
241
|
+
* browser.close() kills the child processes; any residual process-group
|
|
242
|
+
* member is killed too (see killBrowserProcessGroup). Idempotent.
|
|
243
|
+
*
|
|
244
|
+
* browser.close() can itself hang if the browser process is wedged, so it
|
|
245
|
+
* runs under a short timeout — teardown must never block the session
|
|
246
|
+
* (killBrowserProcessGroup() is the guaranteed kill after it).
|
|
247
|
+
*/
|
|
248
|
+
async close(): Promise<void> {
|
|
249
|
+
const browser = this.browser
|
|
250
|
+
this.browser = null
|
|
251
|
+
this.context = null
|
|
252
|
+
this.page = null
|
|
253
|
+
this.pageClosed = true
|
|
254
|
+
this.consoleEntries = []
|
|
255
|
+
this.networkErrors = []
|
|
256
|
+
this.navigations = []
|
|
257
|
+
if (browser !== null) {
|
|
258
|
+
let timer: NodeJS.Timeout | undefined
|
|
259
|
+
const timeout = new Promise<never>((_, reject) => {
|
|
260
|
+
timer = setTimeout(() => reject(new Error("browser.close() timed out — forcing process kill")), 5_000)
|
|
261
|
+
})
|
|
262
|
+
try {
|
|
263
|
+
await Promise.race([browser.close(), timeout])
|
|
264
|
+
} catch {
|
|
265
|
+
// Browser already gone, or close hung — the process-group kill
|
|
266
|
+
// (dispose) is the backstop; close() alone is best-effort.
|
|
267
|
+
} finally {
|
|
268
|
+
clearTimeout(timer)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Session-end teardown: close the browser AND hard-kill the Chromium
|
|
275
|
+
* process group (the executable is spawned as a detached group leader —
|
|
276
|
+
* see killBrowserProcessGroup), so nothing survives the session. This is
|
|
277
|
+
* the mirror of ToolExecutor.dispose()'s background-command reaping.
|
|
278
|
+
*/
|
|
279
|
+
async dispose(): Promise<void> {
|
|
280
|
+
await this.close()
|
|
281
|
+
killBrowserProcessGroup()
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Run `fn` against the live page, rejecting cleanly when none is open. */
|
|
285
|
+
private async withPage<T>(fn: (page: Page) => Promise<T>): Promise<T> {
|
|
286
|
+
if (this.page === null || this.pageClosed) {
|
|
287
|
+
throw new Error("browser_action: no open page — call launch(url) first")
|
|
288
|
+
}
|
|
289
|
+
const page = this.page
|
|
290
|
+
try {
|
|
291
|
+
return await fn(page)
|
|
292
|
+
} catch (error) {
|
|
293
|
+
// Map a navigation-triggering click to a "page navigated" info
|
|
294
|
+
// instead of an error, then rethrow genuine action failures.
|
|
295
|
+
if (error instanceof Error && /navigation|navigated|target closed|net::/i.test(error.message) && this.browser !== null) {
|
|
296
|
+
throw new Error(`browser_action: the action caused a navigation or the page closed (${error.message})`)
|
|
297
|
+
}
|
|
298
|
+
throw error
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Wire capture-time listeners to the current page. */
|
|
303
|
+
private attachListeners(): void {
|
|
304
|
+
const page = this.page
|
|
305
|
+
if (page === null) {
|
|
306
|
+
return
|
|
307
|
+
}
|
|
308
|
+
page.on("console", (msg) => {
|
|
309
|
+
this.consoleEntries.push({ type: msg.type(), text: msg.text(), ts: Date.now() })
|
|
310
|
+
})
|
|
311
|
+
page.on("requestfailed", (request) => {
|
|
312
|
+
const failure = request.failure()
|
|
313
|
+
this.networkErrors.push({
|
|
314
|
+
url: request.url(),
|
|
315
|
+
failure: failure?.errorText ?? "unknown failure",
|
|
316
|
+
method: request.method(),
|
|
317
|
+
ts: Date.now(),
|
|
318
|
+
})
|
|
319
|
+
})
|
|
320
|
+
// HTTP >= 400 responses are failures for the model's purposes (a 404
|
|
321
|
+
// asset, a 500 API) even though the request itself "succeeded" on the
|
|
322
|
+
// wire, and Playwright only emits `requestfailed` for network-level
|
|
323
|
+
// failures. Track request/response so we can surface both.
|
|
324
|
+
page.on("response", (response) => {
|
|
325
|
+
const status = response.status()
|
|
326
|
+
if (status >= 400) {
|
|
327
|
+
this.networkErrors.push({
|
|
328
|
+
url: response.url(),
|
|
329
|
+
failure: `HTTP ${status}`,
|
|
330
|
+
method: response.request().method(),
|
|
331
|
+
ts: Date.now(),
|
|
332
|
+
})
|
|
333
|
+
}
|
|
334
|
+
})
|
|
335
|
+
page.on("framenavigated", (frame) => {
|
|
336
|
+
if (frame === page.mainFrame()) {
|
|
337
|
+
this.navigations.push({ url: frame.url(), ts: Date.now() })
|
|
338
|
+
}
|
|
339
|
+
})
|
|
340
|
+
page.on("close", () => {
|
|
341
|
+
this.pageClosed = true
|
|
342
|
+
})
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ─── Process-group kill for residual Chromium children ──────────────────────
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Chromium PIDs spawned by this module, tracked so session teardown can
|
|
350
|
+
* hard-kill the whole process group (negative pid) even if the browser
|
|
351
|
+
* process itself already exited. This mirrors the execute_command background
|
|
352
|
+
* child registry (see src/tools/executor.ts) — the one place this module
|
|
353
|
+
* deliberately reaches outside the BrowserSession abstraction, because the
|
|
354
|
+
* browser's own close() must never be the ONLY cleanup path.
|
|
355
|
+
*/
|
|
356
|
+
const browserProcessPids = new Set<number>()
|
|
357
|
+
|
|
358
|
+
/** Record a spawned Chromium process for group-kill cleanup (see above). */
|
|
359
|
+
export function trackBrowserProcess(pid: number): void {
|
|
360
|
+
browserProcessPids.add(pid)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Unregister a Chromium pid that exited on its own. */
|
|
364
|
+
export function untrackBrowserProcess(pid: number): void {
|
|
365
|
+
browserProcessPids.delete(pid)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* SIGKILL every tracked Chromium process group. Detached groups are killed
|
|
370
|
+
* with `process.kill(-pid)` so grandchildren (renderer/GPU/network processes)
|
|
371
|
+
* are reaped too, falling back to the direct pid where group signals are
|
|
372
|
+
* unsupported — exactly the dispose() pattern in src/tools/executor.ts.
|
|
373
|
+
*/
|
|
374
|
+
export function killBrowserProcessGroup(): void {
|
|
375
|
+
for (const pid of browserProcessPids) {
|
|
376
|
+
try {
|
|
377
|
+
process.kill(-pid, "SIGKILL")
|
|
378
|
+
} catch {
|
|
379
|
+
try {
|
|
380
|
+
process.kill(pid, "SIGKILL")
|
|
381
|
+
} catch {
|
|
382
|
+
// Already gone — nothing to clean up.
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
browserProcessPids.clear()
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ─── Module-level idempotent hook: register the process-group kill ───────────
|
|
390
|
+
|
|
391
|
+
let disposeHookInstalled = false
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Idempotently register `killBrowserProcessGroup` as a process exit hook so
|
|
395
|
+
* the browser never outlives the harness process, even when a session is torn
|
|
396
|
+
* down by a path that skips ToolExecutor.dispose() (e.g. process.exit during
|
|
397
|
+
* startup). The per-session dispose() path above is the primary cleanup; this
|
|
398
|
+
* is a belt-and-suspenders guarantee.
|
|
399
|
+
*/
|
|
400
|
+
export function installBrowserDisposeHook(): void {
|
|
401
|
+
if (disposeHookInstalled) {
|
|
402
|
+
return
|
|
403
|
+
}
|
|
404
|
+
disposeHookInstalled = true
|
|
405
|
+
process.on("exit", killBrowserProcessGroup)
|
|
406
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manual smoke test: drive the REAL browser_action tool against the REAL
|
|
3
|
+
* `headlesscode dashboard` dev server (http://127.0.0.1:4390) — the spec's
|
|
4
|
+
* recommended real target. Exercises launch / screenshot / click / type /
|
|
5
|
+
* getConsoleLogs / getNetworkErrors / close end-to-end, printing the tool
|
|
6
|
+
* results so they can be pasted into the report.
|
|
7
|
+
*
|
|
8
|
+
* Run: node ./node_modules/tsx/dist/cli.mjs src/tools/browser/smoke.ts
|
|
9
|
+
* Requires the dashboard to be running on 127.0.0.1:4390 first.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as os from "node:os"
|
|
13
|
+
import * as path from "node:path"
|
|
14
|
+
import * as fs from "node:fs/promises"
|
|
15
|
+
|
|
16
|
+
import { createHeadlessExecutor } from "../executor.js"
|
|
17
|
+
|
|
18
|
+
// Keep screenshots under a stable dir in the CURRENT workspace so the report
|
|
19
|
+
// can reference/attach them (the browser tool writes them under the executor's
|
|
20
|
+
// workspace root; for the smoke test that's a temp dir, so we copy them out).
|
|
21
|
+
const ws = await fs.mkdtemp(path.join(os.tmpdir(), "hc-browser-smoke-"))
|
|
22
|
+
const OUT_DIR = path.join(process.cwd(), ".smoke-artifacts")
|
|
23
|
+
await fs.mkdir(OUT_DIR, { recursive: true })
|
|
24
|
+
const executor = createHeadlessExecutor(ws)
|
|
25
|
+
const DASH = "http://127.0.0.1:4390/"
|
|
26
|
+
|
|
27
|
+
async function copyScreenshot(resultContent: string, name: string): Promise<void> {
|
|
28
|
+
const m = resultContent.match(/saved to [^(]+\(([^)]+\.png)\)/)
|
|
29
|
+
if (!m) return
|
|
30
|
+
await fs.copyFile(m[1], path.join(OUT_DIR, name))
|
|
31
|
+
console.log(` (screenshot copied to .smoke-artifacts/${name})`)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
console.log("=== 1. launch the real dashboard ===")
|
|
36
|
+
let r = await executor.execute("browser_action", { action: "launch", url: DASH })
|
|
37
|
+
console.log(r.content)
|
|
38
|
+
|
|
39
|
+
console.log("\n=== 2. screenshot the dashboard (PNG saved to workspace) ===")
|
|
40
|
+
r = await executor.execute("browser_action", { action: "screenshot" })
|
|
41
|
+
console.log(r.content)
|
|
42
|
+
await copyScreenshot(r.content, "dashboard-1.png")
|
|
43
|
+
|
|
44
|
+
console.log("\n=== 3. console logs (dashboard page) ===")
|
|
45
|
+
r = await executor.execute("browser_action", { action: "getConsoleLogs" })
|
|
46
|
+
console.log(r.content)
|
|
47
|
+
|
|
48
|
+
console.log("\n=== 4. network errors ===")
|
|
49
|
+
r = await executor.execute("browser_action", { action: "getNetworkErrors" })
|
|
50
|
+
console.log(r.content)
|
|
51
|
+
|
|
52
|
+
console.log("\n=== 5. type a task into the launch textarea (#launchTask) ===")
|
|
53
|
+
r = await executor.execute("browser_action", { action: "type", selector: "#launchTask", text: "verify browser_action smoke test" })
|
|
54
|
+
console.log(r.content)
|
|
55
|
+
|
|
56
|
+
console.log("\n=== 6. screenshot again (shows the typed text in the UI) ===")
|
|
57
|
+
r = await executor.execute("browser_action", { action: "screenshot" })
|
|
58
|
+
console.log(r.content)
|
|
59
|
+
await copyScreenshot(r.content, "dashboard-2-typed.png")
|
|
60
|
+
|
|
61
|
+
console.log("\n=== 6b. click the ▶ Start button (empty task -> validation error in #launchMsg) ===")
|
|
62
|
+
r = await executor.execute("browser_action", { action: "click", selector: "#btnStart" })
|
|
63
|
+
console.log(r.content)
|
|
64
|
+
await new Promise((res) => setTimeout(res, 500))
|
|
65
|
+
console.log("\n=== 6c. console logs after click (page JS response) ===")
|
|
66
|
+
r = await executor.execute("browser_action", { action: "getConsoleLogs" })
|
|
67
|
+
console.log(r.content)
|
|
68
|
+
|
|
69
|
+
console.log("\n=== 7. close the browser ===")
|
|
70
|
+
r = await executor.execute("browser_action", { action: "close" })
|
|
71
|
+
console.log(r.content)
|
|
72
|
+
|
|
73
|
+
console.log("\n=== 8. session teardown (dispose) ===")
|
|
74
|
+
await executor.dispose()
|
|
75
|
+
console.log("dispose() completed — browser torn down at session end")
|
|
76
|
+
} finally {
|
|
77
|
+
await fs.rm(ws, { recursive: true, force: true })
|
|
78
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `browser_action` — a real browser-automation native tool for the coding
|
|
3
|
+
* agent, backed by Playwright (headless Chromium).
|
|
4
|
+
*
|
|
5
|
+
* NEW design work, NOT a port: no browser tool exists in the upstream Zoo
|
|
6
|
+
* Code sources this project vendors (re-verified: `grep -rli browser` in
|
|
7
|
+
* `zoo-code/src/core/tools/` and `zoo-code/src/core/prompts/tools/
|
|
8
|
+
* native-tools/` finds nothing). The only browser precedent in this
|
|
9
|
+
* project's orbit is QA-side (a sibling project's `qa/runner.js` +
|
|
10
|
+
* `browserInstrumentation.js`),
|
|
11
|
+
* which generates structured QA reports — a different concern from this
|
|
12
|
+
* interactive mid-session inspection tool. The Playwright dependency is the
|
|
13
|
+
* one thing shared, deliberately.
|
|
14
|
+
*
|
|
15
|
+
* Scope (deliberately minimal, no full browser DSL): launch / screenshot /
|
|
16
|
+
* click / type / getConsoleLogs / getNetworkErrors / close. No arbitrary
|
|
17
|
+
* Playwright API passthrough.
|
|
18
|
+
*
|
|
19
|
+
* Image handling: ToolResult (src/engine/types.ts) carries only a string
|
|
20
|
+
* `content` — there is NO image-content part in this harness's
|
|
21
|
+
* ChatMessage/tool-result plumbing, and the pinned primary model
|
|
22
|
+
* (deepseek/deepseek-v4-flash) cannot process images at all. Screenshots are
|
|
23
|
+
* therefore described by the cloud vision captioner (src/vision/describe.ts):
|
|
24
|
+
* the PNG is saved inside the workspace, sent to an OpenRouter vision model,
|
|
25
|
+
* and the resulting TEXT description is embedded directly in the tool result
|
|
26
|
+
* (cost flows through the session's BudgetTracker via onAuxLlmUsage). The raw
|
|
27
|
+
* image never reaches the main model — but the file path is kept in the
|
|
28
|
+
* result too, for callers that want the PNG itself.
|
|
29
|
+
*
|
|
30
|
+
* The schema's `action` union is shared with the handler in
|
|
31
|
+
* src/tools/browser/handler.ts, which owns the actual session lifecycle and
|
|
32
|
+
* timeout enforcement.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import type OpenAI from "openai"
|
|
36
|
+
|
|
37
|
+
export const BROWSER_ACTION_NAME = "browser_action"
|
|
38
|
+
|
|
39
|
+
const BROWSER_ACTION_DESCRIPTION = `Request to control a headless Chromium browser (via Playwright) to inspect a running web app / dev server and verify UI changes visually. The browser starts on the first launch call and stays alive for the rest of the session; it is torn down automatically when the session ends, so you do not need to call close() for cleanup (but may, to free memory).
|
|
40
|
+
|
|
41
|
+
This tool is for VERIFICATION — a browser is stateful: check the page with screenshot, then interact with click/type, then screenshot again. Do not use it to run shell commands (use execute_command for that).
|
|
42
|
+
|
|
43
|
+
Parameters:
|
|
44
|
+
- action: (required) One of:
|
|
45
|
+
- "launch": Start the headless browser and navigate to url. Re-launching later closes the previous browser and starts fresh.
|
|
46
|
+
- "screenshot": Capture the current page as a PNG. The PNG is written to a file under the workspace (path: <workspaceRoot>/.headlesscode/browser-screenshots/<timestamp>.png) and then described by a cloud vision model — the result includes a real TEXT description of the page (visible text, layout, error messages, UI state) plus the file path + current page title/URL. No raw image reaches the model; act on the description directly.
|
|
47
|
+
- "click": Click the first element matching selector (a CSS selector).
|
|
48
|
+
- "type": Type text into the first element matching selector (a CSS selector; the element must be focusable — a text input or textarea).
|
|
49
|
+
- "getConsoleLogs": Return console messages (including errors) captured from the page since the last getConsoleLogs call (each call returns only NEW entries).
|
|
50
|
+
- "getNetworkErrors": Return failed network requests captured since the last getNetworkErrors call (each call returns only NEW entries).
|
|
51
|
+
- "close": Shut down the browser.
|
|
52
|
+
- url: (required for "launch") The URL to open — typically a local dev server you started with execute_command, e.g. http://127.0.0.1:4390.
|
|
53
|
+
- selector: (required for "click" and "type") A CSS selector.
|
|
54
|
+
- text: (required for "type") The text to type into the element.
|
|
55
|
+
|
|
56
|
+
Examples:
|
|
57
|
+
- Inspect a local dev server: { "action": "launch", "url": "http://127.0.0.1:4390" }
|
|
58
|
+
- See the page: { "action": "screenshot" }
|
|
59
|
+
- Click a button: { "action": "click", "selector": "#btnStart" }
|
|
60
|
+
- Fill a field: { "action": "type", "selector": "#launchTask", "text": "check the UI" }
|
|
61
|
+
- Check for page errors: { "action": "getConsoleLogs" }`
|
|
62
|
+
|
|
63
|
+
const ACTION_PARAMETER_DESCRIPTION = `The browser action to perform: launch | screenshot | click | type | getConsoleLogs | getNetworkErrors | close`
|
|
64
|
+
const URL_PARAMETER_DESCRIPTION = `URL to navigate to (required for launch)`
|
|
65
|
+
const SELECTOR_PARAMETER_DESCRIPTION = `CSS selector for the element to click or type into (required for click/type)`
|
|
66
|
+
const TEXT_PARAMETER_DESCRIPTION = `Text to type into the element (required for type)`
|
|
67
|
+
|
|
68
|
+
export const browserActionTool = {
|
|
69
|
+
type: "function",
|
|
70
|
+
function: {
|
|
71
|
+
name: BROWSER_ACTION_NAME,
|
|
72
|
+
description: BROWSER_ACTION_DESCRIPTION,
|
|
73
|
+
strict: true,
|
|
74
|
+
parameters: {
|
|
75
|
+
type: "object",
|
|
76
|
+
properties: {
|
|
77
|
+
action: {
|
|
78
|
+
type: "string",
|
|
79
|
+
description: ACTION_PARAMETER_DESCRIPTION,
|
|
80
|
+
enum: ["launch", "screenshot", "click", "type", "getConsoleLogs", "getNetworkErrors", "close"],
|
|
81
|
+
},
|
|
82
|
+
url: {
|
|
83
|
+
type: ["string", "null"],
|
|
84
|
+
description: URL_PARAMETER_DESCRIPTION,
|
|
85
|
+
},
|
|
86
|
+
selector: {
|
|
87
|
+
type: ["string", "null"],
|
|
88
|
+
description: SELECTOR_PARAMETER_DESCRIPTION,
|
|
89
|
+
},
|
|
90
|
+
text: {
|
|
91
|
+
type: ["string", "null"],
|
|
92
|
+
description: TEXT_PARAMETER_DESCRIPTION,
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
required: ["action", "url", "selector", "text"],
|
|
96
|
+
additionalProperties: false,
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
} satisfies OpenAI.Chat.ChatCompletionTool
|