Rename task widget to todos
This commit is contained in:
@@ -98,10 +98,10 @@ A task starts timing the first time it enters `in-progress`. Leaving that state
|
|||||||
|
|
||||||
## Pi Widget
|
## 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
|
```text
|
||||||
Tasks 2/6 · !1 ━━━━━────────
|
Todos 2/6 · !1 ━━━━━────────
|
||||||
I. ◐ Build auth · 12:08
|
I. ◐ Build auth · 12:08
|
||||||
├─ 1. ! Resolve token issue · 03:41
|
├─ 1. ! Resolve token issue · 03:41
|
||||||
│ └─ ○ Inspect provider response
|
│ └─ ○ Inspect provider response
|
||||||
|
|||||||
@@ -42,22 +42,22 @@ export default function piTodoHerdr(pi: ExtensionAPI): void {
|
|||||||
|
|
||||||
registerTaskTools(pi, state, refresh);
|
registerTaskTools(pi, state, refresh);
|
||||||
|
|
||||||
pi.registerCommand("tasks", {
|
pi.registerCommand("todo", {
|
||||||
description: "Show or hide the task tree widget",
|
description: "Show or hide the todo tree widget",
|
||||||
handler: async (_args, ctx) => {
|
handler: async (_args, ctx) => {
|
||||||
if (state.list().length === 0) {
|
if (state.list().length === 0) {
|
||||||
clearTaskWidget(ctx);
|
clearTaskWidget(ctx);
|
||||||
ctx.ui.notify("No tasks yet.", "info");
|
ctx.ui.notify("No todos yet.", "info");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
widgetVisible = !widgetVisible;
|
widgetVisible = !widgetVisible;
|
||||||
if (widgetVisible) {
|
if (widgetVisible) {
|
||||||
updateTaskWidget(state, ctx, true);
|
updateTaskWidget(state, ctx, true);
|
||||||
ctx.ui.notify("Task widget shown.", "info");
|
ctx.ui.notify("Todo widget shown.", "info");
|
||||||
} else {
|
} else {
|
||||||
clearTaskWidget(ctx);
|
clearTaskWidget(ctx);
|
||||||
ctx.ui.notify("Task widget hidden.", "info");
|
ctx.ui.notify("Todo widget hidden.", "info");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ export function renderWidgetTree(
|
|||||||
|
|
||||||
export function progressLabel(stats: TaskStats): string {
|
export function progressLabel(stats: TaskStats): string {
|
||||||
const blocked = stats.blocked > 0 ? ` · !${stats.blocked}` : "";
|
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 {
|
export function renderProgressHeader(stats: TaskStats, theme: Theme, width: number): string {
|
||||||
|
|||||||
+2
-2
@@ -3,7 +3,7 @@ import type { TaskState } from "./state.ts";
|
|||||||
import { renderProgressHeader, renderWidgetTree } from "./ui.ts";
|
import { renderProgressHeader, renderWidgetTree } from "./ui.ts";
|
||||||
|
|
||||||
const WIDGET_ID = "pi-todo-herdr";
|
const WIDGET_ID = "pi-todo-herdr";
|
||||||
const TIMER_REFRESH_MS = 1000;
|
export const TODO_ANIMATION_INTERVAL_MS = 500;
|
||||||
|
|
||||||
export function clearTaskWidget(ctx: ExtensionContext): void {
|
export function clearTaskWidget(ctx: ExtensionContext): void {
|
||||||
ctx.ui.setWidget(WIDGET_ID, undefined);
|
ctx.ui.setWidget(WIDGET_ID, undefined);
|
||||||
@@ -13,7 +13,7 @@ export function updateTaskWidget(
|
|||||||
state: TaskState,
|
state: TaskState,
|
||||||
ctx: ExtensionContext,
|
ctx: ExtensionContext,
|
||||||
visible: boolean,
|
visible: boolean,
|
||||||
refreshMs = TIMER_REFRESH_MS,
|
refreshMs = TODO_ANIMATION_INTERVAL_MS,
|
||||||
): void {
|
): void {
|
||||||
const tasks = state.list();
|
const tasks = state.list();
|
||||||
if (!visible || tasks.length === 0) {
|
if (!visible || tasks.length === 0) {
|
||||||
|
|||||||
@@ -10,6 +10,38 @@ import {
|
|||||||
} from "../src/prompt.ts";
|
} from "../src/prompt.ts";
|
||||||
import { TaskState } from "../src/state.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<void>;
|
||||||
|
};
|
||||||
|
const commands = new Map<string, RegisteredCommand>();
|
||||||
|
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", () => {
|
test("keeps task guidance short with planning after exploration", () => {
|
||||||
assert.equal(TASK_PROMPT_GUIDELINES.length, 2);
|
assert.equal(TASK_PROMPT_GUIDELINES.length, 2);
|
||||||
const prompt = TASK_PROMPT_GUIDELINES.join("\n");
|
const prompt = TASK_PROMPT_GUIDELINES.join("\n");
|
||||||
|
|||||||
+6
-2
@@ -12,7 +12,7 @@ import {
|
|||||||
renderWidgetTree,
|
renderWidgetTree,
|
||||||
snapshotStats,
|
snapshotStats,
|
||||||
} from "../src/ui.ts";
|
} from "../src/ui.ts";
|
||||||
import { updateTaskWidget } from "../src/widget.ts";
|
import { TODO_ANIMATION_INTERVAL_MS, updateTaskWidget } from "../src/widget.ts";
|
||||||
|
|
||||||
const plainTheme = {
|
const plainTheme = {
|
||||||
fg: (_color: string, text: string) => text,
|
fg: (_color: string, text: string) => text,
|
||||||
@@ -49,7 +49,7 @@ function fixture(): TaskState {
|
|||||||
test("renders an adaptive progress bar and stays within width", () => {
|
test("renders an adaptive progress bar and stays within width", () => {
|
||||||
const stats = fixture().stats();
|
const stats = fixture().stats();
|
||||||
const wide = stripAnsi(renderProgressHeader(stats, plainTheme as never, 40));
|
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);
|
assert.ok(visibleWidth(wide) <= 40);
|
||||||
|
|
||||||
const narrow = renderProgressHeader(stats, plainTheme as never, 12);
|
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));
|
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 () => {
|
test("live widgets request rerenders and dispose their interval", async () => {
|
||||||
let now = 0;
|
let now = 0;
|
||||||
const state = new TaskState(() => now);
|
const state = new TaskState(() => now);
|
||||||
|
|||||||
Reference in New Issue
Block a user