@vitos-pizza/vitos-pizza 0.1.0 → 0.2.0

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.
Files changed (259) hide show
  1. package/AGENTS.md +158 -158
  2. package/CHANGELOG.md +42 -24
  3. package/LICENSE +21 -21
  4. package/README.md +237 -234
  5. package/assets/logo.svg +28 -28
  6. package/node_modules/@vitos-pizza/agent-mode/extensions/index.ts +138 -117
  7. package/node_modules/@vitos-pizza/agent-mode/package.json +3 -3
  8. package/node_modules/@vitos-pizza/agent-mode/src/apply-mode.ts +60 -60
  9. package/node_modules/@vitos-pizza/agent-mode/src/mode-events.ts +102 -102
  10. package/node_modules/@vitos-pizza/agent-mode/src/modes.ts +43 -43
  11. package/node_modules/@vitos-pizza/agent-mode/src/plan-instructions.ts +85 -44
  12. package/node_modules/@vitos-pizza/agent-mode/tests/apply-mode.test.ts +36 -36
  13. package/node_modules/@vitos-pizza/agent-mode/tests/mode-events.test.ts +22 -22
  14. package/node_modules/@vitos-pizza/agent-mode/tests/modes.test.ts +38 -38
  15. package/node_modules/@vitos-pizza/agent-mode/tests/plan-instructions.test.ts +68 -45
  16. package/node_modules/@vitos-pizza/agent-mode/tsconfig.json +13 -13
  17. package/node_modules/@vitos-pizza/agent-mode/vitest.config.ts +8 -8
  18. package/node_modules/@vitos-pizza/keybindings/extensions/index.ts +104 -104
  19. package/node_modules/@vitos-pizza/keybindings/package.json +1 -1
  20. package/node_modules/@vitos-pizza/keybindings/presets/shortcuts.json +3 -3
  21. package/node_modules/@vitos-pizza/keybindings/src/bind.ts +57 -57
  22. package/node_modules/@vitos-pizza/keybindings/src/config.ts +84 -84
  23. package/node_modules/@vitos-pizza/keybindings/src/registry.ts +43 -43
  24. package/node_modules/@vitos-pizza/keybindings/src/types.ts +54 -54
  25. package/node_modules/@vitos-pizza/keybindings/tests/config.test.ts +83 -83
  26. package/node_modules/@vitos-pizza/keybindings/tests/registry.test.ts +142 -142
  27. package/node_modules/@vitos-pizza/keybindings/tsconfig.json +13 -13
  28. package/node_modules/@vitos-pizza/keybindings/vitest.config.ts +8 -8
  29. package/node_modules/@vitos-pizza/permission-system/extensions/index.ts +162 -162
  30. package/node_modules/@vitos-pizza/permission-system/package.json +2 -2
  31. package/node_modules/@vitos-pizza/permission-system/src/bash/matcher.ts +54 -51
  32. package/node_modules/@vitos-pizza/permission-system/src/bash/parser.ts +40 -40
  33. package/node_modules/@vitos-pizza/permission-system/src/config-paths.ts +23 -23
  34. package/node_modules/@vitos-pizza/permission-system/src/detect-preset-mode.ts +61 -61
  35. package/node_modules/@vitos-pizza/permission-system/src/expand-home.ts +12 -12
  36. package/node_modules/@vitos-pizza/permission-system/src/forwarding/forwarder.ts +224 -224
  37. package/node_modules/@vitos-pizza/permission-system/src/forwarding/subagent-registry.ts +61 -61
  38. package/node_modules/@vitos-pizza/permission-system/src/gates/path-utils.ts +66 -66
  39. package/node_modules/@vitos-pizza/permission-system/src/gates/pipeline.ts +151 -155
  40. package/node_modules/@vitos-pizza/permission-system/src/handlers/before-agent-start.ts +86 -65
  41. package/node_modules/@vitos-pizza/permission-system/src/handlers/skill-input.ts +1 -1
  42. package/node_modules/@vitos-pizza/permission-system/src/handlers/tool-call-gate.ts +204 -199
  43. package/node_modules/@vitos-pizza/permission-system/src/mode-api.ts +44 -44
  44. package/node_modules/@vitos-pizza/permission-system/src/permission-evaluator.ts +233 -216
  45. package/node_modules/@vitos-pizza/permission-system/src/permission-session.ts +62 -62
  46. package/node_modules/@vitos-pizza/permission-system/src/policy-loader.ts +211 -211
  47. package/node_modules/@vitos-pizza/permission-system/src/sanitizers/skill-prompt.ts +99 -99
  48. package/node_modules/@vitos-pizza/permission-system/src/sanitizers/system-prompt.ts +221 -221
  49. package/node_modules/@vitos-pizza/permission-system/src/service.ts +66 -66
  50. package/node_modules/@vitos-pizza/permission-system/src/session-approvals.ts +4 -1
  51. package/node_modules/@vitos-pizza/permission-system/src/types.ts +80 -80
  52. package/node_modules/@vitos-pizza/permission-system/src/ui/approval-summary.ts +43 -43
  53. package/node_modules/@vitos-pizza/permission-system/src/ui/config-modal.ts +36 -36
  54. package/node_modules/@vitos-pizza/permission-system/src/ui/permission-dialog.ts +52 -52
  55. package/node_modules/@vitos-pizza/permission-system/src/wildcard-matcher.ts +81 -81
  56. package/node_modules/@vitos-pizza/permission-system/src/yaml-frontmatter.ts +84 -84
  57. package/node_modules/@vitos-pizza/permission-system/src/yolo/runtime-api.ts +49 -49
  58. package/node_modules/@vitos-pizza/permission-system/tests/bash-parser.test.ts +32 -32
  59. package/node_modules/@vitos-pizza/permission-system/tests/before-agent-start.test.ts +61 -0
  60. package/node_modules/@vitos-pizza/permission-system/tests/detect-preset-mode.test.ts +68 -68
  61. package/node_modules/@vitos-pizza/permission-system/tests/forwarder.test.ts +55 -55
  62. package/node_modules/@vitos-pizza/permission-system/tests/gates.test.ts +170 -61
  63. package/node_modules/@vitos-pizza/permission-system/tests/path-utils.test.ts +68 -68
  64. package/node_modules/@vitos-pizza/permission-system/tests/permission-evaluator.test.ts +82 -55
  65. package/node_modules/@vitos-pizza/permission-system/tests/policy-loader.test.ts +25 -25
  66. package/node_modules/@vitos-pizza/permission-system/tests/system-prompt-sanitizer.test.ts +22 -22
  67. package/node_modules/@vitos-pizza/permission-system/tests/wildcard-matcher.test.ts +27 -27
  68. package/node_modules/@vitos-pizza/permission-system/tsconfig.json +13 -13
  69. package/node_modules/@vitos-pizza/permission-system/vitest.config.ts +8 -8
  70. package/node_modules/@vitos-pizza/question/package.json +1 -1
  71. package/node_modules/@vitos-pizza/question/src/question-ui.ts +5 -1
  72. package/node_modules/@vitos-pizza/question/src/register-tool.ts +1 -4
  73. package/node_modules/@vitos-pizza/question/tests/question-ui-width-cache.test.ts +45 -0
  74. package/node_modules/@vitos-pizza/question/tests/register-tool-render.test.ts +48 -45
  75. package/node_modules/@vitos-pizza/session-title/extensions/index.ts +137 -137
  76. package/node_modules/@vitos-pizza/session-title/package.json +3 -3
  77. package/node_modules/@vitos-pizza/session-title/src/build-title-prompt.ts +16 -16
  78. package/node_modules/@vitos-pizza/session-title/src/extract-title-context.ts +55 -55
  79. package/node_modules/@vitos-pizza/session-title/src/fast-rules.ts +48 -48
  80. package/node_modules/@vitos-pizza/session-title/src/load-settings.ts +70 -70
  81. package/node_modules/@vitos-pizza/session-title/src/normalize-title.ts +20 -20
  82. package/node_modules/@vitos-pizza/session-title/src/parse-title-response.ts +6 -6
  83. package/node_modules/@vitos-pizza/session-title/src/resolve-title-model.ts +33 -33
  84. package/node_modules/@vitos-pizza/session-title/src/schedule-title-job.ts +53 -53
  85. package/node_modules/@vitos-pizza/session-title/src/should-schedule.ts +30 -30
  86. package/node_modules/@vitos-pizza/session-title/src/title-agent.ts +60 -60
  87. package/node_modules/@vitos-pizza/session-title/src/truncate-input.ts +10 -10
  88. package/node_modules/@vitos-pizza/session-title/src/types.ts +29 -29
  89. package/node_modules/@vitos-pizza/session-title/tests/build-title-prompt.test.ts +34 -34
  90. package/node_modules/@vitos-pizza/session-title/tests/extension.test.ts +198 -198
  91. package/node_modules/@vitos-pizza/session-title/tests/extract-title-context.test.ts +48 -48
  92. package/node_modules/@vitos-pizza/session-title/tests/fast-rules.test.ts +43 -43
  93. package/node_modules/@vitos-pizza/session-title/tests/parse-title-response.test.ts +34 -34
  94. package/node_modules/@vitos-pizza/session-title/tests/should-schedule.test.ts +30 -30
  95. package/node_modules/@vitos-pizza/session-title/tests/title-agent.test.ts +154 -154
  96. package/node_modules/@vitos-pizza/session-title/tests/truncate-input.test.ts +15 -15
  97. package/node_modules/@vitos-pizza/session-title/tsconfig.json +13 -13
  98. package/node_modules/@vitos-pizza/session-title/vitest.config.ts +8 -8
  99. package/node_modules/@vitos-pizza/subagents/agents/planner.md +46 -43
  100. package/node_modules/@vitos-pizza/subagents/agents/scout.md +40 -40
  101. package/node_modules/@vitos-pizza/subagents/agents/title.md +13 -13
  102. package/node_modules/@vitos-pizza/subagents/agents/worker.md +36 -36
  103. package/node_modules/@vitos-pizza/subagents/extensions/index.ts +164 -164
  104. package/node_modules/@vitos-pizza/subagents/package.json +1 -1
  105. package/node_modules/@vitos-pizza/subagents/skills/subagents/SKILL.md +86 -80
  106. package/node_modules/@vitos-pizza/subagents/src/agents.ts +166 -166
  107. package/node_modules/@vitos-pizza/subagents/src/async/job-tracker.ts +71 -71
  108. package/node_modules/@vitos-pizza/subagents/src/async/spawn-async.ts +220 -220
  109. package/node_modules/@vitos-pizza/subagents/src/async/storage.ts +57 -57
  110. package/node_modules/@vitos-pizza/subagents/src/env.ts +4 -4
  111. package/node_modules/@vitos-pizza/subagents/src/execute-subagent.ts +285 -285
  112. package/node_modules/@vitos-pizza/subagents/src/progress.ts +187 -187
  113. package/node_modules/@vitos-pizza/subagents/src/render-animation.ts +40 -40
  114. package/node_modules/@vitos-pizza/subagents/src/render.ts +428 -428
  115. package/node_modules/@vitos-pizza/subagents/src/rpc/channels.ts +21 -21
  116. package/node_modules/@vitos-pizza/subagents/src/rpc/client.ts +144 -144
  117. package/node_modules/@vitos-pizza/subagents/src/rpc/server.ts +11 -11
  118. package/node_modules/@vitos-pizza/subagents/src/run-agent.ts +313 -313
  119. package/node_modules/@vitos-pizza/subagents/src/runtime.ts +102 -102
  120. package/node_modules/@vitos-pizza/subagents/src/schema.ts +62 -62
  121. package/node_modules/@vitos-pizza/subagents/src/spawn.ts +104 -104
  122. package/node_modules/@vitos-pizza/subagents/src/types.ts +92 -92
  123. package/node_modules/@vitos-pizza/subagents/src/utils.ts +100 -100
  124. package/node_modules/@vitos-pizza/subagents/src/wait.ts +159 -159
  125. package/node_modules/@vitos-pizza/subagents/tests/agents.test.ts +19 -19
  126. package/node_modules/@vitos-pizza/subagents/tests/execute-subagent.test.ts +96 -96
  127. package/node_modules/@vitos-pizza/subagents/tests/extension.test.ts +42 -42
  128. package/node_modules/@vitos-pizza/subagents/tests/progress.test.ts +206 -206
  129. package/node_modules/@vitos-pizza/subagents/tests/rpc.test.ts +74 -74
  130. package/node_modules/@vitos-pizza/subagents/tsconfig.json +13 -13
  131. package/node_modules/@vitos-pizza/subagents/vitest.config.ts +8 -8
  132. package/node_modules/@vitos-pizza/ui-enhancements/package.json +1 -1
  133. package/node_modules/@vitos-pizza/ui-enhancements/src/chrome/agent-mode-badge.ts +60 -60
  134. package/node_modules/@vitos-pizza/ui-enhancements/src/chrome/border-status.ts +14 -1
  135. package/node_modules/@vitos-pizza/ui-enhancements/src/chrome/resize-recovery.ts +63 -0
  136. package/node_modules/@vitos-pizza/ui-enhancements/tests/agent-mode-badge.test.ts +35 -35
  137. package/node_modules/@vitos-pizza/ui-enhancements/tests/resize-recovery.test.ts +135 -0
  138. package/node_modules/@vitos-pizza/websearch/package.json +1 -1
  139. package/node_modules/node-addon-api/tools/conversion.js +0 -0
  140. package/node_modules/node-gyp-build/bin.js +0 -0
  141. package/node_modules/node-gyp-build/build-test.js +0 -0
  142. package/node_modules/node-gyp-build/optional.js +0 -0
  143. package/package.json +11 -9
  144. package/packages/agent-mode/extensions/index.ts +138 -117
  145. package/packages/agent-mode/package.json +3 -3
  146. package/packages/agent-mode/src/apply-mode.ts +60 -60
  147. package/packages/agent-mode/src/mode-events.ts +102 -102
  148. package/packages/agent-mode/src/modes.ts +43 -43
  149. package/packages/agent-mode/src/plan-instructions.ts +85 -44
  150. package/packages/agent-mode/tsconfig.json +13 -13
  151. package/packages/agent-mode/vitest.config.ts +8 -8
  152. package/packages/keybindings/extensions/index.ts +104 -104
  153. package/packages/keybindings/package.json +1 -1
  154. package/packages/keybindings/presets/shortcuts.json +3 -3
  155. package/packages/keybindings/src/bind.ts +57 -57
  156. package/packages/keybindings/src/config.ts +84 -84
  157. package/packages/keybindings/src/registry.ts +43 -43
  158. package/packages/keybindings/src/types.ts +54 -54
  159. package/packages/keybindings/tsconfig.json +13 -13
  160. package/packages/keybindings/vitest.config.ts +8 -8
  161. package/packages/permission-system/extensions/index.ts +162 -162
  162. package/packages/permission-system/node_modules/web-tree-sitter/debug/tree-sitter.wasm +0 -0
  163. package/packages/permission-system/node_modules/web-tree-sitter/tree-sitter.wasm +0 -0
  164. package/packages/permission-system/package.json +2 -2
  165. package/packages/permission-system/src/bash/matcher.ts +54 -51
  166. package/packages/permission-system/src/bash/parser.ts +40 -40
  167. package/packages/permission-system/src/config-paths.ts +23 -23
  168. package/packages/permission-system/src/detect-preset-mode.ts +61 -61
  169. package/packages/permission-system/src/expand-home.ts +12 -12
  170. package/packages/permission-system/src/forwarding/forwarder.ts +224 -224
  171. package/packages/permission-system/src/forwarding/subagent-registry.ts +61 -61
  172. package/packages/permission-system/src/gates/path-utils.ts +66 -66
  173. package/packages/permission-system/src/gates/pipeline.ts +151 -155
  174. package/packages/permission-system/src/handlers/before-agent-start.ts +86 -65
  175. package/packages/permission-system/src/handlers/skill-input.ts +1 -1
  176. package/packages/permission-system/src/handlers/tool-call-gate.ts +204 -199
  177. package/packages/permission-system/src/mode-api.ts +44 -44
  178. package/packages/permission-system/src/permission-evaluator.ts +233 -216
  179. package/packages/permission-system/src/permission-session.ts +62 -62
  180. package/packages/permission-system/src/policy-loader.ts +211 -211
  181. package/packages/permission-system/src/sanitizers/skill-prompt.ts +99 -99
  182. package/packages/permission-system/src/sanitizers/system-prompt.ts +221 -221
  183. package/packages/permission-system/src/service.ts +66 -66
  184. package/packages/permission-system/src/session-approvals.ts +4 -1
  185. package/packages/permission-system/src/types.ts +80 -80
  186. package/packages/permission-system/src/ui/approval-summary.ts +43 -43
  187. package/packages/permission-system/src/ui/config-modal.ts +36 -36
  188. package/packages/permission-system/src/ui/permission-dialog.ts +52 -52
  189. package/packages/permission-system/src/wildcard-matcher.ts +81 -81
  190. package/packages/permission-system/src/yaml-frontmatter.ts +84 -84
  191. package/packages/permission-system/src/yolo/runtime-api.ts +49 -49
  192. package/packages/permission-system/tsconfig.json +13 -13
  193. package/packages/permission-system/vitest.config.ts +8 -8
  194. package/packages/question/package.json +1 -1
  195. package/packages/question/src/question-ui.ts +5 -1
  196. package/packages/question/src/register-tool.ts +1 -4
  197. package/packages/session-title/extensions/index.ts +137 -137
  198. package/packages/session-title/package.json +3 -3
  199. package/packages/session-title/src/build-title-prompt.ts +16 -16
  200. package/packages/session-title/src/extract-title-context.ts +55 -55
  201. package/packages/session-title/src/fast-rules.ts +48 -48
  202. package/packages/session-title/src/load-settings.ts +70 -70
  203. package/packages/session-title/src/normalize-title.ts +20 -20
  204. package/packages/session-title/src/parse-title-response.ts +6 -6
  205. package/packages/session-title/src/resolve-title-model.ts +33 -33
  206. package/packages/session-title/src/schedule-title-job.ts +53 -53
  207. package/packages/session-title/src/should-schedule.ts +30 -30
  208. package/packages/session-title/src/title-agent.ts +60 -60
  209. package/packages/session-title/src/truncate-input.ts +10 -10
  210. package/packages/session-title/src/types.ts +29 -29
  211. package/packages/session-title/tsconfig.json +13 -13
  212. package/packages/session-title/vitest.config.ts +8 -8
  213. package/packages/subagents/agents/planner.md +46 -43
  214. package/packages/subagents/agents/scout.md +40 -40
  215. package/packages/subagents/agents/title.md +13 -13
  216. package/packages/subagents/agents/worker.md +36 -36
  217. package/packages/subagents/extensions/index.ts +164 -164
  218. package/packages/subagents/package.json +1 -1
  219. package/packages/subagents/skills/subagents/SKILL.md +86 -80
  220. package/packages/subagents/src/agents.ts +166 -166
  221. package/packages/subagents/src/async/job-tracker.ts +71 -71
  222. package/packages/subagents/src/async/spawn-async.ts +220 -220
  223. package/packages/subagents/src/async/storage.ts +57 -57
  224. package/packages/subagents/src/env.ts +4 -4
  225. package/packages/subagents/src/execute-subagent.ts +285 -285
  226. package/packages/subagents/src/progress.ts +187 -187
  227. package/packages/subagents/src/render-animation.ts +40 -40
  228. package/packages/subagents/src/render.ts +428 -428
  229. package/packages/subagents/src/rpc/channels.ts +21 -21
  230. package/packages/subagents/src/rpc/client.ts +144 -144
  231. package/packages/subagents/src/rpc/server.ts +11 -11
  232. package/packages/subagents/src/run-agent.ts +313 -313
  233. package/packages/subagents/src/runtime.ts +102 -102
  234. package/packages/subagents/src/schema.ts +62 -62
  235. package/packages/subagents/src/spawn.ts +104 -104
  236. package/packages/subagents/src/types.ts +92 -92
  237. package/packages/subagents/src/utils.ts +100 -100
  238. package/packages/subagents/src/wait.ts +159 -159
  239. package/packages/subagents/tsconfig.json +13 -13
  240. package/packages/subagents/vitest.config.ts +8 -8
  241. package/packages/todoist/extensions/index.ts +163 -0
  242. package/packages/todoist/package.json +34 -0
  243. package/packages/todoist/src/events.ts +7 -0
  244. package/packages/todoist/src/silent-render.ts +8 -0
  245. package/packages/todoist/src/state.ts +147 -0
  246. package/packages/todoist/src/tools.ts +175 -0
  247. package/packages/todoist/src/types.ts +34 -0
  248. package/packages/todoist/src/widget.ts +109 -0
  249. package/packages/todoist/tsconfig.json +13 -0
  250. package/packages/todoist/vitest.config.ts +8 -0
  251. package/packages/ui-enhancements/package.json +1 -1
  252. package/packages/ui-enhancements/src/chrome/agent-mode-badge.ts +60 -60
  253. package/packages/ui-enhancements/src/chrome/border-status.ts +14 -1
  254. package/packages/ui-enhancements/src/chrome/resize-recovery.ts +63 -0
  255. package/packages/websearch/package.json +1 -1
  256. package/scripts/sync-pi-manifest.mjs +1 -1
  257. package/packages/permission-system/node_modules/web-tree-sitter/lib/tree-sitter.cjs +0 -2988
  258. package/packages/permission-system/node_modules/web-tree-sitter/lib/tree-sitter.wasm +0 -0
  259. package/packages/permission-system/node_modules/web-tree-sitter/lib/tree-sitter.wasm.map +0 -1
@@ -0,0 +1,147 @@
1
+ import type { TodoItem, TodoListFilter, TodoPriority, TodoSnapshot, TodoStatus, TodoStoreEvents } from "./types.ts";
2
+
3
+ let nextId = 1;
4
+
5
+ function generateId(): string {
6
+ return String(nextId++);
7
+ }
8
+
9
+ function now(): string {
10
+ return new Date().toISOString();
11
+ }
12
+
13
+ export class TodoStore {
14
+ private items = new Map<string, TodoItem>();
15
+ private events: TodoStoreEvents = {};
16
+
17
+ constructor(events?: TodoStoreEvents) {
18
+ this.events = events ?? {};
19
+ }
20
+
21
+ setOnChange(cb: (snapshot: TodoSnapshot) => void): void {
22
+ this.events.onChange = cb;
23
+ }
24
+
25
+ add(text: string, priority: TodoPriority = 3, project?: string): TodoItem {
26
+ const item: TodoItem = {
27
+ id: generateId(),
28
+ text,
29
+ status: "pending",
30
+ priority,
31
+ project,
32
+ createdAt: now(),
33
+ updatedAt: now(),
34
+ };
35
+ this.items.set(item.id, item);
36
+ this.emitChange();
37
+ return item;
38
+ }
39
+
40
+ list(filter?: TodoListFilter): TodoItem[] {
41
+ let result = Array.from(this.items.values());
42
+
43
+ if (filter?.status && filter.status !== "all") {
44
+ result = result.filter((t) => {
45
+ if (filter.status === "active") return t.status !== "done";
46
+ if (filter.status === "done") return t.status === "done";
47
+ return true;
48
+ });
49
+ }
50
+
51
+ if (filter?.project) {
52
+ result = result.filter((t) => t.project === filter.project);
53
+ }
54
+
55
+ return result.sort((a, b) => {
56
+ // Sort by: pending first, in_progress, done last; then by creation time
57
+ const statusOrder = { pending: 0, in_progress: 1, done: 2 };
58
+ const aOrder = statusOrder[a.status];
59
+ const bOrder = statusOrder[b.status];
60
+ if (aOrder !== bOrder) return aOrder - bOrder;
61
+ return a.createdAt.localeCompare(b.createdAt);
62
+ });
63
+ }
64
+
65
+ get(id: string): TodoItem | undefined {
66
+ return this.items.get(id);
67
+ }
68
+
69
+ update(
70
+ id: string,
71
+ changes: { text?: string; status?: TodoStatus; priority?: TodoPriority; project?: string },
72
+ ): TodoItem | null {
73
+ const item = this.items.get(id);
74
+ if (!item) return null;
75
+
76
+ if (changes.text !== undefined) item.text = changes.text;
77
+ if (changes.status !== undefined) item.status = changes.status;
78
+ if (changes.priority !== undefined) item.priority = changes.priority;
79
+ if (changes.project !== undefined) item.project = changes.project;
80
+ item.updatedAt = now();
81
+
82
+ // Claude Code: when every remaining task is done, clear the list for the next round
83
+ this.clearIfAllDone();
84
+ this.emitChange();
85
+ return item;
86
+ }
87
+
88
+ remove(id: string): boolean {
89
+ const result = this.items.delete(id);
90
+ if (result) this.emitChange();
91
+ return result;
92
+ }
93
+
94
+ clear(): void {
95
+ this.items.clear();
96
+ this.emitChange();
97
+ }
98
+
99
+ getSnapshot(): TodoSnapshot {
100
+ const todos = Array.from(this.items.values());
101
+ return {
102
+ todos,
103
+ totalCount: todos.length,
104
+ doneCount: todos.filter((t) => t.status === "done").length,
105
+ todoistConnected: false,
106
+ };
107
+ }
108
+
109
+ /**
110
+ * Rebuild state from a series of historical tool calls (conversation replay).
111
+ * Each entry should have: { toolName, input, result }
112
+ */
113
+ restoreFromHistory(
114
+ entries: Array<{ toolName: string; input: Record<string, unknown>; result?: Record<string, unknown> }>,
115
+ ): void {
116
+ // Clear and reset id counter
117
+ this.items.clear();
118
+ nextId = 1;
119
+
120
+ for (const entry of entries) {
121
+ const details = entry.result?.details as { todos?: TodoItem[] } | undefined;
122
+ if (!details?.todos) continue;
123
+ // Use the full snapshot from the last entry — replay forward
124
+ for (const todo of details.todos) {
125
+ const idNum = Number.parseInt(todo.id, 10);
126
+ if (idNum >= nextId) nextId = idNum + 1;
127
+ this.items.set(todo.id, { ...todo });
128
+ }
129
+ }
130
+ }
131
+
132
+ getTodoistConnected(): boolean {
133
+ return false;
134
+ }
135
+
136
+ /** After a mutation: if every remaining item is done, clear the list (Claude Code). */
137
+ private clearIfAllDone(): void {
138
+ if (this.items.size === 0) return;
139
+ if ([...this.items.values()].every((t) => t.status === "done")) {
140
+ this.items.clear();
141
+ }
142
+ }
143
+
144
+ private emitChange(): void {
145
+ this.events.onChange?.(this.getSnapshot());
146
+ }
147
+ }
@@ -0,0 +1,175 @@
1
+ import { StringEnum } from "@earendil-works/pi-ai";
2
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
+ import { Type } from "typebox";
4
+ import type { TodoStore } from "./state.ts";
5
+
6
+ /** Debug-visible result — short text in transcript; full snapshot in details for replay */
7
+ function silentResult(
8
+ snap: ReturnType<TodoStore["getSnapshot"]>,
9
+ summary: string,
10
+ ) {
11
+ return {
12
+ content: [{ type: "text" as const, text: summary }],
13
+ details: { todos: snap.todos, totalCount: snap.totalCount, doneCount: snap.doneCount },
14
+ };
15
+ }
16
+
17
+ /** Shared complete-vs-delete boundary (Claude Code style). */
18
+ const COMPLETE_VS_DELETE = [
19
+ "Complete finished work with todo_update status 'done' (shows ☑). Do not use todo_remove to mark work finished.",
20
+ "When every task in the list is done, the list clears automatically — then start the next multi-step request with fresh todo_add calls.",
21
+ "Use todo_remove only for cancelled, duplicate, mistaken, or no-longer-relevant tasks.",
22
+ ] as const;
23
+
24
+ export function registerTodoistTools(pi: ExtensionAPI, store: TodoStore): void {
25
+ pi.registerTool({
26
+ name: "todo_add",
27
+ label: "Add Todo",
28
+ description: "Create a new task in the in-memory todo list.",
29
+ promptSnippet: "Add a task to the todo list",
30
+ promptGuidelines: [
31
+ "Use todo_add when you need to create a new task to track",
32
+ "After a previous list was fully completed (auto-cleared), start the next multi-step request with fresh todo_add calls",
33
+ "Priority: 1 (urgent), 2 (high), 3 (normal), 4 (low)",
34
+ "Project is optional — use it to group related tasks",
35
+ ],
36
+ parameters: Type.Object({
37
+ text: Type.String({ description: "Task description" }),
38
+ priority: Type.Optional(
39
+ Type.Number({ description: "Priority 1-4 (1=urgent, 4=low)", default: 3 }),
40
+ ),
41
+ project: Type.Optional(
42
+ Type.String({ description: "Optional project/group name" }),
43
+ ),
44
+ }),
45
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
46
+ const input = params as { text: string; priority?: number; project?: string };
47
+ store.add(
48
+ input.text,
49
+ (input.priority ?? 3) as 1 | 2 | 3 | 4,
50
+ input.project,
51
+ );
52
+ const snap = store.getSnapshot();
53
+ return silentResult(snap, `added: ${input.text} (${snap.totalCount} total)`);
54
+ },
55
+ });
56
+
57
+ pi.registerTool({
58
+ name: "todo_list",
59
+ label: "List Todos",
60
+ description:
61
+ "List all tasks in the todo list, optionally filtered by status or project.",
62
+ promptSnippet: "List tasks from the todo list",
63
+ promptGuidelines: [
64
+ "Use todo_list to see current tasks",
65
+ "Filter by 'active' to see only unfinished tasks",
66
+ "Filter by 'done' to see completed tasks",
67
+ ],
68
+ parameters: Type.Object({
69
+ filter: Type.Optional(
70
+ StringEnum(["all", "active", "done"] as const, {
71
+ description: "Filter by status",
72
+ default: "all",
73
+ }),
74
+ ),
75
+ project: Type.Optional(
76
+ Type.String({ description: "Filter by project name" }),
77
+ ),
78
+ }),
79
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
80
+ const input = params as {
81
+ filter?: "all" | "active" | "done";
82
+ project?: string;
83
+ };
84
+ const listed = store.list({
85
+ status: input.filter ?? "all",
86
+ project: input.project,
87
+ });
88
+ const snap = store.getSnapshot();
89
+ return silentResult(
90
+ snap,
91
+ `listed ${listed.length} (filter=${input.filter ?? "all"})`,
92
+ );
93
+ },
94
+ });
95
+
96
+ pi.registerTool({
97
+ name: "todo_update",
98
+ label: "Update Todo",
99
+ description:
100
+ "Update a task's text, status, or priority. Set status to 'done' when work is finished (checked off). When all tasks are done, the list clears automatically.",
101
+ promptSnippet: "Update a task in the todo list",
102
+ promptGuidelines: [
103
+ "Use todo_update to mark tasks done, change priority, or edit text",
104
+ "When work is finished, set status to 'done' immediately — do not batch completions",
105
+ "Set status to 'in_progress' when starting work on a task (ideally one at a time)",
106
+ ...COMPLETE_VS_DELETE,
107
+ ],
108
+ parameters: Type.Object({
109
+ id: Type.String({ description: "Task ID to update" }),
110
+ text: Type.Optional(Type.String({ description: "New task text" })),
111
+ status: Type.Optional(
112
+ StringEnum(["pending", "in_progress", "done"] as const, {
113
+ description: "New status",
114
+ }),
115
+ ),
116
+ priority: Type.Optional(
117
+ Type.Number({ description: "New priority 1-4" }),
118
+ ),
119
+ project: Type.Optional(
120
+ Type.String({ description: "New project name" }),
121
+ ),
122
+ }),
123
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
124
+ const input = params as {
125
+ id: string;
126
+ text?: string;
127
+ status?: "pending" | "in_progress" | "done";
128
+ priority?: number;
129
+ project?: string;
130
+ };
131
+ const item = store.update(input.id, {
132
+ text: input.text,
133
+ status: input.status,
134
+ priority: input.priority as 1 | 2 | 3 | 4 | undefined,
135
+ project: input.project,
136
+ });
137
+ if (!item) {
138
+ return silentResult(
139
+ { todos: [], totalCount: 0, doneCount: 0, todoistConnected: false },
140
+ `update failed: id=${input.id} not found`,
141
+ );
142
+ }
143
+ const snap = store.getSnapshot();
144
+ const bits = [
145
+ input.status ? `status=${input.status}` : null,
146
+ input.text ? `text=${input.text}` : null,
147
+ ].filter(Boolean);
148
+ return silentResult(
149
+ snap,
150
+ `updated #${input.id}${bits.length ? ` (${bits.join(", ")})` : ""}`,
151
+ );
152
+ },
153
+ });
154
+
155
+ pi.registerTool({
156
+ name: "todo_remove",
157
+ label: "Remove Todo",
158
+ description:
159
+ "Permanently delete a task that is cancelled, duplicate, mistaken, or no longer relevant. Do not use this to mark finished work — use todo_update with status 'done' instead.",
160
+ promptSnippet: "Permanently delete a no-longer-relevant task",
161
+ promptGuidelines: [
162
+ ...COMPLETE_VS_DELETE,
163
+ "Only call todo_remove when the user cancels the task, the plan changed, the item was a duplicate/mistake, or they explicitly ask to delete it",
164
+ ],
165
+ parameters: Type.Object({
166
+ id: Type.String({ description: "Task ID to permanently delete" }),
167
+ }),
168
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
169
+ const input = params as { id: string };
170
+ store.remove(input.id);
171
+ const snap = store.getSnapshot();
172
+ return silentResult(snap, `removed #${input.id} (${snap.totalCount} left)`);
173
+ },
174
+ });
175
+ }
@@ -0,0 +1,34 @@
1
+ /** Priority: 1 = highest (p1), 4 = lowest (p4) */
2
+ export type TodoPriority = 1 | 2 | 3 | 4;
3
+
4
+ export type TodoStatus = "pending" | "in_progress" | "done";
5
+
6
+ export interface TodoItem {
7
+ id: string;
8
+ text: string;
9
+ status: TodoStatus;
10
+ priority: TodoPriority;
11
+ project?: string;
12
+ createdAt: string;
13
+ updatedAt: string;
14
+ }
15
+
16
+ /**
17
+ * Full snapshot carried in tool call details.
18
+ * Used for conversation replay to rebuild in-memory state.
19
+ */
20
+ export interface TodoSnapshot {
21
+ todos: TodoItem[];
22
+ totalCount: number;
23
+ doneCount: number;
24
+ todoistConnected: boolean;
25
+ }
26
+
27
+ export interface TodoListFilter {
28
+ status?: "all" | "active" | "done";
29
+ project?: string;
30
+ }
31
+
32
+ export interface TodoStoreEvents {
33
+ onChange?: (snapshot: TodoSnapshot) => void;
34
+ }
@@ -0,0 +1,109 @@
1
+ import type { TodoItem, TodoSnapshot } from "./types.ts";
2
+
3
+ export type WidgetTheme = {
4
+ fg: (color: string, text: string) => string;
5
+ bold: (text: string) => string;
6
+ };
7
+
8
+ const MAX_WIDGET_ROWS = 10;
9
+
10
+ function formatElapsed(updatedAt: string, nowMs = Date.now()): string {
11
+ const diff = nowMs - new Date(updatedAt).getTime();
12
+ const secs = Math.floor(diff / 1000);
13
+ if (secs < 60) return `${secs}s`;
14
+ const mins = Math.floor(secs / 60);
15
+ if (mins < 60) return `${mins}m`;
16
+ const hrs = Math.floor(mins / 60);
17
+ return `${hrs}h ${mins % 60}m`;
18
+ }
19
+
20
+ function sortForWidget(todos: TodoItem[]): TodoItem[] {
21
+ const statusOrder = { in_progress: 0, pending: 1, done: 2 } as const;
22
+ return [...todos].sort((a, b) => {
23
+ const orderDiff = statusOrder[a.status] - statusOrder[b.status];
24
+ if (orderDiff !== 0) return orderDiff;
25
+ return a.createdAt.localeCompare(b.createdAt);
26
+ });
27
+ }
28
+
29
+ function formatMeta(todo: TodoItem, theme: WidgetTheme, nowMs: number): string {
30
+ const parts: Array<string | null> = [];
31
+ if (todo.status === "in_progress") {
32
+ parts.push(formatElapsed(todo.updatedAt, nowMs));
33
+ }
34
+ if (todo.priority <= 2) parts.push(`p${todo.priority}`);
35
+ if (todo.project) parts.push(todo.project);
36
+ const meta = parts.filter(Boolean).join(" · ");
37
+ return meta ? ` ${theme.fg("dim", meta)}` : "";
38
+ }
39
+
40
+ function formatTodoLine(todo: TodoItem, theme: WidgetTheme, nowMs: number): string {
41
+ const metaStr = formatMeta(todo, theme, nowMs);
42
+ if (todo.status === "in_progress") {
43
+ return ` ${theme.fg("warning", "✳")} ${theme.fg("text", todo.text)}${metaStr}`;
44
+ }
45
+ if (todo.status === "done") {
46
+ return ` ${theme.fg("muted", "☑")} ${theme.fg("muted", todo.text)}${metaStr}`;
47
+ }
48
+ return ` ${theme.fg("muted", "◻")} ${theme.fg("text", todo.text)}${metaStr}`;
49
+ }
50
+
51
+ export type TodoWidgetView = {
52
+ lines: string[] | undefined;
53
+ status: string | undefined;
54
+ };
55
+
56
+ /**
57
+ * Build widget lines + status for the current snapshot.
58
+ * Done tasks stay visible with ☑; widget clears only when the list is empty.
59
+ */
60
+ export function buildTodoWidgetView(
61
+ snapshot: TodoSnapshot,
62
+ theme: WidgetTheme,
63
+ nowMs = Date.now(),
64
+ ): TodoWidgetView {
65
+ if (snapshot.todos.length === 0) {
66
+ return { lines: undefined, status: undefined };
67
+ }
68
+
69
+ const activeTodos = snapshot.todos.filter((t) => t.status !== "done");
70
+ const doneCount = snapshot.doneCount;
71
+ const sorted = sortForWidget(snapshot.todos);
72
+ const visible = sorted.slice(0, MAX_WIDGET_ROWS);
73
+ const hidden = sorted.length - visible.length;
74
+
75
+ const lines: string[] = [];
76
+ const headerIcon = activeTodos.some((t) => t.status === "in_progress")
77
+ ? "●"
78
+ : activeTodos.length > 0
79
+ ? "○"
80
+ : "☑";
81
+
82
+ if (activeTodos.length > 0) {
83
+ lines.push(
84
+ theme.fg(
85
+ "accent",
86
+ `${headerIcon} ${activeTodos.length} task${activeTodos.length > 1 ? "s" : ""}`,
87
+ ) + (doneCount > 0 ? theme.fg("muted", ` (${doneCount} done)`) : ""),
88
+ );
89
+ } else {
90
+ lines.push(
91
+ theme.fg("muted", `☑ ${doneCount} done`),
92
+ );
93
+ }
94
+
95
+ for (const todo of visible) {
96
+ lines.push(formatTodoLine(todo, theme, nowMs));
97
+ }
98
+
99
+ if (hidden > 0) {
100
+ lines.push(` ${theme.fg("dim", `⋯ and ${hidden} more`)}`);
101
+ }
102
+
103
+ const status =
104
+ activeTodos.length > 0
105
+ ? theme.fg("accent", `● ${activeTodos.length}`)
106
+ : theme.fg("muted", `☑ ${doneCount}`);
107
+
108
+ return { lines, status };
109
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "allowImportingTsExtensions": true
5
+ },
6
+ "include": [
7
+ "extensions/**/*.ts",
8
+ "src/**/*.ts",
9
+ "tests/**/*.ts",
10
+ "vitest.config.ts"
11
+ ],
12
+ "exclude": ["node_modules"]
13
+ }
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ environment: "node",
6
+ include: ["tests/**/*.test.ts"],
7
+ },
8
+ });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitos-pizza/ui-enhancements",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "TUI and UI enhancements for Vito's Pizzeria Pi distribution",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,60 +1,60 @@
1
- import type { ExtensionAPI, Theme } from "@earendil-works/pi-coding-agent";
2
-
3
- export const AGENT_MODE_CHANGED = "agent-mode:changed";
4
-
5
- export type AgentModeDisplayColor = "dim" | "accent" | "warning";
6
-
7
- export interface AgentModeChangedPayload {
8
- mode: "agent" | "plan" | "execute";
9
- label: string;
10
- color: AgentModeDisplayColor;
11
- }
12
-
13
- let currentAgentMode: AgentModeChangedPayload | null = null;
14
-
15
- function isPayload(value: unknown): value is AgentModeChangedPayload {
16
- if (!value || typeof value !== "object") return false;
17
- const payload = value as Partial<AgentModeChangedPayload>;
18
- return (
19
- (payload.mode === "agent" ||
20
- payload.mode === "plan" ||
21
- payload.mode === "execute") &&
22
- typeof payload.label === "string" &&
23
- (payload.color === "dim" ||
24
- payload.color === "accent" ||
25
- payload.color === "warning")
26
- );
27
- }
28
-
29
- export function getAgentModeBadge(): AgentModeChangedPayload | null {
30
- return currentAgentMode;
31
- }
32
-
33
- export function formatAgentModeSuffix(
34
- payload: AgentModeChangedPayload | null,
35
- theme: Theme,
36
- ): string {
37
- if (!payload || payload.mode === "agent") return "";
38
- return theme.fg(payload.color, ` · ${payload.label}`);
39
- }
40
-
41
- export function registerAgentModeBadge(
42
- pi: ExtensionAPI,
43
- requestRender: () => void,
44
- ): () => void {
45
- const unsubChanged = pi.events.on(AGENT_MODE_CHANGED, (value) => {
46
- if (!isPayload(value)) return;
47
- currentAgentMode = value;
48
- requestRender();
49
- });
50
-
51
- pi.on("session_shutdown", () => {
52
- currentAgentMode = null;
53
- });
54
-
55
- return unsubChanged;
56
- }
57
-
58
- export function resetAgentModeBadgeForTests(): void {
59
- currentAgentMode = null;
60
- }
1
+ import type { ExtensionAPI, Theme } from "@earendil-works/pi-coding-agent";
2
+
3
+ export const AGENT_MODE_CHANGED = "agent-mode:changed";
4
+
5
+ export type AgentModeDisplayColor = "dim" | "accent" | "warning";
6
+
7
+ export interface AgentModeChangedPayload {
8
+ mode: "agent" | "plan" | "execute";
9
+ label: string;
10
+ color: AgentModeDisplayColor;
11
+ }
12
+
13
+ let currentAgentMode: AgentModeChangedPayload | null = null;
14
+
15
+ function isPayload(value: unknown): value is AgentModeChangedPayload {
16
+ if (!value || typeof value !== "object") return false;
17
+ const payload = value as Partial<AgentModeChangedPayload>;
18
+ return (
19
+ (payload.mode === "agent" ||
20
+ payload.mode === "plan" ||
21
+ payload.mode === "execute") &&
22
+ typeof payload.label === "string" &&
23
+ (payload.color === "dim" ||
24
+ payload.color === "accent" ||
25
+ payload.color === "warning")
26
+ );
27
+ }
28
+
29
+ export function getAgentModeBadge(): AgentModeChangedPayload | null {
30
+ return currentAgentMode;
31
+ }
32
+
33
+ export function formatAgentModeSuffix(
34
+ payload: AgentModeChangedPayload | null,
35
+ theme: Theme,
36
+ ): string {
37
+ if (!payload || payload.mode === "agent") return "";
38
+ return theme.fg(payload.color, ` · ${payload.label}`);
39
+ }
40
+
41
+ export function registerAgentModeBadge(
42
+ pi: ExtensionAPI,
43
+ requestRender: () => void,
44
+ ): () => void {
45
+ const unsubChanged = pi.events.on(AGENT_MODE_CHANGED, (value) => {
46
+ if (!isPayload(value)) return;
47
+ currentAgentMode = value;
48
+ requestRender();
49
+ });
50
+
51
+ pi.on("session_shutdown", () => {
52
+ currentAgentMode = null;
53
+ });
54
+
55
+ return unsubChanged;
56
+ }
57
+
58
+ export function resetAgentModeBadgeForTests(): void {
59
+ currentAgentMode = null;
60
+ }
@@ -8,12 +8,13 @@ import type { Component, EditorTheme, TUI } from "@earendil-works/pi-tui";
8
8
  import type { UiEnhancementsSettings } from "../config.ts";
9
9
  import { loadUiEnhancementsSettings } from "../config.ts";
10
10
  import { shortenPath } from "../utils/shorten-path.ts";
11
- import { fitBorder } from "./format-border.ts";
12
11
  import {
13
12
  formatAgentModeSuffix,
14
13
  getAgentModeBadge,
15
14
  registerAgentModeBadge,
16
15
  } from "./agent-mode-badge.ts";
16
+ import { fitBorder } from "./format-border.ts";
17
+ import { attachResizeRecovery } from "./resize-recovery.ts";
17
18
 
18
19
  type GetSettings = () => UiEnhancementsSettings;
19
20
 
@@ -46,6 +47,7 @@ export function registerBorderStatusBar(
46
47
  let spinnerIndex = 0;
47
48
  let spinnerTimer: ReturnType<typeof setInterval> | undefined;
48
49
  let activeTui: TUI | undefined;
50
+ let detachResizeRecovery: (() => void) | undefined;
49
51
  const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
50
52
 
51
53
  const stopSpinner = () => {
@@ -55,6 +57,11 @@ export function registerBorderStatusBar(
55
57
  }
56
58
  };
57
59
 
60
+ const stopResizeRecovery = () => {
61
+ detachResizeRecovery?.();
62
+ detachResizeRecovery = undefined;
63
+ };
64
+
58
65
  pi.on("agent_start", () => {
59
66
  isWorking = true;
60
67
  stopSpinner();
@@ -73,6 +80,7 @@ export function registerBorderStatusBar(
73
80
 
74
81
  pi.on("session_shutdown", () => {
75
82
  stopSpinner();
83
+ stopResizeRecovery();
76
84
  activeTui = undefined;
77
85
  });
78
86
 
@@ -84,6 +92,11 @@ export function registerBorderStatusBar(
84
92
  const settings = loadUiEnhancementsSettings(ctx.cwd);
85
93
  if (!settings.borderStatusBar || !ctx.hasUI || ctx.mode !== "tui") return;
86
94
 
95
+ stopResizeRecovery();
96
+ detachResizeRecovery = attachResizeRecovery({
97
+ getTarget: () => activeTui,
98
+ });
99
+
87
100
  ctx.ui.setWorkingVisible(false);
88
101
  ctx.ui.setFooter(() => new EmptyFooter());
89
102