import type { ExtensionAPI } from "@earendil-works/pi-coding-agent" import { truncateHead, DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, keyHint, } from "@earendil-works/pi-coding-agent" import { Text } from "@earendil-works/pi-tui" import { Type } from "typebox" import { fetchPage, formatResult } from "../src/fetch.js" export default function (pi: ExtensionAPI) { pi.registerTool({ name: "web_fetch", label: "Web Fetch", description: "Fetch a webpage and extract its main content as clean Markdown. " + "Strips navigation, ads, sidebars, and clutter. " + "Returns title, author, description, word count, and the article content.", promptSnippet: "Fetch a URL and return its readable content as clean Markdown", promptGuidelines: [ "Use web_fetch when the user provides a URL to read, analyze, or summarize.", "Prefer web_fetch over bash curl for reading webpage content — it extracts clean text and saves tokens.", ], parameters: Type.Object({ url: Type.String({ description: "URL of the webpage to fetch" }), }), renderCall(args, theme, context) { const text = (context.lastComponent as Text) ?? new Text("", 0, 0) const url = typeof args?.url === "string" ? args.url : "..." text.setText( `${theme.fg("toolTitle", theme.bold("web_fetch"))} ${theme.fg("accent", url)}` ) return text }, renderResult(result, options, theme, context) { const text = (context.lastComponent as Text) ?? new Text("", 0, 0) const output = result.content ?.filter((c: any) => c.type === "text") .map((c: any) => c.text || "") .join("\n") || "" const lines = output.split("\n") const maxLines = options.expanded ? lines.length : 10 const displayLines = lines.slice(0, maxLines) const remaining = lines.length - maxLines let rendered = `\n${displayLines.map((line: string) => theme.fg("toolOutput", line)).join("\n")}` if (remaining > 0) { rendered += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("app.tools.expand", "to expand")})` } text.setText(rendered) return text }, async execute(_toolCallId, params, signal, _onUpdate, _ctx) { const result = await fetchPage(params.url, signal ?? undefined) const output = formatResult(result) // Truncate if output exceeds context limits const truncation = truncateHead(output, { maxLines: DEFAULT_MAX_LINES, maxBytes: DEFAULT_MAX_BYTES, }) let text = truncation.content if (truncation.truncated) { text += `\n\n[Truncated: showing ${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}]` } return { content: [{ type: "text", text }], details: { url: result.url, title: result.title, wordCount: result.wordCount, truncated: truncation.truncated, }, } }, }) }