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" ;
27import { Type } from "typebox" ;
38
49const DEFAULT_HOST = "127.0.0.1" ;
@@ -13,6 +18,16 @@ interface StatusContext {
1318 ui : { setStatus : ( key : string , value : string | undefined ) => void } ;
1419}
1520
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+
1631interface DevToolsPage {
1732 id : string ;
1833 type : string ;
@@ -48,6 +63,8 @@ const listPagesTool = defineTool({
4863 description : "List Chrome tabs/pages from a running Chrome DevTools Protocol endpoint." ,
4964 promptSnippet : "List Chrome tabs/pages available over Chrome DevTools Protocol" ,
5065 parameters : Type . Object ( { } ) ,
66+ renderCall : renderToolCall ( "list pages" ) ,
67+ renderResult : renderTextResult ,
5168 async execute ( _toolCallId , _params , _signal , _onUpdate , ctx ) {
5269 return withStatus ( ctx , "🌐 list pages" , async ( ) => {
5370 const pages = await listPages ( ) ;
@@ -64,6 +81,8 @@ const selectPageTool = defineTool({
6481 parameters : Type . Object ( {
6582 pageId : Type . String ( { description : "Page id from chrome_devtools_list_pages." } ) ,
6683 } ) ,
84+ renderCall : renderToolCall ( "select page" ) ,
85+ renderResult : renderTextResult ,
6786 async execute ( _toolCallId , params , _signal , _onUpdate , ctx ) {
6887 return withStatus ( ctx , "🌐 select page" , async ( ) => {
6988 const page = await getPage ( params . pageId ) ;
@@ -87,6 +106,8 @@ const navigateTool = defineTool({
87106 Type . String ( { description : "Optional page id. Defaults to selected or first page." } ) ,
88107 ) ,
89108 } ) ,
109+ renderCall : renderToolCall ( "navigate" ) ,
110+ renderResult : renderTextResult ,
90111 async execute ( _toolCallId , params , _signal , _onUpdate , ctx ) {
91112 return withStatus ( ctx , "🌐 navigate" , async ( ) => {
92113 const { created, page } = await resolvePageForNavigation ( params . pageId ) ;
@@ -120,6 +141,8 @@ const evaluateTool = defineTool({
120141 Type . Boolean ( { description : "Whether to await a returned Promise. Defaults to true." } ) ,
121142 ) ,
122143 } ) ,
144+ renderCall : renderToolCall ( "evaluate" ) ,
145+ renderResult : renderTextResult ,
123146 async execute ( _toolCallId , params , _signal , _onUpdate , ctx ) {
124147 return withStatus ( ctx , "🌐 evaluate" , async ( ) => {
125148 const page = await resolvePage ( params . pageId ) ;
@@ -150,6 +173,8 @@ const screenshotTool = defineTool({
150173 Type . Boolean ( { description : "Capture the full document, not just the viewport." } ) ,
151174 ) ,
152175 } ) ,
176+ renderCall : renderToolCall ( "screenshot" ) ,
177+ renderResult : renderScreenshotResult ,
153178 async execute ( _toolCallId , params , _signal , _onUpdate , ctx ) {
154179 return withStatus ( ctx , "🌐 screenshot" , async ( ) => {
155180 const page = await resolvePage ( params . pageId ) ;
@@ -405,6 +430,77 @@ function textResult(text: string, details: unknown) {
405430 } ;
406431}
407432
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+
408504async function withStatus < T > ( ctx : StatusContext , status : string , callback : ( ) => Promise < T > ) {
409505 ctx . ui . setStatus ( STATUS_KEY , status ) ;
410506 try {
0 commit comments