From 9a46828dbf640b742742fa5abd9db3ed15f5be66 Mon Sep 17 00:00:00 2001 From: wassname <1103714+wassname@users.noreply.github.com> Date: Sun, 14 Jun 2026 11:20:07 +0800 Subject: [PATCH] simplify /lgtm back to viewer-only --- src/index.ts | 100 ++++++++++++++++++++++++---------- test/lgtm-command.test.ts | 109 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 181 insertions(+), 28 deletions(-) create mode 100644 test/lgtm-command.test.ts diff --git a/src/index.ts b/src/index.ts index df2d82a..3711ddf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,7 +20,7 @@ * /tasks — Interactive task management menu * /lgtm — View the proof log for one or more tasks * /lgtm * — View all open task proof logs - * /lgtm — Pick from open tasks to inspect proof logs + * /lgtm — Pick a task to inspect proof logs */ import { spawn } from "node:child_process"; @@ -58,6 +58,25 @@ function textResult(msg: string) { return { content: [{ type: "text" as const, text: msg }], details: undefined as any }; } +export type LgtmCommandSpec = + | { kind: "menu" } + | { kind: "view_all" } + | { kind: "view"; ids: string[] } + | { kind: "error"; message: string }; + +export function parseLgtmArgs(args: string): LgtmCommandSpec { + const trimmed = args.trim(); + if (!trimmed) return { kind: "menu" }; + if (trimmed === "*") return { kind: "view_all" }; + + const tokens = trimmed.split(/[\s,]+/).map(token => token.trim()).filter(Boolean); + if (tokens[0] === "clear") { + return { kind: "error", message: "Task clearing lives in /tasks now. /lgtm is viewer-only." }; + } + + return { kind: "view", ids: tokens.map(token => token.replace(/^#/, "")).filter(Boolean) }; +} + const TASK_TOOL_NAMES = new Set(["TaskCreate", "TaskList", "TaskGet", "TaskUpdate", "TaskClaimDone", "lgtm_supersede", "robot_review_ask", "robot_review_run"]); const REMINDER_INTERVAL = 4; const AUTO_CLEAR_DELAY = 4; @@ -1555,48 +1574,73 @@ This appends a new robot-review iteration. If accepted for a top-level proof tas return renderProofLog(task); } + function showProofLog(task: Task) { + pi.sendMessage({ + customType: "proof-log", + content: renderTaskEvidenceForHuman(task), + display: true, + details: { taskId: task.id }, + }); + } + + function getLgtmTaskLabel(task: Task): string { + const tag = task.status === "completed" + ? "[DONE] " + : task.status === "in_progress" + ? "[ACTIVE] " + : "[PENDING] "; + return `${tag}#${task.id} ${task.subject}`; + } + async function viewEvidence(taskId: string, ctx: ExtensionCommandContext): Promise { const task = store.get(taskId); - if (!task) { ctx.ui.notify(`Task #${taskId} not found`, "error"); return; } - ctx.ui.notify(renderTaskEvidenceForHuman(task), "info"); + if (!task) { + ctx.ui.notify(`Task #${taskId} not found`, "error"); + return; + } + showProofLog(task); + } + + async function viewAllOpenProofLogs(ctx: ExtensionCommandContext): Promise { + const open = store.list().filter(t => t.status !== "completed"); + if (open.length === 0) { + ctx.ui.notify("No open tasks to inspect.", "info"); + return; + } + for (const task of open) showProofLog(task); } pi.registerCommand("lgtm", { description: - "View the proof log and judge notes. /lgtm [...] shows specific tasks; /lgtm * shows all open tasks. It does not complete tasks.", + "View the proof log and judge notes. /lgtm [...] shows specific tasks; /lgtm * shows all open tasks; task management lives in /tasks.", handler: async (args: string, ctx: ExtensionCommandContext) => { - const trimmed = args.trim(); - if (trimmed === "*") { - const open = store.list().filter(t => t.status !== "completed"); - if (open.length === 0) { - ctx.ui.notify("No open tasks to inspect.", "info"); - return; - } - ctx.ui.notify(open.map(renderTaskEvidenceForHuman).join("\n\n---\n\n"), "info"); + const parsed = parseLgtmArgs(args); + if (parsed.kind === "error") { + ctx.ui.notify(parsed.message, "error"); return; } - if (!trimmed) { - const open = store.list(); - if (open.length === 0) { - ctx.ui.notify("No tasks to inspect.", "info"); - return; - } - const tag = (t: typeof open[number]) => { - if (t.status === "completed") return "[DONE] "; - if (t.status === "in_progress") return "[ACTIVE] "; - return "[PENDING] "; - }; + if (parsed.kind === "menu") { + const tasks = store.list(); const choice = await ctx.ui.select( - "View proof log:", - open.map(t => `${tag(t)}#${t.id} ${t.subject}`).concat(["← Cancel"]), + "LGTM", + ["View all open proof logs", ...tasks.map(getLgtmTaskLabel), "← Cancel"], ); if (!choice || choice === "← Cancel") return; + if (choice === "View all open proof logs") return viewAllOpenProofLogs(ctx); const match = choice.match(/#(\d+)/); - if (match) await viewEvidence(match[1], ctx); + if (match) return viewEvidence(match[1], ctx); return; } - const ids = trimmed.split(/[\s,]+/).map(t => t.replace(/^#/, "")).filter(Boolean); - for (const id of ids) await viewEvidence(id, ctx); + if (parsed.kind === "view_all") return viewAllOpenProofLogs(ctx); + for (const id of parsed.ids) await viewEvidence(id, ctx); + }, + getArgumentCompletions: (args: string) => { + const trimmed = args.trim(); + const tasks = store.list(); + if (!trimmed) return [{ value: "*", label: "*" }]; + const prefix = trimmed.replace(/^#/, ""); + return ["*", ...tasks.filter(task => task.id.startsWith(prefix)).map(task => task.id)] + .map(value => ({ value, label: value })); }, }); } diff --git a/test/lgtm-command.test.ts b/test/lgtm-command.test.ts new file mode 100644 index 0000000..40deb65 --- /dev/null +++ b/test/lgtm-command.test.ts @@ -0,0 +1,109 @@ +import { describe, expect, it, vi } from "vitest"; +import proofTasksExtension, { parseLgtmArgs } from "../src/index.js"; + +type RegisteredTool = { + name: string; + execute: (...args: any[]) => Promise; +}; + +type RegisteredCommand = { + handler: (args: string, ctx: any) => Promise; + getArgumentCompletions?: (args: string) => Promise; +}; + +function makeHarness() { + const tools = new Map(); + const commands = new Map(); + const sentMessages: any[] = []; + + const pi = { + on: vi.fn(), + registerTool: vi.fn((tool: RegisteredTool) => tools.set(tool.name, tool)), + registerCommand: vi.fn((name: string, command: RegisteredCommand) => commands.set(name, command)), + sendMessage: vi.fn((message: any) => sentMessages.push(message)), + }; + + proofTasksExtension(pi as any); + + async function execTool(name: string, params: Record) { + const tool = tools.get(name); + if (!tool) throw new Error(`Tool ${name} not registered`); + return tool.execute("tool-call", params, undefined, undefined, {}); + } + + function makeUi(overrides: { + select?: Array; + confirm?: Array; + } = {}) { + const selectQueue = [...(overrides.select ?? [])]; + const confirmQueue = [...(overrides.confirm ?? [])]; + return { + notify: vi.fn(), + select: vi.fn(async () => selectQueue.shift()), + confirm: vi.fn(async () => confirmQueue.shift() ?? false), + }; + } + + return { tools, commands, sentMessages, execTool, makeUi }; +} + +describe("parseLgtmArgs", () => { + it("parses menu and view forms", () => { + expect(parseLgtmArgs("")).toEqual({ kind: "menu" }); + expect(parseLgtmArgs("*")).toEqual({ kind: "view_all" }); + expect(parseLgtmArgs("1 #2")).toEqual({ kind: "view", ids: ["1", "2"] }); + }); + + it("rejects task-management forms", () => { + expect(parseLgtmArgs("clear")).toEqual({ kind: "error", message: "Task clearing lives in /tasks now. /lgtm is viewer-only." }); + expect(parseLgtmArgs("clear *")).toEqual({ kind: "error", message: "Task clearing lives in /tasks now. /lgtm is viewer-only." }); + expect(parseLgtmArgs("clear #7")).toEqual({ kind: "error", message: "Task clearing lives in /tasks now. /lgtm is viewer-only." }); + }); +}); + +describe("/lgtm command", () => { + it("shows all open proof logs from the picker", async () => { + const harness = makeHarness(); + await harness.execTool("TaskCreate", { subject: "Task A", description: "Desc", done_criterion: "done" }); + await harness.execTool("TaskCreate", { subject: "Task B", description: "Desc", done_criterion: "done" }); + + const ui = harness.makeUi({ select: ["View all open proof logs"] }); + const command = harness.commands.get("lgtm"); + if (!command) throw new Error("/lgtm not registered"); + + await command.handler("", { ui }); + + expect(harness.sentMessages).toHaveLength(2); + expect(harness.sentMessages[0].customType).toBe("proof-log"); + expect(harness.sentMessages[0].content).toContain("Task #1"); + expect(harness.sentMessages[1].content).toContain("Task #2"); + }); + + it("shows one proof log from the picker", async () => { + const harness = makeHarness(); + await harness.execTool("TaskCreate", { subject: "Task A", description: "Desc", done_criterion: "done" }); + + const ui = harness.makeUi({ select: ["[PENDING] #1 Task A"] }); + const command = harness.commands.get("lgtm"); + if (!command) throw new Error("/lgtm not registered"); + + await command.handler("", { ui }); + + expect(harness.sentMessages).toHaveLength(1); + expect(harness.sentMessages[0].content).toContain("Task #1"); + }); + + it("rejects /lgtm clear and points task management back to /tasks", async () => { + const harness = makeHarness(); + await harness.execTool("TaskCreate", { subject: "Task A", description: "Desc", done_criterion: "done" }); + + const ui = harness.makeUi(); + const command = harness.commands.get("lgtm"); + if (!command) throw new Error("/lgtm not registered"); + + await command.handler("clear 1", { ui }); + + expect(harness.sentMessages).toHaveLength(0); + expect(ui.notify).toHaveBeenCalledWith("Task clearing lives in /tasks now. /lgtm is viewer-only.", "error"); + }); +});