mirror of
https://github.com/wassname/pi-lgtm.git
synced 2026-06-27 15:31:29 +08:00
simplify collapsed task rows
This commit is contained in:
+25
-24
@@ -32,13 +32,11 @@ import { Type } from "@sinclair/typebox";
|
||||
import { AutoClearManager } from "./auto-clear.js";
|
||||
import {
|
||||
type CompletionMode,
|
||||
type DisplayStatus,
|
||||
getCompletionMode,
|
||||
getDisplayStatus,
|
||||
getGateStatus,
|
||||
getReviewBadges,
|
||||
getReviewState,
|
||||
getStateTag,
|
||||
type ReviewState,
|
||||
} from "./review-badges.js";
|
||||
import {
|
||||
@@ -955,42 +953,47 @@ export default function (pi: ExtensionAPI) {
|
||||
pi.registerTool({
|
||||
name: "TaskList",
|
||||
label: "TaskList",
|
||||
description: `List all tasks grouped by status. State tag: [ACTIVE] [PENDING] [DONE]. Pipeline stages: [🛠🤖✓] = evidence→review→completed (·=pending).`,
|
||||
description: `List all tasks in a compact one-line format with one primary state per row. Proof details live in TaskGet and /lgtm.`,
|
||||
parameters: Type.Object({}),
|
||||
|
||||
execute(_toolCallId, _params, _signal, _onUpdate, _ctx) {
|
||||
const tasks = store.list();
|
||||
if (tasks.length === 0) return Promise.resolve(textResult("No tasks found"));
|
||||
|
||||
const counts = { completed: 0, in_progress: 0, pending: 0 };
|
||||
for (const task of tasks) counts[getDisplayStatus(task)]++;
|
||||
|
||||
const parts: string[] = [];
|
||||
if (counts.completed > 0) parts.push(`${counts.completed} done`);
|
||||
if (counts.in_progress > 0) parts.push(`${counts.in_progress} in progress`);
|
||||
if (counts.pending > 0) parts.push(`${counts.pending} open`);
|
||||
|
||||
const statusIcon = (task: typeof tasks[number]) => {
|
||||
if (task.status === "completed") return "✔";
|
||||
if (task.status === "in_progress") return "◼";
|
||||
return "◻";
|
||||
};
|
||||
|
||||
const renderTask = (task: typeof tasks[number]) => {
|
||||
const parent = task.parentId ? ` [subtask of #${task.parentId}]` : "";
|
||||
let line = ` [${getStateTag(task).padEnd(7)}] #${task.id} ${task.subject}${parent} ${getReviewBadges(task)}`;
|
||||
const parent = task.parentId ? ` › subtask of #${task.parentId}` : "";
|
||||
let blocked = "";
|
||||
if (task.blockedBy.length > 0) {
|
||||
const openBlockers = task.blockedBy.filter(bid => {
|
||||
const blocker = store.get(bid);
|
||||
return blocker && blocker.status !== "completed";
|
||||
});
|
||||
if (openBlockers.length > 0) line += ` [blocked by ${openBlockers.map(id => "#" + id).join(", ")}]`;
|
||||
if (openBlockers.length > 0) blocked = ` › blocked by ${openBlockers.map(id => "#" + id).join(", ")}`;
|
||||
}
|
||||
return line;
|
||||
const subject = task.status === "completed" ? `${task.subject}` : task.subject;
|
||||
return ` ${statusIcon(task)} #${task.id} ${subject}${parent}${blocked}`;
|
||||
};
|
||||
|
||||
const buckets: { label: string; status: DisplayStatus }[] = [
|
||||
{ label: "Active", status: "in_progress" },
|
||||
{ label: "Pending", status: "pending" },
|
||||
{ label: "Completed", status: "completed" },
|
||||
const lines = [
|
||||
`● ${tasks.length} tasks (${parts.join(", ")})`,
|
||||
...tasks.sort((a, b) => Number(a.id) - Number(b.id)).map(renderTask),
|
||||
];
|
||||
|
||||
const sections: string[] = [];
|
||||
for (const { label, status } of buckets) {
|
||||
const inBucket = tasks
|
||||
.filter(t => getDisplayStatus(t) === status)
|
||||
.sort((a, b) => Number(a.id) - Number(b.id));
|
||||
if (inBucket.length === 0) continue;
|
||||
sections.push(`${label}:\n${inBucket.map(renderTask).join("\n")}`);
|
||||
}
|
||||
|
||||
return Promise.resolve(textResult(sections.join("\n\n")));
|
||||
return Promise.resolve(textResult(lines.join("\n")));
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1491,9 +1494,7 @@ This appends a new robot-review iteration. If accepted for a top-level proof tas
|
||||
return "◻";
|
||||
};
|
||||
|
||||
const choices = tasks.map(t => {
|
||||
return `${statusIcon(t)} #${t.id} [${t.status}] ${t.subject} ${getReviewBadges(t)}`;
|
||||
});
|
||||
const choices = tasks.map(t => `${statusIcon(t)} #${t.id} ${t.subject}`);
|
||||
choices.push("← Back");
|
||||
|
||||
const selected = await ui.select("Tasks", choices);
|
||||
|
||||
+4
-18
@@ -9,7 +9,7 @@
|
||||
*/
|
||||
|
||||
import { truncateToWidth } from "@mariozechner/pi-tui";
|
||||
import { getDisplayStatus, getReviewBadges, getStateTag, getStateTagColor } from "../review-badges.js";
|
||||
import { getDisplayStatus } from "../review-badges.js";
|
||||
import type { TaskStore } from "../task-store.js";
|
||||
|
||||
// ---- Types ----
|
||||
@@ -141,12 +141,6 @@ export class TaskWidget {
|
||||
for (let i = 0; i < visible.length; i++) {
|
||||
const task = visible[i];
|
||||
const isActive = this.activeTaskIds.has(task.id) && task.status === "in_progress";
|
||||
const reviewSuffix = ` ${getReviewBadges(task)}`;
|
||||
const tag = getStateTag(task);
|
||||
// [ACTIVE ] [PENDING] [DONE ] — pad so columns line up.
|
||||
const tagColour = getStateTagColor(tag);
|
||||
const tagBox = `[${tag.padEnd(7)}]`;
|
||||
const tagPrefix = (tagColour ? theme.fg(tagColour, tagBox) : tagBox) + " ";
|
||||
|
||||
let icon: string;
|
||||
if (isActive) {
|
||||
@@ -173,8 +167,6 @@ export class TaskWidget {
|
||||
let text: string;
|
||||
if (isActive) {
|
||||
const form = task.progress_label || task.subject;
|
||||
const agentId = task.metadata?.agentId;
|
||||
const agentLabel = agentId ? ` (agent ${agentId.slice(0, 5)})` : "";
|
||||
const m = this.metrics.get(task.id);
|
||||
let stats = "";
|
||||
if (m) {
|
||||
@@ -186,20 +178,14 @@ export class TaskWidget {
|
||||
? ` ${theme.fg("dim", `(${elapsed} · ${tokenParts.join(" ")})`)}`
|
||||
: ` ${theme.fg("dim", `(${elapsed})`)}`;
|
||||
}
|
||||
text = ` ${icon} ${tagPrefix}${theme.fg("dim", "#" + task.id)} ${theme.fg("accent", form + agentLabel + "…")}${reviewSuffix}${stats}`;
|
||||
text = ` ${icon} ${theme.fg("dim", "#" + task.id)} ${theme.fg("accent", form + "…")}${stats}`;
|
||||
} else if (task.status === "completed") {
|
||||
text = ` ${icon} ${tagPrefix}${theme.fg("dim", theme.strikethrough("#" + task.id + " " + task.subject))}${reviewSuffix}`;
|
||||
text = ` ${icon} ${theme.fg("dim", theme.strikethrough("#" + task.id + " " + task.subject))}`;
|
||||
} else {
|
||||
const agentSuffix = task.status === "in_progress" && task.metadata?.agentId
|
||||
? theme.fg("dim", ` (agent ${task.metadata.agentId.slice(0, 5)})`)
|
||||
: "";
|
||||
text = ` ${icon} ${tagPrefix}${theme.fg("dim", "#" + task.id)} ${task.subject}${agentSuffix}${reviewSuffix}`;
|
||||
text = ` ${icon} ${theme.fg("dim", "#" + task.id)} ${task.subject}`;
|
||||
}
|
||||
|
||||
lines.push(truncate(text + suffix));
|
||||
if (task.status !== "completed" && (task as any).done_criterion) {
|
||||
lines.push(truncate(` test: ${(task as any).done_criterion}`));
|
||||
}
|
||||
}
|
||||
|
||||
if (tasks.length > MAX_VISIBLE_TASKS) {
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import proofTasksExtension from "../src/index.js";
|
||||
|
||||
type RegisteredTool = {
|
||||
name: string;
|
||||
execute: (...args: any[]) => Promise<any>;
|
||||
};
|
||||
|
||||
function makeHarness() {
|
||||
const tools = new Map<string, RegisteredTool>();
|
||||
const pi = {
|
||||
on: vi.fn(),
|
||||
registerTool: vi.fn((tool: RegisteredTool) => tools.set(tool.name, tool)),
|
||||
registerCommand: vi.fn(),
|
||||
sendMessage: vi.fn(),
|
||||
};
|
||||
|
||||
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, {});
|
||||
}
|
||||
|
||||
return { execTool };
|
||||
}
|
||||
|
||||
describe("TaskList", () => {
|
||||
it("renders a compact one-line-per-task summary", async () => {
|
||||
const harness = makeHarness();
|
||||
await harness.execTool("TaskCreate", {
|
||||
subject: "Design the flux capacitor",
|
||||
description: "Desc",
|
||||
done_criterion: "done",
|
||||
});
|
||||
await harness.execTool("TaskCreate", {
|
||||
subject: "Acquiring plutonium",
|
||||
description: "Desc",
|
||||
done_criterion: "done",
|
||||
progress_label: "Acquiring plutonium",
|
||||
});
|
||||
await harness.execTool("TaskCreate", {
|
||||
subject: "Install flux capacitor in DeLorean",
|
||||
description: "Desc",
|
||||
done_criterion: "done",
|
||||
parentId: "1",
|
||||
});
|
||||
await harness.execTool("TaskCreate", {
|
||||
subject: "Test time travel at 88 mph",
|
||||
description: "Desc",
|
||||
done_criterion: "done",
|
||||
});
|
||||
|
||||
await harness.execTool("TaskUpdate", { taskId: "1", status: "completed" });
|
||||
await harness.execTool("TaskUpdate", { taskId: "2", status: "in_progress" });
|
||||
await harness.execTool("TaskUpdate", { taskId: "3", add_blocked_by: ["1"] });
|
||||
await harness.execTool("TaskUpdate", { taskId: "4", add_blocked_by: ["2", "3"] });
|
||||
|
||||
const result = await harness.execTool("TaskList", {});
|
||||
const text = result.content[0].text;
|
||||
|
||||
expect(text).toContain("● 4 tasks (1 in progress, 3 open)");
|
||||
expect(text).toContain("◻ #1 Design the flux capacitor");
|
||||
expect(text).toContain("◼ #2 Acquiring plutonium");
|
||||
expect(text).toContain("◻ #3 Install flux capacitor in DeLorean › subtask of #1 › blocked by #1");
|
||||
expect(text).toContain("◻ #4 Test time travel at 88 mph › blocked by #2, #3");
|
||||
expect(text).not.toContain("[ACTIVE]");
|
||||
expect(text).not.toContain("[PENDING]");
|
||||
expect(text).not.toContain("[DONE");
|
||||
expect(text).not.toContain("🛠");
|
||||
expect(text).not.toContain("test:");
|
||||
});
|
||||
|
||||
it("shows completed subtasks without proof-lane clutter", async () => {
|
||||
const harness = makeHarness();
|
||||
await harness.execTool("TaskCreate", {
|
||||
subject: "Top-level goal",
|
||||
description: "Desc",
|
||||
done_criterion: "done",
|
||||
});
|
||||
await harness.execTool("TaskCreate", {
|
||||
subject: "Finished checklist item",
|
||||
description: "Desc",
|
||||
done_criterion: "done",
|
||||
parentId: "1",
|
||||
});
|
||||
|
||||
await harness.execTool("TaskUpdate", { taskId: "2", status: "completed" });
|
||||
|
||||
const result = await harness.execTool("TaskList", {});
|
||||
const text = result.content[0].text;
|
||||
|
||||
expect(text).toContain("● 2 tasks (1 done, 1 open)");
|
||||
expect(text).toContain("✔ #2 Finished checklist item › subtask of #1");
|
||||
expect(text).not.toContain("[DONE");
|
||||
expect(text).not.toContain("🛠");
|
||||
});
|
||||
});
|
||||
+12
-10
@@ -1,5 +1,4 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { REVIEW_BADGES } from "../src/review-badges.js";
|
||||
import { TaskStore } from "../src/task-store.js";
|
||||
import { TaskWidget, type Theme, type UICtx } from "../src/ui/task-widget.js";
|
||||
|
||||
@@ -73,11 +72,12 @@ describe("TaskWidget", () => {
|
||||
widget.update();
|
||||
|
||||
const lines = renderWidget(ui.state);
|
||||
expect(lines).toHaveLength(3); // header + 1 task + done_criterion
|
||||
expect(lines).toHaveLength(2); // header + 1 task
|
||||
expect(lines[0]).toContain("1 tasks");
|
||||
expect(lines[0]).toContain("1 open");
|
||||
expect(lines[1]).toContain("◻");
|
||||
expect(lines[1]).toContain("Do something");
|
||||
expect(lines[1]).not.toContain("done");
|
||||
});
|
||||
|
||||
it("renders in-progress tasks with ◼ icon", () => {
|
||||
@@ -100,16 +100,18 @@ describe("TaskWidget", () => {
|
||||
expect(lines[1]).toContain("~~#1 Done task~~");
|
||||
});
|
||||
|
||||
it("renders robot review badges on completed tasks", () => {
|
||||
it("does not render proof badges on collapsed rows", () => {
|
||||
store.create("Done task", "Desc", "done");
|
||||
store.update("1", {
|
||||
metadata: { robot_review_observations: ["Observed output drift on seed 2"] },
|
||||
metadata: { robot_review_observations: ["Observed output drift on seed 2"], lgtm_evidence: "verbatim output" },
|
||||
});
|
||||
store.complete("1");
|
||||
widget.update();
|
||||
|
||||
const lines = renderWidget(ui.state);
|
||||
expect(lines[1]).toContain(REVIEW_BADGES.robot);
|
||||
expect(lines[1]).not.toContain("[");
|
||||
expect(lines[1]).not.toContain("🛠");
|
||||
expect(lines[1]).not.toContain("🤖");
|
||||
});
|
||||
|
||||
it("renders active tasks with spinner icon", () => {
|
||||
@@ -181,9 +183,9 @@ describe("TaskWidget", () => {
|
||||
widget.update();
|
||||
|
||||
const lines = renderWidget(ui.state);
|
||||
// header + 5 visible tasks (each has 2 lines: task + done_criterion) + "...and 10 more"
|
||||
expect(lines).toHaveLength(12);
|
||||
expect(lines[11]).toContain("10 more");
|
||||
// header + 5 visible tasks + "...and 10 more"
|
||||
expect(lines).toHaveLength(7);
|
||||
expect(lines[6]).toContain("10 more");
|
||||
});
|
||||
|
||||
it("tracks token usage for active tasks", () => {
|
||||
@@ -241,7 +243,7 @@ describe("TaskWidget", () => {
|
||||
|
||||
const lines = renderWidget(ui.state);
|
||||
expect(lines[1]).toContain("Processing A…");
|
||||
expect(lines[3]).toContain("Processing B…");
|
||||
expect(lines[2]).toContain("Processing B…");
|
||||
});
|
||||
|
||||
it("distributes token usage across all active tasks", () => {
|
||||
@@ -257,7 +259,7 @@ describe("TaskWidget", () => {
|
||||
const lines = renderWidget(ui.state);
|
||||
// Both tasks should have the same token counts
|
||||
expect(lines[1]).toContain("↑ 100");
|
||||
expect(lines[3]).toContain("↑ 100");
|
||||
expect(lines[2]).toContain("↑ 100");
|
||||
});
|
||||
|
||||
it("dispose clears widget and timer", () => {
|
||||
|
||||
Reference in New Issue
Block a user