import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { Text } from "@earendil-works/pi-tui"; import { StringEnum } from "@earendil-works/pi-ai"; import { Type } from "typebox"; import type { ListTicketsParams, TicketStatus } from "../types.js"; import { listTickets, isInitialized, ticketFileName } from "../utils.js"; const KANBAN_EMOJI: Record = { backlog: "📋", ready: "✅", "in-progress": "🚧", review: "👀", done: "🎉", blocked: "🚫", }; export function registerDocgraphTicketList(pi: ExtensionAPI): void { pi.registerTool({ name: "docgraph_ticket_list", label: "Docgraph Ticket List", description: "List all tickets or filter by status. Returns a kanban-style summary. Use to determine the next ticket to work on.", parameters: Type.Object({ status: Type.Optional( StringEnum(["backlog", "ready", "in-progress", "review", "done", "blocked"] as const), ), }), async execute(_toolCallId, params, _signal, _onUpdate, ctx) { const p = params as ListTicketsParams; if (!isInitialized(ctx)) { return { content: [ { type: "text", text: "Documentation not initialized. Run `docgraph_init` first.", }, ], details: { action: "ticket_list", error: "not_initialized", state: { initialized: false, schemaVersion: 1 }, }, }; } const allTickets = listTickets(ctx.cwd); const filtered = p.status ? allTickets.filter((t) => t.status === p.status) : allTickets; if (filtered.length === 0) { return { content: [ { type: "text", text: p.status ? `No tickets with status: ${p.status}` : "No tickets found. Use `docgraph_ticket_create` to create one.", }, ], details: { action: "ticket_list", tickets: [], total: allTickets.length, state: { initialized: true, schemaVersion: 1 }, }, }; } // Group by status for kanban view const groups = new Map(); for (const t of filtered) { const existing = groups.get(t.status); if (existing) existing.push(t); else groups.set(t.status, [t]); } const lines: string[] = []; lines.push(`${filtered.length} ticket(s)`); if (!p.status) lines.push(""); const order: TicketStatus[] = [ "backlog", "ready", "in-progress", "review", "done", "blocked", ]; for (const status of order) { const tickets = groups.get(status); if (!tickets || tickets.length === 0) continue; const emoji = KANBAN_EMOJI[status] ?? "•"; lines.push(`## ${emoji} ${status.toUpperCase()} (${tickets.length})`); for (const t of tickets) { lines.push( `- [T-${t.id}](${`docs/tickets/${ticketFileName(t)}`}) **${t.title}** (${t.priority})`, ); } lines.push(""); } return { content: [ { type: "text", text: lines.join("\n").trim(), }, ], details: { action: "ticket_list", tickets: filtered.map((t) => ({ id: t.id, title: t.title, status: t.status, priority: t.priority })), total: allTickets.length, filtered: filtered.length, state: { initialized: true, schemaVersion: 1 }, }, }; }, renderCall(args, theme) { let text = theme.fg("toolTitle", theme.bold("docgraph-ticket-list")); if (args.status) text += theme.fg("muted", ` (${args.status})`); return new Text(text, 0, 0); }, renderResult(result, _opts, theme) { const details = result.details as { filtered?: number; total?: number } | undefined; const count = details?.filtered ?? 0; const total = details?.total ?? 0; return new Text( theme.fg("muted", `${count}/${total} ticket(s)`), 0, 0, ); }, }); }