@hover-dev/core 0.12.0 → 0.14.0
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/README.md +8 -5
- package/dist/playwright/preflight.js +1 -1
- package/dist/plugin-api.d.ts +23 -0
- package/dist/plugin-api.d.ts.map +1 -1
- package/dist/service.d.ts +12 -0
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +115 -45
- package/dist/specs/writeSpec.js +50 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The local Node service. Owns:
|
|
4
4
|
|
|
5
|
-
- **Agent invocation** (`src/agents/`) — Local CLI Agent First. Spawns `claude` / `codex` / `cursor` /
|
|
5
|
+
- **Agent invocation** (`src/agents/`) — Local CLI Agent First. Spawns `claude` / `codex` / `cursor-agent` / `aider` / `gemini-cli` / `qwen-code` and normalizes their output into a single `InvokeEvent` stream.
|
|
6
6
|
- **Playwright preflight** (`src/playwright/`) — verifies CDP connection to the user's Chrome.
|
|
7
7
|
- **Smoke test** (`src/smoke.ts`) — end-to-end verification of the whole chain.
|
|
8
8
|
|
|
@@ -20,6 +20,9 @@ The local Node service. Owns:
|
|
|
20
20
|
| `claude.ts` | Claude Code descriptor: `claude -p`, stream-json parser, hard sandbox flags |
|
|
21
21
|
| `codex.ts` | OpenAI Codex CLI descriptor: `codex exec --json`, JSONL parser, soft sandbox (`--sandbox read-only`) |
|
|
22
22
|
| `cursor.ts` | Cursor CLI descriptor (v0.9): stream-JSON / NDJSON parser, soft sandbox |
|
|
23
|
+
| `aider.ts` | Aider CLI descriptor (v0.10): JSON-stream parser, soft sandbox |
|
|
24
|
+
| `gemini.ts` | Gemini CLI descriptor (v0.10): stream parser, soft sandbox |
|
|
25
|
+
| `qwen.ts` | Qwen Code descriptor (v0.10): stream parser, soft sandbox |
|
|
23
26
|
|
|
24
27
|
To add an agent: implement an `AgentDescriptor`, register it in `registry.ts`. Done.
|
|
25
28
|
|
|
@@ -45,7 +48,7 @@ pnpm smoke http://localhost:5173/ "log in then add a todo named 'verify hover'"
|
|
|
45
48
|
Environment variables:
|
|
46
49
|
|
|
47
50
|
- `HOVER_CDP` — CDP URL (default `http://localhost:9222`)
|
|
48
|
-
- `HOVER_AGENT` — agent id (omit to auto-detect; tries the user's stated preference, then the first installed agent in registry order — `claude` → `codex` → `cursor-agent` today)
|
|
51
|
+
- `HOVER_AGENT` — agent id (omit to auto-detect; tries the user's stated preference, then the first installed agent in registry order — `claude` → `codex` → `cursor-agent` → `aider` → `gemini-cli` → `qwen-code` today)
|
|
49
52
|
- `HOVER_MODEL` — model for the agent (default `sonnet`, much cheaper than opus)
|
|
50
53
|
|
|
51
54
|
## Sandboxing (what the smoke test enforces)
|
|
@@ -53,9 +56,9 @@ Environment variables:
|
|
|
53
56
|
The `claude -p` invocation is locked down so Claude can only drive the browser:
|
|
54
57
|
|
|
55
58
|
- `--strict-mcp-config` — ignore any MCP servers in `~/.claude/` or `.mcp.json`
|
|
56
|
-
- `--allowedTools mcp__playwright` — only Playwright MCP
|
|
57
|
-
- `--disallowedTools Bash Edit Write Read Grep Glob Task WebFetch WebSearch
|
|
59
|
+
- `--allowedTools mcp__playwright Skill` — only Playwright MCP and the Skill tool are callable
|
|
60
|
+
- `--disallowedTools Bash Edit Write Read Grep Glob Task WebFetch WebSearch EnterWorktree CronCreate …` — every built-in tool explicitly denied (full list in `CLAUDE_DEFAULT_DISALLOWED_TOOLS` in `claude.ts`)
|
|
58
61
|
- `--permission-mode dontAsk` — anything not whitelisted aborts the run
|
|
59
|
-
- `--max-budget-usd
|
|
62
|
+
- `--max-budget-usd <n>` — optional hard $ ceiling per session (no default; pass `maxBudgetUsd` in plugin options or via the CLI flag to enable)
|
|
60
63
|
|
|
61
64
|
Together these enforce the rule that the spawned agent can only reach the browser via Playwright MCP — never the host filesystem, shell, or network. A hijacked prompt or hallucinated destructive action has nowhere to land.
|
|
@@ -34,7 +34,7 @@ export async function preflightCDP(cdpUrl, timeoutMs = 2000) {
|
|
|
34
34
|
catch (err) {
|
|
35
35
|
return {
|
|
36
36
|
ok: false,
|
|
37
|
-
reason: `Chrome debug session not detected at ${cdpUrl}.
|
|
37
|
+
reason: `Chrome debug session not detected at ${cdpUrl}. Click the ✨ launcher in the widget to start it, or run \`pnpm exec hover-chrome\` (npx hover-chrome).`,
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
40
|
if (!versionRes.ok) {
|
package/dist/plugin-api.d.ts
CHANGED
|
@@ -33,6 +33,10 @@ export interface HoverPluginMode {
|
|
|
33
33
|
label: string;
|
|
34
34
|
/** One-liner help text shown in the dropdown. */
|
|
35
35
|
description?: string;
|
|
36
|
+
/** Short status shown in the mode bar's right-hand hint slot while this
|
|
37
|
+
* mode is engaged. Defaults to "active" if omitted. Keep it terse — e.g.
|
|
38
|
+
* "MITM proxy active". */
|
|
39
|
+
engagedHint?: string;
|
|
36
40
|
/** Mode ids this mode cannot be active alongside. Two plugins both
|
|
37
41
|
* needing an exclusive proxy would set each other here. */
|
|
38
42
|
conflictsWith?: string[];
|
|
@@ -113,10 +117,29 @@ export interface ModeActivateCtx extends HoverHookCtxBase {
|
|
|
113
117
|
export interface ModeDeactivateCtx extends HoverHookCtxBase {
|
|
114
118
|
modeId: string;
|
|
115
119
|
}
|
|
120
|
+
/** Fired exactly once when the host service starts, BEFORE the debug Chrome
|
|
121
|
+
* is (auto-)launched. A plugin that needs Chrome to be born with specific
|
|
122
|
+
* flags — e.g. a resident MITM proxy that Chrome must point through from the
|
|
123
|
+
* first navigation — boots that sidecar here and calls setChromeProxy so the
|
|
124
|
+
* host bakes the flags into the single Chrome launch. This is what lets the
|
|
125
|
+
* security plugin run one always-on (transparent-by-default) proxy instead
|
|
126
|
+
* of launching a second Chrome on mode entry. */
|
|
127
|
+
export interface ServiceStartCtx extends HoverHookCtxBase {
|
|
128
|
+
/** Tell the host "the debug Chrome should be launched with these proxy
|
|
129
|
+
* settings". Set once here; persists for the whole session. */
|
|
130
|
+
setChromeProxy(proxy: {
|
|
131
|
+
port: number;
|
|
132
|
+
spki: string;
|
|
133
|
+
} | null): void;
|
|
134
|
+
/** Same as the activate-time variant — seed runtime env for a declared MCP
|
|
135
|
+
* server before it's spawned. */
|
|
136
|
+
setMcpServerEnv(id: string, env: Record<string, string>): void;
|
|
137
|
+
}
|
|
116
138
|
/** Fired exactly once when the host service is shutting down for any
|
|
117
139
|
* reason. Hooks must release subprocesses and file handles. */
|
|
118
140
|
export type ShutdownCtx = HoverHookCtxBase;
|
|
119
141
|
export interface HoverHooks {
|
|
142
|
+
'hover:service:start'?: (ctx: ServiceStartCtx) => void | Promise<void>;
|
|
120
143
|
'hover:mode:activate'?: (ctx: ModeActivateCtx) => void | Promise<void>;
|
|
121
144
|
'hover:mode:deactivate'?: (ctx: ModeDeactivateCtx) => void | Promise<void>;
|
|
122
145
|
'hover:service:shutdown'?: (ctx: ShutdownCtx) => void | Promise<void>;
|
package/dist/plugin-api.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin-api.d.ts","sourceRoot":"","sources":["../src/plugin-api.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"plugin-api.d.ts","sourceRoot":"","sources":["../src/plugin-api.ts"],"names":[],"mappings":"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"}
|
package/dist/service.d.ts
CHANGED
|
@@ -18,6 +18,18 @@ export interface ServiceOptions {
|
|
|
18
18
|
* pre-plugin Hover" — important for the long tail of users who never
|
|
19
19
|
* install one. */
|
|
20
20
|
plugins?: HoverPluginManifest[];
|
|
21
|
+
/** When true, the service launches the single debug Chrome itself at
|
|
22
|
+
* startup — AFTER firing plugin `hover:service:start` hooks, so a plugin
|
|
23
|
+
* that set a resident proxy (e.g. security's MITM) has its flags baked
|
|
24
|
+
* into that one Chrome. Previously each bundler shim called
|
|
25
|
+
* launchDebugChrome() directly, which bypassed the service and so couldn't
|
|
26
|
+
* see the proxy; moving it here is what enables the single-Chrome model.
|
|
27
|
+
* Default false (shims pass it through from their own option). */
|
|
28
|
+
autoLaunchChrome?: boolean;
|
|
29
|
+
/** The dev-server URL the auto-launched Chrome should open. Each shim knows
|
|
30
|
+
* its own framework's dev URL and passes it here. Defaults to the cdp host
|
|
31
|
+
* if unset, but shims should always provide it. */
|
|
32
|
+
devUrl?: string;
|
|
21
33
|
}
|
|
22
34
|
export interface ServiceHandle {
|
|
23
35
|
/** The port the WebSocketServer actually bound to. May differ from
|
package/dist/service.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AA8EA,OAAO,EAEL,KAAK,mBAAmB,EAEzB,MAAM,iBAAiB,CAAC;AAEzB,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gFAAgF;IAChF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;6EAGyE;IACzE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;uBAImB;IACnB,OAAO,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAChC;;;;;;uEAMmE;IACnE,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;wDAEoD;IACpD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B;4EACwE;IACxE,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAiED,wBAAsB,YAAY,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC,CAs2B/E"}
|
package/dist/service.js
CHANGED
|
@@ -54,6 +54,7 @@ import { listAgentAvailability, pickPrimaryAgent, } from './agents/detect.js';
|
|
|
54
54
|
import { getAgent } from './agents/registry.js';
|
|
55
55
|
import { getPreflight, invalidatePreflight } from './playwright/preflightCache.js';
|
|
56
56
|
import { resolveMcpConfig } from './playwright/resolveMcpConfig.js';
|
|
57
|
+
import { launchDebugChrome } from './playwright/launchChrome.js';
|
|
57
58
|
import { listSkills } from './skills/writeSkill.js';
|
|
58
59
|
import { listSpecs } from './specs/listSpecs.js';
|
|
59
60
|
import { send, sendIfOpen } from './service/types.js';
|
|
@@ -65,6 +66,19 @@ import { CURRENT_API_VERSION, } from './plugin-api.js';
|
|
|
65
66
|
// handler modules can share them. See those files for the wire shape.
|
|
66
67
|
const PROTOCOL_VERSION = 1;
|
|
67
68
|
const PORT_RETRIES = 10;
|
|
69
|
+
/** CJK-presence test — mirrors voice.js's detectLanguage. Any Han character
|
|
70
|
+
* in the prompt flips the agent's prose output to Chinese. */
|
|
71
|
+
const CJK_RE = /[一-鿿]/;
|
|
72
|
+
/** Appended to the agent's system prompt when the user's prompt contains CJK,
|
|
73
|
+
* so the human-facing prose (verification summary / ## Findings / step
|
|
74
|
+
* narration) comes back in Chinese — matching how Voice mode picks a Chinese
|
|
75
|
+
* TTS voice for the same prompt. Deliberately scoped to PROSE only: the agent
|
|
76
|
+
* must still use the page's real (often English) accessible names, labels,
|
|
77
|
+
* and selectors when driving the browser. */
|
|
78
|
+
const ZH_OUTPUT_DIRECTIVE = '用户使用中文下达指令。请用简体中文撰写所有面向用户的文字输出:验证结论摘要、' +
|
|
79
|
+
'`## Findings` 区块(bug / 问题 / 备注)、以及每一步的中文描述。' +
|
|
80
|
+
'注意:这只影响你写给用户看的文字。操作浏览器时仍要使用页面真实的(通常是英文的)' +
|
|
81
|
+
'角色名、标签、可访问名称和选择器——不要把它们翻译成中文。';
|
|
68
82
|
/**
|
|
69
83
|
* Try to bind a WebSocketServer to <host>:<port>. Resolves with the wss on
|
|
70
84
|
* success; rejects with the bind error (typically EADDRINUSE) on failure.
|
|
@@ -175,15 +189,11 @@ export async function startService(opts) {
|
|
|
175
189
|
}
|
|
176
190
|
}
|
|
177
191
|
}
|
|
178
|
-
//
|
|
179
|
-
// Chrome
|
|
180
|
-
//
|
|
181
|
-
const extras = effectiveLaunchExtras();
|
|
182
|
-
const effectiveCdpUrl = extras?.cdpPort
|
|
183
|
-
? `http://localhost:${extras.cdpPort}`
|
|
184
|
-
: cdpUrl;
|
|
192
|
+
// Single-Chrome model: the Playwright MCP always points at the one debug
|
|
193
|
+
// Chrome on the normal cdpUrl. (Pre-single-Chrome this branched to a
|
|
194
|
+
// mode-specific port like 9333; there's no second Chrome anymore.)
|
|
185
195
|
return resolveMcpConfig({
|
|
186
|
-
cdpUrl
|
|
196
|
+
cdpUrl,
|
|
187
197
|
port,
|
|
188
198
|
extra,
|
|
189
199
|
// Suffix the filename by the mode so different mode toggles within
|
|
@@ -223,44 +233,31 @@ export async function startService(opts) {
|
|
|
223
233
|
}
|
|
224
234
|
/** id of the currently-active mode, or null for normal (unmoded) mode. */
|
|
225
235
|
let currentModeId = null;
|
|
226
|
-
/** Chrome-proxy settings
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
|
|
236
|
+
/** Chrome-proxy settings a plugin's `hover:service:start` hook set on us
|
|
237
|
+
* (security's resident MITM). RESIDENT for the whole session — set once
|
|
238
|
+
* before Chrome launches, never cleared on mode change — so the single
|
|
239
|
+
* debug Chrome is born with `--proxy-server` + the SPKI pin and entering
|
|
240
|
+
* Security mode is just a runtime flip of the proxy, not a Chrome relaunch.
|
|
241
|
+
* Read by `effectiveLaunchExtras()` and threaded into every cdp handler
|
|
242
|
+
* (check-cdp / launch-chrome / focus-debug) plus the initial auto-launch. */
|
|
243
|
+
let residentChromeProxy = null;
|
|
232
244
|
/** Runtime env overrides keyed by mcpServer id, set by plugin
|
|
233
245
|
* activate hooks (via ctx.setMcpServerEnv). Cleared on mode change.
|
|
234
246
|
* Merged with the manifest-declared env when the agent's spawn-time
|
|
235
247
|
* MCP config is built. */
|
|
236
248
|
const mcpEnvOverrides = new Map();
|
|
237
|
-
/** The cdp-handler extras (
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
249
|
+
/** The cdp-handler extras (proxy) threaded into launch-chrome / check-cdp /
|
|
250
|
+
* focus-debug and the initial auto-launch. In the single-Chrome model this
|
|
251
|
+
* is driven purely by the RESIDENT proxy (set in `hover:service:start`),
|
|
252
|
+
* NOT by the active mode — there is one Chrome on the normal CDP port that
|
|
253
|
+
* is always proxied; entering Security mode flips the proxy's behaviour,
|
|
254
|
+
* it does not relaunch Chrome on a different port. Returns undefined when
|
|
255
|
+
* no plugin set a resident proxy (the common no-security case), so plain
|
|
256
|
+
* Hover is byte-for-byte unchanged. */
|
|
241
257
|
const effectiveLaunchExtras = () => {
|
|
242
|
-
if (!
|
|
258
|
+
if (!residentChromeProxy)
|
|
243
259
|
return undefined;
|
|
244
|
-
|
|
245
|
-
const flags = plugin?.chromeFlags;
|
|
246
|
-
if (!flags && !modeChromeProxy)
|
|
247
|
-
return undefined;
|
|
248
|
-
// Belt + suspenders — flags.activeInModes is honoured if set, but
|
|
249
|
-
// since chromeFlags lives on the plugin that contributed this mode,
|
|
250
|
-
// the default of "applies in own mode" matches what we want.
|
|
251
|
-
if (flags?.activeInModes && !flags.activeInModes.includes('*') && !flags.activeInModes.includes(currentModeId)) {
|
|
252
|
-
// Plugin explicitly restricted its chromeFlags to a different mode.
|
|
253
|
-
// Honour that and only carry modeChromeProxy (set by setChromeProxy).
|
|
254
|
-
return modeChromeProxy ? { proxy: modeChromeProxy } : undefined;
|
|
255
|
-
}
|
|
256
|
-
return {
|
|
257
|
-
cdpPort: flags?.cdpPort,
|
|
258
|
-
userDataDir: flags?.userDataDir,
|
|
259
|
-
// modeChromeProxy wins over flags.proxy because it's the runtime
|
|
260
|
-
// value the activate hook computed (after starting mockttp);
|
|
261
|
-
// flags.proxy is only ever set by tests stubbing the manifest.
|
|
262
|
-
proxy: modeChromeProxy ?? flags?.proxy,
|
|
263
|
-
};
|
|
260
|
+
return { proxy: residentChromeProxy };
|
|
264
261
|
};
|
|
265
262
|
/** Send the current mode catalogue to one ws (or all if undefined). */
|
|
266
263
|
const broadcastModes = (target) => {
|
|
@@ -309,8 +306,14 @@ export async function startService(opts) {
|
|
|
309
306
|
}
|
|
310
307
|
}
|
|
311
308
|
}
|
|
312
|
-
|
|
313
|
-
|
|
309
|
+
// NOTE: neither residentChromeProxy NOR mcpEnvOverrides is cleared here.
|
|
310
|
+
// In the single-Chrome model both are RESIDENT — set once in
|
|
311
|
+
// service:start (e.g. security's HOVER_SECURITY_API base + token), they
|
|
312
|
+
// must survive every mode toggle so the agent's spawned MCP server can
|
|
313
|
+
// always reach the control plane. Clearing them on mode change was the
|
|
314
|
+
// pre-resident behaviour and would leave the security MCP server with no
|
|
315
|
+
// env → it exits with "failed". Mode changes now only flip plugin runtime
|
|
316
|
+
// state via the plugin's own activate/deactivate hooks.
|
|
314
317
|
currentModeId = null;
|
|
315
318
|
// Bring up new mode
|
|
316
319
|
if (newModeId) {
|
|
@@ -325,7 +328,10 @@ export async function startService(opts) {
|
|
|
325
328
|
broadcast: broadcastPluginEvent,
|
|
326
329
|
modeId: newModeId,
|
|
327
330
|
setChromeProxy(proxy) {
|
|
328
|
-
|
|
331
|
+
// Retained for API compatibility. In the single-Chrome model the
|
|
332
|
+
// proxy is normally set once in service:start; if an activate hook
|
|
333
|
+
// still calls this, treat it as updating the resident proxy.
|
|
334
|
+
residentChromeProxy = proxy;
|
|
329
335
|
},
|
|
330
336
|
setMcpServerEnv(id, env) {
|
|
331
337
|
mcpEnvOverrides.set(id, env);
|
|
@@ -339,9 +345,10 @@ export async function startService(opts) {
|
|
|
339
345
|
// pretend to be in `newModeId` with no sidecars running.
|
|
340
346
|
// Widget still trusts the broadcast below to learn we're back
|
|
341
347
|
// to default. The error is rethrown so the caller can surface
|
|
342
|
-
// it to the user.
|
|
343
|
-
|
|
344
|
-
|
|
348
|
+
// it to the user. residentChromeProxy and mcpEnvOverrides are NOT
|
|
349
|
+
// touched — both are owned by service:start, independent of mode
|
|
350
|
+
// activation (clearing the env would break the resident security
|
|
351
|
+
// MCP server).
|
|
345
352
|
currentModeId = null;
|
|
346
353
|
broadcastModes();
|
|
347
354
|
throw err;
|
|
@@ -694,6 +701,17 @@ export async function startService(opts) {
|
|
|
694
701
|
}
|
|
695
702
|
}
|
|
696
703
|
}
|
|
704
|
+
// Mirror the prompt's language in the agent's *prose* output — the
|
|
705
|
+
// verification summary (Result card), the ## Findings block, and the
|
|
706
|
+
// step narration — the same way Voice mode mirrors it in TTS. A
|
|
707
|
+
// Chinese prompt should produce a Chinese report. This does NOT change
|
|
708
|
+
// how the agent operates the browser: selectors, role names, and the
|
|
709
|
+
// app's own (often English) UI text are unaffected — only the agent's
|
|
710
|
+
// human-facing writing follows the user. Detection mirrors voice.js's
|
|
711
|
+
// detectLanguage (CJK presence → zh).
|
|
712
|
+
if (CJK_RE.test(text)) {
|
|
713
|
+
appendSystemPrompt = `${appendSystemPrompt}\n\n${ZH_OUTPUT_DIRECTIVE}`;
|
|
714
|
+
}
|
|
697
715
|
// Snapshot the agent id so a switch-agent message during the run
|
|
698
716
|
// can't smear two agents across one invocation. (We also gate
|
|
699
717
|
// switch-agent on `busy`, but defense in depth.)
|
|
@@ -844,6 +862,58 @@ export async function startService(opts) {
|
|
|
844
862
|
}
|
|
845
863
|
});
|
|
846
864
|
});
|
|
865
|
+
// ───────────────────────── service:start + single Chrome ─────────────────
|
|
866
|
+
// Fire plugin `hover:service:start` hooks BEFORE launching Chrome, so a
|
|
867
|
+
// plugin (security) can boot its resident proxy and call setChromeProxy.
|
|
868
|
+
// residentChromeProxy is then baked into the one auto-launched Chrome.
|
|
869
|
+
for (const p of plugins) {
|
|
870
|
+
const hook = p.hooks?.['hover:service:start'];
|
|
871
|
+
if (!hook)
|
|
872
|
+
continue;
|
|
873
|
+
try {
|
|
874
|
+
await hook({
|
|
875
|
+
devRoot,
|
|
876
|
+
broadcast: broadcastPluginEvent,
|
|
877
|
+
setChromeProxy(proxy) {
|
|
878
|
+
residentChromeProxy = proxy;
|
|
879
|
+
},
|
|
880
|
+
setMcpServerEnv(id, env) {
|
|
881
|
+
mcpEnvOverrides.set(id, env);
|
|
882
|
+
},
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
catch (err) {
|
|
886
|
+
process.stderr.write(`[hover] plugin "${p.name}" service:start failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
// Auto-launch the single debug Chrome here (moved out of the bundler shims
|
|
890
|
+
// so it happens AFTER service:start and can carry residentChromeProxy).
|
|
891
|
+
// Fire-and-forget — startup must not block on Chrome, and a launch failure
|
|
892
|
+
// is non-fatal (the widget's amber ✨ lets the user retry on demand).
|
|
893
|
+
if (opts.autoLaunchChrome) {
|
|
894
|
+
const launchPort = (() => {
|
|
895
|
+
try {
|
|
896
|
+
return Number(new URL(cdpUrl).port) || 9222;
|
|
897
|
+
}
|
|
898
|
+
catch {
|
|
899
|
+
return 9222;
|
|
900
|
+
}
|
|
901
|
+
})();
|
|
902
|
+
const launchUrl = opts.devUrl ?? cdpUrl;
|
|
903
|
+
launchDebugChrome({
|
|
904
|
+
url: launchUrl,
|
|
905
|
+
port: launchPort,
|
|
906
|
+
proxy: residentChromeProxy ?? undefined,
|
|
907
|
+
})
|
|
908
|
+
.then((r) => {
|
|
909
|
+
if (!r.ok) {
|
|
910
|
+
process.stderr.write(`[hover] auto-launch Chrome failed: ${r.reason}\n`);
|
|
911
|
+
}
|
|
912
|
+
})
|
|
913
|
+
.catch((err) => {
|
|
914
|
+
process.stderr.write(`[hover] auto-launch Chrome error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
915
|
+
});
|
|
916
|
+
}
|
|
847
917
|
return {
|
|
848
918
|
port,
|
|
849
919
|
async close() {
|
package/dist/specs/writeSpec.js
CHANGED
|
@@ -122,10 +122,19 @@ function renderSpec(slug, displayName, description, steps, assertions) {
|
|
|
122
122
|
continue;
|
|
123
123
|
const calls = translateStep(s.tool, s.input);
|
|
124
124
|
for (const c of calls) {
|
|
125
|
+
// Each emitted line gets the test-body indent (2 spaces). Lines
|
|
126
|
+
// inside an emitInteraction block carry an additional 2 spaces of
|
|
127
|
+
// relative indent embedded in the line itself, so this single
|
|
128
|
+
// 2-space prefix produces correct 4-space nesting inside `{ … }`.
|
|
125
129
|
lines.push(` ${c}`);
|
|
126
130
|
hasAwait = true;
|
|
127
131
|
}
|
|
128
132
|
}
|
|
133
|
+
// `expect` is imported at the top of the generated file regardless of
|
|
134
|
+
// whether assertions are present — the visibility preludes emitted by
|
|
135
|
+
// translateStep depend on it for any element-targeting step. (Cheap:
|
|
136
|
+
// unused imports get tree-shaken out of any future test bundle, and
|
|
137
|
+
// Playwright's runner doesn't care.)
|
|
129
138
|
if (assertions.length > 0) {
|
|
130
139
|
if (hasAwait)
|
|
131
140
|
lines.push('');
|
|
@@ -152,30 +161,30 @@ function translateStep(tool, rawInput) {
|
|
|
152
161
|
return [`await page.goto(${JSON.stringify(path)});`];
|
|
153
162
|
}
|
|
154
163
|
case 'browser_click':
|
|
155
|
-
return
|
|
164
|
+
return emitInteraction(selectorFromDescription(String(input.element ?? '')), 'click()');
|
|
156
165
|
case 'browser_double_click':
|
|
157
|
-
return
|
|
166
|
+
return emitInteraction(selectorFromDescription(String(input.element ?? '')), 'dblclick()');
|
|
158
167
|
case 'browser_hover':
|
|
159
|
-
return
|
|
168
|
+
return emitInteraction(selectorFromDescription(String(input.element ?? '')), 'hover()');
|
|
160
169
|
case 'browser_fill_form': {
|
|
161
170
|
const fields = input.fields ?? [];
|
|
162
|
-
return fields.
|
|
171
|
+
return fields.flatMap(raw => {
|
|
163
172
|
const f = raw;
|
|
164
173
|
const value = String(f.value ?? '');
|
|
165
174
|
const target = f.name ?? f.element ?? '';
|
|
166
|
-
return
|
|
175
|
+
return emitInteraction(selectorForFormField(target, f.type), `fill(${JSON.stringify(value)})`);
|
|
167
176
|
});
|
|
168
177
|
}
|
|
169
178
|
case 'browser_type': {
|
|
170
179
|
const text = String(input.text ?? '');
|
|
171
180
|
const target = String(input.element ?? '');
|
|
172
|
-
return
|
|
181
|
+
return emitInteraction(selectorFromDescription(target), `fill(${JSON.stringify(text)})`);
|
|
173
182
|
}
|
|
174
183
|
case 'browser_select_option': {
|
|
175
184
|
const target = String(input.element ?? '');
|
|
176
185
|
const values = input.values;
|
|
177
186
|
const val = (values && values.length > 0 ? values[0] : input.value) ?? '';
|
|
178
|
-
return
|
|
187
|
+
return emitInteraction(selectorFromDescription(target), `selectOption(${JSON.stringify(String(val))})`);
|
|
179
188
|
}
|
|
180
189
|
case 'browser_press_key': {
|
|
181
190
|
const key = String(input.key ?? '');
|
|
@@ -197,6 +206,40 @@ function translateStep(tool, rawInput) {
|
|
|
197
206
|
return [`// TODO: translate ${tool} (skipped — unknown tool for spec emission)`];
|
|
198
207
|
}
|
|
199
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* Wrap an interaction (click / dblclick / hover / fill / selectOption) in a
|
|
211
|
+
* block-scoped visibility prelude. Replaces the prior one-liner emit:
|
|
212
|
+
*
|
|
213
|
+
* // before
|
|
214
|
+
* await page.getByRole('button', { name: 'Submit' }).click();
|
|
215
|
+
*
|
|
216
|
+
* // after
|
|
217
|
+
* {
|
|
218
|
+
* const el = page.getByRole('button', { name: 'Submit' });
|
|
219
|
+
* await expect(el).toBeVisible();
|
|
220
|
+
* await el.click();
|
|
221
|
+
* }
|
|
222
|
+
*
|
|
223
|
+
* Why: `getByRole` is "visible OR attached" by default. A button that drifted
|
|
224
|
+
* behind a closed `<details>` / kebab menu / drawer is still in the role tree,
|
|
225
|
+
* so the locator stays green AND `.click()` may still fire — but the actual
|
|
226
|
+
* user flow has degraded. Asserting visibility before each interaction makes
|
|
227
|
+
* that drift fail loudly with "Locator expected to be visible" instead of
|
|
228
|
+
* silently passing or timing out generically. Issue raised externally;
|
|
229
|
+
* the fix is a 1-emit change here.
|
|
230
|
+
*
|
|
231
|
+
* Block-scoped (`{ … }`) so each step's local `el` doesn't shadow the next.
|
|
232
|
+
* The 4-line shape keeps the diff readable when re-record regenerates a spec.
|
|
233
|
+
*/
|
|
234
|
+
function emitInteraction(selectorExpr, action) {
|
|
235
|
+
return [
|
|
236
|
+
`{`,
|
|
237
|
+
` const el = ${selectorExpr};`,
|
|
238
|
+
` await expect(el).toBeVisible();`,
|
|
239
|
+
` await el.${action};`,
|
|
240
|
+
`}`,
|
|
241
|
+
];
|
|
242
|
+
}
|
|
200
243
|
/**
|
|
201
244
|
* Parse element descriptions like "Submit button" / "+1 button" / "Email
|
|
202
245
|
* textbox" / "Plan radio" into `getByRole(role, { name })` selectors. The
|