feat(todo): add an edit action, so correcting one item needs no rebuild
The vendored upstream example exposes list/add/toggle/clear and no way to change an item's text. On a long-lived list that leaves two bad options: add a "patch" item that describes a DIFFERENT item, or clear and re-add everything. Both were hit for real on 2026-08-17 while tracking a 17-item fleet plan -- the list ended up with #18 correcting #17, which reads as nonsense in a sidebar that truncates each item to a few words. edit takes id + text, replaces the text, and KEEPS the id and the done status. Id stability is the point: ids are the only handle the palace snapshot of a plan can refer to, so a correction must not renumber anything. nextId is untouched. Error branches match the existing style: distinct messages for a missing id, a missing text, and an unknown id, each returning the full todos array in details so state stays consistent. No pi-atelier change needed. Its tool_result hook only checks that details.todos is a well-shaped array and ignores the action string, so the new action flows through its normalizer and sidebar untouched. Note renderResult's switch has no default case, so an action without a render arm returns undefined; edit adds one. The file is a verbatim vendored copy of upstream's example, so divergence has a cost: the header now carries a numbered LOCAL DELTAS list (the earlier ctx.mode -> ctx.hasUI API fix, and this) to keep reconciling a future upstream version mechanical. Worth offering upstream as a PR -- the example is arguably incomplete without it. Verified live in-container by pointing ~/.pi/agent/extensions/todo.ts at this copy for one session: edit on an unknown id and with no text both error as intended, and editing a completed item preserved both its id and its done state. Symlink since reverted to the image copy.
This commit is contained in:
+53
-5
@@ -8,6 +8,15 @@
|
||||
* State is stored in tool result details (not external files), which allows
|
||||
* proper branching - when you branch, the todo state is automatically
|
||||
* correct for that point in history.
|
||||
*
|
||||
* LOCAL DELTAS vs upstream examples/extensions/todo.ts (keep this list current,
|
||||
* so reconciling a future upstream version stays mechanical):
|
||||
* 1. `ctx.mode !== "tui"` -> `!ctx.hasUI` in the /todos command (API change).
|
||||
* 2. `edit` action (id + text) - rewrite an item's text in place, keeping its
|
||||
* id and done status. Added 2026-08-17: without it, correcting one item in
|
||||
* a long-lived list means either a "patch" item that describes a DIFFERENT
|
||||
* item, or clear + re-add of everything. Both were hit in practice on
|
||||
* 2026-08-17 while tracking a 17-item fleet plan.
|
||||
*/
|
||||
|
||||
import { StringEnum } from "@earendil-works/pi-ai";
|
||||
@@ -22,16 +31,16 @@ interface Todo {
|
||||
}
|
||||
|
||||
interface TodoDetails {
|
||||
action: "list" | "add" | "toggle" | "clear";
|
||||
action: "list" | "add" | "toggle" | "edit" | "clear";
|
||||
todos: Todo[];
|
||||
nextId: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const TodoParams = Type.Object({
|
||||
action: StringEnum(["list", "add", "toggle", "clear"] as const),
|
||||
text: Type.Optional(Type.String({ description: "Todo text (for add)" })),
|
||||
id: Type.Optional(Type.Number({ description: "Todo ID (for toggle)" })),
|
||||
action: StringEnum(["list", "add", "toggle", "edit", "clear"] as const),
|
||||
text: Type.Optional(Type.String({ description: "Todo text (for add, or the replacement text for edit)" })),
|
||||
id: Type.Optional(Type.Number({ description: "Todo ID (for toggle and edit)" })),
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -136,7 +145,8 @@ export default function (pi: ExtensionAPI) {
|
||||
pi.registerTool({
|
||||
name: "todo",
|
||||
label: "Todo",
|
||||
description: "Manage a todo list. Actions: list, add (text), toggle (id), clear",
|
||||
description:
|
||||
"Manage a todo list. Actions: list, add (text), toggle (id), edit (id + text, replaces the text and keeps the done status), clear",
|
||||
parameters: TodoParams,
|
||||
|
||||
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
||||
@@ -195,6 +205,38 @@ export default function (pi: ExtensionAPI) {
|
||||
};
|
||||
}
|
||||
|
||||
case "edit": {
|
||||
if (params.id === undefined || !params.text) {
|
||||
const missing = params.id === undefined ? "id" : "text";
|
||||
return {
|
||||
content: [{ type: "text", text: `Error: ${missing} required for edit` }],
|
||||
details: {
|
||||
action: "edit",
|
||||
todos: [...todos],
|
||||
nextId,
|
||||
error: `${missing} required`,
|
||||
} as TodoDetails,
|
||||
};
|
||||
}
|
||||
const target = todos.find((t) => t.id === params.id);
|
||||
if (!target) {
|
||||
return {
|
||||
content: [{ type: "text", text: `Todo #${params.id} not found` }],
|
||||
details: {
|
||||
action: "edit",
|
||||
todos: [...todos],
|
||||
nextId,
|
||||
error: `#${params.id} not found`,
|
||||
} as TodoDetails,
|
||||
};
|
||||
}
|
||||
target.text = params.text;
|
||||
return {
|
||||
content: [{ type: "text", text: `Todo #${target.id} updated: ${target.text}` }],
|
||||
details: { action: "edit", todos: [...todos], nextId } as TodoDetails,
|
||||
};
|
||||
}
|
||||
|
||||
case "clear": {
|
||||
const count = todos.length;
|
||||
todos = [];
|
||||
@@ -274,6 +316,12 @@ export default function (pi: ExtensionAPI) {
|
||||
return new Text(theme.fg("success", "✓ ") + theme.fg("muted", msg), 0, 0);
|
||||
}
|
||||
|
||||
case "edit": {
|
||||
const text = result.content[0];
|
||||
const msg = text?.type === "text" ? text.text : "";
|
||||
return new Text(theme.fg("success", "✎ ") + theme.fg("muted", msg), 0, 0);
|
||||
}
|
||||
|
||||
case "clear":
|
||||
return new Text(theme.fg("success", "✓ ") + theme.fg("muted", "Cleared all todos"), 0, 0);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user