simplify collapsed task rows

This commit is contained in:
wassname
2026-06-14 11:49:36 +08:00
parent da2879d94c
commit 29c928c805
4 changed files with 140 additions and 52 deletions
+25 -24
View File
@@ -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
View File
@@ -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) {
+99
View File
@@ -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
View File
@@ -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", () => {