@narumitw/pi-chrome-devtools 0.1.31 → 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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narumitw/pi-chrome-devtools",
3
- "version": "0.1.31",
3
+ "version": "0.1.33",
4
4
  "description": "Pi extension that exposes Chrome DevTools Protocol tools.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,4 +1,9 @@
1
- import { defineTool, type ExtensionAPI } from "@mariozechner/pi-coding-agent";
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 {