From 729015d90dd7d2be7366bc60281ebea47d5ba1d3 Mon Sep 17 00:00:00 2001 From: wassname <1103714+wassname@users.noreply.github.com> Date: Sun, 14 Jun 2026 11:49:36 +0800 Subject: [PATCH] simplify collapsed task rows --- README.md | 12 ++--- src/index.ts | 49 ++++++++--------- src/ui/task-widget.ts | 22 ++------ test/task-list-render.test.ts | 99 +++++++++++++++++++++++++++++++++++ test/task-widget.test.ts | 22 ++++---- 5 files changed, 144 insertions(+), 60 deletions(-) create mode 100644 test/task-list-render.test.ts diff --git a/README.md b/README.md index 5f52e3f..66d8800 100644 --- a/README.md +++ b/README.md @@ -39,15 +39,11 @@ Stripped: `TaskExecute`, `TaskOutput`, `TaskStop`, `process-tracker.ts`, subagen ``` ● 3 tasks (1 done, 1 in progress, 1 open) ✔ #1 Design schema - ✳ #2 Implementing cache layer... (2m 49s · ↑ 4.1k ↓ 1.2k) - ◻ #3 Load test 🛠 🤖 ✓ + ✳ #2 Implementing cache layer… (2m 49s · ↑ 4.1k ↓ 1.2k) + ◻ #3 Load test ``` -Badges: - -- `🛠` proof evidence attached via `TaskClaimDone` -- `🤖` one or more robot review iterations attached -- `✓` task completed after accepted proof review +Collapsed rows stay simple. Proof details live in `TaskGet` and `/lgtm`, not in the widget row itself. ## Tools @@ -63,7 +59,7 @@ Omit `parentId` for a proof-gated top-level goal. Set `parentId` for a directly ### `TaskList` -Lists all tasks. Badges show evidence, review, and completion state. +Lists all tasks in the same compact one-line style as the widget. Proof details live in `TaskGet` and `/lgtm`. ### `TaskGet` diff --git a/src/index.ts b/src/index.ts index c5741a9..3dae1c8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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); diff --git a/src/ui/task-widget.ts b/src/ui/task-widget.ts index 29b3855..cc0bba6 100644 --- a/src/ui/task-widget.ts +++ b/src/ui/task-widget.ts @@ -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) { diff --git a/test/task-list-render.test.ts b/test/task-list-render.test.ts new file mode 100644 index 0000000..821b21b --- /dev/null +++ b/test/task-list-render.test.ts @@ -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; +}; + +function makeHarness() { + const tools = new Map(); + 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) { + 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("🛠"); + }); +}); diff --git a/test/task-widget.test.ts b/test/task-widget.test.ts index ca0880d..a687b1d 100644 --- a/test/task-widget.test.ts +++ b/test/task-widget.test.ts @@ -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", () => {