diff options
| author | amankumarsingh77 <amankumarsingh7702@gmail.com> | 2026-07-28 00:28:45 +0530 |
|---|---|---|
| committer | Aman Kumar <145339995+amankumarsingh77@users.noreply.github.com> | 2026-08-02 19:12:46 +0530 |
| commit | e46f080c03a511b626b21455cfd6094554b02733 (patch) | |
| tree | 54e9866e066ed14fd3a1d20bf14b4435b2fcd9ce /src/domains/form.ts | |
| parent | bfba73fa2f1373abd8a5a704c9c833d6fce1b44f (diff) | |
fix(domains): restore fill_form and set_checked, drop the stale fork
domains/forms.ts was a stale fork of form.ts imported only by a manual
test. It held two working tools the registry never registered: fill_form
and set_checked. Ported both onto the current pattern and deleted the
fork; form.ts versions of the shared tools win.
The fork's tools took ref as Type.Integer (a raw CDP backendNodeId).
browser_snapshot has since moved to string refs ('e7') resolved through
the session ref map, so as written the tools were not callable at all.
Ported them onto resolveRefToObjectId and Type.String refs; the only
user-facing strings that changed are the four that documented the old
[ref=N] syntax. All page-side JS is carried byte-for-byte.
form.ts exceeded 250 lines, so fillForm moved to form-batch.ts per the
single-field/batch split. form.ts is still 425 lines — the pre-existing
three tools account for that, so the split does not reach the target.
test/manual/forms-test.ts is repointed and adapted to publish an 'eN'
ref map, but NOT executed — it needs a live Chrome. Type-checked only.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'src/domains/form.ts')
| -rw-r--r-- | src/domains/form.ts | 112 |
1 files changed, 112 insertions, 0 deletions
diff --git a/src/domains/form.ts b/src/domains/form.ts index a1ae7f5..26268d9 100644 --- a/src/domains/form.ts +++ b/src/domains/form.ts @@ -311,3 +311,115 @@ export const selectOptionTool = defineBrowserTool({ }); }, }); + +export type PageResult = { + readonly ok: boolean; + readonly reason?: string; + readonly kind?: string; + readonly value?: unknown; + readonly text?: string; + readonly checked?: boolean; + readonly changed?: boolean; + readonly options?: ReadonlyArray<{ value: string; text: string }>; +}; + +const isOption = (v: unknown): v is { readonly value: string; readonly text: string } => + typeof v === "object" && v !== null + && "value" in v && typeof v.value === "string" + && "text" in v && typeof v.text === "string"; + +const isPageResult = (v: unknown): v is PageResult => { + if (typeof v !== "object" || v === null) return false; + if (!("ok" in v) || typeof v.ok !== "boolean") return false; + if ("reason" in v && typeof v.reason !== "string") return false; + if ("kind" in v && typeof v.kind !== "string") return false; + if ("text" in v && typeof v.text !== "string") return false; + if ("checked" in v && typeof v.checked !== "boolean") return false; + if ("changed" in v && typeof v.changed !== "boolean") return false; + if ("options" in v && !(Array.isArray(v.options) && v.options.every(isOption))) return false; + return true; +}; + +export const resolveAndCall = async ( + client: BrowserClient, + ref: string, + functionDeclaration: string, + args: ReadonlyArray<unknown>, +): Promise<Result<PageResult, ToolErr>> => { + const objectId = await resolveRefToObjectId(client, ref); + if (!objectId.success) return objectId; + const handle = objectId.data; + try { + const called = await client.session().call("Runtime.callFunctionOn", { + objectId: handle, + functionDeclaration, + arguments: args.map((v) => ({ value: v })), + returnByValue: true, + awaitPromise: true, + }); + if (!called.success) return err({ kind: "cdp_error", message: called.error.message, details: { ref } }); + if (called.data.exceptionDetails !== undefined) { + return err({ kind: "cdp_error", message: `page function threw: ${JSON.stringify(called.data.exceptionDetails)}`, details: { ref } }); + } + const value = called.data.result.value; + if (!isPageResult(value)) { + return err({ kind: "internal", message: "page function returned no result object", details: { ref } }); + } + return ok(value); + } finally { + await client.session().call("Runtime.releaseObject", { objectId: handle }); + } +}; + +export const detailsOf = (ref: string, res: PageResult, extra?: Readonly<Record<string, unknown>>): Readonly<Record<string, unknown>> => { + const { ok: _ok, ...rest } = res; + return { ref, ...rest, ...(extra ?? {}) }; +}; + +const CHECK_FN = ` +function(checked) { + const el = this; + const tag = (el.tagName || "").toLowerCase(); + const type = (el.type || "").toLowerCase(); + if (tag !== "input" || (type !== "checkbox" && type !== "radio")) return { ok: false, reason: "ref is not a checkbox or radio" }; + if (el.disabled) return { ok: false, reason: "element is disabled" }; + const want = checked === true; + let changed = false; + if (el.checked !== want) { + try { el.click(); } catch (e) {} + if (el.checked !== want) { el.checked = want; el.dispatchEvent(new Event("change", { bubbles: true })); } + changed = true; + } + return { ok: true, checked: el.checked, changed: changed }; +}`; + +const SetCheckedArgs = Type.Object({ + ref: Type.String({ description: "Stable element ref of a checkbox or radio from browser_snapshot (e.g. 'e7')." }), + checked: Type.Boolean({ description: "Desired checked state." }), +}); + +export const setCheckedTool = defineBrowserTool({ + name: "browser_set_checked", + label: "Browser Set Checked", + description: + "Set a checkbox or radio to a desired checked state by ref. Only acts if the current state differs, fires change, and reports the final state. Get the ref from browser_snapshot's [eN].", + promptSnippet: "Set a checkbox/radio checked state by ref", + promptGuidelines: [ + "checked:true ticks the box, checked:false unticks it. Idempotent — no-op if already in the desired state.", + "For radios, set checked:true on the option you want; the group deselects the others.", + ], + parameters: SetCheckedArgs, + concurrency: "serialized", + async handler(args, { client }): Promise<Result<ToolOk, ToolErr>> { + const r = await resolveAndCall(client, args.ref, CHECK_FN, [args.checked]); + if (!r.success) return r; + const res = r.data; + if (!res.ok) { + return err({ kind: "invalid_state", message: `Could not set checked on ref ${args.ref}: ${res.reason ?? "unknown reason"}`, details: detailsOf(args.ref, res) }); + } + return ok({ + text: `Set ref ${args.ref} checked=${res.checked}` + (res.changed === false ? " (already)" : ""), + details: detailsOf(args.ref, res), + }); + }, +}); |
