import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; import { Type } from "@sinclair/typebox"; import { getServiceKey } from "../shared/auth"; import { fetchContext7Docs, resolveContext7Library } from "../clients/context7"; import { PREVIEW_LINES, type DocsSearchDetails } from "../types"; import { buildExpandedParams, kylinFrameForCall, kylinFrameForResult } from "../../../vera-theme/src/public"; function truncate(text: string, max = 70): string { return text.length > max ? text.slice(0, max - 1) + "…" : text; } function buildParamSummary(args: any, theme: any): string { return ( theme.fg("accent", truncate(String(args.library ?? ""), 32)) + theme.fg("borderMuted", " · ") + theme.fg("text", `"${truncate(String(args.query ?? ""), 58)}"`) ); } function extractTextPreview(content: any, maxLines: number): { lines: string[]; totalLines: number } { if (content?.type !== "text" || typeof content.text !== "string") return { lines: [], totalLines: 0 }; const lines = content.text.split("\n"); return { lines: lines.slice(0, maxLines), totalLines: lines.length }; } export function registerDocsSearchTool(pi: ExtensionAPI): void { pi.registerTool({ name: "docs_search", label: "Docs Search", description: "Search library documentation via Context7. Provide a library name (e.g. 'react', 'express', 'langchain') and a query describing what you need. Returns relevant documentation snippets.", promptSnippet: "Use docs_search to look up library/framework documentation when you need API references, usage examples, or version-specific behavior.", parameters: Type.Object({ library: Type.String({ description: "Library or framework name (e.g. 'react', 'nextjs', 'fastapi')" }), query: Type.String({ description: "What you want to find in the docs (e.g. 'how to create a context provider')" }), }), renderShell: "self" as const, renderCall(args, theme, ctx) { const state = ctx.state as { startedAt?: number }; if (state.startedAt === undefined) state.startedAt = Date.now(); return kylinFrameForCall(ctx, { name: "docs_search", theme, status: "pending", paramSummary: buildParamSummary(args, theme), startedAt: state.startedAt, }); }, renderResult(result, { expanded, isPartial }, theme, context) { const details = result.details as DocsSearchDetails | undefined; const state = context.state as { startedAt?: number }; const startedAt = state.startedAt; const paramSummary = buildParamSummary(context.args, theme); const expandedParams = expanded ? buildExpandedParams([ { label: "library", value: context.args?.library }, { label: "query", value: context.args?.query, multiline: true }, ], theme) : undefined; if (isPartial) { return kylinFrameForResult(context, { name: "docs_search", theme, status: "pending", paramSummary, resultSummary: theme.fg("warning", details?.phase ?? "resolving"), expanded, expandedParams, startedAt, }); } if (details?.error) { return kylinFrameForResult(context, { name: "docs_search", theme, status: "error", paramSummary, errorMessage: details.error, expanded, expandedParams, startedAt, }); } if (!details) { return kylinFrameForResult(context, { name: "docs_search", theme, status: "error", paramSummary, errorMessage: "No result", expanded, expandedParams, startedAt, }); } const preview = extractTextPreview(result.content[0], PREVIEW_LINES); const resolved = details.resolvedName ?? details.library; const counts = [ details.snippets !== undefined ? `${details.snippets} snippets` : undefined, details.lines !== undefined ? `${details.lines} lines` : undefined, ].filter(Boolean).join(" · "); return kylinFrameForResult(context, { name: "docs_search", theme, status: "success", paramSummary, resultSummary: theme.fg("accent", resolved) + (counts ? theme.fg("dim", ` · ${counts}`) : ""), expanded, expandedParams, expandedBody: expanded ? preview.lines.map((line) => theme.fg("dim", line)) : undefined, expandedTotalLines: preview.totalLines, startedAt, }); }, async execute(_toolCallId, params, signal, onUpdate) { const apiKey = getServiceKey("context7", "CONTEXT7_API_KEY"); onUpdate?.({ content: [{ type: "text" as const, text: "Resolving library..." }], details: { library: params.library, query: params.query, phase: "resolving" } as DocsSearchDetails, }); const library = await resolveContext7Library(params.library, params.query, apiKey, signal); if ("error" in library) { return { content: [{ type: "text" as const, text: library.error }], details: { library: params.library, query: params.query, phase: "done", error: library.error } as DocsSearchDetails, }; } onUpdate?.({ content: [{ type: "text" as const, text: `Fetching docs for ${library.name}...` }], details: { library: params.library, query: params.query, phase: "fetching", resolvedId: library.id, resolvedName: library.name, } as DocsSearchDetails, }); const docs = await fetchContext7Docs(library.id, params.query, apiKey, signal); if ("error" in docs) { return { content: [{ type: "text" as const, text: docs.error }], details: { library: params.library, query: params.query, phase: "done", resolvedId: library.id, resolvedName: library.name, error: docs.error, } as DocsSearchDetails, }; } const lines = docs.content.split("\n").length; const snippets = (docs.content.match(/^#{1,3}\s/gm) ?? []).length; return { content: [{ type: "text" as const, text: docs.content }], details: { library: params.library, query: params.query, phase: "done", resolvedId: library.id, resolvedName: library.name, snippets, lines, } as DocsSearchDetails, }; }, }); }