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,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `browser_action` handler: the session lifecycle + timeout enforcement.
|
|
3
|
+
*
|
|
4
|
+
* Every browser action runs under an overall wall-clock timeout
|
|
5
|
+
* (DEFAULT_BROWSER_ACTION_TIMEOUT_MS) using Promise.race — a hung or crashed
|
|
6
|
+
* browser action surfaces as a clear tool error and NEVER hangs the session.
|
|
7
|
+
* The timeout kills (rather than backgrounds) the stuck action: unlike a
|
|
8
|
+
* timed-out execute_command (which the model intentionally backgrounds to
|
|
9
|
+
* keep a dev server running), a stuck browser action is never something the
|
|
10
|
+
* model asked to keep running — it is a failure of the action itself.
|
|
11
|
+
*
|
|
12
|
+
* Session lifecycle mirrors the harness's non-fatal/cleanup idioms:
|
|
13
|
+
* - The browser is created lazily on the first launch and stays alive for
|
|
14
|
+
* the session (a dev server inspection span multiple calls).
|
|
15
|
+
* - ToolExecutor.dispose() calls BrowserSession.dispose() (via
|
|
16
|
+
* disposeBrowserSessions) so the browser is torn down at session end even
|
|
17
|
+
* if the model never calls close() — same discipline as the backgrounded
|
|
18
|
+
* execute_command children (see src/tools/executor.ts).
|
|
19
|
+
* - A launched browser is also hard-killed via the module-level process
|
|
20
|
+
* exit hook (installBrowserDisposeHook in service.ts), belt-and-suspenders
|
|
21
|
+
* for teardown paths that skip dispose().
|
|
22
|
+
*
|
|
23
|
+
* Screenshot results are written to `<workspaceRoot>/.headlesscode/
|
|
24
|
+
* browser-screenshots/<timestamp>.png` and the PNG is immediately described by
|
|
25
|
+
* the cloud vision captioner (src/vision/describe.ts), so the tool result
|
|
26
|
+
* carries a real TEXT description the main model can act on — no raw image
|
|
27
|
+
* ever reaches it. The file path stays in the result too, for callers that
|
|
28
|
+
* want the raw PNG.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import * as fsp from "node:fs/promises"
|
|
32
|
+
import * as path from "node:path"
|
|
33
|
+
import { setTimeout as sleep } from "node:timers/promises"
|
|
34
|
+
|
|
35
|
+
import type { AuxLlmUsage, ToolContext, ToolResult } from "../../engine/types.js"
|
|
36
|
+
import { resolveWithinWorkspace } from "../executor.js"
|
|
37
|
+
import { describeImage, type DescribeImageResult } from "../../vision/describe.js"
|
|
38
|
+
import {
|
|
39
|
+
BrowserSession,
|
|
40
|
+
MAX_BROWSER_EVENT_ENTRIES,
|
|
41
|
+
DEFAULT_BROWSER_ACTION_TIMEOUT_MS,
|
|
42
|
+
installBrowserDisposeHook,
|
|
43
|
+
} from "./service.js"
|
|
44
|
+
import { browserActionTool, BROWSER_ACTION_NAME } from "./tool.js"
|
|
45
|
+
|
|
46
|
+
export { browserActionTool, BROWSER_ACTION_NAME }
|
|
47
|
+
|
|
48
|
+
/** Subdirectory (under the workspace root) where screenshots are written. */
|
|
49
|
+
const SCREENSHOTS_SUBDIR = ".headlesscode/browser-screenshots"
|
|
50
|
+
|
|
51
|
+
// ─── Session registry ────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Active browser sessions, keyed by workspace root. One ToolExecutor serves
|
|
55
|
+
* one session, and the browser may outlive a single tool call (that is the
|
|
56
|
+
* point — an interactive inspection spans multiple browser_action calls), so
|
|
57
|
+
* the session lives at module scope and ToolExecutor.dispose() tears it down
|
|
58
|
+
* at true session end. Keying by workspace root keeps independent sessions
|
|
59
|
+
* (e.g. a QA executor in the same process) from sharing a browser.
|
|
60
|
+
*/
|
|
61
|
+
const browserSessions = new Map<string, BrowserSession>()
|
|
62
|
+
|
|
63
|
+
function sessionFor(ctx: ToolContext): BrowserSession {
|
|
64
|
+
let session = browserSessions.get(ctx.workspaceRoot)
|
|
65
|
+
if (session === undefined) {
|
|
66
|
+
session = new BrowserSession()
|
|
67
|
+
browserSessions.set(ctx.workspaceRoot, session)
|
|
68
|
+
}
|
|
69
|
+
return session
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Session teardown for every known browser session: hard-close the browser
|
|
74
|
+
* AND kill the residual Chromium process group (see BrowserSession.dispose).
|
|
75
|
+
* Called from ToolExecutor.dispose(), so a launched browser never survives
|
|
76
|
+
* its session even when the model never calls close().
|
|
77
|
+
*/
|
|
78
|
+
export function disposeBrowserSessions(): void {
|
|
79
|
+
for (const session of browserSessions.values()) {
|
|
80
|
+
void session.dispose()
|
|
81
|
+
}
|
|
82
|
+
browserSessions.clear()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ─── Arg validation ──────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
export type BrowserAction =
|
|
88
|
+
| "launch"
|
|
89
|
+
| "screenshot"
|
|
90
|
+
| "click"
|
|
91
|
+
| "type"
|
|
92
|
+
| "getConsoleLogs"
|
|
93
|
+
| "getNetworkErrors"
|
|
94
|
+
| "close"
|
|
95
|
+
|
|
96
|
+
function requireStringArg(args: Record<string, unknown>, key: string): string {
|
|
97
|
+
const v = args[key]
|
|
98
|
+
if (typeof v !== "string" || v.trim() === "") {
|
|
99
|
+
throw new Error(`browser_action: missing or invalid string argument '${key}' (got ${JSON.stringify(v)})`)
|
|
100
|
+
}
|
|
101
|
+
return v
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const VALID_ACTIONS: readonly BrowserAction[] = [
|
|
105
|
+
"launch",
|
|
106
|
+
"screenshot",
|
|
107
|
+
"click",
|
|
108
|
+
"type",
|
|
109
|
+
"getConsoleLogs",
|
|
110
|
+
"getNetworkErrors",
|
|
111
|
+
"close",
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
function parseAction(args: Record<string, unknown>): BrowserAction {
|
|
115
|
+
const raw = args.action
|
|
116
|
+
const action = typeof raw === "string" ? raw : ""
|
|
117
|
+
if (!(VALID_ACTIONS as readonly string[]).includes(action)) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`browser_action: invalid action '${raw}'. Valid actions: ${VALID_ACTIONS.join(", ")}`,
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
return action as BrowserAction
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ─── Timeout enforcement ─────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Run a browser action under an overall wall-clock timeout. The action's
|
|
129
|
+
* Playwright internals already carry per-step timeouts; this is the outer
|
|
130
|
+
* bound that catches the "whole browser hung" case (e.g. the page never
|
|
131
|
+
* settles, the browser crashed mid-action). We KILL on timeout rather than
|
|
132
|
+
* background: unlike execute_command, nothing about a stuck browser action is
|
|
133
|
+
* legitimately long-running work the model asked for.
|
|
134
|
+
*/
|
|
135
|
+
async function withActionTimeout<T>(fn: () => Promise<T>): Promise<T> {
|
|
136
|
+
// Env override so tests can exercise the timeout fast; production default
|
|
137
|
+
// is DEFAULT_BROWSER_ACTION_TIMEOUT_MS.
|
|
138
|
+
const timeoutMs = Number(process.env.BROWSER_ACTION_TIMEOUT_MS ?? "") || DEFAULT_BROWSER_ACTION_TIMEOUT_MS
|
|
139
|
+
let timer: NodeJS.Timeout | undefined
|
|
140
|
+
const timeout = new Promise<never>((_, reject) => {
|
|
141
|
+
timer = setTimeout(() => {
|
|
142
|
+
reject(
|
|
143
|
+
new Error(
|
|
144
|
+
`browser_action: action timed out after ${timeoutMs / 1000}s — ` +
|
|
145
|
+
`the browser or page appears hung. Call close() (or let the session end) to reset.`,
|
|
146
|
+
),
|
|
147
|
+
)
|
|
148
|
+
}, timeoutMs)
|
|
149
|
+
})
|
|
150
|
+
try {
|
|
151
|
+
return await Promise.race([fn(), timeout])
|
|
152
|
+
} finally {
|
|
153
|
+
clearTimeout(timer)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ─── Formatting helpers ──────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
function formatConsoleEntries(entries: Array<{ type: string; text: string }>): string {
|
|
160
|
+
if (entries.length === 0) {
|
|
161
|
+
return "no console messages captured since the last check"
|
|
162
|
+
}
|
|
163
|
+
const lines = entries.map((e) => `[${e.type}] ${e.text}`)
|
|
164
|
+
return lines.join("\n")
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function formatNetworkErrors(entries: Array<{ url: string; failure: string; method: string }>): string {
|
|
168
|
+
if (entries.length === 0) {
|
|
169
|
+
return "no failed network requests captured since the last check"
|
|
170
|
+
}
|
|
171
|
+
const lines = entries.map((e) => `${e.method} ${e.url} — ${e.failure}`)
|
|
172
|
+
return lines.join("\n")
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function ok(content: string): ToolResult {
|
|
176
|
+
return { content, isError: false }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function err(content: string): ToolResult {
|
|
180
|
+
return { content: `[Error] ${content}`, isError: true }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Resolve a screenshot target path inside the workspace and ensure the
|
|
185
|
+
* screenshots directory exists.
|
|
186
|
+
*/
|
|
187
|
+
async function resolveScreenshotPath(workspaceRoot: string): Promise<string> {
|
|
188
|
+
const dir = resolveWithinWorkspace(workspaceRoot, SCREENSHOTS_SUBDIR)
|
|
189
|
+
await fsp.mkdir(dir, { recursive: true })
|
|
190
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-")
|
|
191
|
+
return path.join(dir, `screenshot-${stamp}.png`)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ─── Handler ─────────────────────────────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* The screenshot action's captioner, injectable for tests. Real sessions
|
|
198
|
+
* always use describeImage (src/vision/describe.ts); browser-action.test.ts
|
|
199
|
+
* swaps in a fake so the suite needs no network/API key.
|
|
200
|
+
*/
|
|
201
|
+
let visionCaptioner: (imagePath: string) => Promise<DescribeImageResult> = (imagePath) => describeImage(imagePath)
|
|
202
|
+
|
|
203
|
+
/** Test seam: replace the screenshot captioner (see browser-action.test.ts). */
|
|
204
|
+
export function setVisionCaptionerForTest(
|
|
205
|
+
captioner: (imagePath: string) => Promise<DescribeImageResult>,
|
|
206
|
+
): void {
|
|
207
|
+
visionCaptioner = captioner
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export async function browserActionHandler(
|
|
211
|
+
args: Record<string, unknown>,
|
|
212
|
+
ctx: ToolContext,
|
|
213
|
+
): Promise<ToolResult> {
|
|
214
|
+
const action = parseAction(args)
|
|
215
|
+
|
|
216
|
+
// A closed browser is not a hard error — it is the session's normal
|
|
217
|
+
// "explicitly cleaned up" state, so a model that calls close() then
|
|
218
|
+
// re-launches gets a clean result, not an error that counts as a mistake.
|
|
219
|
+
if (action !== "launch") {
|
|
220
|
+
const session = browserSessions.get(ctx.workspaceRoot)
|
|
221
|
+
if (session === undefined || !session.isLaunched) {
|
|
222
|
+
return ok("browser: no browser is open — call launch(url) first to start one")
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Module-level process-exit cleanup, idempotent; ensures the browser never
|
|
227
|
+
// outlives the harness process even on teardown paths that skip dispose().
|
|
228
|
+
installBrowserDisposeHook()
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
return await withActionTimeout(async () => {
|
|
232
|
+
switch (action) {
|
|
233
|
+
case "launch": {
|
|
234
|
+
const url = requireStringArg(args, "url")
|
|
235
|
+
const session = sessionFor(ctx)
|
|
236
|
+
// Re-launch is an explicit reset: close the previous browser
|
|
237
|
+
// first so its state never leaks into the fresh session.
|
|
238
|
+
await session.dispose()
|
|
239
|
+
await session.launch(url)
|
|
240
|
+
// Let the page settle so capture-time console/network events
|
|
241
|
+
// are recorded before the model drains them.
|
|
242
|
+
await session.waitForIdle()
|
|
243
|
+
return ok(await session.describe())
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
case "screenshot": {
|
|
247
|
+
const session = sessionFor(ctx)
|
|
248
|
+
const base64 = await session.screenshot()
|
|
249
|
+
const target = await resolveScreenshotPath(ctx.workspaceRoot)
|
|
250
|
+
await fsp.writeFile(target, Buffer.from(base64, "base64"))
|
|
251
|
+
const desc = await session.describe()
|
|
252
|
+
// Cloud vision captioning: the PNG never reaches the main
|
|
253
|
+
// model (deepseek-v4-flash is text-only), so describe it
|
|
254
|
+
// here and put a real text description in the result. Only
|
|
255
|
+
// when a real accounting session wired onAuxLlmUsage (cost
|
|
256
|
+
// flows through BudgetTracker); bare/read-only executors
|
|
257
|
+
// skip the call and keep the path so the model can still
|
|
258
|
+
// invoke describe_image explicitly.
|
|
259
|
+
let captionNote = ""
|
|
260
|
+
if (ctx.onAuxLlmUsage !== undefined) {
|
|
261
|
+
try {
|
|
262
|
+
const { description, usage } = await visionCaptioner(target)
|
|
263
|
+
ctx.onAuxLlmUsage(usage)
|
|
264
|
+
captionNote = `\nimage description: ${description}`
|
|
265
|
+
} catch (err) {
|
|
266
|
+
// Captioning is a bonus on top of the screenshot —
|
|
267
|
+
// a vision failure must never fail the screenshot
|
|
268
|
+
// itself, just say so and keep the file path.
|
|
269
|
+
captionNote = `\n[image description failed: ${err instanceof Error ? err.message : String(err)}]`
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return ok(
|
|
273
|
+
`${desc}\nscreenshot saved to ${path.relative(ctx.workspaceRoot, target).toPosix()} (${target})${captionNote}`,
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
case "click": {
|
|
278
|
+
const selector = requireStringArg(args, "selector")
|
|
279
|
+
const session = sessionFor(ctx)
|
|
280
|
+
await session.click(selector)
|
|
281
|
+
// Interaction may have triggered a navigation or console
|
|
282
|
+
// errors — settle, then report the current state.
|
|
283
|
+
await session.waitForIdle()
|
|
284
|
+
const navigations = await session.getNavigations()
|
|
285
|
+
const navNote =
|
|
286
|
+
navigations.length > 0
|
|
287
|
+
? `\npage navigated to: ${navigations.map((n) => n.url).join(", ")}`
|
|
288
|
+
: ""
|
|
289
|
+
return ok(`${await session.describe()}\nclicked ${selector}${navNote}`)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
case "type": {
|
|
293
|
+
const selector = requireStringArg(args, "selector")
|
|
294
|
+
const text = requireStringArg(args, "text")
|
|
295
|
+
const session = sessionFor(ctx)
|
|
296
|
+
await session.type(selector, text)
|
|
297
|
+
return ok(`${await session.describe()}\ntyped text into ${selector}`)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
case "getConsoleLogs": {
|
|
301
|
+
const session = sessionFor(ctx)
|
|
302
|
+
const entries = await session.getConsoleLogs()
|
|
303
|
+
const shown = entries.slice(0, MAX_BROWSER_EVENT_ENTRIES)
|
|
304
|
+
const truncated =
|
|
305
|
+
entries.length > MAX_BROWSER_EVENT_ENTRIES
|
|
306
|
+
? `\n…${entries.length - MAX_BROWSER_EVENT_ENTRIES} more entries (capped at ${MAX_BROWSER_EVENT_ENTRIES})`
|
|
307
|
+
: ""
|
|
308
|
+
return ok(formatConsoleEntries(shown) + truncated)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
case "getNetworkErrors": {
|
|
312
|
+
const session = sessionFor(ctx)
|
|
313
|
+
const entries = await session.getNetworkErrors()
|
|
314
|
+
const shown = entries.slice(0, MAX_BROWSER_EVENT_ENTRIES)
|
|
315
|
+
const truncated =
|
|
316
|
+
entries.length > MAX_BROWSER_EVENT_ENTRIES
|
|
317
|
+
? `\n…${entries.length - MAX_BROWSER_EVENT_ENTRIES} more entries (capped at ${MAX_BROWSER_EVENT_ENTRIES})`
|
|
318
|
+
: ""
|
|
319
|
+
return ok(formatNetworkErrors(shown) + truncated)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
case "close": {
|
|
323
|
+
const session = sessionFor(ctx)
|
|
324
|
+
await session.close()
|
|
325
|
+
return ok("browser: closed")
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
})
|
|
329
|
+
} catch (error) {
|
|
330
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
331
|
+
// A crashed/hung browser must never take the whole session down with
|
|
332
|
+
// it: tear it down (awaiting, so a subsequent launch starts from a
|
|
333
|
+
// deterministic closed state) so the model can recover with a fresh
|
|
334
|
+
// launch.
|
|
335
|
+
try {
|
|
336
|
+
await browserSessions.get(ctx.workspaceRoot)?.dispose()
|
|
337
|
+
} catch {
|
|
338
|
+
// Best-effort teardown — the original error is the one to report.
|
|
339
|
+
}
|
|
340
|
+
return err(message)
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Reference the schema export so bundlers/type-checkers keep it reachable
|
|
345
|
+
// from this module (it is also re-exported above).
|
|
346
|
+
void browserActionTool
|