@narumitw/pi-chrome-devtools 0.1.32 → 0.1.33
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 +1 -0
- package/package.json +1 -1
- package/src/chrome-devtools.ts +97 -1
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@ This package is inspired by [`chrome-devtools-mcp`](https://github.com/ChromeDev
|
|
|
16
16
|
- Recovers from stale active page selections by falling back to an available page.
|
|
17
17
|
- Evaluates JavaScript in the selected page.
|
|
18
18
|
- Captures PNG screenshots, including optional full-page screenshots.
|
|
19
|
+
- Renders compact tool results that expand/collapse with Pi's default output toggle (`Ctrl+O`).
|
|
19
20
|
- Uses a local Chrome DevTools Protocol endpoint.
|
|
20
21
|
- Retries briefly while Chrome is starting and reports actionable endpoint errors.
|
|
21
22
|
- Shows statusline activity only while Chrome DevTools tools are running.
|
package/package.json
CHANGED
package/src/chrome-devtools.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
defineTool,
|
|
3
|
+
type AgentToolResult,
|
|
4
|
+
type ExtensionAPI,
|
|
5
|
+
type ToolRenderResultOptions,
|
|
6
|
+
} from "@mariozechner/pi-coding-agent";
|
|
2
7
|
import { Type } from "typebox";
|
|
3
8
|
|
|
4
9
|
const DEFAULT_HOST = "127.0.0.1";
|
|
@@ -13,6 +18,16 @@ interface StatusContext {
|
|
|
13
18
|
ui: { setStatus: (key: string, value: string | undefined) => void };
|
|
14
19
|
}
|
|
15
20
|
|
|
21
|
+
interface RenderTheme {
|
|
22
|
+
bold(text: string): string;
|
|
23
|
+
fg(color: string, text: string): string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface RenderComponent {
|
|
27
|
+
invalidate(): void;
|
|
28
|
+
render(width: number): string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
16
31
|
interface DevToolsPage {
|
|
17
32
|
id: string;
|
|
18
33
|
type: string;
|
|
@@ -48,6 +63,8 @@ const listPagesTool = defineTool({
|
|
|
48
63
|
description: "List Chrome tabs/pages from a running Chrome DevTools Protocol endpoint.",
|
|
49
64
|
promptSnippet: "List Chrome tabs/pages available over Chrome DevTools Protocol",
|
|
50
65
|
parameters: Type.Object({}),
|
|
66
|
+
renderCall: renderToolCall("list pages"),
|
|
67
|
+
renderResult: renderTextResult,
|
|
51
68
|
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
52
69
|
return withStatus(ctx, "🌐 list pages", async () => {
|
|
53
70
|
const pages = await listPages();
|
|
@@ -64,6 +81,8 @@ const selectPageTool = defineTool({
|
|
|
64
81
|
parameters: Type.Object({
|
|
65
82
|
pageId: Type.String({ description: "Page id from chrome_devtools_list_pages." }),
|
|
66
83
|
}),
|
|
84
|
+
renderCall: renderToolCall("select page"),
|
|
85
|
+
renderResult: renderTextResult,
|
|
67
86
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
68
87
|
return withStatus(ctx, "🌐 select page", async () => {
|
|
69
88
|
const page = await getPage(params.pageId);
|
|
@@ -87,6 +106,8 @@ const navigateTool = defineTool({
|
|
|
87
106
|
Type.String({ description: "Optional page id. Defaults to selected or first page." }),
|
|
88
107
|
),
|
|
89
108
|
}),
|
|
109
|
+
renderCall: renderToolCall("navigate"),
|
|
110
|
+
renderResult: renderTextResult,
|
|
90
111
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
91
112
|
return withStatus(ctx, "🌐 navigate", async () => {
|
|
92
113
|
const { created, page } = await resolvePageForNavigation(params.pageId);
|
|
@@ -120,6 +141,8 @@ const evaluateTool = defineTool({
|
|
|
120
141
|
Type.Boolean({ description: "Whether to await a returned Promise. Defaults to true." }),
|
|
121
142
|
),
|
|
122
143
|
}),
|
|
144
|
+
renderCall: renderToolCall("evaluate"),
|
|
145
|
+
renderResult: renderTextResult,
|
|
123
146
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
124
147
|
return withStatus(ctx, "🌐 evaluate", async () => {
|
|
125
148
|
const page = await resolvePage(params.pageId);
|
|
@@ -150,6 +173,8 @@ const screenshotTool = defineTool({
|
|
|
150
173
|
Type.Boolean({ description: "Capture the full document, not just the viewport." }),
|
|
151
174
|
),
|
|
152
175
|
}),
|
|
176
|
+
renderCall: renderToolCall("screenshot"),
|
|
177
|
+
renderResult: renderScreenshotResult,
|
|
153
178
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
154
179
|
return withStatus(ctx, "🌐 screenshot", async () => {
|
|
155
180
|
const page = await resolvePage(params.pageId);
|
|
@@ -405,6 +430,77 @@ function textResult(text: string, details: unknown) {
|
|
|
405
430
|
};
|
|
406
431
|
}
|
|
407
432
|
|
|
433
|
+
function renderToolCall(action: string) {
|
|
434
|
+
return () => new PiTextComponent(`Chrome DevTools: ${action}`);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function renderTextResult(
|
|
438
|
+
result: AgentToolResult<unknown>,
|
|
439
|
+
options: ToolRenderResultOptions,
|
|
440
|
+
theme: RenderTheme,
|
|
441
|
+
) {
|
|
442
|
+
const output = formatCollapsibleOutput(textContent(result), options);
|
|
443
|
+
return new PiTextComponent(output.text, theme, output.color);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function renderScreenshotResult(
|
|
447
|
+
result: AgentToolResult<unknown>,
|
|
448
|
+
options: ToolRenderResultOptions,
|
|
449
|
+
theme: RenderTheme,
|
|
450
|
+
): RenderComponent {
|
|
451
|
+
const output = formatCollapsibleOutput(textContent(result), options);
|
|
452
|
+
return new PiTextComponent(output.text, theme, output.color);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function textContent(result: AgentToolResult<unknown>) {
|
|
456
|
+
return result.content
|
|
457
|
+
.flatMap((content) => (content.type === "text" ? [content.text] : []))
|
|
458
|
+
.join("\n");
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function formatCollapsibleOutput(
|
|
462
|
+
text: string,
|
|
463
|
+
options: ToolRenderResultOptions,
|
|
464
|
+
): { text: string; color?: string } {
|
|
465
|
+
if (options.isPartial) return { text: "Running...", color: "warning" };
|
|
466
|
+
if (options.expanded) return { text, color: "toolOutput" };
|
|
467
|
+
|
|
468
|
+
return { text: "" };
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
class PiTextComponent implements RenderComponent {
|
|
472
|
+
constructor(
|
|
473
|
+
private text = "",
|
|
474
|
+
private readonly theme?: RenderTheme,
|
|
475
|
+
private readonly color?: string,
|
|
476
|
+
) {}
|
|
477
|
+
|
|
478
|
+
setText(text: string) {
|
|
479
|
+
this.text = text;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
invalidate() {
|
|
483
|
+
// Stateless renderer: no cached layout to invalidate.
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
render(width: number) {
|
|
487
|
+
if (!this.text.trim()) return [];
|
|
488
|
+
return this.text
|
|
489
|
+
.replace(/\t/g, " ")
|
|
490
|
+
.split(/\r?\n/)
|
|
491
|
+
.map((line) => {
|
|
492
|
+
const truncatedLine = truncateLine(line, Math.max(1, width));
|
|
493
|
+
return this.theme && this.color
|
|
494
|
+
? this.theme.fg(this.color, truncatedLine)
|
|
495
|
+
: truncatedLine;
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function truncateLine(line: string, maxWidth: number) {
|
|
501
|
+
return Array.from(line).slice(0, maxWidth).join("");
|
|
502
|
+
}
|
|
503
|
+
|
|
408
504
|
async function withStatus<T>(ctx: StatusContext, status: string, callback: () => Promise<T>) {
|
|
409
505
|
ctx.ui.setStatus(STATUS_KEY, status);
|
|
410
506
|
try {
|