@narumitw/pi-chrome-devtools 0.51.0 → 0.52.1

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.
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/tools.ts", "../src/cdp-client.ts", "../src/render.ts", "../src/screenshot.ts", "../src/chrome-devtools.ts"],
4
+ "sourcesContent": ["import { defineTool } from \"@earendil-works/pi-coding-agent\";\nimport { Type } from \"typebox\";\nimport {\n\tformatPage,\n\tgetPage,\n\tlistPages,\n\tresolvePage,\n\tresolvePageForNavigation,\n\ttextResult,\n\twithCdp,\n} from \"./cdp-client.js\";\nimport { renderScreenshotResult, renderTextResult, renderToolCall, withStatus } from \"./render.js\";\nimport { state } from \"./runtime.js\";\nimport { formatScreenshotText, saveScreenshot, throwIfAborted } from \"./screenshot.js\";\nimport { CHROME_DEVTOOLS_TOOL_NAMES } from \"./tool-names.js\";\n\nexport const listPagesTool = defineTool({\n\tname: CHROME_DEVTOOLS_TOOL_NAMES[0],\n\tlabel: \"Chrome DevTools: List Pages\",\n\tdescription: \"List Chrome tabs/pages from a running Chrome DevTools Protocol endpoint.\",\n\tparameters: Type.Object({}),\n\trenderCall: renderToolCall(\"list pages\"),\n\trenderResult: renderTextResult,\n\tasync execute(_toolCallId, _params, _signal, _onUpdate, ctx) {\n\t\treturn withStatus(ctx, \"list pages\", async () => {\n\t\t\tconst pages = await listPages();\n\t\t\treturn textResult(JSON.stringify(pages.map(formatPage), null, 2), { pages });\n\t\t});\n\t},\n});\n\nexport const selectPageTool = defineTool({\n\tname: CHROME_DEVTOOLS_TOOL_NAMES[1],\n\tlabel: \"Chrome DevTools: Select Page\",\n\tdescription: \"Select the active Chrome page for later chrome_devtools_* tool calls.\",\n\tparameters: Type.Object({\n\t\tpageId: Type.String({ description: \"Page id from chrome_devtools_list_pages.\" }),\n\t}),\n\trenderCall: renderToolCall(\"select page\"),\n\trenderResult: renderTextResult,\n\tasync execute(_toolCallId, params, _signal, _onUpdate, ctx) {\n\t\treturn withStatus(ctx, \"select page\", async () => {\n\t\t\tconst page = await getPage(params.pageId);\n\t\t\tstate.activePageId = page.id;\n\t\t\treturn textResult(`Selected page ${page.id}: ${page.title}\\n${page.url}`, {\n\t\t\t\tpage: formatPage(page),\n\t\t\t});\n\t\t});\n\t},\n});\n\nexport const navigateTool = defineTool({\n\tname: CHROME_DEVTOOLS_TOOL_NAMES[2],\n\tlabel: \"Chrome DevTools: Navigate\",\n\tdescription:\n\t\t\"Navigate a Chrome page to a URL through Chrome DevTools Protocol, creating a page first if none is available.\",\n\tparameters: Type.Object({\n\t\turl: Type.String({ description: \"URL to navigate to.\" }),\n\t\tpageId: Type.Optional(\n\t\t\tType.String({ description: \"Optional page id. Defaults to selected or first page.\" }),\n\t\t),\n\t}),\n\trenderCall: renderToolCall(\"navigate\"),\n\trenderResult: renderTextResult,\n\tasync execute(_toolCallId, params, _signal, _onUpdate, ctx) {\n\t\treturn withStatus(ctx, \"navigate\", async () => {\n\t\t\tconst { created, page } = await resolvePageForNavigation(params.pageId);\n\t\t\tconst result = await withCdp(page, async (client) => {\n\t\t\t\tawait client.send(\"Page.enable\");\n\t\t\t\treturn client.send(\"Page.navigate\", { url: params.url });\n\t\t\t});\n\n\t\t\tstate.activePageId = page.id;\n\t\t\tconst action = created ? \"Created page and navigated\" : \"Navigated\";\n\t\t\treturn textResult(`${action} ${page.id} to ${params.url}`, {\n\t\t\t\tcreated,\n\t\t\t\tpage: formatPage(page),\n\t\t\t\tresult,\n\t\t\t});\n\t\t});\n\t},\n});\n\nexport const evaluateTool = defineTool({\n\tname: CHROME_DEVTOOLS_TOOL_NAMES[3],\n\tlabel: \"Chrome DevTools: Evaluate\",\n\tdescription: \"Evaluate JavaScript in a Chrome page through Chrome DevTools Protocol.\",\n\tparameters: Type.Object({\n\t\texpression: Type.String({ description: \"JavaScript expression to evaluate.\" }),\n\t\tpageId: Type.Optional(\n\t\t\tType.String({ description: \"Optional page id. Defaults to selected or first page.\" }),\n\t\t),\n\t\tawaitPromise: Type.Optional(\n\t\t\tType.Boolean({ description: \"Whether to await a returned Promise. Defaults to true.\" }),\n\t\t),\n\t}),\n\trenderCall: renderToolCall(\"evaluate\"),\n\trenderResult: renderTextResult,\n\tasync execute(_toolCallId, params, _signal, _onUpdate, ctx) {\n\t\treturn withStatus(ctx, \"evaluate\", async () => {\n\t\t\tconst page = await resolvePage(params.pageId);\n\t\t\tconst result = await withCdp(page, (client) =>\n\t\t\t\tclient.send(\"Runtime.evaluate\", {\n\t\t\t\t\texpression: params.expression,\n\t\t\t\t\tawaitPromise: params.awaitPromise ?? true,\n\t\t\t\t\treturnByValue: true,\n\t\t\t\t}),\n\t\t\t);\n\n\t\t\tstate.activePageId = page.id;\n\t\t\treturn textResult(JSON.stringify(result, null, 2), { page: formatPage(page), result });\n\t\t});\n\t},\n});\n\nexport const screenshotTool = defineTool({\n\tname: CHROME_DEVTOOLS_TOOL_NAMES[4],\n\tlabel: \"Chrome DevTools: Screenshot\",\n\tdescription: \"Capture a PNG screenshot from a Chrome page through Chrome DevTools Protocol.\",\n\tparameters: Type.Object({\n\t\tpageId: Type.Optional(\n\t\t\tType.String({ description: \"Optional page id. Defaults to selected or first page.\" }),\n\t\t),\n\t\tfullPage: Type.Optional(\n\t\t\tType.Boolean({ description: \"Capture the full document, not just the viewport.\" }),\n\t\t),\n\t\tsavePath: Type.Optional(\n\t\t\tType.String({\n\t\t\t\tdescription:\n\t\t\t\t\t\"Screenshot is always saved as a PNG file. Optional output path; omitted defaults to a unique temp file. Relative paths resolve from the current working directory. A single leading @ is stripped to match Pi file-mention paths. Existing regular files are replaced.\",\n\t\t\t}),\n\t\t),\n\t}),\n\trenderCall: renderToolCall(\"screenshot\"),\n\trenderResult: renderScreenshotResult,\n\tasync execute(_toolCallId, params, signal, _onUpdate, ctx) {\n\t\treturn withStatus(ctx, \"screenshot\", async () => {\n\t\t\tconst page = await resolvePage(params.pageId);\n\t\t\tconst result = await withCdp(page, async (client) => {\n\t\t\t\tthrowIfAborted(signal);\n\t\t\t\tawait client.send(\"Page.enable\");\n\n\t\t\t\tif (!params.fullPage) {\n\t\t\t\t\treturn client.send<{ data: string }>(\"Page.captureScreenshot\", { format: \"png\" });\n\t\t\t\t}\n\n\t\t\t\tconst metrics = await client.send<{\n\t\t\t\t\tcontentSize: { x: number; y: number; width: number; height: number };\n\t\t\t\t}>(\"Page.getLayoutMetrics\");\n\n\t\t\t\tthrowIfAborted(signal);\n\t\t\t\treturn client.send<{ data: string }>(\"Page.captureScreenshot\", {\n\t\t\t\t\tcaptureBeyondViewport: true,\n\t\t\t\t\tformat: \"png\",\n\t\t\t\t\tclip: {\n\t\t\t\t\t\tx: metrics.contentSize.x,\n\t\t\t\t\t\ty: metrics.contentSize.y,\n\t\t\t\t\t\twidth: metrics.contentSize.width,\n\t\t\t\t\t\theight: metrics.contentSize.height,\n\t\t\t\t\t\tscale: 1,\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t});\n\n\t\t\tstate.activePageId = page.id;\n\t\t\tconst savedScreenshot = await saveScreenshot(result.data, params.savePath, ctx.cwd, signal);\n\t\t\treturn {\n\t\t\t\tcontent: [\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\ttext: formatScreenshotText(page, savedScreenshot),\n\t\t\t\t\t},\n\t\t\t\t\t{ type: \"image\", data: result.data, mimeType: \"image/png\" },\n\t\t\t\t],\n\t\t\t\tdetails: {\n\t\t\t\t\tpage: formatPage(page),\n\t\t\t\t\tbytes: savedScreenshot.bytes,\n\t\t\t\t\tsavedPath: savedScreenshot.savedPath,\n\t\t\t\t\tisDefaultPath: savedScreenshot.isDefaultPath,\n\t\t\t\t},\n\t\t\t};\n\t\t});\n\t},\n});\n", "import {\n\tdevToolsEndpoint,\n\tensureDevToolsEndpoint,\n\tfetchDevToolsJson,\n\tformatPageListItem,\n\tlaunchHint,\n\twithEndpointRetry,\n} from \"./browser-manager.js\";\nimport {\n\tDEFAULT_ENDPOINT_WAIT_MS,\n\tDEFAULT_TIMEOUT_MS,\n\ttype DevToolsPage,\n\tstate,\n} from \"./runtime.js\";\n\ninterface CdpResponse<T = unknown> {\n\tid: number;\n\tresult?: T;\n\terror?: { code: number; message: string; data?: unknown };\n}\n\nexport async function listPages(options: { waitMs?: number } = {}) {\n\tconst waitMs = options.waitMs ?? DEFAULT_ENDPOINT_WAIT_MS;\n\tawait ensureDevToolsEndpoint(waitMs);\n\treturn withEndpointRetry(async () => {\n\t\tconst pages = await fetchDevToolsJson<DevToolsPage[]>(\"/json/list\");\n\t\treturn pages.filter((page) => page.type === \"page\" && page.webSocketDebuggerUrl);\n\t}, waitMs);\n}\n\nexport async function getPage(pageId: string) {\n\tconst pages = await listPages();\n\treturn requirePage(pageId, pages);\n}\n\nexport async function resolvePage(pageId?: string) {\n\tconst pages = await listPages();\n\tif (pageId) return requirePage(pageId, pages);\n\n\tconst page = resolveDefaultPage(pages);\n\tif (!page) {\n\t\tthrow new Error(\n\t\t\t[\n\t\t\t\t`No Chrome pages found at ${devToolsEndpoint()}.`,\n\t\t\t\t\"Use chrome_devtools_navigate with a URL to create a page, or open a Chrome tab manually.\",\n\t\t\t\tlaunchHint(),\n\t\t\t].join(\"\\n\"),\n\t\t);\n\t}\n\n\treturn page;\n}\n\nexport async function resolvePageForNavigation(pageId?: string) {\n\tconst pages = await listPages();\n\tif (pageId) return { created: false, page: requirePage(pageId, pages) };\n\n\tconst page = resolveDefaultPage(pages);\n\tif (page) return { created: false, page };\n\n\treturn { created: true, page: await createPage(\"about:blank\") };\n}\n\nfunction resolveDefaultPage(pages: DevToolsPage[]) {\n\tif (!state.activePageId) return pages[0];\n\n\tconst activePage = pages.find((candidate) => candidate.id === state.activePageId);\n\tif (activePage) return activePage;\n\n\tstate.activePageId = undefined;\n\treturn pages[0];\n}\n\nfunction requirePage(pageId: string, pages: DevToolsPage[]) {\n\tconst page = pages.find((candidate) => candidate.id === pageId);\n\tif (page) return page;\n\n\tconst availablePages = pages.map(formatPageListItem).join(\"\\n\");\n\tthrow new Error(\n\t\t[\n\t\t\t`Chrome DevTools page not found: ${pageId}.`,\n\t\t\tavailablePages\n\t\t\t\t? `Available pages:\\n${availablePages}`\n\t\t\t\t: \"No inspectable Chrome pages are currently available.\",\n\t\t].join(\"\\n\"),\n\t);\n}\n\nexport async function createPage(url: string, options: { waitMs?: number } = {}) {\n\tconst waitMs = options.waitMs ?? DEFAULT_ENDPOINT_WAIT_MS;\n\tawait ensureDevToolsEndpoint(waitMs);\n\tconst page = await withEndpointRetry(\n\t\t() =>\n\t\t\tfetchDevToolsJson<DevToolsPage>(`/json/new?${encodeURIComponent(url)}`, {\n\t\t\t\tmethod: \"PUT\",\n\t\t\t}),\n\t\twaitMs,\n\t);\n\tif (page.type !== \"page\" || !page.webSocketDebuggerUrl) {\n\t\tthrow new Error(\"Chrome DevTools created a target that is not an inspectable page.\");\n\t}\n\n\treturn page;\n}\n\nexport function formatPage(page: DevToolsPage) {\n\treturn {\n\t\tid: page.id,\n\t\ttype: page.type,\n\t\ttitle: page.title,\n\t\turl: page.url,\n\t};\n}\n\nexport function textResult(text: string, details: unknown) {\n\treturn {\n\t\tcontent: [{ type: \"text\" as const, text }],\n\t\tdetails,\n\t};\n}\n\nexport async function withCdp<T>(page: DevToolsPage, callback: (client: CdpClient) => Promise<T>) {\n\tif (!page.webSocketDebuggerUrl) throw new Error(`Page has no webSocketDebuggerUrl: ${page.id}`);\n\n\tconst client = await CdpClient.connect(page.webSocketDebuggerUrl);\n\ttry {\n\t\treturn await callback(client);\n\t} finally {\n\t\tclient.close();\n\t}\n}\n\nclass CdpClient {\n\t#nextId = 1;\n\t#pending = new Map<\n\t\tnumber,\n\t\t{\n\t\t\tresolve: (value: unknown) => void;\n\t\t\treject: (reason: unknown) => void;\n\t\t\ttimeout: NodeJS.Timeout;\n\t\t}\n\t>();\n\tprivate readonly socket: WebSocket;\n\n\tprivate constructor(socket: WebSocket) {\n\t\tthis.socket = socket;\n\t\tsocket.addEventListener(\"message\", (event) => {\n\t\t\tconst response = JSON.parse(String(event.data)) as CdpResponse;\n\t\t\tif (typeof response.id !== \"number\") return;\n\n\t\t\tconst pending = this.#pending.get(response.id);\n\t\t\tif (!pending) return;\n\n\t\t\tclearTimeout(pending.timeout);\n\t\t\tthis.#pending.delete(response.id);\n\n\t\t\tif (response.error) {\n\t\t\t\tpending.reject(new Error(`CDP error ${response.error.code}: ${response.error.message}`));\n\t\t\t} else {\n\t\t\t\tpending.resolve(response.result);\n\t\t\t}\n\t\t});\n\n\t\tsocket.addEventListener(\"close\", () => {\n\t\t\tthis.rejectAll(new Error(\"Chrome DevTools WebSocket closed\"));\n\t\t});\n\n\t\tsocket.addEventListener(\"error\", () => {\n\t\t\tthis.rejectAll(new Error(\"Chrome DevTools WebSocket error\"));\n\t\t});\n\t}\n\n\tstatic connect(url: string) {\n\t\treturn new Promise<CdpClient>((resolve, reject) => {\n\t\t\tconst socket = new WebSocket(url);\n\t\t\tconst timeout = setTimeout(() => {\n\t\t\t\tsocket.close();\n\t\t\t\treject(new Error(`Timed out connecting to Chrome DevTools WebSocket: ${url}`));\n\t\t\t}, DEFAULT_TIMEOUT_MS);\n\n\t\t\tsocket.addEventListener(\"open\", () => {\n\t\t\t\tclearTimeout(timeout);\n\t\t\t\tresolve(new CdpClient(socket));\n\t\t\t});\n\n\t\t\tsocket.addEventListener(\"error\", () => {\n\t\t\t\tclearTimeout(timeout);\n\t\t\t\treject(new Error(`Failed to connect to Chrome DevTools WebSocket: ${url}`));\n\t\t\t});\n\t\t});\n\t}\n\n\tsend<T = unknown>(method: string, params?: Record<string, unknown>) {\n\t\tconst id = this.#nextId;\n\t\tthis.#nextId += 1;\n\n\t\treturn new Promise<T>((resolve, reject) => {\n\t\t\tconst timeout = setTimeout(() => {\n\t\t\t\tthis.#pending.delete(id);\n\t\t\t\treject(new Error(`Timed out waiting for CDP response: ${method}`));\n\t\t\t}, DEFAULT_TIMEOUT_MS);\n\n\t\t\tthis.#pending.set(id, { resolve: resolve as (value: unknown) => void, reject, timeout });\n\t\t\tthis.socket.send(JSON.stringify({ id, method, params: params ?? {} }));\n\t\t});\n\t}\n\n\tclose() {\n\t\tthis.socket.close();\n\t}\n\n\tprivate rejectAll(error: Error) {\n\t\tfor (const [id, pending] of this.#pending) {\n\t\t\tclearTimeout(pending.timeout);\n\t\t\tpending.reject(error);\n\t\t\tthis.#pending.delete(id);\n\t\t}\n\t}\n}\n", "import type { AgentToolResult, ToolRenderResultOptions } from \"@earendil-works/pi-coding-agent\";\n\nconst STATUS_KEY = \"chrome-devtools\";\ninterface StatusContext {\n\tui: { setStatus: (key: string, value: string | undefined) => void };\n}\ninterface RenderTheme {\n\tbold(text: string): string;\n\tfg(color: string, text: string): string;\n}\ninterface RenderComponent {\n\tinvalidate(): void;\n\trender(width: number): string[];\n}\n\nexport function renderToolCall(action: string) {\n\treturn () => new PiTextComponent(`Chrome DevTools: ${action}`);\n}\n\nexport function renderTextResult(\n\tresult: AgentToolResult<unknown>,\n\toptions: ToolRenderResultOptions,\n\ttheme: RenderTheme,\n) {\n\tconst output = formatCollapsibleOutput(textContent(result), options);\n\treturn new PiTextComponent(output.text, theme, output.color);\n}\n\nexport function renderScreenshotResult(\n\tresult: AgentToolResult<unknown>,\n\toptions: ToolRenderResultOptions,\n\ttheme: RenderTheme,\n): RenderComponent {\n\tconst output = formatCollapsibleOutput(screenshotTextContent(result), options);\n\treturn new PiTextComponent(output.text, theme, output.color);\n}\n\nfunction textContent(result: AgentToolResult<unknown>) {\n\treturn result.content\n\t\t.flatMap((content) => (content.type === \"text\" ? [content.text] : []))\n\t\t.join(\"\\n\");\n}\n\nfunction screenshotTextContent(result: AgentToolResult<unknown>) {\n\tconst text = textContent(result);\n\tif (text.trim()) return text;\n\n\tconst details = result.details as { savedPath?: unknown; bytes?: unknown } | undefined;\n\tif (typeof details?.savedPath !== \"string\") return text;\n\tconst bytes = typeof details.bytes === \"number\" ? ` (${details.bytes} bytes)` : \"\";\n\treturn `Saved screenshot to ${details.savedPath}${bytes}`;\n}\n\nfunction formatCollapsibleOutput(\n\ttext: string,\n\toptions: ToolRenderResultOptions,\n): { text: string; color?: string } {\n\tif (options.isPartial) return { text: \"Running...\", color: \"warning\" };\n\tif (options.expanded) return { text, color: \"toolOutput\" };\n\n\treturn { text: \"\" };\n}\n\nclass PiTextComponent implements RenderComponent {\n\tprivate text: string;\n\tprivate readonly theme?: RenderTheme;\n\tprivate readonly color?: string;\n\n\tconstructor(text = \"\", theme?: RenderTheme, color?: string) {\n\t\tthis.text = text;\n\t\tthis.theme = theme;\n\t\tthis.color = color;\n\t}\n\n\tsetText(text: string) {\n\t\tthis.text = text;\n\t}\n\n\tinvalidate() {\n\t\t// Stateless renderer: no cached layout to invalidate.\n\t}\n\n\trender(width: number) {\n\t\tif (!this.text.trim()) return [];\n\t\treturn this.text\n\t\t\t.replace(/\\t/g, \" \")\n\t\t\t.split(/\\r?\\n/)\n\t\t\t.map((line) => {\n\t\t\t\tconst truncatedLine = truncateLine(line, Math.max(1, width));\n\t\t\t\treturn this.theme && this.color ? this.theme.fg(this.color, truncatedLine) : truncatedLine;\n\t\t\t});\n\t}\n}\n\nfunction truncateLine(line: string, maxWidth: number) {\n\treturn Array.from(line).slice(0, maxWidth).join(\"\");\n}\n\nexport async function withStatus<T>(\n\tctx: StatusContext,\n\tstatus: string,\n\tcallback: () => Promise<T>,\n) {\n\tctx.ui.setStatus(STATUS_KEY, status);\n\ttry {\n\t\treturn await callback();\n\t} finally {\n\t\tctx.ui.setStatus(STATUS_KEY, undefined);\n\t}\n}\n", "import { randomUUID } from \"node:crypto\";\nimport { lstat, mkdir, realpath, rename, rm, writeFile } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { basename, dirname, isAbsolute, join, relative, resolve, sep } from \"node:path\";\nimport { withFileMutationQueue } from \"@earendil-works/pi-coding-agent\";\nimport type { DevToolsPage } from \"./runtime.js\";\n\nfunction unique<T>(values: T[]) {\n\treturn Array.from(new Set(values));\n}\n\nfunction isNodeError(error: unknown): error is NodeJS.ErrnoException {\n\treturn error instanceof Error && \"code\" in error;\n}\n\nexport interface ResolvedScreenshotPath {\n\tpath: string;\n\tallowedRoots: string[];\n\tisDefault: boolean;\n}\nexport interface ScreenshotSaveResult {\n\tsavedPath: string;\n\tbytes: number;\n\tisDefaultPath: boolean;\n}\n\nexport async function saveScreenshot(\n\tbase64Png: string,\n\tsavePath: string | undefined,\n\tcwd: string,\n\tsignal: AbortSignal | undefined,\n): Promise<ScreenshotSaveResult> {\n\tconst resolvedPath = resolveScreenshotPath(savePath, cwd);\n\tconst pngBytes = Buffer.from(base64Png, \"base64\");\n\n\tawait withFileMutationQueue(resolvedPath.path, async () => {\n\t\tthrowIfAborted(signal);\n\t\tawait ensureSafeScreenshotParent(resolvedPath);\n\t\tawait assertSafeScreenshotTargetPath(resolvedPath);\n\t\tawait writeScreenshotFileSafely(resolvedPath, pngBytes, signal);\n\t});\n\n\treturn {\n\t\tsavedPath: resolvedPath.path,\n\t\tbytes: pngBytes.byteLength,\n\t\tisDefaultPath: resolvedPath.isDefault,\n\t};\n}\n\nexport function resolveScreenshotPath(\n\tsavePath: string | undefined,\n\tcwd: string,\n): ResolvedScreenshotPath {\n\tconst cwdRoot = resolve(cwd);\n\tconst tempRoot = resolve(tmpdir());\n\n\tif (savePath === undefined) {\n\t\treturn {\n\t\t\tpath: join(tempRoot, `pi-chrome-devtools-screenshot-${randomUUID()}.png`),\n\t\t\tallowedRoots: [tempRoot],\n\t\t\tisDefault: true,\n\t\t};\n\t}\n\n\tconst normalizedPath = stripLeadingAtPath(savePath);\n\tif (!normalizedPath.trim()) {\n\t\tthrow new Error(\"Screenshot savePath must not be empty.\");\n\t}\n\tif (normalizedPath.includes(\"\\0\")) {\n\t\tthrow new Error(\"Screenshot savePath must not contain NUL bytes.\");\n\t}\n\tif (hasParentPathSegment(normalizedPath)) {\n\t\tthrow new Error(\"Screenshot savePath must not contain '..' path segments.\");\n\t}\n\n\tconst isAbsolutePath = isAbsolute(normalizedPath);\n\tconst path = isAbsolutePath ? resolve(normalizedPath) : resolve(cwdRoot, normalizedPath);\n\tconst allowedRoots = isAbsolutePath ? unique([cwdRoot, tempRoot]) : [cwdRoot];\n\tif (!allowedRoots.some((root) => isPathInsideRoot(path, root))) {\n\t\tthrow new Error(\n\t\t\t\"Screenshot savePath must be relative to the current working directory, or an absolute path inside the current working directory or OS temp directory.\",\n\t\t);\n\t}\n\n\treturn { path, allowedRoots, isDefault: false };\n}\n\nfunction stripLeadingAtPath(path: string) {\n\treturn path.startsWith(\"@\") ? path.slice(1) : path;\n}\n\nexport function hasParentPathSegment(path: string) {\n\treturn path.split(/[\\\\/]+/).some((part) => part === \"..\");\n}\n\nasync function ensureSafeScreenshotParent(resolvedPath: ResolvedScreenshotPath) {\n\tconst parentPath = dirname(resolvedPath.path);\n\tconst rootPath = selectAllowedRoot(parentPath, resolvedPath.allowedRoots);\n\tif (!rootPath) {\n\t\tthrow new Error(\n\t\t\t\"Screenshot savePath parent must stay inside the current working directory or OS temp directory.\",\n\t\t);\n\t}\n\n\tconst realRootPath = await realpath(rootPath);\n\tlet currentPath = rootPath;\n\tconst parentSegments = relative(rootPath, parentPath)\n\t\t.split(/[\\\\/]+/)\n\t\t.filter((part) => part.length > 0);\n\n\tfor (const segment of parentSegments) {\n\t\tcurrentPath = join(currentPath, segment);\n\t\tawait ensureSafeDirectorySegment(currentPath, realRootPath);\n\t}\n}\n\nexport function selectAllowedRoot(path: string, roots: readonly string[]) {\n\tconst matchingRoots = roots.filter((root) => isPathInsideRoot(path, root));\n\tmatchingRoots.sort(\n\t\t(left, right) =>\n\t\t\tnormalizePathForComparison(resolve(right)).length -\n\t\t\tnormalizePathForComparison(resolve(left)).length,\n\t);\n\treturn matchingRoots[0];\n}\n\nasync function ensureSafeDirectorySegment(path: string, realRootPath: string) {\n\tconst existingDirectory = await lstat(path).catch(async (error: unknown) => {\n\t\tif (!isNodeError(error) || error.code !== \"ENOENT\") throw error;\n\t\tawait mkdir(path).catch((mkdirError: unknown) => {\n\t\t\tif (!isNodeError(mkdirError) || mkdirError.code !== \"EEXIST\") throw mkdirError;\n\t\t});\n\t\treturn lstat(path);\n\t});\n\n\tif (existingDirectory.isSymbolicLink()) {\n\t\tthrow new Error(\"Screenshot savePath parent directories must not contain symbolic links.\");\n\t}\n\tif (!existingDirectory.isDirectory()) {\n\t\tthrow new Error(\"Screenshot savePath parent must be a directory.\");\n\t}\n\tawait assertPathWithinRealRoot(path, realRootPath);\n}\n\nasync function assertSafeScreenshotTargetPath(resolvedPath: ResolvedScreenshotPath) {\n\tconst existingTarget = await lstat(resolvedPath.path).catch((error: unknown) => {\n\t\tif (isNodeError(error) && error.code === \"ENOENT\") return undefined;\n\t\tthrow error;\n\t});\n\tif (existingTarget?.isSymbolicLink()) {\n\t\tthrow new Error(\"Screenshot savePath must not point to a symbolic link.\");\n\t}\n\tif (existingTarget?.isDirectory()) {\n\t\tthrow new Error(\"Screenshot savePath must point to a file, not a directory.\");\n\t}\n\tif (existingTarget && !existingTarget.isFile()) {\n\t\tthrow new Error(\"Screenshot savePath may only replace regular files.\");\n\t}\n\n\tconst realAllowedRoots = await Promise.all(resolvedPath.allowedRoots.map(realpathOrResolvedPath));\n\tconst realParent = await realpath(dirname(resolvedPath.path));\n\tconst realTargetPath = join(realParent, basename(resolvedPath.path));\n\tif (!realAllowedRoots.some((root) => isPathInsideRoot(realTargetPath, root))) {\n\t\tthrow new Error(\n\t\t\t\"Screenshot savePath resolves outside the current working directory or OS temp directory.\",\n\t\t);\n\t}\n}\n\nasync function assertPathWithinRealRoot(path: string, realRootPath: string) {\n\tconst realPath = await realpath(path);\n\tif (!isPathInsideRoot(realPath, realRootPath)) {\n\t\tthrow new Error(\n\t\t\t\"Screenshot savePath parent resolves outside the current working directory or OS temp directory.\",\n\t\t);\n\t}\n}\n\nasync function writeScreenshotFileSafely(\n\tresolvedPath: ResolvedScreenshotPath,\n\tpngBytes: Buffer,\n\tsignal: AbortSignal | undefined,\n) {\n\tconst tempFile = join(\n\t\tdirname(resolvedPath.path),\n\t\t`.${basename(resolvedPath.path)}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`,\n\t);\n\ttry {\n\t\tawait writeFile(tempFile, pngBytes, { flag: \"wx\", signal });\n\t\tthrowIfAborted(signal);\n\t\tawait replaceScreenshotFile(resolvedPath, tempFile, signal);\n\t} catch (error) {\n\t\tawait rm(tempFile, { force: true }).catch(() => undefined);\n\t\tthrow error;\n\t}\n}\n\nasync function replaceScreenshotFile(\n\tresolvedPath: ResolvedScreenshotPath,\n\ttempFile: string,\n\tsignal: AbortSignal | undefined,\n) {\n\ttry {\n\t\tawait rename(tempFile, resolvedPath.path);\n\t\treturn;\n\t} catch (error) {\n\t\tif (!shouldRetryRenameAfterRemovingDestination(error)) throw error;\n\t}\n\n\t// Some Windows filesystems reject renaming over an existing file. Revalidate before\n\t// removing the destination so the fallback still refuses directories and symlinks.\n\tawait assertSafeScreenshotTargetPath(resolvedPath);\n\tthrowIfAborted(signal);\n\tawait rm(resolvedPath.path, { force: true });\n\tawait rename(tempFile, resolvedPath.path);\n}\n\nfunction shouldRetryRenameAfterRemovingDestination(error: unknown) {\n\treturn (\n\t\tprocess.platform === \"win32\" &&\n\t\tisNodeError(error) &&\n\t\t[\"EACCES\", \"EEXIST\", \"EPERM\"].includes(error.code ?? \"\")\n\t);\n}\n\nasync function realpathOrResolvedPath(path: string) {\n\treturn realpath(path).catch(() => resolve(path));\n}\n\nexport function isPathInsideRoot(path: string, root: string) {\n\tconst normalizedPath = normalizePathForComparison(resolve(path));\n\tconst normalizedRoot = normalizePathForComparison(resolve(root));\n\tif (normalizedPath === normalizedRoot) return true;\n\tconst rootWithSeparator = normalizedRoot.endsWith(sep)\n\t\t? normalizedRoot\n\t\t: `${normalizedRoot}${sep}`;\n\treturn normalizedPath.startsWith(rootWithSeparator);\n}\n\nfunction normalizePathForComparison(path: string) {\n\treturn process.platform === \"win32\" ? path.toLowerCase() : path;\n}\n\nexport function throwIfAborted(signal: AbortSignal | undefined) {\n\tif (!signal?.aborted) return;\n\tthrow signal.reason instanceof Error ? signal.reason : new Error(\"Screenshot capture cancelled.\");\n}\n\nexport function formatScreenshotText(page: DevToolsPage, screenshot: ScreenshotSaveResult) {\n\tconst pathLabel = screenshot.isDefaultPath ? \"Saved to temp file\" : \"Saved to\";\n\treturn [\n\t\t`Captured PNG screenshot from ${page.title || page.url || page.id}.`,\n\t\t`${pathLabel}: ${screenshot.savedPath}`,\n\t\t`Bytes: ${screenshot.bytes}`,\n\t\t`Use read({ path: ${JSON.stringify(screenshot.savedPath)} }) to inspect the saved screenshot if inline image content is not available.`,\n\t].join(\"\\n\");\n}\n", "import type { ExtensionAPI, ExtensionCommandContext } from \"@earendil-works/pi-coding-agent\";\nimport { shutdownManagedBrowser } from \"./browser-manager.js\";\nimport {\n\tavailableChromeDevtoolsTools,\n\tconfigureLazyChromeDevtoolsTools,\n\tcreateChromeDevtoolsLoadTool,\n\tinitializeAvailableChromeDevtoolsTools,\n} from \"./lazy-tools.js\";\nimport { applyRuntimeBrowserSettings, state } from \"./runtime.js\";\nimport { loadSettings, waitForSettingsWrites } from \"./settings.js\";\nimport {\n\tallChromeDevtoolsTools,\n\tbuildCommandGuide,\n\tbuildQuickstartMessage,\n\tbuildToolStatusMessage,\n\tsanitizeChromeDevtoolsDisplay,\n\tupdateChromeDevtoolsTools,\n\twaitForChromeDevtoolsSettings,\n} from \"./tool-selector.js\";\nimport {\n\tevaluateTool,\n\tlistPagesTool,\n\tnavigateTool,\n\tscreenshotTool,\n\tselectPageTool,\n} from \"./tools.js\";\n\ntype CommandAction =\n\t| \"menu\"\n\t| \"help\"\n\t| \"quickstart\"\n\t| \"status\"\n\t| \"settings\"\n\t| \"tools\"\n\t| \"enable\"\n\t| \"disable\";\ntype CommandContext = ExtensionCommandContext;\nconst STATUS_KEY = \"chrome-devtools\";\nconst COMMAND_COMPLETIONS = [\n\t{ value: \"help\", label: \"help\", description: \"Show command usage\" },\n\t{ value: \"quickstart\", label: \"quickstart\", description: \"Show endpoint and launch help\" },\n\t{ value: \"status\", label: \"status\", description: \"Show tool and settings status\" },\n\t{ value: \"settings\", label: \"settings\", description: \"Edit browser connection settings\" },\n\t{ value: \"tools\", label: \"tools\", description: \"Choose lazy-loadable Chrome DevTools tools\" },\n\t{ value: \"toggle\", label: \"toggle\", description: \"Alias for tools\" },\n\t{ value: \"select\", label: \"select\", description: \"Compatibility alias for tools\" },\n\t{ value: \"enable\", label: \"enable\", description: \"Make all Chrome DevTools tools available\" },\n\t{ value: \"on\", label: \"on\", description: \"Compatibility alias for enable\" },\n\t{ value: \"disable\", label: \"disable\", description: \"Make all Chrome DevTools tools unavailable\" },\n\t{ value: \"off\", label: \"off\", description: \"Compatibility alias for disable\" },\n];\nexport default function chromeDevtools(pi: ExtensionAPI) {\n\tpi.registerTool(listPagesTool);\n\tpi.registerTool(selectPageTool);\n\tpi.registerTool(navigateTool);\n\tpi.registerTool(evaluateTool);\n\tpi.registerTool(screenshotTool);\n\tpi.registerTool(createChromeDevtoolsLoadTool(pi));\n\n\tpi.registerCommand(\"chrome-devtools\", {\n\t\tdescription: \"Open Chrome DevTools help and tool controls\",\n\t\tgetArgumentCompletions: (prefix) => commandCompletions(prefix),\n\t\thandler: async (args, ctx) => {\n\t\t\tinitializeAvailableChromeDevtoolsTools(pi);\n\t\t\tconst generation = state.sessionGeneration;\n\t\t\tawait handleChromeDevtoolsCommand(pi, args, ctx, generation);\n\t\t},\n\t});\n\n\tpi.on(\"session_start\", async (_event, ctx) => {\n\t\tconst generation = ++state.sessionGeneration;\n\t\tinitializeAvailableChromeDevtoolsTools(pi);\n\t\treplaceSessionController(\"Chrome DevTools session replaced\");\n\t\tstate.shuttingDown = false;\n\t\tstate.settingsNotice = undefined;\n\t\tctx.ui.setStatus(STATUS_KEY, undefined);\n\t\tawait shutdownManagedBrowser();\n\t\tif (generation !== state.sessionGeneration) return;\n\t\tstate.activePageId = undefined;\n\t\tstate.lastLaunchAttempt = undefined;\n\t\tconst projectTrusted = ctx.isProjectTrusted();\n\t\tconst settings = await loadSettings({ cwd: ctx.cwd, projectTrusted });\n\t\tif (generation !== state.sessionGeneration) return;\n\t\tapplyRuntimeBrowserSettings(settings.effectiveBrowser, settings.paths, projectTrusted);\n\t\tstate.settingsNotice = settings.notice;\n\t\tfor (const warning of settings.warnings) {\n\t\t\tctx.ui.notify(sanitizeChromeDevtoolsDisplay(warning), \"warning\");\n\t\t}\n\t\tconst availableTools =\n\t\t\tsettings.kind === \"loaded\" && settings.settings.tools\n\t\t\t\t? settings.settings.tools\n\t\t\t\t: availableChromeDevtoolsTools(pi);\n\t\tconfigureLazyChromeDevtoolsTools(pi, availableTools);\n\t});\n\n\tpi.on(\"session_shutdown\", async (_event, ctx) => {\n\t\tstate.sessionGeneration += 1;\n\t\treplaceSessionController(\"Chrome DevTools session shut down\");\n\t\tctx.ui.setStatus(STATUS_KEY, undefined);\n\t\tconst browserShutdown = shutdownManagedBrowser(undefined, { cancelLaunch: true });\n\t\tawait waitForChromeDevtoolsSettings();\n\t\tawait waitForSettingsWrites();\n\t\tawait browserShutdown;\n\t});\n}\n\nasync function handleChromeDevtoolsCommand(\n\tpi: ExtensionAPI,\n\targs: string,\n\tctx: CommandContext,\n\tgeneration: number,\n) {\n\tconst command = parseCommand(args);\n\tswitch (command) {\n\t\tcase \"menu\":\n\t\t\tawait showMenu(pi, ctx, generation);\n\t\t\treturn;\n\t\tcase \"help\":\n\t\t\trequireObservableUi(ctx, \"help\");\n\t\t\tctx.ui.notify(buildCommandGuide(), \"info\");\n\t\t\treturn;\n\t\tcase \"quickstart\":\n\t\t\trequireObservableUi(ctx, \"quickstart\");\n\t\t\tctx.ui.notify(buildQuickstartMessage(), \"info\");\n\t\t\treturn;\n\t\tcase \"status\": {\n\t\t\trequireObservableUi(ctx, \"status\");\n\t\t\tconst status = await buildToolStatusMessage(pi);\n\t\t\tif (generation !== state.sessionGeneration) return;\n\t\t\tctx.ui.notify(status, \"info\");\n\t\t\treturn;\n\t\t}\n\t\tcase \"settings\": {\n\t\t\tif (!ctx.hasUI || (ctx.mode !== \"tui\" && ctx.mode !== \"rpc\")) {\n\t\t\t\tthrow new Error(\"/chrome-devtools settings requires TUI or RPC mode\");\n\t\t\t}\n\t\t\tconst { showChromeDevtoolsBrowserSettings } = await import(\"./browser-settings-menu.js\");\n\t\t\tif (generation !== state.sessionGeneration) return;\n\t\t\tawait showChromeDevtoolsBrowserSettings(ctx, generation);\n\t\t\treturn;\n\t\t}\n\t\tcase \"tools\": {\n\t\t\tif (!ctx.hasUI || (ctx.mode !== \"tui\" && ctx.mode !== \"rpc\")) {\n\t\t\t\tthrow new Error(\"/chrome-devtools tools requires TUI or RPC mode\");\n\t\t\t}\n\t\t\tconst { showChromeDevtoolsToolWorkflow } = await import(\"./menu.js\");\n\t\t\tif (generation !== state.sessionGeneration) return;\n\t\t\tawait showChromeDevtoolsToolWorkflow(pi, ctx, generation);\n\t\t\treturn;\n\t\t}\n\t\tcase \"enable\":\n\t\t\tawait updateChromeDevtoolsTools(pi, ctx, allChromeDevtoolsTools(), \"made all available\");\n\t\t\treturn;\n\t\tcase \"disable\":\n\t\t\tawait updateChromeDevtoolsTools(pi, ctx, [], \"made all unavailable\");\n\t\t\treturn;\n\t}\n\n\tif (!ctx.hasUI || (ctx.mode !== \"tui\" && ctx.mode !== \"rpc\")) {\n\t\tthrow new Error(`Unknown /chrome-devtools command: ${args.trim()}`);\n\t}\n\tctx.ui.notify(\n\t\t`Unknown /chrome-devtools command: ${args.trim()}\n\n${buildCommandGuide()}`,\n\t\t\"warning\",\n\t);\n}\n\nfunction requireObservableUi(ctx: CommandContext, route: string) {\n\tif (!ctx.hasUI || (ctx.mode !== \"tui\" && ctx.mode !== \"rpc\")) {\n\t\tthrow new Error(`/chrome-devtools ${route} requires TUI or RPC mode`);\n\t}\n}\n\nasync function showMenu(pi: ExtensionAPI, ctx: CommandContext, generation: number) {\n\tif (!ctx.hasUI || (ctx.mode !== \"tui\" && ctx.mode !== \"rpc\")) {\n\t\tthrow new Error(\"/chrome-devtools menu requires TUI or RPC mode; use a direct subcommand\");\n\t}\n\tconst { showChromeDevtoolsMenu } = await import(\"./menu.js\");\n\tif (generation !== state.sessionGeneration) return;\n\tawait showChromeDevtoolsMenu(pi, ctx, generation);\n}\n\nfunction replaceSessionController(reason: string) {\n\tstate.sessionController.abort(new DOMException(reason, \"AbortError\"));\n\tstate.sessionController = new AbortController();\n}\n\nexport function parseCommand(args: string): CommandAction | \"unknown\" {\n\tconst command = args.trim().toLowerCase();\n\tif (!command) return \"menu\";\n\tif (command === \"help\") return \"help\";\n\tif (command === \"quickstart\") return \"quickstart\";\n\tif (command === \"status\") return \"status\";\n\tif (command === \"settings\") return \"settings\";\n\tif (command === \"tools\" || command === \"select\" || command === \"toggle\") return \"tools\";\n\tif (command === \"enable\" || command === \"on\") return \"enable\";\n\tif (command === \"disable\" || command === \"off\") return \"disable\";\n\treturn \"unknown\";\n}\n\nexport function commandCompletions(prefix: string) {\n\tconst normalized = prefix.trimStart().toLowerCase();\n\tif (/\\s/.test(normalized)) return null;\n\n\tconst matches = COMMAND_COMPLETIONS.filter((completion) =>\n\t\tcompletion.value.startsWith(normalized),\n\t);\n\treturn matches.length > 0 ? matches : null;\n}\n\nexport {\n\tformatHostForUrl,\n\tisLocalDevToolsHost,\n\tquoteCommandPart,\n} from \"./browser-manager.js\";\nexport { CHROME_DEVTOOLS_LOAD_TOOL_NAME } from \"./lazy-tools.js\";\nexport { parseConfiguredPort } from \"./runtime.js\";\nexport {\n\thasParentPathSegment,\n\tisPathInsideRoot,\n\tresolveScreenshotPath,\n\tselectAllowedRoot,\n} from \"./screenshot.js\";\nexport { normalizeChromeDevtoolsSettings } from \"./settings.js\";\nexport {\n\torderedChromeDevtoolsTools,\n\tsanitizeChromeDevtoolsDisplay,\n} from \"./tool-selector.js\";\n"],
5
+ "mappings": "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",
6
+ "names": ["resolve", "STATUS_KEY"]
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narumitw/pi-chrome-devtools",
3
- "version": "0.51.0",
3
+ "version": "0.52.1",
4
4
  "description": "Pi extension that exposes Chrome DevTools Protocol tools.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -15,30 +15,34 @@
15
15
  ],
16
16
  "files": [
17
17
  "src",
18
+ "dist",
18
19
  "README.md",
19
20
  "LICENSE"
20
21
  ],
21
22
  "pi": {
22
23
  "extensions": [
23
- "./src/index.ts"
24
+ "./dist/index.ts"
24
25
  ]
25
26
  },
26
27
  "piExtension": {
27
28
  "lifecycle": "stable"
28
29
  },
29
30
  "scripts": {
30
- "check": "biome check . && npm run typecheck",
31
+ "build": "node scripts/build-runtime.mjs",
32
+ "check": "npm run build && biome check . && npm run typecheck",
31
33
  "format": "biome check --write .",
32
- "typecheck": "tsc --noEmit"
34
+ "typecheck": "tsc --noEmit",
35
+ "prepack": "npm run build"
33
36
  },
34
37
  "peerDependencies": {
35
38
  "@earendil-works/pi-coding-agent": "*",
36
39
  "typebox": "*"
37
40
  },
38
41
  "devDependencies": {
39
- "@biomejs/biome": "2.5.7",
40
- "@earendil-works/pi-coding-agent": "0.84.1",
41
- "typebox": "1.3.11",
42
+ "@biomejs/biome": "2.5.8",
43
+ "@earendil-works/pi-coding-agent": "0.84.2",
44
+ "esbuild": "0.28.1",
45
+ "typebox": "1.3.14",
42
46
  "typescript": "7.0.2"
43
47
  },
44
48
  "dependencies": {
@@ -143,7 +143,7 @@ async function prepareManagedBrowserLaunch(
143
143
  throwIfBrowserLaunchCancelled(generation, signal);
144
144
  if (!available) {
145
145
  throw new DevToolsEndpointError(
146
- `Cannot launch the extension-owned browser because explicit port ${state.port} is already in use. Choose a free PI_CHROME_DEVTOOLS_PORT or remove the override for a dynamic port.`,
146
+ `Cannot launch the extension-owned browser because explicit port ${state.port} is already in use. Choose a free browser.endpoint port or reset the endpoint to use a dynamic managed port.`,
147
147
  );
148
148
  }
149
149
  }
@@ -609,9 +609,7 @@ export function formatHostForUrl(host: string) {
609
609
  }
610
610
 
611
611
  export function endpointSourceLabel() {
612
- const hostSource = state.hostConfigured ? "PI_CHROME_DEVTOOLS_HOST" : "default host";
613
- const portSource = state.portConfigured ? "PI_CHROME_DEVTOOLS_PORT" : "default/dynamic port";
614
- return `${hostSource}; ${portSource}`;
612
+ return state.endpointSource;
615
613
  }
616
614
 
617
615
  export type BrowserLifecycleState = "starting" | "running" | "exited" | "failed" | "unobserved";
@@ -710,7 +708,7 @@ export function quoteCommandPart(value: string) {
710
708
  }
711
709
 
712
710
  export function endpointConfigHint() {
713
- return "Set PI_CHROME_DEVTOOLS_HOST and PI_CHROME_DEVTOOLS_PORT for a manual endpoint, PI_CHROME_DEVTOOLS_BROWSER to choose an executable, or PI_CHROME_DEVTOOLS_AUTO_LAUNCH=0 to disable auto-launch.";
711
+ return "Configure browser.endpoint, browser.executablePath, and browser.autoLaunch in pi-chrome-devtools.json or the /chrome-devtools Settings flow.";
714
712
  }
715
713
 
716
714
  export function isLocalDevToolsHost(host: string) {
@@ -0,0 +1,311 @@
1
+ import { isAbsolute } from "node:path";
2
+ import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
3
+ import { defineMenu, runMenu } from "@narumitw/pi-tui-kit";
4
+ import { shutdownManagedBrowser } from "./browser-manager.js";
5
+ import { applyRuntimeBrowserSettings, state } from "./runtime.js";
6
+ import {
7
+ type BrowserSettingsPatch,
8
+ type BrowserSettingsSource,
9
+ loadSettings,
10
+ parseBrowserEndpoint,
11
+ type SettingsLoadResult,
12
+ saveBrowserSettings,
13
+ } from "./settings.js";
14
+ import { sanitizeChromeDevtoolsDisplay } from "./tool-selector.js";
15
+
16
+ type BrowserSettingsScreen = "settings" | "endpoint" | "executable" | "extensions";
17
+ type BrowserSettingsAction =
18
+ | "open-endpoint"
19
+ | "save-endpoint"
20
+ | "set-auto-launch"
21
+ | "open-executable"
22
+ | "save-executable"
23
+ | "show-extensions";
24
+
25
+ interface BrowserSettingsMenuState {
26
+ load: SettingsLoadResult;
27
+ }
28
+
29
+ type CommandContext = ExtensionCommandContext;
30
+
31
+ export async function showChromeDevtoolsBrowserSettings(
32
+ ctx: CommandContext,
33
+ generation: number,
34
+ ): Promise<{ closeParent: boolean }> {
35
+ const ownerSignal = state.sessionController.signal;
36
+ const isCurrent = () => generation === state.sessionGeneration && !ownerSignal.aborted;
37
+ const projectTrusted = ctx.isProjectTrusted();
38
+ const menu = defineMenu<BrowserSettingsMenuState, BrowserSettingsScreen, BrowserSettingsAction>({
39
+ start: "settings",
40
+ screens: {
41
+ settings: ({ state: current }) => {
42
+ if (current.load.userFile.kind === "invalid") return invalidSettingsScreen(current.load);
43
+ const browser = current.load.effectiveBrowser;
44
+ return {
45
+ kind: "settings",
46
+ title: "Chrome DevTools Browser settings",
47
+ lines: settingsContextLines(current.load),
48
+ items: [
49
+ {
50
+ id: "endpoint",
51
+ label: "DevTools endpoint",
52
+ description:
53
+ 'HTTP origin with an explicit port; enter "default" to restore 127.0.0.1:9222.',
54
+ currentValue: sanitizeChromeDevtoolsDisplay(browser.endpoint),
55
+ action: "open-endpoint",
56
+ },
57
+ {
58
+ id: "auto-launch",
59
+ label: "Auto-launch",
60
+ description:
61
+ "Launch an isolated managed browser when a local endpoint is unavailable.",
62
+ currentValue: browser.autoLaunchEnabled ? "On" : "Off",
63
+ values: ["On", "Off"],
64
+ action: "set-auto-launch",
65
+ },
66
+ {
67
+ id: "executable",
68
+ label: "Browser executable",
69
+ description:
70
+ 'Absolute executable path; enter "automatic" to restore browser discovery.',
71
+ currentValue: sanitizeChromeDevtoolsDisplay(browser.executablePath ?? "Automatic"),
72
+ action: "open-executable",
73
+ },
74
+ {
75
+ id: "extensions",
76
+ label: "Unpacked extensions",
77
+ description: "Inspect trusted extension paths configured in user or project JSON.",
78
+ currentValue: `${browser.extensionPaths.length} configured`,
79
+ action: "show-extensions",
80
+ },
81
+ ],
82
+ };
83
+ },
84
+ endpoint: ({ state: current }) => ({
85
+ kind: "input",
86
+ title: "DevTools endpoint",
87
+ lines: [
88
+ sanitizeChromeDevtoolsDisplay(
89
+ `Current effective endpoint: ${current.load.effectiveBrowser.endpoint}`,
90
+ ),
91
+ 'Enter an HTTP origin with an explicit port, or "default".',
92
+ ],
93
+ placeholder: "http://127.0.0.1:9222",
94
+ action: "save-endpoint",
95
+ hint: "back",
96
+ }),
97
+ executable: ({ state: current }) => ({
98
+ kind: "input",
99
+ title: "Browser executable",
100
+ lines: [
101
+ sanitizeChromeDevtoolsDisplay(
102
+ `Current effective executable: ${current.load.effectiveBrowser.executablePath ?? "automatic discovery"}`,
103
+ ),
104
+ 'Enter an absolute path, or "automatic".',
105
+ ],
106
+ placeholder: "absolute browser executable path",
107
+ action: "save-executable",
108
+ hint: "back",
109
+ }),
110
+ extensions: ({ state: current }) => ({
111
+ kind: "detail",
112
+ title: "Unpacked extensions",
113
+ lines: extensionDetailLines(current.load),
114
+ hint: "back",
115
+ }),
116
+ },
117
+ actions: {
118
+ "open-endpoint": () => ({ kind: "to", screen: "endpoint" }),
119
+ "save-endpoint": async ({ value, signal }) => {
120
+ if (value === undefined) return { kind: "rejected" };
121
+ let endpoint: string | null;
122
+ try {
123
+ const trimmed = value.trim();
124
+ endpoint =
125
+ trimmed.toLowerCase() === "default" ? null : parseBrowserEndpoint(trimmed).endpoint;
126
+ } catch (error) {
127
+ notifySaveFailure(ctx, error);
128
+ return { kind: "rejected" };
129
+ }
130
+ const saved = await saveAndApplyBrowserPatch(
131
+ ctx,
132
+ generation,
133
+ ownerSignal,
134
+ signal,
135
+ projectTrusted,
136
+ { endpoint },
137
+ );
138
+ if (!saved) return { kind: "rejected" };
139
+ notifyEffectiveSave(
140
+ ctx,
141
+ "endpoint",
142
+ saved.effectiveBrowser.endpoint,
143
+ saved.effectiveBrowser.endpointSource,
144
+ );
145
+ return { kind: "back" };
146
+ },
147
+ "set-auto-launch": async ({ value, signal }) => {
148
+ if (value !== "On" && value !== "Off") return { kind: "rejected" };
149
+ const saved = await saveAndApplyBrowserPatch(
150
+ ctx,
151
+ generation,
152
+ ownerSignal,
153
+ signal,
154
+ projectTrusted,
155
+ { autoLaunch: value === "On" },
156
+ );
157
+ if (!saved) return { kind: "rejected" };
158
+ notifyEffectiveSave(
159
+ ctx,
160
+ "auto-launch",
161
+ saved.effectiveBrowser.autoLaunchEnabled ? "On" : "Off",
162
+ saved.effectiveBrowser.autoLaunchSource,
163
+ );
164
+ return { kind: "stay" };
165
+ },
166
+ "open-executable": () => ({ kind: "to", screen: "executable" }),
167
+ "save-executable": async ({ value, signal }) => {
168
+ if (value === undefined) return { kind: "rejected" };
169
+ const trimmed = value.trim();
170
+ const reset = ["automatic", "default", "none"].includes(trimmed.toLowerCase());
171
+ if (!reset && (!trimmed || !isAbsolute(trimmed))) {
172
+ notifySaveFailure(ctx, new Error('Enter an absolute path or "automatic".'));
173
+ return { kind: "rejected" };
174
+ }
175
+ const executablePath = reset ? null : trimmed;
176
+ const saved = await saveAndApplyBrowserPatch(
177
+ ctx,
178
+ generation,
179
+ ownerSignal,
180
+ signal,
181
+ projectTrusted,
182
+ { executablePath },
183
+ );
184
+ if (!saved) return { kind: "rejected" };
185
+ notifyEffectiveSave(
186
+ ctx,
187
+ "browser executable",
188
+ saved.effectiveBrowser.executablePath ?? "automatic discovery",
189
+ saved.effectiveBrowser.executablePathSource,
190
+ );
191
+ return { kind: "back" };
192
+ },
193
+ "show-extensions": () => ({ kind: "to", screen: "extensions" }),
194
+ },
195
+ });
196
+ const result = await runMenu(ctx, menu, {
197
+ getState: async ({ signal }) => {
198
+ signal.throwIfAborted();
199
+ const load = await loadSettings({ cwd: ctx.cwd, projectTrusted });
200
+ signal.throwIfAborted();
201
+ return { load };
202
+ },
203
+ signal: ownerSignal,
204
+ isCurrent,
205
+ onError: (currentCtx, error) =>
206
+ currentCtx.ui.notify(
207
+ sanitizeChromeDevtoolsDisplay(
208
+ `Chrome DevTools browser settings failed: ${formatError(error)}`,
209
+ ),
210
+ "error",
211
+ ),
212
+ });
213
+ return { closeParent: result.kind === "closed" && result.reason === "close" };
214
+ }
215
+
216
+ async function saveAndApplyBrowserPatch(
217
+ ctx: CommandContext,
218
+ generation: number,
219
+ ownerSignal: AbortSignal,
220
+ actionSignal: AbortSignal,
221
+ projectTrusted: boolean,
222
+ patch: BrowserSettingsPatch,
223
+ ) {
224
+ try {
225
+ await ctx.waitForIdle();
226
+ if (!isCurrent(generation, ownerSignal, actionSignal)) return false;
227
+ await saveBrowserSettings(patch);
228
+ if (!isCurrent(generation, ownerSignal, actionSignal)) return false;
229
+ await shutdownManagedBrowser();
230
+ if (!isCurrent(generation, ownerSignal, actionSignal)) return false;
231
+ const loaded = await loadSettings({ cwd: ctx.cwd, projectTrusted });
232
+ if (!isCurrent(generation, ownerSignal, actionSignal)) return false;
233
+ applyRuntimeBrowserSettings(loaded.effectiveBrowser, loaded.paths, projectTrusted);
234
+ state.settingsNotice = loaded.notice;
235
+ return loaded;
236
+ } catch (error) {
237
+ if (isCurrent(generation, ownerSignal, actionSignal)) notifySaveFailure(ctx, error);
238
+ return false;
239
+ }
240
+ }
241
+
242
+ function isCurrent(generation: number, ownerSignal: AbortSignal, actionSignal: AbortSignal) {
243
+ return generation === state.sessionGeneration && !ownerSignal.aborted && !actionSignal.aborted;
244
+ }
245
+
246
+ function invalidSettingsScreen(load: SettingsLoadResult) {
247
+ const reason =
248
+ load.userFile.kind === "invalid"
249
+ ? load.userFile.reason
250
+ : "The user settings file is unavailable.";
251
+ return {
252
+ kind: "detail" as const,
253
+ title: "Chrome DevTools Browser settings · Read only",
254
+ lines: [
255
+ sanitizeChromeDevtoolsDisplay(
256
+ `Fix the active user settings before saving; the file will not be overwritten. ${reason}`,
257
+ ),
258
+ ],
259
+ hint: "back" as const,
260
+ };
261
+ }
262
+
263
+ function settingsContextLines(load: SettingsLoadResult) {
264
+ const browser = load.effectiveBrowser;
265
+ return [
266
+ `User settings · ${load.paths.user}`,
267
+ `Effective sources: endpoint ${browser.endpointSource} · auto-launch ${browser.autoLaunchSource} · executable ${browser.executablePathSource}`,
268
+ ...(load.notice ? [`Warning: ${load.notice}`] : []),
269
+ ].map((line) => sanitizeChromeDevtoolsDisplay(line));
270
+ }
271
+
272
+ function extensionDetailLines(load: SettingsLoadResult) {
273
+ const browser = load.effectiveBrowser;
274
+ return [
275
+ `Effective source: ${browser.extensionPathsSource}`,
276
+ ...(browser.extensionPaths.length > 0
277
+ ? browser.extensionPaths.map((extensionPath) => `- ${extensionPath}`)
278
+ : ["No unpacked extensions are configured."]),
279
+ "Edit browser.extensionPaths in the user JSON or a trusted project JSON, then run /reload.",
280
+ "Unpacked extensions execute privileged browser code; load only paths you trust.",
281
+ ].map((line) => sanitizeChromeDevtoolsDisplay(line));
282
+ }
283
+
284
+ function notifyEffectiveSave(
285
+ ctx: CommandContext,
286
+ setting: string,
287
+ effectiveValue: string,
288
+ source: BrowserSettingsSource,
289
+ ) {
290
+ const overrideNotice =
291
+ source === "environment" ? " The deprecated environment override remains effective." : "";
292
+ ctx.ui.notify(
293
+ sanitizeChromeDevtoolsDisplay(
294
+ `Chrome DevTools ${setting} saved. Effective ${setting}: ${effectiveValue} (${source}).${overrideNotice}`,
295
+ ),
296
+ "info",
297
+ );
298
+ }
299
+
300
+ function notifySaveFailure(ctx: CommandContext, error: unknown) {
301
+ ctx.ui.notify(
302
+ sanitizeChromeDevtoolsDisplay(
303
+ `Chrome DevTools browser settings save failed; previous settings remain active: ${formatError(error)}`,
304
+ ),
305
+ "warning",
306
+ );
307
+ }
308
+
309
+ function formatError(error: unknown) {
310
+ return error instanceof Error ? error.message : String(error);
311
+ }
@@ -7,7 +7,7 @@ import {
7
7
  initializeAvailableChromeDevtoolsTools,
8
8
  } from "./lazy-tools.js";
9
9
  import { applyRuntimeBrowserSettings, state } from "./runtime.js";
10
- import { loadSettings } from "./settings.js";
10
+ import { loadSettings, waitForSettingsWrites } from "./settings.js";
11
11
  import {
12
12
  allChromeDevtoolsTools,
13
13
  buildCommandGuide,
@@ -25,13 +25,22 @@ import {
25
25
  selectPageTool,
26
26
  } from "./tools.js";
27
27
 
28
- type CommandAction = "menu" | "help" | "quickstart" | "status" | "tools" | "enable" | "disable";
28
+ type CommandAction =
29
+ | "menu"
30
+ | "help"
31
+ | "quickstart"
32
+ | "status"
33
+ | "settings"
34
+ | "tools"
35
+ | "enable"
36
+ | "disable";
29
37
  type CommandContext = ExtensionCommandContext;
30
38
  const STATUS_KEY = "chrome-devtools";
31
39
  const COMMAND_COMPLETIONS = [
32
40
  { value: "help", label: "help", description: "Show command usage" },
33
41
  { value: "quickstart", label: "quickstart", description: "Show endpoint and launch help" },
34
42
  { value: "status", label: "status", description: "Show tool and settings status" },
43
+ { value: "settings", label: "settings", description: "Edit browser connection settings" },
35
44
  { value: "tools", label: "tools", description: "Choose lazy-loadable Chrome DevTools tools" },
36
45
  { value: "toggle", label: "toggle", description: "Alias for tools" },
37
46
  { value: "select", label: "select", description: "Compatibility alias for tools" },
@@ -90,6 +99,7 @@ export default function chromeDevtools(pi: ExtensionAPI) {
90
99
  ctx.ui.setStatus(STATUS_KEY, undefined);
91
100
  const browserShutdown = shutdownManagedBrowser(undefined, { cancelLaunch: true });
92
101
  await waitForChromeDevtoolsSettings();
102
+ await waitForSettingsWrites();
93
103
  await browserShutdown;
94
104
  });
95
105
  }
@@ -120,6 +130,15 @@ async function handleChromeDevtoolsCommand(
120
130
  ctx.ui.notify(status, "info");
121
131
  return;
122
132
  }
133
+ case "settings": {
134
+ if (!ctx.hasUI || (ctx.mode !== "tui" && ctx.mode !== "rpc")) {
135
+ throw new Error("/chrome-devtools settings requires TUI or RPC mode");
136
+ }
137
+ const { showChromeDevtoolsBrowserSettings } = await import("./browser-settings-menu.js");
138
+ if (generation !== state.sessionGeneration) return;
139
+ await showChromeDevtoolsBrowserSettings(ctx, generation);
140
+ return;
141
+ }
123
142
  case "tools": {
124
143
  if (!ctx.hasUI || (ctx.mode !== "tui" && ctx.mode !== "rpc")) {
125
144
  throw new Error("/chrome-devtools tools requires TUI or RPC mode");
@@ -174,6 +193,7 @@ export function parseCommand(args: string): CommandAction | "unknown" {
174
193
  if (command === "help") return "help";
175
194
  if (command === "quickstart") return "quickstart";
176
195
  if (command === "status") return "status";
196
+ if (command === "settings") return "settings";
177
197
  if (command === "tools" || command === "select" || command === "toggle") return "tools";
178
198
  if (command === "enable" || command === "on") return "enable";
179
199
  if (command === "disable" || command === "off") return "disable";
package/src/menu.ts CHANGED
@@ -8,14 +8,13 @@ import { CHROME_DEVTOOLS_TOOL_NAMES, type ChromeDevToolsToolName } from "./tool-
8
8
  import {
9
9
  buildBrowserStatusMessage,
10
10
  buildCommandGuide,
11
- buildSettingsSetupMessage,
12
11
  sanitizeChromeDevtoolsDisplay,
13
12
  setSelectedChromeDevtoolsTools,
14
13
  } from "./tool-selector.js";
15
14
 
16
15
  type CommandContext = ExtensionCommandContext;
17
- type MainScreen = "main" | "browserStatus" | "settings" | "help";
18
- type MainAction = "tools" | "bulk";
16
+ type MainScreen = "main" | "browserStatus" | "help";
17
+ type MainAction = "tools" | "bulk" | "settings";
19
18
  type ToolScreen = "tools" | "review";
20
19
  type ToolAction = "toggle" | "selectAll" | "selectNone" | "review" | "apply" | "cancel";
21
20
 
@@ -104,9 +103,9 @@ export async function showChromeDevtoolsMenu(
104
103
  },
105
104
  {
106
105
  id: "settings",
107
- label: "Settings & setup",
108
- description: "Files, sources, trust, and reload steps.",
109
- to: "settings",
106
+ label: "Browser settings",
107
+ description: "Edit endpoint, auto-launch, and browser executable settings.",
108
+ action: "settings",
110
109
  },
111
110
  {
112
111
  id: "help",
@@ -125,14 +124,6 @@ export async function showChromeDevtoolsMenu(
125
124
  viewportSize: "adaptive",
126
125
  hint: "back",
127
126
  }),
128
- settings: () => ({
129
- kind: "review",
130
- title: "Settings & setup",
131
- content: buildSettingsSetupMessage(),
132
- format: { kind: "text" },
133
- viewportSize: "adaptive",
134
- hint: "back",
135
- }),
136
127
  help: () => ({
137
128
  kind: "review",
138
129
  title: "Chrome DevTools help",
@@ -163,6 +154,13 @@ export async function showChromeDevtoolsMenu(
163
154
  if (result?.applied && isCurrent()) updateSnapshotAfterApply(current, result.selectedTools);
164
155
  return result?.closeParent ? { kind: "close" } : { kind: "stay" };
165
156
  },
157
+ settings: async () => {
158
+ const { showChromeDevtoolsBrowserSettings } = await import("./browser-settings-menu.js");
159
+ if (!isCurrent()) return { kind: "stay" };
160
+ const result = await showChromeDevtoolsBrowserSettings(ctx, generation);
161
+ if (!isCurrent()) return { kind: "stay" };
162
+ return result.closeParent ? { kind: "close" } : { kind: "stay" };
163
+ },
166
164
  },
167
165
  });
168
166
  await runMenu(ctx, menu, {