Add Chromium Control Canvas extension#1994
Conversation
There was a problem hiding this comment.
main, but PRs should target staged.
The main branch is auto-published from staged and should not receive direct PRs.
Please close this PR and re-open it against the staged branch.
You can change the base branch using the Edit button at the top of this PR,
or run: gh pr edit 1994 --base staged
|
🔴 Contributor Reputation Check: HIGH risk
Maintainers: please review this contributor before merging. |
|
🔴 Contributor Reputation Check: HIGH risk
Maintainers: please review this contributor before merging. |
|
🔴 Contributor Reputation Check: HIGH risk
Maintainers: please review this contributor before merging. |
2e8fad6 to
dbcb140
Compare
|
🔴 Contributor Reputation Check: HIGH risk
Maintainers: please review this contributor before merging. |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
dbcb140 to
ba9d245
Compare
|
🔴 Contributor Reputation Check: HIGH risk
Maintainers: please review this contributor before merging. |
|
🔴 Contributor Reputation Check: HIGH risk
Maintainers: please review this contributor before merging. |
There was a problem hiding this comment.
Pull request overview
Adds a new Copilot Canvas extension (chromium-control-canvas) that launches/attaches to a real headful Chromium instance via Playwright, exposing a small control-strip UI in the canvas panel and agent-callable actions for navigation, inspection, and interaction.
Changes:
- Introduces a new extension runtime (
extension.mjs) that manages Chromium lifecycle, a loopback control server, and agent actions (navigate/snapshot/click/type/screenshot). - Adds the panel UI (
index.html) and installation documentation (README.md) for using the extension. - Adds extension/package metadata (
copilot-extension.json,package.json,package-lock.json) including Playwright dependency.
Show a summary per file
| File | Description |
|---|---|
| extensions/chromium-control-canvas/README.md | Documents purpose, install steps, prerequisites, and supported agent actions. |
| extensions/chromium-control-canvas/package.json | Declares extension package metadata and dependencies. |
| extensions/chromium-control-canvas/package-lock.json | Locks dependency graph for the extension. |
| extensions/chromium-control-canvas/index.html | Implements the canvas panel control-strip UI and preview behavior. |
| extensions/chromium-control-canvas/extension.mjs | Implements Chromium/Playwright control plane, loopback HTTP server, and canvas actions. |
| extensions/chromium-control-canvas/copilot-extension.json | Declares extension identity metadata for the host. |
Copilot's findings
Files not reviewed (1)
- extensions/chromium-control-canvas/package-lock.json: Generated file
- Files reviewed: 5/6 changed files
- Comments generated: 6
| - **Node.js 18 or newer** (Playwright 1.60 requires `node >=18`; older versions hit a | ||
| cryptic engine error). The extension runs as a Node child process. |
| if (req.method === "GET" && pathname.startsWith("/shot/")) { | ||
| const name = decodeURIComponent(pathname.slice("/shot/".length)); | ||
| if (!/^shot-[\w-]+\.png$/.test(name)) { | ||
| sendJson(res, 400, { error: "invalid name" }); | ||
| return; | ||
| } | ||
| const bytes = await readFile(join(ARTIFACTS_DIR, name)); | ||
| res.writeHead(200, { "Content-Type": "image/png", "Cache-Control": "no-store" }); | ||
| res.end(bytes); | ||
| return; | ||
| } |
| const src = "/shot/" + encodeURIComponent(data.name) + "?t=" + Date.now(); | ||
| previewEl.innerHTML = ""; | ||
| const img = document.createElement("img"); | ||
| img.src = src; | ||
| img.alt = data.name; | ||
| previewEl.appendChild(img); | ||
| shotNameEl.textContent = data.name; | ||
| shotOpenEl.href = src; | ||
| shotOpenEl.style.display = ""; |
| const simple = { "/back": "goBack", "/forward": "goForward", "/reload": "reload" }; | ||
| if (req.method === "POST" && simple[pathname]) { | ||
| await page[simple[pathname]]({ waitUntil: "domcontentloaded" }).catch(() => {}); | ||
| sendJson(res, 200, await pageState(page)); | ||
| return; | ||
| } | ||
|
|
||
| if (req.method === "POST" && pathname === "/screenshot") { | ||
| const shot = await screenshot(page); | ||
| sendJson(res, 200, shot); | ||
| return; | ||
| } |
| function normalizeUrl(input) { | ||
| const raw = String(input ?? "").trim(); | ||
| if (!raw) return "about:blank"; | ||
| if (raw === "about:blank") return raw; | ||
| if (/^[a-z][a-z0-9+.-]*:\/\//i.test(raw)) return raw; | ||
| // Local dev servers have no dot; send them to http, not search. | ||
| if (/^(localhost|127\.0\.0\.1|\[::1\])(:\d+)?([/?#]|$)/i.test(raw)) return `http://${raw}`; | ||
| if (!/\s/.test(raw) && /\.[a-z]{2,}/i.test(raw)) return `https://${raw}`; | ||
| return `https://www.google.com/search?q=${encodeURIComponent(raw)}`; | ||
| } |
| if (req.method === "POST" && pathname === "/navigate") { | ||
| const body = JSON.parse((await readBody(req)) || "{}"); | ||
| try { | ||
| const state = await navigate(page, body?.url); | ||
| await audit({ source: "panel", instanceId: entry.instanceId, action: "navigate", input: body?.url, url: state.url, ok: true }); | ||
| sendJson(res, 200, state); | ||
| } catch (err) { | ||
| sendJson(res, 200, { ...(await pageState(page)), error: publicErrorMessage(err, "Navigation failed.") }); | ||
| } | ||
| return; | ||
| } |
Summary
extensions/chromium-control-canvasValidation
node --check extensions/chromium-control-canvas/extension.mjsnpm run buildnpm run skill:validatenpm run website:data