@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.
- package/README.md +74 -42
- package/dist/chunks/browser-settings-menu-L4CZINWO.ts +266 -0
- package/dist/chunks/browser-settings-menu-L4CZINWO.ts.map +7 -0
- package/dist/chunks/chunk-RM7OGXVL.ts +1607 -0
- package/dist/chunks/chunk-RM7OGXVL.ts.map +7 -0
- package/dist/chunks/menu-U4AIAN7V.ts +397 -0
- package/dist/chunks/menu-U4AIAN7V.ts.map +7 -0
- package/dist/index.ts +762 -0
- package/dist/index.ts.map +7 -0
- package/package.json +11 -7
- package/src/browser-manager.ts +3 -5
- package/src/browser-settings-menu.ts +311 -0
- package/src/chrome-devtools.ts +22 -2
- package/src/menu.ts +12 -14
- package/src/runtime.ts +28 -23
- package/src/settings.ts +204 -38
- package/src/tool-selector.ts +3 -1
|
@@ -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": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,kBAAkB;AAC3B,SAAS,YAAY;;;ACoBrB,eAAsB,UAAU,UAA+B,CAAC,GAAG;AAClE,QAAM,SAAS,QAAQ,UAAU;AACjC,QAAM,uBAAuB,MAAM;AACnC,SAAO,kBAAkB,YAAY;AACpC,UAAM,QAAQ,MAAM,kBAAkC,YAAY;AAClE,WAAO,MAAM,OAAO,CAAC,SAAS,KAAK,SAAS,UAAU,KAAK,oBAAoB;AAAA,EAChF,GAAG,MAAM;AACV;AAEA,eAAsB,QAAQ,QAAgB;AAC7C,QAAM,QAAQ,MAAM,UAAU;AAC9B,SAAO,YAAY,QAAQ,KAAK;AACjC;AAEA,eAAsB,YAAY,QAAiB;AAClD,QAAM,QAAQ,MAAM,UAAU;AAC9B,MAAI,OAAQ,QAAO,YAAY,QAAQ,KAAK;AAE5C,QAAM,OAAO,mBAAmB,KAAK;AACrC,MAAI,CAAC,MAAM;AACV,UAAM,IAAI;AAAA,MACT;AAAA,QACC,4BAA4B,iBAAiB,CAAC;AAAA,QAC9C;AAAA,QACA,WAAW;AAAA,MACZ,EAAE,KAAK,IAAI;AAAA,IACZ;AAAA,EACD;AAEA,SAAO;AACR;AAEA,eAAsB,yBAAyB,QAAiB;AAC/D,QAAM,QAAQ,MAAM,UAAU;AAC9B,MAAI,OAAQ,QAAO,EAAE,SAAS,OAAO,MAAM,YAAY,QAAQ,KAAK,EAAE;AAEtE,QAAM,OAAO,mBAAmB,KAAK;AACrC,MAAI,KAAM,QAAO,EAAE,SAAS,OAAO,KAAK;AAExC,SAAO,EAAE,SAAS,MAAM,MAAM,MAAM,WAAW,aAAa,EAAE;AAC/D;AAEA,SAAS,mBAAmB,OAAuB;AAClD,MAAI,CAAC,MAAM,aAAc,QAAO,MAAM,CAAC;AAEvC,QAAM,aAAa,MAAM,KAAK,CAAC,cAAc,UAAU,OAAO,MAAM,YAAY;AAChF,MAAI,WAAY,QAAO;AAEvB,QAAM,eAAe;AACrB,SAAO,MAAM,CAAC;AACf;AAEA,SAAS,YAAY,QAAgB,OAAuB;AAC3D,QAAM,OAAO,MAAM,KAAK,CAAC,cAAc,UAAU,OAAO,MAAM;AAC9D,MAAI,KAAM,QAAO;AAEjB,QAAM,iBAAiB,MAAM,IAAI,kBAAkB,EAAE,KAAK,IAAI;AAC9D,QAAM,IAAI;AAAA,IACT;AAAA,MACC,mCAAmC,MAAM;AAAA,MACzC,iBACG;AAAA,EAAqB,cAAc,KACnC;AAAA,IACJ,EAAE,KAAK,IAAI;AAAA,EACZ;AACD;AAEA,eAAsB,WAAW,KAAa,UAA+B,CAAC,GAAG;AAChF,QAAM,SAAS,QAAQ,UAAU;AACjC,QAAM,uBAAuB,MAAM;AACnC,QAAM,OAAO,MAAM;AAAA,IAClB,MACC,kBAAgC,aAAa,mBAAmB,GAAG,CAAC,IAAI;AAAA,MACvE,QAAQ;AAAA,IACT,CAAC;AAAA,IACF;AAAA,EACD;AACA,MAAI,KAAK,SAAS,UAAU,CAAC,KAAK,sBAAsB;AACvD,UAAM,IAAI,MAAM,mEAAmE;AAAA,EACpF;AAEA,SAAO;AACR;AAEO,SAAS,WAAW,MAAoB;AAC9C,SAAO;AAAA,IACN,IAAI,KAAK;AAAA,IACT,MAAM,KAAK;AAAA,IACX,OAAO,KAAK;AAAA,IACZ,KAAK,KAAK;AAAA,EACX;AACD;AAEO,SAAS,WAAW,MAAc,SAAkB;AAC1D,SAAO;AAAA,IACN,SAAS,CAAC,EAAE,MAAM,QAAiB,KAAK,CAAC;AAAA,IACzC;AAAA,EACD;AACD;AAEA,eAAsB,QAAW,MAAoB,UAA6C;AACjG,MAAI,CAAC,KAAK,qBAAsB,OAAM,IAAI,MAAM,qCAAqC,KAAK,EAAE,EAAE;AAE9F,QAAM,SAAS,MAAM,UAAU,QAAQ,KAAK,oBAAoB;AAChE,MAAI;AACH,WAAO,MAAM,SAAS,MAAM;AAAA,EAC7B,UAAE;AACD,WAAO,MAAM;AAAA,EACd;AACD;AAEA,IAAM,YAAN,MAAM,WAAU;AAAA,EACf,UAAU;AAAA,EACV,WAAW,oBAAI,IAOb;AAAA,EACe;AAAA,EAET,YAAY,QAAmB;AACtC,SAAK,SAAS;AACd,WAAO,iBAAiB,WAAW,CAAC,UAAU;AAC7C,YAAM,WAAW,KAAK,MAAM,OAAO,MAAM,IAAI,CAAC;AAC9C,UAAI,OAAO,SAAS,OAAO,SAAU;AAErC,YAAM,UAAU,KAAK,SAAS,IAAI,SAAS,EAAE;AAC7C,UAAI,CAAC,QAAS;AAEd,mBAAa,QAAQ,OAAO;AAC5B,WAAK,SAAS,OAAO,SAAS,EAAE;AAEhC,UAAI,SAAS,OAAO;AACnB,gBAAQ,OAAO,IAAI,MAAM,aAAa,SAAS,MAAM,IAAI,KAAK,SAAS,MAAM,OAAO,EAAE,CAAC;AAAA,MACxF,OAAO;AACN,gBAAQ,QAAQ,SAAS,MAAM;AAAA,MAChC;AAAA,IACD,CAAC;AAED,WAAO,iBAAiB,SAAS,MAAM;AACtC,WAAK,UAAU,IAAI,MAAM,kCAAkC,CAAC;AAAA,IAC7D,CAAC;AAED,WAAO,iBAAiB,SAAS,MAAM;AACtC,WAAK,UAAU,IAAI,MAAM,iCAAiC,CAAC;AAAA,IAC5D,CAAC;AAAA,EACF;AAAA,EAEA,OAAO,QAAQ,KAAa;AAC3B,WAAO,IAAI,QAAmB,CAACA,UAAS,WAAW;AAClD,YAAM,SAAS,IAAI,UAAU,GAAG;AAChC,YAAM,UAAU,WAAW,MAAM;AAChC,eAAO,MAAM;AACb,eAAO,IAAI,MAAM,sDAAsD,GAAG,EAAE,CAAC;AAAA,MAC9E,GAAG,kBAAkB;AAErB,aAAO,iBAAiB,QAAQ,MAAM;AACrC,qBAAa,OAAO;AACpB,QAAAA,SAAQ,IAAI,WAAU,MAAM,CAAC;AAAA,MAC9B,CAAC;AAED,aAAO,iBAAiB,SAAS,MAAM;AACtC,qBAAa,OAAO;AACpB,eAAO,IAAI,MAAM,mDAAmD,GAAG,EAAE,CAAC;AAAA,MAC3E,CAAC;AAAA,IACF,CAAC;AAAA,EACF;AAAA,EAEA,KAAkB,QAAgB,QAAkC;AACnE,UAAM,KAAK,KAAK;AAChB,SAAK,WAAW;AAEhB,WAAO,IAAI,QAAW,CAACA,UAAS,WAAW;AAC1C,YAAM,UAAU,WAAW,MAAM;AAChC,aAAK,SAAS,OAAO,EAAE;AACvB,eAAO,IAAI,MAAM,uCAAuC,MAAM,EAAE,CAAC;AAAA,MAClE,GAAG,kBAAkB;AAErB,WAAK,SAAS,IAAI,IAAI,EAAE,SAASA,UAAqC,QAAQ,QAAQ,CAAC;AACvF,WAAK,OAAO,KAAK,KAAK,UAAU,EAAE,IAAI,QAAQ,QAAQ,UAAU,CAAC,EAAE,CAAC,CAAC;AAAA,IACtE,CAAC;AAAA,EACF;AAAA,EAEA,QAAQ;AACP,SAAK,OAAO,MAAM;AAAA,EACnB;AAAA,EAEQ,UAAU,OAAc;AAC/B,eAAW,CAAC,IAAI,OAAO,KAAK,KAAK,UAAU;AAC1C,mBAAa,QAAQ,OAAO;AAC5B,cAAQ,OAAO,KAAK;AACpB,WAAK,SAAS,OAAO,EAAE;AAAA,IACxB;AAAA,EACD;AACD;;;ACxNA,IAAM,aAAa;AAaZ,SAAS,eAAe,QAAgB;AAC9C,SAAO,MAAM,IAAI,gBAAgB,oBAAoB,MAAM,EAAE;AAC9D;AAEO,SAAS,iBACf,QACA,SACA,OACC;AACD,QAAM,SAAS,wBAAwB,YAAY,MAAM,GAAG,OAAO;AACnE,SAAO,IAAI,gBAAgB,OAAO,MAAM,OAAO,OAAO,KAAK;AAC5D;AAEO,SAAS,uBACf,QACA,SACA,OACkB;AAClB,QAAM,SAAS,wBAAwB,sBAAsB,MAAM,GAAG,OAAO;AAC7E,SAAO,IAAI,gBAAgB,OAAO,MAAM,OAAO,OAAO,KAAK;AAC5D;AAEA,SAAS,YAAY,QAAkC;AACtD,SAAO,OAAO,QACZ,QAAQ,CAAC,YAAa,QAAQ,SAAS,SAAS,CAAC,QAAQ,IAAI,IAAI,CAAC,CAAE,EACpE,KAAK,IAAI;AACZ;AAEA,SAAS,sBAAsB,QAAkC;AAChE,QAAM,OAAO,YAAY,MAAM;AAC/B,MAAI,KAAK,KAAK,EAAG,QAAO;AAExB,QAAM,UAAU,OAAO;AACvB,MAAI,OAAO,SAAS,cAAc,SAAU,QAAO;AACnD,QAAM,QAAQ,OAAO,QAAQ,UAAU,WAAW,KAAK,QAAQ,KAAK,YAAY;AAChF,SAAO,uBAAuB,QAAQ,SAAS,GAAG,KAAK;AACxD;AAEA,SAAS,wBACR,MACA,SACmC;AACnC,MAAI,QAAQ,UAAW,QAAO,EAAE,MAAM,cAAc,OAAO,UAAU;AACrE,MAAI,QAAQ,SAAU,QAAO,EAAE,MAAM,OAAO,aAAa;AAEzD,SAAO,EAAE,MAAM,GAAG;AACnB;AAEA,IAAM,kBAAN,MAAiD;AAAA,EACxC;AAAA,EACS;AAAA,EACA;AAAA,EAEjB,YAAY,OAAO,IAAI,OAAqB,OAAgB;AAC3D,SAAK,OAAO;AACZ,SAAK,QAAQ;AACb,SAAK,QAAQ;AAAA,EACd;AAAA,EAEA,QAAQ,MAAc;AACrB,SAAK,OAAO;AAAA,EACb;AAAA,EAEA,aAAa;AAAA,EAEb;AAAA,EAEA,OAAO,OAAe;AACrB,QAAI,CAAC,KAAK,KAAK,KAAK,EAAG,QAAO,CAAC;AAC/B,WAAO,KAAK,KACV,QAAQ,OAAO,KAAK,EACpB,MAAM,OAAO,EACb,IAAI,CAAC,SAAS;AACd,YAAM,gBAAgB,aAAa,MAAM,KAAK,IAAI,GAAG,KAAK,CAAC;AAC3D,aAAO,KAAK,SAAS,KAAK,QAAQ,KAAK,MAAM,GAAG,KAAK,OAAO,aAAa,IAAI;AAAA,IAC9E,CAAC;AAAA,EACH;AACD;AAEA,SAAS,aAAa,MAAc,UAAkB;AACrD,SAAO,MAAM,KAAK,IAAI,EAAE,MAAM,GAAG,QAAQ,EAAE,KAAK,EAAE;AACnD;AAEA,eAAsB,WACrB,KACA,QACA,UACC;AACD,MAAI,GAAG,UAAU,YAAY,MAAM;AACnC,MAAI;AACH,WAAO,MAAM,SAAS;AAAA,EACvB,UAAE;AACD,QAAI,GAAG,UAAU,YAAY,MAAS;AAAA,EACvC;AACD;;;AC7GA,SAAS,kBAAkB;AAC3B,SAAS,OAAO,OAAO,UAAU,QAAQ,IAAI,iBAAiB;AAC9D,SAAS,cAAc;AACvB,SAAS,UAAU,SAAS,YAAY,MAAM,UAAU,SAAS,WAAW;AAC5E,SAAS,6BAA6B;AAGtC,SAAS,OAAU,QAAa;AAC/B,SAAO,MAAM,KAAK,IAAI,IAAI,MAAM,CAAC;AAClC;AAEA,SAAS,YAAY,OAAgD;AACpE,SAAO,iBAAiB,SAAS,UAAU;AAC5C;AAaA,eAAsB,eACrB,WACA,UACA,KACA,QACgC;AAChC,QAAM,eAAe,sBAAsB,UAAU,GAAG;AACxD,QAAM,WAAW,OAAO,KAAK,WAAW,QAAQ;AAEhD,QAAM,sBAAsB,aAAa,MAAM,YAAY;AAC1D,mBAAe,MAAM;AACrB,UAAM,2BAA2B,YAAY;AAC7C,UAAM,+BAA+B,YAAY;AACjD,UAAM,0BAA0B,cAAc,UAAU,MAAM;AAAA,EAC/D,CAAC;AAED,SAAO;AAAA,IACN,WAAW,aAAa;AAAA,IACxB,OAAO,SAAS;AAAA,IAChB,eAAe,aAAa;AAAA,EAC7B;AACD;AAEO,SAAS,sBACf,UACA,KACyB;AACzB,QAAM,UAAU,QAAQ,GAAG;AAC3B,QAAM,WAAW,QAAQ,OAAO,CAAC;AAEjC,MAAI,aAAa,QAAW;AAC3B,WAAO;AAAA,MACN,MAAM,KAAK,UAAU,iCAAiC,WAAW,CAAC,MAAM;AAAA,MACxE,cAAc,CAAC,QAAQ;AAAA,MACvB,WAAW;AAAA,IACZ;AAAA,EACD;AAEA,QAAM,iBAAiB,mBAAmB,QAAQ;AAClD,MAAI,CAAC,eAAe,KAAK,GAAG;AAC3B,UAAM,IAAI,MAAM,wCAAwC;AAAA,EACzD;AACA,MAAI,eAAe,SAAS,IAAI,GAAG;AAClC,UAAM,IAAI,MAAM,iDAAiD;AAAA,EAClE;AACA,MAAI,qBAAqB,cAAc,GAAG;AACzC,UAAM,IAAI,MAAM,0DAA0D;AAAA,EAC3E;AAEA,QAAM,iBAAiB,WAAW,cAAc;AAChD,QAAM,OAAO,iBAAiB,QAAQ,cAAc,IAAI,QAAQ,SAAS,cAAc;AACvF,QAAM,eAAe,iBAAiB,OAAO,CAAC,SAAS,QAAQ,CAAC,IAAI,CAAC,OAAO;AAC5E,MAAI,CAAC,aAAa,KAAK,CAAC,SAAS,iBAAiB,MAAM,IAAI,CAAC,GAAG;AAC/D,UAAM,IAAI;AAAA,MACT;AAAA,IACD;AAAA,EACD;AAEA,SAAO,EAAE,MAAM,cAAc,WAAW,MAAM;AAC/C;AAEA,SAAS,mBAAmB,MAAc;AACzC,SAAO,KAAK,WAAW,GAAG,IAAI,KAAK,MAAM,CAAC,IAAI;AAC/C;AAEO,SAAS,qBAAqB,MAAc;AAClD,SAAO,KAAK,MAAM,QAAQ,EAAE,KAAK,CAAC,SAAS,SAAS,IAAI;AACzD;AAEA,eAAe,2BAA2B,cAAsC;AAC/E,QAAM,aAAa,QAAQ,aAAa,IAAI;AAC5C,QAAM,WAAW,kBAAkB,YAAY,aAAa,YAAY;AACxE,MAAI,CAAC,UAAU;AACd,UAAM,IAAI;AAAA,MACT;AAAA,IACD;AAAA,EACD;AAEA,QAAM,eAAe,MAAM,SAAS,QAAQ;AAC5C,MAAI,cAAc;AAClB,QAAM,iBAAiB,SAAS,UAAU,UAAU,EAClD,MAAM,QAAQ,EACd,OAAO,CAAC,SAAS,KAAK,SAAS,CAAC;AAElC,aAAW,WAAW,gBAAgB;AACrC,kBAAc,KAAK,aAAa,OAAO;AACvC,UAAM,2BAA2B,aAAa,YAAY;AAAA,EAC3D;AACD;AAEO,SAAS,kBAAkB,MAAc,OAA0B;AACzE,QAAM,gBAAgB,MAAM,OAAO,CAAC,SAAS,iBAAiB,MAAM,IAAI,CAAC;AACzE,gBAAc;AAAA,IACb,CAAC,MAAM,UACN,2BAA2B,QAAQ,KAAK,CAAC,EAAE,SAC3C,2BAA2B,QAAQ,IAAI,CAAC,EAAE;AAAA,EAC5C;AACA,SAAO,cAAc,CAAC;AACvB;AAEA,eAAe,2BAA2B,MAAc,cAAsB;AAC7E,QAAM,oBAAoB,MAAM,MAAM,IAAI,EAAE,MAAM,OAAO,UAAmB;AAC3E,QAAI,CAAC,YAAY,KAAK,KAAK,MAAM,SAAS,SAAU,OAAM;AAC1D,UAAM,MAAM,IAAI,EAAE,MAAM,CAAC,eAAwB;AAChD,UAAI,CAAC,YAAY,UAAU,KAAK,WAAW,SAAS,SAAU,OAAM;AAAA,IACrE,CAAC;AACD,WAAO,MAAM,IAAI;AAAA,EAClB,CAAC;AAED,MAAI,kBAAkB,eAAe,GAAG;AACvC,UAAM,IAAI,MAAM,yEAAyE;AAAA,EAC1F;AACA,MAAI,CAAC,kBAAkB,YAAY,GAAG;AACrC,UAAM,IAAI,MAAM,iDAAiD;AAAA,EAClE;AACA,QAAM,yBAAyB,MAAM,YAAY;AAClD;AAEA,eAAe,+BAA+B,cAAsC;AACnF,QAAM,iBAAiB,MAAM,MAAM,aAAa,IAAI,EAAE,MAAM,CAAC,UAAmB;AAC/E,QAAI,YAAY,KAAK,KAAK,MAAM,SAAS,SAAU,QAAO;AAC1D,UAAM;AAAA,EACP,CAAC;AACD,MAAI,gBAAgB,eAAe,GAAG;AACrC,UAAM,IAAI,MAAM,wDAAwD;AAAA,EACzE;AACA,MAAI,gBAAgB,YAAY,GAAG;AAClC,UAAM,IAAI,MAAM,4DAA4D;AAAA,EAC7E;AACA,MAAI,kBAAkB,CAAC,eAAe,OAAO,GAAG;AAC/C,UAAM,IAAI,MAAM,qDAAqD;AAAA,EACtE;AAEA,QAAM,mBAAmB,MAAM,QAAQ,IAAI,aAAa,aAAa,IAAI,sBAAsB,CAAC;AAChG,QAAM,aAAa,MAAM,SAAS,QAAQ,aAAa,IAAI,CAAC;AAC5D,QAAM,iBAAiB,KAAK,YAAY,SAAS,aAAa,IAAI,CAAC;AACnE,MAAI,CAAC,iBAAiB,KAAK,CAAC,SAAS,iBAAiB,gBAAgB,IAAI,CAAC,GAAG;AAC7E,UAAM,IAAI;AAAA,MACT;AAAA,IACD;AAAA,EACD;AACD;AAEA,eAAe,yBAAyB,MAAc,cAAsB;AAC3E,QAAM,WAAW,MAAM,SAAS,IAAI;AACpC,MAAI,CAAC,iBAAiB,UAAU,YAAY,GAAG;AAC9C,UAAM,IAAI;AAAA,MACT;AAAA,IACD;AAAA,EACD;AACD;AAEA,eAAe,0BACd,cACA,UACA,QACC;AACD,QAAM,WAAW;AAAA,IAChB,QAAQ,aAAa,IAAI;AAAA,IACzB,IAAI,SAAS,aAAa,IAAI,CAAC,IAAI,QAAQ,GAAG,IAAI,KAAK,IAAI,CAAC,IAAI,WAAW,CAAC;AAAA,EAC7E;AACA,MAAI;AACH,UAAM,UAAU,UAAU,UAAU,EAAE,MAAM,MAAM,OAAO,CAAC;AAC1D,mBAAe,MAAM;AACrB,UAAM,sBAAsB,cAAc,UAAU,MAAM;AAAA,EAC3D,SAAS,OAAO;AACf,UAAM,GAAG,UAAU,EAAE,OAAO,KAAK,CAAC,EAAE,MAAM,MAAM,MAAS;AACzD,UAAM;AAAA,EACP;AACD;AAEA,eAAe,sBACd,cACA,UACA,QACC;AACD,MAAI;AACH,UAAM,OAAO,UAAU,aAAa,IAAI;AACxC;AAAA,EACD,SAAS,OAAO;AACf,QAAI,CAAC,0CAA0C,KAAK,EAAG,OAAM;AAAA,EAC9D;AAIA,QAAM,+BAA+B,YAAY;AACjD,iBAAe,MAAM;AACrB,QAAM,GAAG,aAAa,MAAM,EAAE,OAAO,KAAK,CAAC;AAC3C,QAAM,OAAO,UAAU,aAAa,IAAI;AACzC;AAEA,SAAS,0CAA0C,OAAgB;AAClE,SACC,QAAQ,aAAa,WACrB,YAAY,KAAK,KACjB,CAAC,UAAU,UAAU,OAAO,EAAE,SAAS,MAAM,QAAQ,EAAE;AAEzD;AAEA,eAAe,uBAAuB,MAAc;AACnD,SAAO,SAAS,IAAI,EAAE,MAAM,MAAM,QAAQ,IAAI,CAAC;AAChD;AAEO,SAAS,iBAAiB,MAAc,MAAc;AAC5D,QAAM,iBAAiB,2BAA2B,QAAQ,IAAI,CAAC;AAC/D,QAAM,iBAAiB,2BAA2B,QAAQ,IAAI,CAAC;AAC/D,MAAI,mBAAmB,eAAgB,QAAO;AAC9C,QAAM,oBAAoB,eAAe,SAAS,GAAG,IAClD,iBACA,GAAG,cAAc,GAAG,GAAG;AAC1B,SAAO,eAAe,WAAW,iBAAiB;AACnD;AAEA,SAAS,2BAA2B,MAAc;AACjD,SAAO,QAAQ,aAAa,UAAU,KAAK,YAAY,IAAI;AAC5D;AAEO,SAAS,eAAe,QAAiC;AAC/D,MAAI,CAAC,QAAQ,QAAS;AACtB,QAAM,OAAO,kBAAkB,QAAQ,OAAO,SAAS,IAAI,MAAM,+BAA+B;AACjG;AAEO,SAAS,qBAAqB,MAAoB,YAAkC;AAC1F,QAAM,YAAY,WAAW,gBAAgB,uBAAuB;AACpE,SAAO;AAAA,IACN,gCAAgC,KAAK,SAAS,KAAK,OAAO,KAAK,EAAE;AAAA,IACjE,GAAG,SAAS,KAAK,WAAW,SAAS;AAAA,IACrC,UAAU,WAAW,KAAK;AAAA,IAC1B,oBAAoB,KAAK,UAAU,WAAW,SAAS,CAAC;AAAA,EACzD,EAAE,KAAK,IAAI;AACZ;;;AHhPO,IAAM,gBAAgB,WAAW;AAAA,EACvC,MAAM,2BAA2B,CAAC;AAAA,EAClC,OAAO;AAAA,EACP,aAAa;AAAA,EACb,YAAY,KAAK,OAAO,CAAC,CAAC;AAAA,EAC1B,YAAY,eAAe,YAAY;AAAA,EACvC,cAAc;AAAA,EACd,MAAM,QAAQ,aAAa,SAAS,SAAS,WAAW,KAAK;AAC5D,WAAO,WAAW,KAAK,cAAc,YAAY;AAChD,YAAM,QAAQ,MAAM,UAAU;AAC9B,aAAO,WAAW,KAAK,UAAU,MAAM,IAAI,UAAU,GAAG,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC;AAAA,IAC5E,CAAC;AAAA,EACF;AACD,CAAC;AAEM,IAAM,iBAAiB,WAAW;AAAA,EACxC,MAAM,2BAA2B,CAAC;AAAA,EAClC,OAAO;AAAA,EACP,aAAa;AAAA,EACb,YAAY,KAAK,OAAO;AAAA,IACvB,QAAQ,KAAK,OAAO,EAAE,aAAa,2CAA2C,CAAC;AAAA,EAChF,CAAC;AAAA,EACD,YAAY,eAAe,aAAa;AAAA,EACxC,cAAc;AAAA,EACd,MAAM,QAAQ,aAAa,QAAQ,SAAS,WAAW,KAAK;AAC3D,WAAO,WAAW,KAAK,eAAe,YAAY;AACjD,YAAM,OAAO,MAAM,QAAQ,OAAO,MAAM;AACxC,YAAM,eAAe,KAAK;AAC1B,aAAO,WAAW,iBAAiB,KAAK,EAAE,KAAK,KAAK,KAAK;AAAA,EAAK,KAAK,GAAG,IAAI;AAAA,QACzE,MAAM,WAAW,IAAI;AAAA,MACtB,CAAC;AAAA,IACF,CAAC;AAAA,EACF;AACD,CAAC;AAEM,IAAM,eAAe,WAAW;AAAA,EACtC,MAAM,2BAA2B,CAAC;AAAA,EAClC,OAAO;AAAA,EACP,aACC;AAAA,EACD,YAAY,KAAK,OAAO;AAAA,IACvB,KAAK,KAAK,OAAO,EAAE,aAAa,sBAAsB,CAAC;AAAA,IACvD,QAAQ,KAAK;AAAA,MACZ,KAAK,OAAO,EAAE,aAAa,wDAAwD,CAAC;AAAA,IACrF;AAAA,EACD,CAAC;AAAA,EACD,YAAY,eAAe,UAAU;AAAA,EACrC,cAAc;AAAA,EACd,MAAM,QAAQ,aAAa,QAAQ,SAAS,WAAW,KAAK;AAC3D,WAAO,WAAW,KAAK,YAAY,YAAY;AAC9C,YAAM,EAAE,SAAS,KAAK,IAAI,MAAM,yBAAyB,OAAO,MAAM;AACtE,YAAM,SAAS,MAAM,QAAQ,MAAM,OAAO,WAAW;AACpD,cAAM,OAAO,KAAK,aAAa;AAC/B,eAAO,OAAO,KAAK,iBAAiB,EAAE,KAAK,OAAO,IAAI,CAAC;AAAA,MACxD,CAAC;AAED,YAAM,eAAe,KAAK;AAC1B,YAAM,SAAS,UAAU,+BAA+B;AACxD,aAAO,WAAW,GAAG,MAAM,IAAI,KAAK,EAAE,OAAO,OAAO,GAAG,IAAI;AAAA,QAC1D;AAAA,QACA,MAAM,WAAW,IAAI;AAAA,QACrB;AAAA,MACD,CAAC;AAAA,IACF,CAAC;AAAA,EACF;AACD,CAAC;AAEM,IAAM,eAAe,WAAW;AAAA,EACtC,MAAM,2BAA2B,CAAC;AAAA,EAClC,OAAO;AAAA,EACP,aAAa;AAAA,EACb,YAAY,KAAK,OAAO;AAAA,IACvB,YAAY,KAAK,OAAO,EAAE,aAAa,qCAAqC,CAAC;AAAA,IAC7E,QAAQ,KAAK;AAAA,MACZ,KAAK,OAAO,EAAE,aAAa,wDAAwD,CAAC;AAAA,IACrF;AAAA,IACA,cAAc,KAAK;AAAA,MAClB,KAAK,QAAQ,EAAE,aAAa,yDAAyD,CAAC;AAAA,IACvF;AAAA,EACD,CAAC;AAAA,EACD,YAAY,eAAe,UAAU;AAAA,EACrC,cAAc;AAAA,EACd,MAAM,QAAQ,aAAa,QAAQ,SAAS,WAAW,KAAK;AAC3D,WAAO,WAAW,KAAK,YAAY,YAAY;AAC9C,YAAM,OAAO,MAAM,YAAY,OAAO,MAAM;AAC5C,YAAM,SAAS,MAAM;AAAA,QAAQ;AAAA,QAAM,CAAC,WACnC,OAAO,KAAK,oBAAoB;AAAA,UAC/B,YAAY,OAAO;AAAA,UACnB,cAAc,OAAO,gBAAgB;AAAA,UACrC,eAAe;AAAA,QAChB,CAAC;AAAA,MACF;AAEA,YAAM,eAAe,KAAK;AAC1B,aAAO,WAAW,KAAK,UAAU,QAAQ,MAAM,CAAC,GAAG,EAAE,MAAM,WAAW,IAAI,GAAG,OAAO,CAAC;AAAA,IACtF,CAAC;AAAA,EACF;AACD,CAAC;AAEM,IAAM,iBAAiB,WAAW;AAAA,EACxC,MAAM,2BAA2B,CAAC;AAAA,EAClC,OAAO;AAAA,EACP,aAAa;AAAA,EACb,YAAY,KAAK,OAAO;AAAA,IACvB,QAAQ,KAAK;AAAA,MACZ,KAAK,OAAO,EAAE,aAAa,wDAAwD,CAAC;AAAA,IACrF;AAAA,IACA,UAAU,KAAK;AAAA,MACd,KAAK,QAAQ,EAAE,aAAa,oDAAoD,CAAC;AAAA,IAClF;AAAA,IACA,UAAU,KAAK;AAAA,MACd,KAAK,OAAO;AAAA,QACX,aACC;AAAA,MACF,CAAC;AAAA,IACF;AAAA,EACD,CAAC;AAAA,EACD,YAAY,eAAe,YAAY;AAAA,EACvC,cAAc;AAAA,EACd,MAAM,QAAQ,aAAa,QAAQ,QAAQ,WAAW,KAAK;AAC1D,WAAO,WAAW,KAAK,cAAc,YAAY;AAChD,YAAM,OAAO,MAAM,YAAY,OAAO,MAAM;AAC5C,YAAM,SAAS,MAAM,QAAQ,MAAM,OAAO,WAAW;AACpD,uBAAe,MAAM;AACrB,cAAM,OAAO,KAAK,aAAa;AAE/B,YAAI,CAAC,OAAO,UAAU;AACrB,iBAAO,OAAO,KAAuB,0BAA0B,EAAE,QAAQ,MAAM,CAAC;AAAA,QACjF;AAEA,cAAM,UAAU,MAAM,OAAO,KAE1B,uBAAuB;AAE1B,uBAAe,MAAM;AACrB,eAAO,OAAO,KAAuB,0BAA0B;AAAA,UAC9D,uBAAuB;AAAA,UACvB,QAAQ;AAAA,UACR,MAAM;AAAA,YACL,GAAG,QAAQ,YAAY;AAAA,YACvB,GAAG,QAAQ,YAAY;AAAA,YACvB,OAAO,QAAQ,YAAY;AAAA,YAC3B,QAAQ,QAAQ,YAAY;AAAA,YAC5B,OAAO;AAAA,UACR;AAAA,QACD,CAAC;AAAA,MACF,CAAC;AAED,YAAM,eAAe,KAAK;AAC1B,YAAM,kBAAkB,MAAM,eAAe,OAAO,MAAM,OAAO,UAAU,IAAI,KAAK,MAAM;AAC1F,aAAO;AAAA,QACN,SAAS;AAAA,UACR;AAAA,YACC,MAAM;AAAA,YACN,MAAM,qBAAqB,MAAM,eAAe;AAAA,UACjD;AAAA,UACA,EAAE,MAAM,SAAS,MAAM,OAAO,MAAM,UAAU,YAAY;AAAA,QAC3D;AAAA,QACA,SAAS;AAAA,UACR,MAAM,WAAW,IAAI;AAAA,UACrB,OAAO,gBAAgB;AAAA,UACvB,WAAW,gBAAgB;AAAA,UAC3B,eAAe,gBAAgB;AAAA,QAChC;AAAA,MACD;AAAA,IACD,CAAC;AAAA,EACF;AACD,CAAC;;;AIlJD,IAAMC,cAAa;AACnB,IAAM,sBAAsB;AAAA,EAC3B,EAAE,OAAO,QAAQ,OAAO,QAAQ,aAAa,qBAAqB;AAAA,EAClE,EAAE,OAAO,cAAc,OAAO,cAAc,aAAa,gCAAgC;AAAA,EACzF,EAAE,OAAO,UAAU,OAAO,UAAU,aAAa,gCAAgC;AAAA,EACjF,EAAE,OAAO,YAAY,OAAO,YAAY,aAAa,mCAAmC;AAAA,EACxF,EAAE,OAAO,SAAS,OAAO,SAAS,aAAa,6CAA6C;AAAA,EAC5F,EAAE,OAAO,UAAU,OAAO,UAAU,aAAa,kBAAkB;AAAA,EACnE,EAAE,OAAO,UAAU,OAAO,UAAU,aAAa,gCAAgC;AAAA,EACjF,EAAE,OAAO,UAAU,OAAO,UAAU,aAAa,2CAA2C;AAAA,EAC5F,EAAE,OAAO,MAAM,OAAO,MAAM,aAAa,iCAAiC;AAAA,EAC1E,EAAE,OAAO,WAAW,OAAO,WAAW,aAAa,6CAA6C;AAAA,EAChG,EAAE,OAAO,OAAO,OAAO,OAAO,aAAa,kCAAkC;AAC9E;AACe,SAAR,eAAgC,IAAkB;AACxD,KAAG,aAAa,aAAa;AAC7B,KAAG,aAAa,cAAc;AAC9B,KAAG,aAAa,YAAY;AAC5B,KAAG,aAAa,YAAY;AAC5B,KAAG,aAAa,cAAc;AAC9B,KAAG,aAAa,6BAA6B,EAAE,CAAC;AAEhD,KAAG,gBAAgB,mBAAmB;AAAA,IACrC,aAAa;AAAA,IACb,wBAAwB,CAAC,WAAW,mBAAmB,MAAM;AAAA,IAC7D,SAAS,OAAO,MAAM,QAAQ;AAC7B,6CAAuC,EAAE;AACzC,YAAM,aAAa,MAAM;AACzB,YAAM,4BAA4B,IAAI,MAAM,KAAK,UAAU;AAAA,IAC5D;AAAA,EACD,CAAC;AAED,KAAG,GAAG,iBAAiB,OAAO,QAAQ,QAAQ;AAC7C,UAAM,aAAa,EAAE,MAAM;AAC3B,2CAAuC,EAAE;AACzC,6BAAyB,kCAAkC;AAC3D,UAAM,eAAe;AACrB,UAAM,iBAAiB;AACvB,QAAI,GAAG,UAAUA,aAAY,MAAS;AACtC,UAAM,uBAAuB;AAC7B,QAAI,eAAe,MAAM,kBAAmB;AAC5C,UAAM,eAAe;AACrB,UAAM,oBAAoB;AAC1B,UAAM,iBAAiB,IAAI,iBAAiB;AAC5C,UAAM,WAAW,MAAM,aAAa,EAAE,KAAK,IAAI,KAAK,eAAe,CAAC;AACpE,QAAI,eAAe,MAAM,kBAAmB;AAC5C,gCAA4B,SAAS,kBAAkB,SAAS,OAAO,cAAc;AACrF,UAAM,iBAAiB,SAAS;AAChC,eAAW,WAAW,SAAS,UAAU;AACxC,UAAI,GAAG,OAAO,8BAA8B,OAAO,GAAG,SAAS;AAAA,IAChE;AACA,UAAM,iBACL,SAAS,SAAS,YAAY,SAAS,SAAS,QAC7C,SAAS,SAAS,QAClB,6BAA6B,EAAE;AACnC,qCAAiC,IAAI,cAAc;AAAA,EACpD,CAAC;AAED,KAAG,GAAG,oBAAoB,OAAO,QAAQ,QAAQ;AAChD,UAAM,qBAAqB;AAC3B,6BAAyB,mCAAmC;AAC5D,QAAI,GAAG,UAAUA,aAAY,MAAS;AACtC,UAAM,kBAAkB,uBAAuB,QAAW,EAAE,cAAc,KAAK,CAAC;AAChF,UAAM,8BAA8B;AACpC,UAAM,sBAAsB;AAC5B,UAAM;AAAA,EACP,CAAC;AACF;AAEA,eAAe,4BACd,IACA,MACA,KACA,YACC;AACD,QAAM,UAAU,aAAa,IAAI;AACjC,UAAQ,SAAS;AAAA,IAChB,KAAK;AACJ,YAAM,SAAS,IAAI,KAAK,UAAU;AAClC;AAAA,IACD,KAAK;AACJ,0BAAoB,KAAK,MAAM;AAC/B,UAAI,GAAG,OAAO,kBAAkB,GAAG,MAAM;AACzC;AAAA,IACD,KAAK;AACJ,0BAAoB,KAAK,YAAY;AACrC,UAAI,GAAG,OAAO,uBAAuB,GAAG,MAAM;AAC9C;AAAA,IACD,KAAK,UAAU;AACd,0BAAoB,KAAK,QAAQ;AACjC,YAAM,SAAS,MAAM,uBAAuB,EAAE;AAC9C,UAAI,eAAe,MAAM,kBAAmB;AAC5C,UAAI,GAAG,OAAO,QAAQ,MAAM;AAC5B;AAAA,IACD;AAAA,IACA,KAAK,YAAY;AAChB,UAAI,CAAC,IAAI,SAAU,IAAI,SAAS,SAAS,IAAI,SAAS,OAAQ;AAC7D,cAAM,IAAI,MAAM,oDAAoD;AAAA,MACrE;AACA,YAAM,EAAE,kCAAkC,IAAI,MAAM,OAAO,4CAA4B;AACvF,UAAI,eAAe,MAAM,kBAAmB;AAC5C,YAAM,kCAAkC,KAAK,UAAU;AACvD;AAAA,IACD;AAAA,IACA,KAAK,SAAS;AACb,UAAI,CAAC,IAAI,SAAU,IAAI,SAAS,SAAS,IAAI,SAAS,OAAQ;AAC7D,cAAM,IAAI,MAAM,iDAAiD;AAAA,MAClE;AACA,YAAM,EAAE,+BAA+B,IAAI,MAAM,OAAO,2BAAW;AACnE,UAAI,eAAe,MAAM,kBAAmB;AAC5C,YAAM,+BAA+B,IAAI,KAAK,UAAU;AACxD;AAAA,IACD;AAAA,IACA,KAAK;AACJ,YAAM,0BAA0B,IAAI,KAAK,uBAAuB,GAAG,oBAAoB;AACvF;AAAA,IACD,KAAK;AACJ,YAAM,0BAA0B,IAAI,KAAK,CAAC,GAAG,sBAAsB;AACnE;AAAA,EACF;AAEA,MAAI,CAAC,IAAI,SAAU,IAAI,SAAS,SAAS,IAAI,SAAS,OAAQ;AAC7D,UAAM,IAAI,MAAM,qCAAqC,KAAK,KAAK,CAAC,EAAE;AAAA,EACnE;AACA,MAAI,GAAG;AAAA,IACN,qCAAqC,KAAK,KAAK,CAAC;AAAA;AAAA,EAEhD,kBAAkB,CAAC;AAAA,IACnB;AAAA,EACD;AACD;AAEA,SAAS,oBAAoB,KAAqB,OAAe;AAChE,MAAI,CAAC,IAAI,SAAU,IAAI,SAAS,SAAS,IAAI,SAAS,OAAQ;AAC7D,UAAM,IAAI,MAAM,oBAAoB,KAAK,2BAA2B;AAAA,EACrE;AACD;AAEA,eAAe,SAAS,IAAkB,KAAqB,YAAoB;AAClF,MAAI,CAAC,IAAI,SAAU,IAAI,SAAS,SAAS,IAAI,SAAS,OAAQ;AAC7D,UAAM,IAAI,MAAM,yEAAyE;AAAA,EAC1F;AACA,QAAM,EAAE,uBAAuB,IAAI,MAAM,OAAO,2BAAW;AAC3D,MAAI,eAAe,MAAM,kBAAmB;AAC5C,QAAM,uBAAuB,IAAI,KAAK,UAAU;AACjD;AAEA,SAAS,yBAAyB,QAAgB;AACjD,QAAM,kBAAkB,MAAM,IAAI,aAAa,QAAQ,YAAY,CAAC;AACpE,QAAM,oBAAoB,IAAI,gBAAgB;AAC/C;AAEO,SAAS,aAAa,MAAyC;AACrE,QAAM,UAAU,KAAK,KAAK,EAAE,YAAY;AACxC,MAAI,CAAC,QAAS,QAAO;AACrB,MAAI,YAAY,OAAQ,QAAO;AAC/B,MAAI,YAAY,aAAc,QAAO;AACrC,MAAI,YAAY,SAAU,QAAO;AACjC,MAAI,YAAY,WAAY,QAAO;AACnC,MAAI,YAAY,WAAW,YAAY,YAAY,YAAY,SAAU,QAAO;AAChF,MAAI,YAAY,YAAY,YAAY,KAAM,QAAO;AACrD,MAAI,YAAY,aAAa,YAAY,MAAO,QAAO;AACvD,SAAO;AACR;AAEO,SAAS,mBAAmB,QAAgB;AAClD,QAAM,aAAa,OAAO,UAAU,EAAE,YAAY;AAClD,MAAI,KAAK,KAAK,UAAU,EAAG,QAAO;AAElC,QAAM,UAAU,oBAAoB;AAAA,IAAO,CAAC,eAC3C,WAAW,MAAM,WAAW,UAAU;AAAA,EACvC;AACA,SAAO,QAAQ,SAAS,IAAI,UAAU;AACvC;",
|
|
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.
|
|
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
|
-
"./
|
|
24
|
+
"./dist/index.ts"
|
|
24
25
|
]
|
|
25
26
|
},
|
|
26
27
|
"piExtension": {
|
|
27
28
|
"lifecycle": "stable"
|
|
28
29
|
},
|
|
29
30
|
"scripts": {
|
|
30
|
-
"
|
|
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.
|
|
40
|
-
"@earendil-works/pi-coding-agent": "0.84.
|
|
41
|
-
"
|
|
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": {
|
package/src/browser-manager.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 "
|
|
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
|
+
}
|
package/src/chrome-devtools.ts
CHANGED
|
@@ -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 =
|
|
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" | "
|
|
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: "
|
|
108
|
-
description: "
|
|
109
|
-
|
|
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, {
|