import { homedir } from "node:os"; import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; import { DynamicBorder, getAgentDir, getMarkdownTheme } from "@earendil-works/pi-coding-agent"; import { Container, Markdown, matchesKey, Text } from "@earendil-works/pi-tui"; import { buildResourceIndex, readRuntimeMcpServers, readRuntimePackages } from "./resources/index.ts"; import { buildMarkdown } from "./render/markdown.ts"; import { createStatePaths, loadState, saveState } from "./state/store.ts"; import { findMatches, pruneState, recordUsageByName } from "./state/usage.ts"; import type { HelpState, ResourceIndex } from "./types.ts"; async function showMarkdownPanel(ctx: ExtensionContext, markdown: string): Promise { if (!ctx.hasUI) { console.log(markdown); return; } await ctx.ui.custom((_tui, theme, _kb, done) => { const container = new Container(); const border = new DynamicBorder((s: string) => theme.fg("accent", s)); const mdTheme = getMarkdownTheme(); container.addChild(border); container.addChild(new Text(theme.fg("accent", theme.bold("Pi 帮助")), 1, 0)); container.addChild(new Markdown(markdown, 1, 1, mdTheme)); container.addChild(new Text(theme.fg("dim", "按 Enter 或 Esc 关闭"), 1, 0)); container.addChild(border); return { render: (width: number) => container.render(width), invalidate: () => container.invalidate(), handleInput: (data: string) => { if (matchesKey(data, "enter") || matchesKey(data, "escape")) done(undefined); }, }; }); } function syncSessionUsage(ctx: ExtensionContext, state: HelpState): void { for (const entry of ctx.sessionManager.getBranch()) { const entryType = (entry as { type?: string }).type; if (entryType === "bashExecution") { recordUsageByName(state, "tool", "bash"); continue; } if (entryType !== "message") continue; const message = (entry as { message?: unknown }).message as { role?: string; toolName?: string; content?: unknown } | undefined; if (!message) continue; if (message.role === "toolResult" && typeof message.toolName === "string") { recordUsageByName(state, "tool", message.toolName); if (message.toolName === "mcp") recordUsageByName(state, "mcp", "mcp"); } if (message.role === "assistant" && Array.isArray(message.content)) { for (const block of message.content) { if (!block || typeof block !== "object") continue; const toolCall = block as { type?: string; name?: string }; if (toolCall.type === "toolCall" && typeof toolCall.name === "string") recordUsageByName(state, "tool", toolCall.name); } } } } function notifySaveWarning(ctx: ExtensionContext, warning: string | undefined): void { if (warning) ctx.ui.notify(warning, "warning"); } export function registerHelpExtension(pi: ExtensionAPI): void { const paths = createStatePaths(getAgentDir()); const loaded = loadState(paths); const state = loaded.state; function rebuildIndex(ctx: ExtensionContext): ResourceIndex { syncSessionUsage(ctx, state); const index = buildResourceIndex({ commands: pi.getCommands(), tools: pi.getAllTools(), packages: readRuntimePackages(getAgentDir(), ctx.cwd), mcpServers: readRuntimeMcpServers(getAgentDir(), homedir(), ctx.cwd), state, }); pruneState(state); return index; } function persist(ctx: ExtensionContext): { ok: boolean; warning?: string } { const result = saveState(paths, state); notifySaveWarning(ctx, result.warning); return result; } pi.registerCommand("help", { description: "Show a dynamic help dashboard for installed Pi resources", handler: async (args, ctx) => { for (const warning of loaded.warnings) ctx.ui.notify(warning, "warning"); loaded.warnings.length = 0; const trimmed = args.trim(); const [subcommand, ...rest] = trimmed.length > 0 ? trimmed.split(/\s+/) : [""]; const query = rest.join(" ").trim(); if (subcommand === "refresh") { rebuildIndex(ctx); const result = persist(ctx); if (result.ok) { ctx.ui.notify("帮助索引已刷新", "info"); } else { ctx.ui.notify(`帮助索引刷新失败:${result.warning ?? "保存状态失败"}`, "error"); } return; } if (subcommand === "pin" || subcommand === "unpin") { rebuildIndex(ctx); const matches = findMatches(state, query); if (matches.length === 0) { ctx.ui.notify(`未找到:${query || "<空>"}`, "warning"); persist(ctx); return; } const target = matches[0]; const wasPinned = target.pinned; target.pinned = subcommand === "pin"; target.lastSeenAt = Date.now(); const result = persist(ctx); if (result.ok) { ctx.ui.notify(`${subcommand === "pin" ? "已固定" : "已取消固定"}:${target.displayName ?? target.name} (${target.kind} · ${target.sourceLabel})`, "info"); } else { target.pinned = wasPinned; // rollback ctx.ui.notify(`${subcommand === "pin" ? "固定" : "取消固定"}失败:${result.warning ?? "保存状态失败"}`, "error"); } return; } const index = rebuildIndex(ctx); persist(ctx); const markdown = buildMarkdown(index, state.lastRefresh, subcommand === "search" ? query : trimmed); await showMarkdownPanel(ctx, markdown); }, }); pi.on("session_start", async (_event, ctx) => { rebuildIndex(ctx); persist(ctx); }); pi.on("tool_result", async (event, ctx) => { if (event.isError) return; recordUsageByName(state, "tool", event.toolName); if (event.toolName === "mcp") { recordUsageByName(state, "mcp", "mcp"); const maybeInput = event as { input?: { server?: string } }; if (maybeInput.input?.server) recordUsageByName(state, "mcp", maybeInput.input.server); } pruneState(state); persist(ctx); }); pi.on("session_shutdown", async (_event, ctx) => { pruneState(state); persist(ctx); }); }