import { StringEnum } from "@earendil-works/pi-ai"; import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { Text } from "@earendil-works/pi-tui"; import { type Static, Type } from "typebox"; import { ArtifactStore, type FetchArtifactFormat } from "./artifacts.ts"; import { DEFAULT_PAGE_LINES, MAX_PAGE_LINES, formatTextPage, paginateText } from "./paging.ts"; const PageFields = { offset: Type.Optional(Type.Integer({ minimum: 1, description: "First line to return (1-based). Default: 1." })), limit: Type.Optional( Type.Integer({ minimum: 1, maximum: MAX_PAGE_LINES, description: `Maximum lines to return. Default: ${DEFAULT_PAGE_LINES}; hard maximum: ${MAX_PAGE_LINES}.`, }), ), }; export const WebFetchReadParameters = Type.Object( { fetch_id: Type.String({ description: "fetch_id returned by web_fetch." }), format: Type.Optional( StringEnum(["source", "text", "markdown", "html"] as const, { description: "Stored view to read. Default: source." }), ), ...PageFields, }, { additionalProperties: false }, ); export type WebFetchReadInput = Static; export interface ArtifactReadDetails { artifactId: string; kind: "fetch"; view: FetchArtifactFormat; source?: string; title?: string; startLine: number; endLine: number; totalLines: number; nextOffset?: number; path: string; } function renderReadCall( label: string, artifactId: string, offset: number | undefined, limit: number | undefined, theme: Parameters[0]["renderCall"]>>[1], ): Text { const start = offset ?? 1; const count = limit ?? DEFAULT_PAGE_LINES; return new Text( `${theme.fg("toolTitle", theme.bold(`${label} `))}${theme.fg("accent", artifactId)}${theme.fg("muted", `:${start}-${start + count - 1}`)}`, 0, 0, ); } function renderReadResult( result: { content: Array<{ type: string; text?: string }>; details?: ArtifactReadDetails }, expanded: boolean, theme: Parameters[0]["renderResult"]>>[2], ): Text { const details = result.details; if (!details) { const error = result.content.find((item) => item.type === "text")?.text ?? "Web artifact read failed"; return new Text(theme.fg("error", error), 0, 0); } let text = theme.fg( "success", `${details.artifactId} ${details.startLine}-${details.endLine}/${details.totalLines}`, ); if (details.nextOffset !== undefined) text += theme.fg("muted", ` ยท next ${details.nextOffset}`); if (expanded) { const output = result.content.find((item) => item.type === "text")?.text; if (output) text += `\n\n${theme.fg("toolOutput", output)}`; } return new Text(text, 0, 0); } export function registerWebFetchReadTool(pi: ExtensionAPI): void { pi.registerTool({ name: "web_fetch_read", label: "Web Fetch Read", description: "Read a bounded source, extracted-text, Markdown, or legacy HTML view created by web_fetch.", parameters: WebFetchReadParameters, executionMode: "parallel", async execute(_toolCallId, params, _signal, _onUpdate, ctx) { const format: FetchArtifactFormat = params.format ?? "source"; const store = new ArtifactStore(ctx.sessionManager.getSessionId()); const artifact = await store.readFetch(params.fetch_id, format); const page = paginateText(artifact.text, params); return { content: [ { type: "text", text: formatTextPage(page, { artifactId: params.fetch_id, view: format, source: artifact.metadata.finalUrl, continuationTool: "web_fetch_read", continuationArgs: { fetch_id: params.fetch_id, format }, }), }, ], details: { artifactId: params.fetch_id, kind: "fetch", view: format, source: artifact.metadata.finalUrl, title: artifact.metadata.title, startLine: page.startLine, endLine: page.endLine, totalLines: page.totalLines, nextOffset: page.nextOffset, path: format === "source" || format === "html" ? (artifact.paths.source ?? artifact.paths.raw ?? artifact.paths.htmlView as string) : format === "text" ? (artifact.paths.text ?? artifact.paths.source ?? artifact.paths.raw as string) : (artifact.paths.markdown ?? artifact.paths.text ?? artifact.paths.source ?? artifact.paths.raw as string), }, }; }, renderCall(args, theme) { return renderReadCall("web_fetch_read", args.fetch_id, args.offset, args.limit, theme); }, renderResult(result, { expanded }, theme) { return renderReadResult(result, expanded, theme); }, }); }