@capdiem/pi-todo 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,7 +5,8 @@ The model writes the complete task plan in one call instead of issuing one
5
5
  `create` call per task.
6
6
 
7
7
  It is deliberately small — only the essential plan-maintenance core, with no
8
- dependencies, reminders, settings menus, or collapse/expand. The live plan
8
+ dependencies or settings menus, no collapse/expand, and exactly one lightweight
9
+ reminder (a run-end reconcile nudge, see [Features](#features)). The live plan
9
10
  renders as a read-only widget above Pi's input box, and when hosted by
10
11
  [pi-agent-desktop](https://github.com/abcwyc/pi-agent-desktop) it also appears
11
12
  in the desktop's **left-nav sidebar** todo panel with zero extra configuration
@@ -25,6 +26,14 @@ in the desktop's **left-nav sidebar** todo panel with zero extra configuration
25
26
  - State persists in tool-result details and survives `/reload` and `/tree`
26
27
  - A read-only widget above Pi's input box using markdown-style glyphs
27
28
  (`[ ]` pending, `[-]` in_progress, `[x]` completed); no collapse/expand
29
+ - **Run-end reconcile nudge**: when a turn settles (`agent_settled`) while tasks
30
+ are still `in_progress`, the extension sends the agent one steer reminding it
31
+ to mark them `completed` if the work is actually done. This catches the
32
+ "forgot to reconcile the plan at the end of the turn" failure without the
33
+ user having to notice a stale plan. It fires at most once per logical user
34
+ turn, is skipped while the plan has no `in_progress` tasks, and never loops:
35
+ the nudge's own delivery is recognized by content and is not treated as a new
36
+ user turn.
28
37
  - When hosted by pi-agent-desktop, the live plan also renders in the desktop's
29
38
  **left-nav sidebar** todo panel (the desktop-owned `pi-agent-desktop:todo`
30
39
  widget extension point) — no extra configuration needed
package/index.min.js CHANGED
@@ -1,8 +1,8 @@
1
- import{StringEnum as g}from"@earendil-works/pi-ai";import{Text as K,truncateToWidth as p}from"@earendil-works/pi-tui";import{Type as V}from"typebox";var v=new Set(["pending","in_progress","completed"]),R=50;function M(F){return typeof F==="string"&&v.has(F)}function L(){return{version:0,tasks:[]}}function S(F){return{version:F.version,tasks:F.tasks.map((H)=>({...H}))}}function D(F,H){let Q=F.trim();if(!/^[a-z0-9][a-z0-9._-]{0,39}$/.test(Q))throw Error(`${H} must be 1-40 lowercase ASCII letters, numbers, dots, underscores, or hyphens`);return Q}function w(F,H){if(F.length!==H.length)return!1;for(let Q=0;Q<F.length;Q++){let B=F[Q],Z=H[Q];if(B.key!==Z.key||B.subject!==Z.subject||B.status!==Z.status)return!1}return!0}function C(F,H){if(H.baseVersion!==void 0&&H.baseVersion!==F.version)throw Error(`stale todo version: expected ${H.baseVersion}, current version is ${F.version}`);if(H.tasks.length>50)throw Error("tasks supports at most 50 items");let Q=new Map(F.tasks.map(($)=>[$.key,$])),B=new Set,Z=[];for(let[$,z]of H.tasks.entries()){let q=D(z.key,`tasks[${$}].key`);if(B.has(q))throw Error(`tasks[${$}].key is duplicated: ${q}`);B.add(q);let P=Q.get(q),I=z.subject??P?.subject;if(I===void 0)throw Error(`tasks[${$}].subject is required for new task ${q}`);let X=I.trim();if(!X)throw Error(`tasks[${$}].subject is required for new task ${q}`);if(X.length>160)throw Error(`tasks[${$}].subject must be at most 160 characters`);let U=z.status??P?.status;if(U===void 0)throw Error(`tasks[${$}].status is required for new task ${q}`);if(!M(U))throw Error(`tasks[${$}].status is invalid: ${String(U)}`);Z.push({key:q,subject:X,status:U})}let J=Z.filter(($)=>$.status==="in_progress");if(J.length>1)throw Error(`only one task may be in_progress at a time (got ${J.length}: ${J.map(($)=>$.key).join(", ")})`);return{version:w(F.tasks,Z)?F.version:F.version+1,tasks:Z}}function Y(F){if(!F||typeof F!=="object")return!1;let H=F;if(typeof H.version!=="number"||!Number.isSafeInteger(H.version)||H.version<0)return!1;if(!Array.isArray(H.tasks)||H.tasks.length>50)return!1;let Q=new Set;for(let Z of H.tasks){if(!Z||typeof Z!=="object")return!1;let J=Z;if(typeof J.key!=="string"||typeof J.subject!=="string"||typeof J.subject.trim()!=="string"||!J.subject.trim()||J.subject.length>160||!M(J.status))return!1;let $;try{$=D(J.key,"key")}catch{return!1}if(Q.has($))return!1;Q.add($)}if(H.tasks.filter((Z)=>Z.status==="in_progress").length>1)return!1;return!0}function W(F,H,Q){let B=L();for(let Z of F.sessionManager.getBranch()){if(!Z||typeof Z!=="object")continue;let J=Z,$;if(J.type==="custom_message"&&J.customType===Q&&Y(J.details))$=J.details;else if(J.type==="message"&&J.message&&J.message.role==="toolResult"&&J.message.toolName===H&&Y(J.message.details))$=J.message.details;if($)B=S($)}return B}function j(F){if(F.tasks.length===0)return`Todo plan cleared (version ${F.version}).`;let H=F.tasks.map((Q)=>`[${Q.status}] ${Q.key}: ${Q.subject}`);return`Todo plan version ${F.version}:
1
+ import{StringEnum as m}from"@earendil-works/pi-ai";import{Text as f,truncateToWidth as x}from"@earendil-works/pi-tui";import{Type as z}from"typebox";var y=new Set(["pending","in_progress","completed"]),D=50;function N(F){return typeof F==="string"&&y.has(F)}function W(){return{version:0,tasks:[]}}function h(F){return{version:F.version,tasks:F.tasks.map((H)=>({...H}))}}function C(F,H){let J=F.trim();if(!/^[a-z0-9][a-z0-9._-]{0,39}$/.test(J))throw Error(`${H} must be 1-40 lowercase ASCII letters, numbers, dots, underscores, or hyphens`);return J}function l(F,H){if(F.length!==H.length)return!1;for(let J=0;J<F.length;J++){let q=F[J],Z=H[J];if(q.key!==Z.key||q.subject!==Z.subject||q.status!==Z.status)return!1}return!0}function K(F,H){if(H.baseVersion!==void 0&&H.baseVersion!==F.version)throw Error(`stale todo version: expected ${H.baseVersion}, current version is ${F.version}`);if(H.tasks.length>50)throw Error("tasks supports at most 50 items");let J=new Map(F.tasks.map(($)=>[$.key,$])),q=new Set,Z=[];for(let[$,G]of H.tasks.entries()){let V=C(G.key,`tasks[${$}].key`);if(q.has(V))throw Error(`tasks[${$}].key is duplicated: ${V}`);q.add(V);let M=J.get(V),R=G.subject??M?.subject;if(R===void 0)throw Error(`tasks[${$}].subject is required for new task ${V}`);let U=R.trim();if(!U)throw Error(`tasks[${$}].subject is required for new task ${V}`);if(U.length>160)throw Error(`tasks[${$}].subject must be at most 160 characters`);let X=G.status??M?.status;if(X===void 0)throw Error(`tasks[${$}].status is required for new task ${V}`);if(!N(X))throw Error(`tasks[${$}].status is invalid: ${String(X)}`);Z.push({key:V,subject:U,status:X})}let Q=Z.filter(($)=>$.status==="in_progress");if(Q.length>1)throw Error(`only one task may be in_progress at a time (got ${Q.length}: ${Q.map(($)=>$.key).join(", ")})`);return{version:l(F.tasks,Z)?F.version:F.version+1,tasks:Z}}function A(F){if(!F||typeof F!=="object")return!1;let H=F;if(typeof H.version!=="number"||!Number.isSafeInteger(H.version)||H.version<0)return!1;if(!Array.isArray(H.tasks)||H.tasks.length>50)return!1;let J=new Set;for(let Z of H.tasks){if(!Z||typeof Z!=="object")return!1;let Q=Z;if(typeof Q.key!=="string"||typeof Q.subject!=="string"||typeof Q.subject.trim()!=="string"||!Q.subject.trim()||Q.subject.length>160||!N(Q.status))return!1;let $;try{$=C(Q.key,"key")}catch{return!1}if(J.has($))return!1;J.add($)}if(H.tasks.filter((Z)=>Z.status==="in_progress").length>1)return!1;return!0}function _(F,H,J){let q=W();for(let Z of F.sessionManager.getBranch()){if(!Z||typeof Z!=="object")continue;let Q=Z,$;if(Q.type==="custom_message"&&Q.customType===J&&A(Q.details))$=Q.details;else if(Q.type==="message"&&Q.message&&Q.message.role==="toolResult"&&Q.message.toolName===H&&A(Q.message.details))$=Q.message.details;if($)q=h($)}return q}function b(F){if(F.tasks.length===0)return`Todo plan cleared (version ${F.version}).`;let H=F.tasks.map((J)=>`[${J.status}] ${J.key}: ${J.subject}`);return`Todo plan version ${F.version}:
2
2
  ${H.join(`
3
- `)}`}var _="todo",O="pi-todo-widget",h="pi-todo-state",x="pi-agent-desktop:todo";function y(F){return F.tasks.map((H)=>JSON.stringify(H))}var l=V.String({description:"Stable 1-40 character lowercase task key, e.g. inspect-api or write-tests",minLength:1,maxLength:40,pattern:"^[a-z0-9][a-z0-9._-]*$"}),m=V.Object({key:l,subject:V.Optional(V.String({description:"Short imperative task subject; required for a new key, omitted to keep an existing value",minLength:1,maxLength:160})),status:V.Optional(g(["pending","in_progress","completed"],{description:"Task status; required for a new key, omitted to keep an existing value"}))}),T=V.Object({tasks:V.Array(m,{description:"Complete authoritative list of tasks to retain; omitted keys are deleted, existing tasks may omit unchanged fields, and new keys require subject and status",maxItems:R}),baseVersion:V.Optional(V.Integer({description:"Version shown in current todo context; rejects stale writes when provided",minimum:0}))});function c(F){return F==="completed"?"[x]":F==="in_progress"?"[-]":"[ ]"}function f(F,H){if(!F.status)return H.fg("text",F.subject);let Q=H.fg(F.status==="completed"?"success":F.status==="in_progress"?"warning":"muted",c(F.status)),B=H.fg(F.status==="completed"?"dim":"text",F.subject);if(F.status==="completed")B=H.strikethrough(B);else if(F.status==="in_progress")B=H.bold(B);return`${Q} ${B}`}function u(F,H,Q){if(F.tasks.length===0)return[];let B=F.tasks.filter((J)=>J.status==="completed").length,Z=[Q.fg("accent",Q.bold(`Todo ${B}/${F.tasks.length} completed`))+Q.fg("dim",` v${F.version}`)];return Z.push(...F.tasks.map((J)=>f(J,Q))),Z.map((J)=>p(J,H,"…"))}function k(F,H){if(!Array.isArray(F))return[];let Q=new Map(H.tasks.map((Z)=>[Z.key,Z])),B=[];for(let Z of F){let J=Z&&typeof Z==="object"?Z:{},$=typeof J.key==="string"?J.key.trim():"",z=$?Q.get($):void 0,q=typeof J.subject==="string"&&J.subject.trim()?J.subject.trim():z?.subject;if(!$||!q)continue;B.push({subject:q,status:M(J.status)?J.status:z?.status})}return B}function o(F){let H=L(),Q,B=!1,Z,J=()=>{if(B&&Q?.hasUI)try{Q.ui.setWidget(O,void 0)}catch{}B=!1,Z=void 0},$=(q)=>{if(q)Q=q;if(!Q?.hasUI)return;if(Q.mode!=="tui"){try{Q.ui.setWidget(x,H.tasks.length>0?y(H):void 0)}catch{}return}if(H.tasks.length===0){J();return}if(!B)Q.ui.setWidget(O,(P,I)=>{return Z=P,{render:(X)=>u(H,X,I),invalidate:()=>{},dispose:()=>{if(Z===P)Z=void 0}}},{placement:"aboveEditor"}),B=!0;else Z?.requestRender()};F.registerTool({name:_,label:"Todo",description:`Maintain the task plan with one atomic update. Every call replaces the task list: include each key to keep, and omit a key to delete it. Existing tasks may omit unchanged fields; new tasks require subject and status. At most one task may be in_progress. Up to ${R} tasks; an empty list clears the plan.`,promptSnippet:"Maintain the task plan with one atomic update",promptGuidelines:["When a task needs a plan of 3+ steps, define it yourself and call todo before beginning implementation or other substantive work.","Organize the plan by work content: a coherent objective that may span multiple turns, and a plan can hold several work contents worked across different turns. Give each work content a stable slug and prefix its todos' keys with it (e.g. auth.validate-token, auth.refresh-token) so pruning can target a whole work content.","Write task subjects in the user's language (e.g. Chinese when the user writes in Chinese). Task keys must stay lowercase ASCII slugs (a-z, 0-9, . _ -) — they are stable identities, not display text.","The list is one flat ordered sequence, not grouped sections: order todos by execution/dependency order, and when work contents are related keep their todos in sequence in the same list rather than splitting them into separate blocks. The UI renders this as a single ordered list — there are no work-content groups or headers.","Each todo call replaces the task list. Include every key to keep; omitted keys are deleted.","Keep keys stable. Existing tasks may omit unchanged fields; new tasks require subject and status. Include baseVersion when available.","Only one task may be in_progress at a time.","Completed todos are never auto-removed. Keep them struck through as history while their work content is still open.","Prune completed todos only when their whole work content is complete: once every todo under a work content is done, remove all of that content's todos together in the next todo call. Work contents are pruned independently — a completed work content is removed even when other work contents remain open. Turn boundaries are not cleanup points.","While a todo plan exists, update todo immediately when task status, order, or scope changes, and reconcile actual progress before the final response. Do not issue a no-op todo call only to acknowledge a reminder."],parameters:T,executionMode:"sequential",async execute(q,P,I,X,U){return H=C(H,P),$(U),{content:[{type:"text",text:j(H)}],details:H}},renderCall(q,P,I){let X=I.lastComponent??new K("",0,0),U=k(q.tasks,H),N=Array.isArray(q.tasks),G=N?q.tasks.length:0,E=N?P.fg("accent",`${G} task${G===1?"":"s"}`):"",A=[[P.fg("toolTitle",P.bold("todo")),E].filter(Boolean).join(" ")];return A.push(...U.map((b)=>f(b,P))),X.setText(A.join(`
4
- `)),X},renderResult(q,P,I,X){let U=X.lastComponent??new K("",0,0);if(X.isError||!Y(q.details)){let N=q.content.filter((G)=>G.type==="text").map((G)=>G.text).join(`
5
- `);return U.setText(I.fg("error",N)),U}return U.setText(""),U}});let z=(q)=>{J(),H=W({sessionManager:{getBranch:()=>q.sessionManager.getBranch()}},_,h),Q=q,$(q)};F.on("session_start",async(q,P)=>z(P)),F.on("session_tree",async(q,P)=>z(P)),F.on("session_shutdown",async()=>{J(),Q=void 0})}export{o as default};
3
+ `)}`}function E(F){return F.tasks.some((H)=>H.status==="in_progress")}function O(){return"[pi-todo] 收尾提醒:本回合已结束,但仍有任务处于 in_progress。若这些任务实际已完成,"+"请立即调用 todo 把它们标记为 completed;若确实仍在进行中,请简短说明并保持现状。"+"不要执行其他操作。"}var v="todo",S="pi-todo-widget",T="pi-todo-state",c="pi-agent-desktop:todo";function k(F){return F.tasks.map((H)=>JSON.stringify(H))}var o=z.String({description:"Stable 1-40 character lowercase task key, e.g. inspect-api or write-tests",minLength:1,maxLength:40,pattern:"^[a-z0-9][a-z0-9._-]*$"}),u=z.Object({key:o,subject:z.Optional(z.String({description:"Short imperative task subject; required for a new key, omitted to keep an existing value",minLength:1,maxLength:160})),status:z.Optional(m(["pending","in_progress","completed"],{description:"Task status; required for a new key, omitted to keep an existing value"}))}),d=z.Object({tasks:z.Array(u,{description:"Complete authoritative list of tasks to retain; omitted keys are deleted, existing tasks may omit unchanged fields, and new keys require subject and status",maxItems:D}),baseVersion:z.Optional(z.Integer({description:"Version shown in current todo context; rejects stale writes when provided",minimum:0}))});function r(F){return F==="completed"?"[x]":F==="in_progress"?"[-]":"[ ]"}function w(F,H){if(!F.status)return H.fg("text",F.subject);let J=H.fg(F.status==="completed"?"success":F.status==="in_progress"?"warning":"muted",r(F.status)),q=H.fg(F.status==="completed"?"dim":"text",F.subject);if(F.status==="completed")q=H.strikethrough(q);else if(F.status==="in_progress")q=H.bold(q);return`${J} ${q}`}function n(F,H,J){if(F.tasks.length===0)return[];let q=F.tasks.filter((Q)=>Q.status==="completed").length,Z=[J.fg("accent",J.bold(`Todo ${q}/${F.tasks.length} completed`))+J.fg("dim",` v${F.version}`)];return Z.push(...F.tasks.map((Q)=>w(Q,J))),Z.map((Q)=>x(Q,H,"…"))}function i(F,H){if(!Array.isArray(F))return[];let J=new Map(H.tasks.map((Z)=>[Z.key,Z])),q=[];for(let Z of F){let Q=Z&&typeof Z==="object"?Z:{},$=typeof Q.key==="string"?Q.key.trim():"",G=$?J.get($):void 0,V=typeof Q.subject==="string"&&Q.subject.trim()?Q.subject.trim():G?.subject;if(!$||!V)continue;q.push({subject:V,status:N(Q.status)?Q.status:G?.status})}return q}function s(F){let H=W(),J,q=!1,Z,Q=!1,$=new Set,G=()=>O(),V=()=>{if(q&&J?.hasUI)try{J.ui.setWidget(S,void 0)}catch{}q=!1,Z=void 0},M=(U)=>{if(U)J=U;if(!J?.hasUI)return;if(J.mode!=="tui"){try{J.ui.setWidget(c,H.tasks.length>0?k(H):void 0)}catch{}return}if(H.tasks.length===0){V();return}if(!q)J.ui.setWidget(S,(X,Y)=>{return Z=X,{render:(I)=>n(H,I,Y),invalidate:()=>{},dispose:()=>{if(Z===X)Z=void 0}}},{placement:"aboveEditor"}),q=!0;else Z?.requestRender()};F.registerTool({name:v,label:"Todo",description:`Maintain the task plan with one atomic update. Every call replaces the task list: include each key to keep, and omit a key to delete it. Existing tasks may omit unchanged fields; new tasks require subject and status. At most one task may be in_progress. Up to ${D} tasks; an empty list clears the plan.`,promptSnippet:"Maintain the task plan with one atomic update",promptGuidelines:["When a task needs a plan of 3+ steps, define it yourself and call todo before beginning implementation or other substantive work.","Organize the plan by work content: a coherent objective that may span multiple turns, and a plan can hold several work contents worked across different turns. Give each work content a stable slug and prefix its todos' keys with it (e.g. auth.validate-token, auth.refresh-token) so pruning can target a whole work content.","Write task subjects in the user's language (e.g. Chinese when the user writes in Chinese). Task keys must stay lowercase ASCII slugs (a-z, 0-9, . _ -) — they are stable identities, not display text.","The list is one flat ordered sequence, not grouped sections: order todos by execution/dependency order, and when work contents are related keep their todos in sequence in the same list rather than splitting them into separate blocks. The UI renders this as a single ordered list — there are no work-content groups or headers.","Each todo call replaces the task list. Include every key to keep; omitted keys are deleted.","Keep keys stable. Existing tasks may omit unchanged fields; new tasks require subject and status. Include baseVersion when available.","Only one task may be in_progress at a time.","Completed todos are never auto-removed. Keep them struck through as history while their work content is still open.","Prune completed todos only when their whole work content is complete: once every todo under a work content is done, remove all of that content's todos together in the next todo call. Work contents are pruned independently — a completed work content is removed even when other work contents remain open. Turn boundaries are not cleanup points.","While a todo plan exists, update todo immediately when task status, order, or scope changes, and reconcile actual progress before the final response. Do not issue a no-op todo call only to acknowledge a reminder."],parameters:d,executionMode:"sequential",async execute(U,X,Y,I,P){return H=K(H,X),M(P),{content:[{type:"text",text:b(H)}],details:H}},renderCall(U,X,Y){let I=Y.lastComponent??new f("",0,0),P=i(U.tasks,H),L=Array.isArray(U.tasks),B=L?U.tasks.length:0,p=L?X.fg("accent",`${B} task${B===1?"":"s"}`):"",j=[[X.fg("toolTitle",X.bold("todo")),p].filter(Boolean).join(" ")];return j.push(...P.map((g)=>w(g,X))),I.setText(j.join(`
4
+ `)),I},renderResult(U,X,Y,I){let P=I.lastComponent??new f("",0,0);if(I.isError||!A(U.details)){let L=U.content.filter((B)=>B.type==="text").map((B)=>B.text).join(`
5
+ `);return P.setText(Y.fg("error",L)),P}return P.setText(""),P}});let R=(U)=>{V(),H=_({sessionManager:{getBranch:()=>U.sessionManager.getBranch()}},v,T),J=U,M(U)};F.on("session_start",async(U,X)=>R(X)),F.on("session_tree",async(U,X)=>R(X)),F.on("session_shutdown",async()=>{V(),J=void 0}),F.on("agent_settled",()=>{if(Q||!E(H))return;Q=!0;let U=G();$.add(U),F.sendUserMessage(U,{deliverAs:"steer"})}),F.on("message_start",(U)=>{if(U.message?.role!=="user")return;let X=a(U.message.content);if(X&&$.has(X)){$.delete(X);return}Q=!1})}function a(F){if(!Array.isArray(F))return"";let H="";for(let J of F)if(J&&typeof J==="object"){let q=J.text;if(typeof q==="string")H+=q}return H}export{s as default};
6
6
 
7
- //# debugId=63C76BB82F4FA11D64756E2164756E21
7
+ //# debugId=7676EBAAA3F657AB64756E2164756E21
8
8
  //# sourceMappingURL=index.min.js.map
package/index.min.js.map CHANGED
@@ -2,10 +2,10 @@
2
2
  "version": 3,
3
3
  "sources": ["../../extensions/pi-todo/index.ts", "../../extensions/pi-todo/state.ts"],
4
4
  "sourcesContent": [
5
- "import { StringEnum } from \"@earendil-works/pi-ai\";\nimport type { ExtensionAPI, ExtensionContext, Theme } from \"@earendil-works/pi-coding-agent\";\nimport { Text, truncateToWidth, type TUI } from \"@earendil-works/pi-tui\";\nimport { Type } from \"typebox\";\nimport {\n MAX_TODO_TASKS,\n createEmptyTodoState,\n formatChange,\n isTodoStatus,\n isValidTodoState,\n replayTodoState,\n writeTodo,\n type TodoState,\n type TodoStatus,\n type TodoTask,\n} from \"./state.ts\";\n\nconst TODO_TOOL_NAME = \"todo\";\nconst WIDGET_KEY = \"pi-todo-widget\";\nconst STATE_CUSTOM_TYPE = \"pi-todo-state\";\n\n// Desktop-owned todo widget protocol (setWidget channel): the widget key and\n// line format are defined by pi-agent-desktop\n// (D:\\Code\\pi-agent-desktop\\lib\\todo-state.ts). This extension conforms by\n// emitting the same key and JSON-per-line payload, so the desktop can route\n// the `setWidget` request straight into its sidebar todo panel.\nconst TODO_WIDGET_KEY = \"pi-agent-desktop:todo\";\n\n/** Serialize a TodoState to widget lines (one task per JSON line) for the\n * desktop todo widget protocol. Mirrors the desktop's serializer. */\nfunction serializeTodoWidgetLines(state: TodoState): string[] {\n return state.tasks.map((task) => JSON.stringify(task));\n}\n\nconst TodoKeySchema = Type.String({\n description:\n \"Stable 1-40 character lowercase task key, e.g. inspect-api or write-tests\",\n minLength: 1,\n maxLength: 40,\n pattern: \"^[a-z0-9][a-z0-9._-]*$\",\n});\n\nconst TodoTaskSchema = Type.Object({\n key: TodoKeySchema,\n subject: Type.Optional(\n Type.String({\n description:\n \"Short imperative task subject; required for a new key, omitted to keep an existing value\",\n minLength: 1,\n maxLength: 160,\n }),\n ),\n status: Type.Optional(\n StringEnum([\"pending\", \"in_progress\", \"completed\"] as const, {\n description:\n \"Task status; required for a new key, omitted to keep an existing value\",\n }),\n ),\n});\n\nconst TodoParamsSchema = Type.Object({\n tasks: Type.Array(TodoTaskSchema, {\n description:\n \"Complete authoritative list of tasks to retain; omitted keys are deleted, existing tasks may omit unchanged fields, and new keys require subject and status\",\n maxItems: MAX_TODO_TASKS,\n }),\n baseVersion: Type.Optional(\n Type.Integer({\n description:\n \"Version shown in current todo context; rejects stale writes when provided\",\n minimum: 0,\n }),\n ),\n});\n\n/** Markdown-style glyphs: [ ] pending, [-] in_progress, [x] completed. */\nfunction todoGlyph(status: TodoStatus): string {\n return status === \"completed\" ? \"[x]\" : status === \"in_progress\" ? \"[-]\" : \"[ ]\";\n}\n\ninterface TodoDisplayTask {\n subject: string;\n status?: TodoStatus;\n}\n\nfunction renderTaskLine(task: TodoDisplayTask, theme: Theme): string {\n if (!task.status) return theme.fg(\"text\", task.subject);\n const glyph = theme.fg(\n task.status === \"completed\"\n ? \"success\"\n : task.status === \"in_progress\"\n ? \"warning\"\n : \"muted\",\n todoGlyph(task.status),\n );\n let subject = theme.fg(\n task.status === \"completed\" ? \"dim\" : \"text\",\n task.subject,\n );\n if (task.status === \"completed\") subject = theme.strikethrough(subject);\n else if (task.status === \"in_progress\") subject = theme.bold(subject);\n return `${glyph} ${subject}`;\n}\n\nfunction renderTodoWidget(state: TodoState, width: number, theme: Theme): string[] {\n if (state.tasks.length === 0) return [];\n const completed = state.tasks.filter((task) => task.status === \"completed\").length;\n const lines = [\n theme.fg(\n \"accent\",\n theme.bold(`Todo ${completed}/${state.tasks.length} completed`),\n ) + theme.fg(\"dim\", ` v${state.version}`),\n ];\n lines.push(...state.tasks.map((task) => renderTaskLine(task, theme)));\n return lines.map((line) => truncateToWidth(line, width, \"…\"));\n}\n\n/** Resolve a partially streamed `todo` call against current state for the live preview. */\nfunction resolveDraftTasks(rawTasks: unknown, state: TodoState): TodoDisplayTask[] {\n if (!Array.isArray(rawTasks)) return [];\n const currentByKey = new Map(state.tasks.map((task) => [task.key, task]));\n const tasks: TodoDisplayTask[] = [];\n for (const rawTask of rawTasks) {\n const draft =\n rawTask && typeof rawTask === \"object\"\n ? (rawTask as { key?: unknown; subject?: unknown; status?: unknown })\n : {};\n const key = typeof draft.key === \"string\" ? draft.key.trim() : \"\";\n const current = key ? currentByKey.get(key) : undefined;\n const subject =\n typeof draft.subject === \"string\" && draft.subject.trim()\n ? draft.subject.trim()\n : current?.subject;\n if (!key || !subject) continue;\n tasks.push({\n subject,\n status: isTodoStatus(draft.status) ? draft.status : current?.status,\n });\n }\n return tasks;\n}\n\nexport default function todoMiniExtension(pi: ExtensionAPI): void {\n let state = createEmptyTodoState();\n let uiContext: ExtensionContext | undefined;\n let widgetRegistered = false;\n let widgetTui: TUI | undefined;\n\n const clearWidget = (): void => {\n if (widgetRegistered && uiContext?.hasUI) {\n try {\n uiContext.ui.setWidget(WIDGET_KEY, undefined);\n } catch {}\n }\n widgetRegistered = false;\n widgetTui = undefined;\n };\n\n const updateWidget = (ctx?: ExtensionContext): void => {\n if (ctx) uiContext = ctx;\n // pi-agent-desktop todo widget protocol (RPC/desktop): push the live plan\n // over `setWidget` with the desktop-reserved key. Fire-and-forget — the\n // desktop parses the JSON lines into the sidebar panel. Cleared (undefined)\n // when the plan is empty so the panel hides. The TUI widget path below\n // stays separate and unchanged.\n if (!uiContext?.hasUI) return;\n if (uiContext.mode !== \"tui\") {\n // RPC / desktop host: live todo widget over the reserved key.\n try {\n uiContext.ui.setWidget(\n TODO_WIDGET_KEY,\n state.tasks.length > 0 ? serializeTodoWidgetLines(state) : undefined,\n );\n } catch {}\n return;\n }\n if (state.tasks.length === 0) {\n clearWidget();\n return;\n }\n if (!widgetRegistered) {\n uiContext.ui.setWidget(\n WIDGET_KEY,\n (tui: TUI, theme: Theme) => {\n widgetTui = tui;\n return {\n render: (width: number) => renderTodoWidget(state, width, theme),\n invalidate: () => {},\n dispose: () => {\n if (widgetTui === tui) widgetTui = undefined;\n },\n };\n },\n { placement: \"aboveEditor\" },\n );\n widgetRegistered = true;\n } else {\n widgetTui?.requestRender();\n }\n };\n\n pi.registerTool({\n name: TODO_TOOL_NAME,\n label: \"Todo\",\n description: `Maintain the task plan with one atomic update. Every call replaces the task list: include each key to keep, and omit a key to delete it. Existing tasks may omit unchanged fields; new tasks require subject and status. At most one task may be in_progress. Up to ${MAX_TODO_TASKS} tasks; an empty list clears the plan.`,\n promptSnippet: \"Maintain the task plan with one atomic update\",\n promptGuidelines: [\n \"When a task needs a plan of 3+ steps, define it yourself and call todo before beginning implementation or other substantive work.\",\n \"Organize the plan by work content: a coherent objective that may span multiple turns, and a plan can hold several work contents worked across different turns. Give each work content a stable slug and prefix its todos' keys with it (e.g. auth.validate-token, auth.refresh-token) so pruning can target a whole work content.\",\n \"Write task subjects in the user's language (e.g. Chinese when the user writes in Chinese). Task keys must stay lowercase ASCII slugs (a-z, 0-9, . _ -) — they are stable identities, not display text.\",\n \"The list is one flat ordered sequence, not grouped sections: order todos by execution/dependency order, and when work contents are related keep their todos in sequence in the same list rather than splitting them into separate blocks. The UI renders this as a single ordered list — there are no work-content groups or headers.\",\n \"Each todo call replaces the task list. Include every key to keep; omitted keys are deleted.\",\n \"Keep keys stable. Existing tasks may omit unchanged fields; new tasks require subject and status. Include baseVersion when available.\",\n \"Only one task may be in_progress at a time.\",\n \"Completed todos are never auto-removed. Keep them struck through as history while their work content is still open.\",\n \"Prune completed todos only when their whole work content is complete: once every todo under a work content is done, remove all of that content's todos together in the next todo call. Work contents are pruned independently — a completed work content is removed even when other work contents remain open. Turn boundaries are not cleanup points.\",\n \"While a todo plan exists, update todo immediately when task status, order, or scope changes, and reconcile actual progress before the final response. Do not issue a no-op todo call only to acknowledge a reminder.\",\n ],\n parameters: TodoParamsSchema,\n executionMode: \"sequential\",\n\n async execute(_toolCallId, params, _signal, _onUpdate, ctx) {\n state = writeTodo(state, params);\n updateWidget(ctx);\n return {\n content: [{ type: \"text\", text: formatChange(state) }],\n details: state,\n };\n },\n\n renderCall(args, theme, context) {\n const text =\n (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n const tasks = resolveDraftTasks(args.tasks, state);\n const hasTaskList = Array.isArray(args.tasks);\n const count = hasTaskList ? args.tasks.length : 0;\n const summary = hasTaskList\n ? theme.fg(\"accent\", `${count} task${count === 1 ? \"\" : \"s\"}`)\n : \"\";\n const lines = [\n [theme.fg(\"toolTitle\", theme.bold(\"todo\")), summary]\n .filter(Boolean)\n .join(\" \"),\n ];\n lines.push(...tasks.map((task) => renderTaskLine(task, theme)));\n text.setText(lines.join(\"\\n\"));\n return text;\n },\n\n renderResult(result, _opts, theme, context) {\n const text =\n (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n if (context.isError || !isValidTodoState(result.details)) {\n const output = result.content\n .filter(\n (item): item is { type: \"text\"; text: string } => item.type === \"text\",\n )\n .map((item) => item.text)\n .join(\"\\n\");\n text.setText(theme.fg(\"error\", output));\n return text;\n }\n // A successful write is already rendered by the tool call above; keep this empty.\n text.setText(\"\");\n return text;\n },\n });\n\n const restore = (ctx: ExtensionContext): void => {\n clearWidget();\n state = replayTodoState(\n { sessionManager: { getBranch: () => ctx.sessionManager.getBranch() } },\n TODO_TOOL_NAME,\n STATE_CUSTOM_TYPE,\n );\n uiContext = ctx;\n updateWidget(ctx);\n };\n\n pi.on(\"session_start\", async (_event, ctx) => restore(ctx));\n pi.on(\"session_tree\", async (_event, ctx) => restore(ctx));\n\n pi.on(\"session_shutdown\", async () => {\n clearWidget();\n uiContext = undefined;\n });\n}\n",
6
- "export type TodoStatus = \"pending\" | \"in_progress\" | \"completed\";\n\nconst TODO_STATUSES: ReadonlySet<string> = new Set([\n \"pending\",\n \"in_progress\",\n \"completed\",\n]);\n\nexport const MAX_TODO_TASKS = 50;\nexport const MAX_TASK_SUBJECT_LENGTH = 160;\n\nexport interface TodoTask {\n key: string;\n subject: string;\n status: TodoStatus;\n}\n\nexport interface TodoState {\n version: number;\n tasks: TodoTask[];\n}\n\nexport interface TodoTaskInput {\n key: string;\n subject?: string;\n status?: TodoStatus;\n}\n\nexport interface TodoSnapshotInput {\n tasks: TodoTaskInput[];\n baseVersion?: number;\n}\n\nexport function isTodoStatus(value: unknown): value is TodoStatus {\n return typeof value === \"string\" && TODO_STATUSES.has(value);\n}\n\nexport function createEmptyTodoState(): TodoState {\n return { version: 0, tasks: [] };\n}\n\nexport function cloneTodoState(state: TodoState): TodoState {\n return { version: state.version, tasks: state.tasks.map((task) => ({ ...task })) };\n}\n\nfunction normalizeTaskKey(value: string, location: string): string {\n const key = value.trim();\n if (!/^[a-z0-9][a-z0-9._-]{0,39}$/.test(key)) {\n throw new Error(\n `${location} must be 1-40 lowercase ASCII letters, numbers, dots, underscores, or hyphens`,\n );\n }\n return key;\n}\n\nfunction tasksEqual(left: readonly TodoTask[], right: readonly TodoTask[]): boolean {\n if (left.length !== right.length) return false;\n for (let index = 0; index < left.length; index++) {\n const a = left[index];\n const b = right[index];\n if (a.key !== b.key || a.subject !== b.subject || a.status !== b.status) return false;\n }\n return true;\n}\n\n/**\n * Write the complete authoritative task list with one atomic update.\n *\n * Semantics:\n * - Every key present in `tasks` is retained; omitted current keys are permanently\n * deleted (there is no cancelled or archived state).\n * - Existing keys inherit omitted fields (`subject`, `status`) from their previous\n * value; a new key requires both.\n * - At most one task may be `in_progress` at a time.\n * - `baseVersion`, when provided, rejects stale writes: if it does not match the\n * current version the write fails without mutating state.\n * - All validation is all-or-nothing: a failed write never mutates state.\n * - A write that leaves the plan unchanged does not bump the version, so redundant\n * submissions do not invalidate the next write.\n */\nexport function writeTodo(state: TodoState, input: TodoSnapshotInput): TodoState {\n if (input.baseVersion !== undefined && input.baseVersion !== state.version) {\n throw new Error(\n `stale todo version: expected ${input.baseVersion}, current version is ${state.version}`,\n );\n }\n if (input.tasks.length > MAX_TODO_TASKS) {\n throw new Error(`tasks supports at most ${MAX_TODO_TASKS} items`);\n }\n\n const existingByKey = new Map(state.tasks.map((task) => [task.key, task]));\n const seenKeys = new Set<string>();\n const next: TodoTask[] = [];\n\n for (const [index, patch] of input.tasks.entries()) {\n const key = normalizeTaskKey(patch.key, `tasks[${index}].key`);\n if (seenKeys.has(key)) {\n throw new Error(`tasks[${index}].key is duplicated: ${key}`);\n }\n seenKeys.add(key);\n\n const existing = existingByKey.get(key);\n const subject = patch.subject ?? existing?.subject;\n if (subject === undefined) {\n throw new Error(`tasks[${index}].subject is required for new task ${key}`);\n }\n const trimmedSubject = subject.trim();\n if (!trimmedSubject) {\n throw new Error(`tasks[${index}].subject is required for new task ${key}`);\n }\n if (trimmedSubject.length > MAX_TASK_SUBJECT_LENGTH) {\n throw new Error(\n `tasks[${index}].subject must be at most ${MAX_TASK_SUBJECT_LENGTH} characters`,\n );\n }\n const status = patch.status ?? existing?.status;\n if (status === undefined) {\n throw new Error(`tasks[${index}].status is required for new task ${key}`);\n }\n if (!isTodoStatus(status)) {\n throw new Error(`tasks[${index}].status is invalid: ${String(status)}`);\n }\n\n next.push({ key, subject: trimmedSubject, status });\n }\n\n const active = next.filter((task) => task.status === \"in_progress\");\n if (active.length > 1) {\n throw new Error(\n `only one task may be in_progress at a time (got ${active.length}: ${active.map((task) => task.key).join(\", \")})`,\n );\n }\n\n return {\n version: tasksEqual(state.tasks, next) ? state.version : state.version + 1,\n tasks: next,\n };\n}\n\nexport function isValidTodoState(value: unknown): value is TodoState {\n if (!value || typeof value !== \"object\") return false;\n const candidate = value as { version?: unknown; tasks?: unknown };\n if (\n typeof candidate.version !== \"number\"\n || !Number.isSafeInteger(candidate.version)\n || candidate.version < 0\n ) {\n return false;\n }\n if (!Array.isArray(candidate.tasks) || candidate.tasks.length > MAX_TODO_TASKS) {\n return false;\n }\n const seen = new Set<string>();\n for (const rawTask of candidate.tasks) {\n if (!rawTask || typeof rawTask !== \"object\") return false;\n const task = rawTask as { key?: unknown; subject?: unknown; status?: unknown };\n if (\n typeof task.key !== \"string\"\n || typeof task.subject !== \"string\"\n || typeof task.subject.trim() !== \"string\"\n || !task.subject.trim()\n || task.subject.length > MAX_TASK_SUBJECT_LENGTH\n || !isTodoStatus(task.status)\n ) {\n return false;\n }\n let key: string;\n try {\n key = normalizeTaskKey(task.key, \"key\");\n } catch {\n return false;\n }\n if (seen.has(key)) return false;\n seen.add(key);\n }\n const active = (candidate.tasks as TodoTask[]).filter((task) => task.status === \"in_progress\");\n if (active.length > 1) return false;\n return true;\n}\n\n/**\n * Reconstruct the current state by replaying the branch. The latest valid tool\n * result (or a legacy hidden custom-message snapshot from an older version\n * that auto-removed completed tasks) for the `todo` tool wins.\n */\nexport function replayTodoState(\n ctx: { sessionManager: { getBranch(): Iterable<unknown> } },\n toolName: string,\n customType: string,\n): TodoState {\n let state = createEmptyTodoState();\n for (const rawEntry of ctx.sessionManager.getBranch()) {\n if (!rawEntry || typeof rawEntry !== \"object\") continue;\n const entry = rawEntry as {\n type?: unknown;\n customType?: unknown;\n details?: unknown;\n message?: { role?: unknown; toolName?: unknown; details?: unknown };\n };\n let restored: TodoState | undefined;\n\n if (\n entry.type === \"custom_message\"\n && entry.customType === customType\n && isValidTodoState(entry.details)\n ) {\n restored = entry.details;\n } else if (\n entry.type === \"message\"\n && entry.message\n && entry.message.role === \"toolResult\"\n && entry.message.toolName === toolName\n && isValidTodoState(entry.message.details)\n ) {\n restored = entry.message.details;\n }\n\n if (restored) state = cloneTodoState(restored);\n }\n return state;\n}\n\nexport function formatChange(state: TodoState): string {\n if (state.tasks.length === 0) {\n return `Todo plan cleared (version ${state.version}).`;\n }\n const lines = state.tasks.map(\n (task) => `[${task.status}] ${task.key}: ${task.subject}`,\n );\n return `Todo plan version ${state.version}:\\n${lines.join(\"\\n\")}`;\n}\n"
5
+ "import { StringEnum } from \"@earendil-works/pi-ai\";\nimport type { ExtensionAPI, ExtensionContext, Theme } from \"@earendil-works/pi-coding-agent\";\nimport { Text, truncateToWidth, type TUI } from \"@earendil-works/pi-tui\";\nimport { Type } from \"typebox\";\nimport {\n MAX_TODO_TASKS,\n buildTodoNudge,\n createEmptyTodoState,\n formatChange,\n hasInProgress,\n isTodoStatus,\n isValidTodoState,\n replayTodoState,\n writeTodo,\n type TodoState,\n type TodoStatus,\n type TodoTask,\n} from \"./state.ts\";\n\nconst TODO_TOOL_NAME = \"todo\";\nconst WIDGET_KEY = \"pi-todo-widget\";\nconst STATE_CUSTOM_TYPE = \"pi-todo-state\";\n\n// Desktop-owned todo widget protocol (setWidget channel): the widget key and\n// line format are defined by pi-agent-desktop\n// (D:\\Code\\pi-agent-desktop\\lib\\todo-state.ts). This extension conforms by\n// emitting the same key and JSON-per-line payload, so the desktop can route\n// the `setWidget` request straight into its sidebar todo panel.\nconst TODO_WIDGET_KEY = \"pi-agent-desktop:todo\";\n\n/** Serialize a TodoState to widget lines (one task per JSON line) for the\n * desktop todo widget protocol. Mirrors the desktop's serializer. */\nfunction serializeTodoWidgetLines(state: TodoState): string[] {\n return state.tasks.map((task) => JSON.stringify(task));\n}\n\nconst TodoKeySchema = Type.String({\n description:\n \"Stable 1-40 character lowercase task key, e.g. inspect-api or write-tests\",\n minLength: 1,\n maxLength: 40,\n pattern: \"^[a-z0-9][a-z0-9._-]*$\",\n});\n\nconst TodoTaskSchema = Type.Object({\n key: TodoKeySchema,\n subject: Type.Optional(\n Type.String({\n description:\n \"Short imperative task subject; required for a new key, omitted to keep an existing value\",\n minLength: 1,\n maxLength: 160,\n }),\n ),\n status: Type.Optional(\n StringEnum([\"pending\", \"in_progress\", \"completed\"] as const, {\n description:\n \"Task status; required for a new key, omitted to keep an existing value\",\n }),\n ),\n});\n\nconst TodoParamsSchema = Type.Object({\n tasks: Type.Array(TodoTaskSchema, {\n description:\n \"Complete authoritative list of tasks to retain; omitted keys are deleted, existing tasks may omit unchanged fields, and new keys require subject and status\",\n maxItems: MAX_TODO_TASKS,\n }),\n baseVersion: Type.Optional(\n Type.Integer({\n description:\n \"Version shown in current todo context; rejects stale writes when provided\",\n minimum: 0,\n }),\n ),\n});\n\n/** Markdown-style glyphs: [ ] pending, [-] in_progress, [x] completed. */\nfunction todoGlyph(status: TodoStatus): string {\n return status === \"completed\" ? \"[x]\" : status === \"in_progress\" ? \"[-]\" : \"[ ]\";\n}\n\ninterface TodoDisplayTask {\n subject: string;\n status?: TodoStatus;\n}\n\nfunction renderTaskLine(task: TodoDisplayTask, theme: Theme): string {\n if (!task.status) return theme.fg(\"text\", task.subject);\n const glyph = theme.fg(\n task.status === \"completed\"\n ? \"success\"\n : task.status === \"in_progress\"\n ? \"warning\"\n : \"muted\",\n todoGlyph(task.status),\n );\n let subject = theme.fg(\n task.status === \"completed\" ? \"dim\" : \"text\",\n task.subject,\n );\n if (task.status === \"completed\") subject = theme.strikethrough(subject);\n else if (task.status === \"in_progress\") subject = theme.bold(subject);\n return `${glyph} ${subject}`;\n}\n\nfunction renderTodoWidget(state: TodoState, width: number, theme: Theme): string[] {\n if (state.tasks.length === 0) return [];\n const completed = state.tasks.filter((task) => task.status === \"completed\").length;\n const lines = [\n theme.fg(\n \"accent\",\n theme.bold(`Todo ${completed}/${state.tasks.length} completed`),\n ) + theme.fg(\"dim\", ` v${state.version}`),\n ];\n lines.push(...state.tasks.map((task) => renderTaskLine(task, theme)));\n return lines.map((line) => truncateToWidth(line, width, \"…\"));\n}\n\n/** Resolve a partially streamed `todo` call against current state for the live preview. */\nfunction resolveDraftTasks(rawTasks: unknown, state: TodoState): TodoDisplayTask[] {\n if (!Array.isArray(rawTasks)) return [];\n const currentByKey = new Map(state.tasks.map((task) => [task.key, task]));\n const tasks: TodoDisplayTask[] = [];\n for (const rawTask of rawTasks) {\n const draft =\n rawTask && typeof rawTask === \"object\"\n ? (rawTask as { key?: unknown; subject?: unknown; status?: unknown })\n : {};\n const key = typeof draft.key === \"string\" ? draft.key.trim() : \"\";\n const current = key ? currentByKey.get(key) : undefined;\n const subject =\n typeof draft.subject === \"string\" && draft.subject.trim()\n ? draft.subject.trim()\n : current?.subject;\n if (!key || !subject) continue;\n tasks.push({\n subject,\n status: isTodoStatus(draft.status) ? draft.status : current?.status,\n });\n }\n return tasks;\n}\n\nexport default function todoMiniExtension(pi: ExtensionAPI): void {\n let state = createEmptyTodoState();\n let uiContext: ExtensionContext | undefined;\n let widgetRegistered = false;\n let widgetTui: TUI | undefined;\n // Plan-B reconcile nudge state (run-end reminder). `nudgedThisTurn` is reset\n // only by a REAL user turn (a user message that is not our own nudge), so our\n // nudge's own delivery can never re-trigger it — no nudge loop.\n let nudgedThisTurn = false;\n const sentNudges = new Set<string>();\n\n const nudgeText = (): string => buildTodoNudge();\n\n const clearWidget = (): void => {\n if (widgetRegistered && uiContext?.hasUI) {\n try {\n uiContext.ui.setWidget(WIDGET_KEY, undefined);\n } catch {}\n }\n widgetRegistered = false;\n widgetTui = undefined;\n };\n\n const updateWidget = (ctx?: ExtensionContext): void => {\n if (ctx) uiContext = ctx;\n // pi-agent-desktop todo widget protocol (RPC/desktop): push the live plan\n // over `setWidget` with the desktop-reserved key. Fire-and-forget — the\n // desktop parses the JSON lines into the sidebar panel. Cleared (undefined)\n // when the plan is empty so the panel hides. The TUI widget path below\n // stays separate and unchanged.\n if (!uiContext?.hasUI) return;\n if (uiContext.mode !== \"tui\") {\n // RPC / desktop host: live todo widget over the reserved key.\n try {\n uiContext.ui.setWidget(\n TODO_WIDGET_KEY,\n state.tasks.length > 0 ? serializeTodoWidgetLines(state) : undefined,\n );\n } catch {}\n return;\n }\n if (state.tasks.length === 0) {\n clearWidget();\n return;\n }\n if (!widgetRegistered) {\n uiContext.ui.setWidget(\n WIDGET_KEY,\n (tui: TUI, theme: Theme) => {\n widgetTui = tui;\n return {\n render: (width: number) => renderTodoWidget(state, width, theme),\n invalidate: () => {},\n dispose: () => {\n if (widgetTui === tui) widgetTui = undefined;\n },\n };\n },\n { placement: \"aboveEditor\" },\n );\n widgetRegistered = true;\n } else {\n widgetTui?.requestRender();\n }\n };\n\n pi.registerTool({\n name: TODO_TOOL_NAME,\n label: \"Todo\",\n description: `Maintain the task plan with one atomic update. Every call replaces the task list: include each key to keep, and omit a key to delete it. Existing tasks may omit unchanged fields; new tasks require subject and status. At most one task may be in_progress. Up to ${MAX_TODO_TASKS} tasks; an empty list clears the plan.`,\n promptSnippet: \"Maintain the task plan with one atomic update\",\n promptGuidelines: [\n \"When a task needs a plan of 3+ steps, define it yourself and call todo before beginning implementation or other substantive work.\",\n \"Organize the plan by work content: a coherent objective that may span multiple turns, and a plan can hold several work contents worked across different turns. Give each work content a stable slug and prefix its todos' keys with it (e.g. auth.validate-token, auth.refresh-token) so pruning can target a whole work content.\",\n \"Write task subjects in the user's language (e.g. Chinese when the user writes in Chinese). Task keys must stay lowercase ASCII slugs (a-z, 0-9, . _ -) — they are stable identities, not display text.\",\n \"The list is one flat ordered sequence, not grouped sections: order todos by execution/dependency order, and when work contents are related keep their todos in sequence in the same list rather than splitting them into separate blocks. The UI renders this as a single ordered list — there are no work-content groups or headers.\",\n \"Each todo call replaces the task list. Include every key to keep; omitted keys are deleted.\",\n \"Keep keys stable. Existing tasks may omit unchanged fields; new tasks require subject and status. Include baseVersion when available.\",\n \"Only one task may be in_progress at a time.\",\n \"Completed todos are never auto-removed. Keep them struck through as history while their work content is still open.\",\n \"Prune completed todos only when their whole work content is complete: once every todo under a work content is done, remove all of that content's todos together in the next todo call. Work contents are pruned independently — a completed work content is removed even when other work contents remain open. Turn boundaries are not cleanup points.\",\n \"While a todo plan exists, update todo immediately when task status, order, or scope changes, and reconcile actual progress before the final response. Do not issue a no-op todo call only to acknowledge a reminder.\",\n ],\n parameters: TodoParamsSchema,\n executionMode: \"sequential\",\n\n async execute(_toolCallId, params, _signal, _onUpdate, ctx) {\n state = writeTodo(state, params);\n updateWidget(ctx);\n return {\n content: [{ type: \"text\", text: formatChange(state) }],\n details: state,\n };\n },\n\n renderCall(args, theme, context) {\n const text =\n (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n const tasks = resolveDraftTasks(args.tasks, state);\n const hasTaskList = Array.isArray(args.tasks);\n const count = hasTaskList ? args.tasks.length : 0;\n const summary = hasTaskList\n ? theme.fg(\"accent\", `${count} task${count === 1 ? \"\" : \"s\"}`)\n : \"\";\n const lines = [\n [theme.fg(\"toolTitle\", theme.bold(\"todo\")), summary]\n .filter(Boolean)\n .join(\" \"),\n ];\n lines.push(...tasks.map((task) => renderTaskLine(task, theme)));\n text.setText(lines.join(\"\\n\"));\n return text;\n },\n\n renderResult(result, _opts, theme, context) {\n const text =\n (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n if (context.isError || !isValidTodoState(result.details)) {\n const output = result.content\n .filter(\n (item): item is { type: \"text\"; text: string } => item.type === \"text\",\n )\n .map((item) => item.text)\n .join(\"\\n\");\n text.setText(theme.fg(\"error\", output));\n return text;\n }\n // A successful write is already rendered by the tool call above; keep this empty.\n text.setText(\"\");\n return text;\n },\n });\n\n const restore = (ctx: ExtensionContext): void => {\n clearWidget();\n state = replayTodoState(\n { sessionManager: { getBranch: () => ctx.sessionManager.getBranch() } },\n TODO_TOOL_NAME,\n STATE_CUSTOM_TYPE,\n );\n uiContext = ctx;\n updateWidget(ctx);\n };\n\n pi.on(\"session_start\", async (_event, ctx) => restore(ctx));\n pi.on(\"session_tree\", async (_event, ctx) => restore(ctx));\n\n pi.on(\"session_shutdown\", async () => {\n clearWidget();\n uiContext = undefined;\n });\n\n // Plan B: run-end reconcile nudge. `agent_settled` fires exactly once per turn\n // (after any auto-retries/compactions/follow-ups), which is the point where Pi\n // confirms the agent will not continue running automatically. If tasks are\n // still in_progress and we have not nudged this logical turn, send one steer\n // reminding the agent to mark them completed if the work is actually done.\n pi.on(\"agent_settled\", () => {\n if (nudgedThisTurn || !hasInProgress(state)) return;\n nudgedThisTurn = true;\n const text = nudgeText();\n sentNudges.add(text);\n pi.sendUserMessage(text, { deliverAs: \"steer\" });\n });\n\n // Reset the nudge budget on a REAL user turn. Our own nudge arrives back as a\n // user message with the exact text we sent — recognizing it by content (same\n // pattern as repetition-guard's RetryBudget) means the nudge's own agent run\n // is not treated as a new user turn, so it can't fire repeatedly in a loop.\n pi.on(\"message_start\", (event) => {\n if (event.message?.role !== \"user\") return;\n const text = contentText(event.message.content);\n if (text && sentNudges.has(text)) {\n sentNudges.delete(text);\n return;\n }\n nudgedThisTurn = false;\n });\n}\n\n/** Concatenate the text of a user message, for matching against our nudge. */\nfunction contentText(content: unknown): string {\n if (!Array.isArray(content)) return \"\";\n let text = \"\";\n for (const item of content) {\n if (item && typeof item === \"object\") {\n const t = (item as { text?: unknown }).text;\n if (typeof t === \"string\") text += t;\n }\n }\n return text;\n}\n",
6
+ "export type TodoStatus = \"pending\" | \"in_progress\" | \"completed\";\n\nconst TODO_STATUSES: ReadonlySet<string> = new Set([\n \"pending\",\n \"in_progress\",\n \"completed\",\n]);\n\nexport const MAX_TODO_TASKS = 50;\nexport const MAX_TASK_SUBJECT_LENGTH = 160;\n\nexport interface TodoTask {\n key: string;\n subject: string;\n status: TodoStatus;\n}\n\nexport interface TodoState {\n version: number;\n tasks: TodoTask[];\n}\n\nexport interface TodoTaskInput {\n key: string;\n subject?: string;\n status?: TodoStatus;\n}\n\nexport interface TodoSnapshotInput {\n tasks: TodoTaskInput[];\n baseVersion?: number;\n}\n\nexport function isTodoStatus(value: unknown): value is TodoStatus {\n return typeof value === \"string\" && TODO_STATUSES.has(value);\n}\n\nexport function createEmptyTodoState(): TodoState {\n return { version: 0, tasks: [] };\n}\n\nexport function cloneTodoState(state: TodoState): TodoState {\n return { version: state.version, tasks: state.tasks.map((task) => ({ ...task })) };\n}\n\nfunction normalizeTaskKey(value: string, location: string): string {\n const key = value.trim();\n if (!/^[a-z0-9][a-z0-9._-]{0,39}$/.test(key)) {\n throw new Error(\n `${location} must be 1-40 lowercase ASCII letters, numbers, dots, underscores, or hyphens`,\n );\n }\n return key;\n}\n\nfunction tasksEqual(left: readonly TodoTask[], right: readonly TodoTask[]): boolean {\n if (left.length !== right.length) return false;\n for (let index = 0; index < left.length; index++) {\n const a = left[index];\n const b = right[index];\n if (a.key !== b.key || a.subject !== b.subject || a.status !== b.status) return false;\n }\n return true;\n}\n\n/**\n * Write the complete authoritative task list with one atomic update.\n *\n * Semantics:\n * - Every key present in `tasks` is retained; omitted current keys are permanently\n * deleted (there is no cancelled or archived state).\n * - Existing keys inherit omitted fields (`subject`, `status`) from their previous\n * value; a new key requires both.\n * - At most one task may be `in_progress` at a time.\n * - `baseVersion`, when provided, rejects stale writes: if it does not match the\n * current version the write fails without mutating state.\n * - All validation is all-or-nothing: a failed write never mutates state.\n * - A write that leaves the plan unchanged does not bump the version, so redundant\n * submissions do not invalidate the next write.\n */\nexport function writeTodo(state: TodoState, input: TodoSnapshotInput): TodoState {\n if (input.baseVersion !== undefined && input.baseVersion !== state.version) {\n throw new Error(\n `stale todo version: expected ${input.baseVersion}, current version is ${state.version}`,\n );\n }\n if (input.tasks.length > MAX_TODO_TASKS) {\n throw new Error(`tasks supports at most ${MAX_TODO_TASKS} items`);\n }\n\n const existingByKey = new Map(state.tasks.map((task) => [task.key, task]));\n const seenKeys = new Set<string>();\n const next: TodoTask[] = [];\n\n for (const [index, patch] of input.tasks.entries()) {\n const key = normalizeTaskKey(patch.key, `tasks[${index}].key`);\n if (seenKeys.has(key)) {\n throw new Error(`tasks[${index}].key is duplicated: ${key}`);\n }\n seenKeys.add(key);\n\n const existing = existingByKey.get(key);\n const subject = patch.subject ?? existing?.subject;\n if (subject === undefined) {\n throw new Error(`tasks[${index}].subject is required for new task ${key}`);\n }\n const trimmedSubject = subject.trim();\n if (!trimmedSubject) {\n throw new Error(`tasks[${index}].subject is required for new task ${key}`);\n }\n if (trimmedSubject.length > MAX_TASK_SUBJECT_LENGTH) {\n throw new Error(\n `tasks[${index}].subject must be at most ${MAX_TASK_SUBJECT_LENGTH} characters`,\n );\n }\n const status = patch.status ?? existing?.status;\n if (status === undefined) {\n throw new Error(`tasks[${index}].status is required for new task ${key}`);\n }\n if (!isTodoStatus(status)) {\n throw new Error(`tasks[${index}].status is invalid: ${String(status)}`);\n }\n\n next.push({ key, subject: trimmedSubject, status });\n }\n\n const active = next.filter((task) => task.status === \"in_progress\");\n if (active.length > 1) {\n throw new Error(\n `only one task may be in_progress at a time (got ${active.length}: ${active.map((task) => task.key).join(\", \")})`,\n );\n }\n\n return {\n version: tasksEqual(state.tasks, next) ? state.version : state.version + 1,\n tasks: next,\n };\n}\n\nexport function isValidTodoState(value: unknown): value is TodoState {\n if (!value || typeof value !== \"object\") return false;\n const candidate = value as { version?: unknown; tasks?: unknown };\n if (\n typeof candidate.version !== \"number\"\n || !Number.isSafeInteger(candidate.version)\n || candidate.version < 0\n ) {\n return false;\n }\n if (!Array.isArray(candidate.tasks) || candidate.tasks.length > MAX_TODO_TASKS) {\n return false;\n }\n const seen = new Set<string>();\n for (const rawTask of candidate.tasks) {\n if (!rawTask || typeof rawTask !== \"object\") return false;\n const task = rawTask as { key?: unknown; subject?: unknown; status?: unknown };\n if (\n typeof task.key !== \"string\"\n || typeof task.subject !== \"string\"\n || typeof task.subject.trim() !== \"string\"\n || !task.subject.trim()\n || task.subject.length > MAX_TASK_SUBJECT_LENGTH\n || !isTodoStatus(task.status)\n ) {\n return false;\n }\n let key: string;\n try {\n key = normalizeTaskKey(task.key, \"key\");\n } catch {\n return false;\n }\n if (seen.has(key)) return false;\n seen.add(key);\n }\n const active = (candidate.tasks as TodoTask[]).filter((task) => task.status === \"in_progress\");\n if (active.length > 1) return false;\n return true;\n}\n\n/**\n * Reconstruct the current state by replaying the branch. The latest valid tool\n * result (or a legacy hidden custom-message snapshot from an older version\n * that auto-removed completed tasks) for the `todo` tool wins.\n */\nexport function replayTodoState(\n ctx: { sessionManager: { getBranch(): Iterable<unknown> } },\n toolName: string,\n customType: string,\n): TodoState {\n let state = createEmptyTodoState();\n for (const rawEntry of ctx.sessionManager.getBranch()) {\n if (!rawEntry || typeof rawEntry !== \"object\") continue;\n const entry = rawEntry as {\n type?: unknown;\n customType?: unknown;\n details?: unknown;\n message?: { role?: unknown; toolName?: unknown; details?: unknown };\n };\n let restored: TodoState | undefined;\n\n if (\n entry.type === \"custom_message\"\n && entry.customType === customType\n && isValidTodoState(entry.details)\n ) {\n restored = entry.details;\n } else if (\n entry.type === \"message\"\n && entry.message\n && entry.message.role === \"toolResult\"\n && entry.message.toolName === toolName\n && isValidTodoState(entry.message.details)\n ) {\n restored = entry.message.details;\n }\n\n if (restored) state = cloneTodoState(restored);\n }\n return state;\n}\n\nexport function formatChange(state: TodoState): string {\n if (state.tasks.length === 0) {\n return `Todo plan cleared (version ${state.version}).`;\n }\n const lines = state.tasks.map(\n (task) => `[${task.status}] ${task.key}: ${task.subject}`,\n );\n return `Todo plan version ${state.version}:\\n${lines.join(\"\\n\")}`;\n}\n\n/** True when any task is currently in_progress. Used by the reconcile nudge to\n * decide whether a run-end reminder is warranted. */\nexport function hasInProgress(state: TodoState): boolean {\n return state.tasks.some((task) => task.status === \"in_progress\");\n}\n\n/**\n * Build the run-end reconcile nudge (Plan B). Sent from `agent_settled` when a\n * turn settled while tasks are still in_progress, so the agent is reminded to\n * mark them completed if the work is actually done. The fixed text doubles as\n * the identity for recognizing our own steer's user-message delivery (content\n * match, same pattern as repetition-guard's RetryBudget) — so the delivery is\n * NOT treated as a new user turn and the nudge can't re-fire in a loop.\n */\nexport function buildTodoNudge(): string {\n return (\n \"[pi-todo] 收尾提醒:本回合已结束,但仍有任务处于 in_progress。若这些任务实际已完成,\" +\n \"请立即调用 todo 把它们标记为 completed;若确实仍在进行中,请简短说明并保持现状。\" +\n \"不要执行其他操作。\"\n );\n}\n"
7
7
  ],
8
- "mappings": "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",
9
- "debugId": "63C76BB82F4FA11D64756E2164756E21",
8
+ "mappings": "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",
9
+ "debugId": "7676EBAAA3F657AB64756E2164756E21",
10
10
  "names": []
11
11
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capdiem/pi-todo",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "A lightweight atomic todo tool for the Pi coding agent: one-call snapshot plans, three states, stale-write guard, plus a TUI widget and a pi-agent-desktop sidebar panel",
5
5
  "type": "module",
6
6
  "pi": {