mirror of
https://github.com/wassname/pi-lgtm.git
synced 2026-06-27 17:01:35 +08:00
simplify /lgtm back to viewer-only
This commit is contained in:
+72
-28
@@ -20,7 +20,7 @@
|
|||||||
* /tasks — Interactive task management menu
|
* /tasks — Interactive task management menu
|
||||||
* /lgtm <id...> — View the proof log for one or more tasks
|
* /lgtm <id...> — View the proof log for one or more tasks
|
||||||
* /lgtm * — View all open task proof logs
|
* /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";
|
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 };
|
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 TASK_TOOL_NAMES = new Set(["TaskCreate", "TaskList", "TaskGet", "TaskUpdate", "TaskClaimDone", "lgtm_supersede", "robot_review_ask", "robot_review_run"]);
|
||||||
const REMINDER_INTERVAL = 4;
|
const REMINDER_INTERVAL = 4;
|
||||||
const AUTO_CLEAR_DELAY = 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);
|
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<void> {
|
async function viewEvidence(taskId: string, ctx: ExtensionCommandContext): Promise<void> {
|
||||||
const task = store.get(taskId);
|
const task = store.get(taskId);
|
||||||
if (!task) { ctx.ui.notify(`Task #${taskId} not found`, "error"); return; }
|
if (!task) {
|
||||||
ctx.ui.notify(renderTaskEvidenceForHuman(task), "info");
|
ctx.ui.notify(`Task #${taskId} not found`, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showProofLog(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function viewAllOpenProofLogs(ctx: ExtensionCommandContext): Promise<void> {
|
||||||
|
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", {
|
pi.registerCommand("lgtm", {
|
||||||
description:
|
description:
|
||||||
"View the proof log and judge notes. /lgtm <id> [<id>...] shows specific tasks; /lgtm * shows all open tasks. It does not complete tasks.",
|
"View the proof log and judge notes. /lgtm <id> [<id>...] shows specific tasks; /lgtm * shows all open tasks; task management lives in /tasks.",
|
||||||
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
||||||
const trimmed = args.trim();
|
const parsed = parseLgtmArgs(args);
|
||||||
if (trimmed === "*") {
|
if (parsed.kind === "error") {
|
||||||
const open = store.list().filter(t => t.status !== "completed");
|
ctx.ui.notify(parsed.message, "error");
|
||||||
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");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!trimmed) {
|
if (parsed.kind === "menu") {
|
||||||
const open = store.list();
|
const tasks = 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] ";
|
|
||||||
};
|
|
||||||
const choice = await ctx.ui.select(
|
const choice = await ctx.ui.select(
|
||||||
"View proof log:",
|
"LGTM",
|
||||||
open.map(t => `${tag(t)}#${t.id} ${t.subject}`).concat(["← Cancel"]),
|
["View all open proof logs", ...tasks.map(getLgtmTaskLabel), "← Cancel"],
|
||||||
);
|
);
|
||||||
if (!choice || choice === "← Cancel") return;
|
if (!choice || choice === "← Cancel") return;
|
||||||
|
if (choice === "View all open proof logs") return viewAllOpenProofLogs(ctx);
|
||||||
const match = choice.match(/#(\d+)/);
|
const match = choice.match(/#(\d+)/);
|
||||||
if (match) await viewEvidence(match[1], ctx);
|
if (match) return viewEvidence(match[1], ctx);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const ids = trimmed.split(/[\s,]+/).map(t => t.replace(/^#/, "")).filter(Boolean);
|
if (parsed.kind === "view_all") return viewAllOpenProofLogs(ctx);
|
||||||
for (const id of ids) await viewEvidence(id, 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 }));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RegisteredCommand = {
|
||||||
|
handler: (args: string, ctx: any) => Promise<void>;
|
||||||
|
getArgumentCompletions?: (args: string) => Promise<string[]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function makeHarness() {
|
||||||
|
const tools = new Map<string, RegisteredTool>();
|
||||||
|
const commands = new Map<string, RegisteredCommand>();
|
||||||
|
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<string, unknown>) {
|
||||||
|
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<string | undefined>;
|
||||||
|
confirm?: Array<boolean>;
|
||||||
|
} = {}) {
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user