From 1a437f50b45a705a2d0bf4d3bb78f69d345b2953 Mon Sep 17 00:00:00 2001 From: JB Date: Wed, 26 Aug 2026 16:40:05 -0400 Subject: [PATCH] Rename task widget to todos --- README.md | 4 ++-- index.ts | 10 +++++----- src/ui.ts | 2 +- src/widget.ts | 4 ++-- tests/prompt.test.ts | 32 ++++++++++++++++++++++++++++++++ tests/ui.test.ts | 8 ++++++-- 6 files changed, 48 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 0a978a1..8c03337 100644 --- a/README.md +++ b/README.md @@ -98,10 +98,10 @@ A task starts timing the first time it enters `in-progress`. Leaving that state ## Pi Widget -The widget appears above Pi's editor while tasks exist. Run `/tasks` to hide or show it for the current Pi runtime. Its header is the virtual task-tree root. Root tasks render as Roman-numeral categories, their children as numbered items, and leaf sub-items use indentation without another label. Internal task IDs remain unchanged and visible in tool output. In-progress circles rotate through `◐ ◓ ◑ ◒` on the existing one-second timer refresh; other status symbols remain still. The widget shows at most 12 task rows plus a compact count of hidden tasks, so larger plans do not crowd the editor out of an 80×24 terminal. +The widget appears above Pi's editor while todos exist. Run `/todo` to hide or show it for the current Pi runtime. Its header is the virtual task-tree root. Root tasks render as Roman-numeral categories, their children as numbered items, and leaf sub-items use indentation without another label. Internal task IDs remain unchanged and visible in tool output. In-progress circles rotate through `◐ ◓ ◑ ◒` twice per second; other status symbols remain still. The widget shows at most 12 task rows plus a compact count of hidden tasks, so larger plans do not crowd the editor out of an 80×24 terminal. ```text - Tasks 2/6 · !1 ━━━━━──────── + Todos 2/6 · !1 ━━━━━──────── I. ◐ Build auth · 12:08 ├─ 1. ! Resolve token issue · 03:41 │ └─ ○ Inspect provider response diff --git a/index.ts b/index.ts index a829677..4a65896 100644 --- a/index.ts +++ b/index.ts @@ -42,22 +42,22 @@ export default function piTodoHerdr(pi: ExtensionAPI): void { registerTaskTools(pi, state, refresh); - pi.registerCommand("tasks", { - description: "Show or hide the task tree widget", + pi.registerCommand("todo", { + description: "Show or hide the todo tree widget", handler: async (_args, ctx) => { if (state.list().length === 0) { clearTaskWidget(ctx); - ctx.ui.notify("No tasks yet.", "info"); + ctx.ui.notify("No todos yet.", "info"); return; } widgetVisible = !widgetVisible; if (widgetVisible) { updateTaskWidget(state, ctx, true); - ctx.ui.notify("Task widget shown.", "info"); + ctx.ui.notify("Todo widget shown.", "info"); } else { clearTaskWidget(ctx); - ctx.ui.notify("Task widget hidden.", "info"); + ctx.ui.notify("Todo widget hidden.", "info"); } }, }); diff --git a/src/ui.ts b/src/ui.ts index 5725aff..3cf06b1 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -210,7 +210,7 @@ export function renderWidgetTree( export function progressLabel(stats: TaskStats): string { const blocked = stats.blocked > 0 ? ` · !${stats.blocked}` : ""; - return ` Tasks ${stats.done}/${stats.total}${blocked}`; + return ` Todos ${stats.done}/${stats.total}${blocked}`; } export function renderProgressHeader(stats: TaskStats, theme: Theme, width: number): string { diff --git a/src/widget.ts b/src/widget.ts index 61db64c..48c9f1a 100644 --- a/src/widget.ts +++ b/src/widget.ts @@ -3,7 +3,7 @@ import type { TaskState } from "./state.ts"; import { renderProgressHeader, renderWidgetTree } from "./ui.ts"; const WIDGET_ID = "pi-todo-herdr"; -const TIMER_REFRESH_MS = 1000; +export const TODO_ANIMATION_INTERVAL_MS = 500; export function clearTaskWidget(ctx: ExtensionContext): void { ctx.ui.setWidget(WIDGET_ID, undefined); @@ -13,7 +13,7 @@ export function updateTaskWidget( state: TaskState, ctx: ExtensionContext, visible: boolean, - refreshMs = TIMER_REFRESH_MS, + refreshMs = TODO_ANIMATION_INTERVAL_MS, ): void { const tasks = state.list(); if (!visible || tasks.length === 0) { diff --git a/tests/prompt.test.ts b/tests/prompt.test.ts index 0d47e91..a9fbc16 100644 --- a/tests/prompt.test.ts +++ b/tests/prompt.test.ts @@ -10,6 +10,38 @@ import { } from "../src/prompt.ts"; import { TaskState } from "../src/state.ts"; +test("registers the conflict-free /todo command", async () => { + type RegisteredCommand = { + description: string; + handler(args: string, ctx: unknown): Promise; + }; + const commands = new Map(); + const pi = { + on() {}, + registerTool() {}, + registerCommand(name: string, command: RegisteredCommand) { + commands.set(name, command); + }, + }; + + piTodoHerdr(pi as never); + + assert.deepEqual([...commands.keys()], ["todo"]); + const todo = commands.get("todo"); + assert.equal(todo?.description, "Show or hide the todo tree widget"); + + const notifications: string[] = []; + await todo?.handler("", { + ui: { + setWidget() {}, + notify(message: string) { + notifications.push(message); + }, + }, + }); + assert.deepEqual(notifications, ["No todos yet."]); +}); + test("keeps task guidance short with planning after exploration", () => { assert.equal(TASK_PROMPT_GUIDELINES.length, 2); const prompt = TASK_PROMPT_GUIDELINES.join("\n"); diff --git a/tests/ui.test.ts b/tests/ui.test.ts index 9daf439..a7e7715 100644 --- a/tests/ui.test.ts +++ b/tests/ui.test.ts @@ -12,7 +12,7 @@ import { renderWidgetTree, snapshotStats, } from "../src/ui.ts"; -import { updateTaskWidget } from "../src/widget.ts"; +import { TODO_ANIMATION_INTERVAL_MS, updateTaskWidget } from "../src/widget.ts"; const plainTheme = { fg: (_color: string, text: string) => text, @@ -49,7 +49,7 @@ function fixture(): TaskState { test("renders an adaptive progress bar and stays within width", () => { const stats = fixture().stats(); const wide = stripAnsi(renderProgressHeader(stats, plainTheme as never, 40)); - assert.match(wide, /^ Tasks 1\/5 · !1 ━{4}─{16}$/); + assert.match(wide, /^ Todos 1\/5 · !1 ━{4}─{16}$/); assert.ok(visibleWidth(wide) <= 40); const narrow = renderProgressHeader(stats, plainTheme as never, 12); @@ -353,6 +353,10 @@ test("hides an empty widget and renders a populated one", () => { assert.ok(lines.every((line) => visibleWidth(line) <= 30)); }); +test("animates active todos twice per second", () => { + assert.equal(TODO_ANIMATION_INTERVAL_MS, 500); +}); + test("live widgets request rerenders and dispose their interval", async () => { let now = 0; const state = new TaskState(() => now);