@diegopetrucci/pi-extensions 0.1.74 → 0.1.76

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 (93) hide show
  1. package/LICENSE +2 -0
  2. package/README.md +9 -4
  3. package/extensions/agent-workflow-audit/index.ts +1398 -1009
  4. package/extensions/agent-workflow-audit/package.json +1 -1
  5. package/extensions/annotate-git-diff/README.md +3 -1
  6. package/extensions/annotate-git-diff/clipboard.ts +109 -100
  7. package/extensions/annotate-git-diff/git.ts +853 -767
  8. package/extensions/annotate-git-diff/glimpseui.d.ts +87 -78
  9. package/extensions/annotate-git-diff/index.ts +712 -671
  10. package/extensions/annotate-git-diff/package.json +1 -1
  11. package/extensions/annotate-git-diff/prompt.ts +45 -45
  12. package/extensions/annotate-git-diff/quiet-glimpse.ts +161 -155
  13. package/extensions/annotate-git-diff/review-server.ts +337 -311
  14. package/extensions/annotate-git-diff/types.ts +132 -132
  15. package/extensions/annotate-git-diff/ui.ts +27 -27
  16. package/extensions/annotate-git-diff/watch.ts +74 -74
  17. package/extensions/annotate-git-diff/web/app.js +1947 -1884
  18. package/extensions/annotate-git-diff/web/index.html +5 -6
  19. package/extensions/annotate-last-message/README.md +1 -1
  20. package/extensions/annotate-last-message/glimpseui.d.ts +87 -78
  21. package/extensions/annotate-last-message/index.ts +210 -206
  22. package/extensions/annotate-last-message/package.json +1 -1
  23. package/extensions/annotate-last-message/prompt.ts +87 -66
  24. package/extensions/annotate-last-message/quiet-glimpse.ts +161 -155
  25. package/extensions/annotate-last-message/session.ts +94 -89
  26. package/extensions/annotate-last-message/theme.ts +199 -193
  27. package/extensions/annotate-last-message/types.ts +23 -23
  28. package/extensions/annotate-last-message/ui.ts +27 -25
  29. package/extensions/annotate-last-message/web/app.js +4 -7
  30. package/extensions/annotate-last-message/web/md-renderer.js +1 -1
  31. package/extensions/brrr/README.md +1 -1
  32. package/extensions/brrr/index.ts +213 -191
  33. package/extensions/brrr/package.json +1 -1
  34. package/extensions/claude-fast/README.md +5 -79
  35. package/extensions/claude-fast/index.ts +31 -289
  36. package/extensions/claude-fast/package.json +2 -3
  37. package/extensions/code-reviewer/README.md +1 -1
  38. package/extensions/code-reviewer/index.ts +1598 -1350
  39. package/extensions/code-reviewer/package.json +1 -1
  40. package/extensions/confirm-destructive/README.md +1 -1
  41. package/extensions/confirm-destructive/index.ts +55 -51
  42. package/extensions/confirm-destructive/package.json +1 -1
  43. package/extensions/context-cap/index.ts +312 -304
  44. package/extensions/context-cap/package.json +1 -1
  45. package/extensions/context-inspector/index.ts +1211 -1064
  46. package/extensions/context-inspector/package.json +1 -1
  47. package/extensions/contrarian/index.ts +1854 -1735
  48. package/extensions/contrarian/package.json +1 -1
  49. package/extensions/dirty-repo-guard/index.ts +41 -41
  50. package/extensions/dynamic-context-pruning/README.md +6 -38
  51. package/extensions/dynamic-context-pruning/index.ts +2124 -1980
  52. package/extensions/dynamic-context-pruning/package.json +1 -1
  53. package/extensions/fast/README.md +50 -12
  54. package/extensions/fast/index.ts +513 -317
  55. package/extensions/fast/package.json +1 -1
  56. package/extensions/git-footer/README.md +1 -1
  57. package/extensions/git-footer/index.ts +458 -411
  58. package/extensions/git-footer/package.json +2 -3
  59. package/extensions/gnosis/index.ts +180 -130
  60. package/extensions/gnosis/package.json +1 -1
  61. package/extensions/inline-bash/index.ts +88 -88
  62. package/extensions/inline-bash/package.json +1 -1
  63. package/extensions/librarian/index.ts +1669 -1419
  64. package/extensions/librarian/package.json +1 -1
  65. package/extensions/minimal-footer/index.ts +994 -1012
  66. package/extensions/minimal-footer/openai-usage.ts +195 -190
  67. package/extensions/notify/index.ts +222 -230
  68. package/extensions/notify/package.json +1 -1
  69. package/extensions/openai-fast/README.md +5 -73
  70. package/extensions/openai-fast/index.ts +32 -272
  71. package/extensions/openai-fast/package.json +2 -3
  72. package/extensions/oracle/index.ts +1731 -1625
  73. package/extensions/oracle/package.json +1 -1
  74. package/extensions/permission-gate/index.ts +658 -608
  75. package/extensions/permission-gate/package.json +1 -1
  76. package/extensions/permission-gate/powershell-safety.ts +840 -793
  77. package/extensions/project-mcp-json/.pi-fleet-tested-version +1 -0
  78. package/extensions/project-mcp-json/README.md +199 -0
  79. package/extensions/project-mcp-json/config.ts +564 -0
  80. package/extensions/project-mcp-json/index.ts +119 -0
  81. package/extensions/project-mcp-json/package.json +38 -0
  82. package/extensions/quiet-tools/index.ts +282 -264
  83. package/extensions/review/README.md +2 -2
  84. package/extensions/review/index.ts +1745 -1872
  85. package/extensions/review/package.json +1 -1
  86. package/extensions/todo/index.ts +284 -265
  87. package/extensions/triage-comments/README.md +2 -2
  88. package/extensions/triage-comments/index.ts +2294 -1882
  89. package/extensions/triage-comments/package.json +1 -1
  90. package/package.json +6 -3
  91. package/assets/social-preview.png +0 -0
  92. package/extensions/claude-fast/claude-fast.example.json +0 -4
  93. package/extensions/openai-fast/openai-fast.example.json +0 -4
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-review",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "description": "A standalone pi extension that adds /review and /end-review commands adapted from mitsuhiko/agent-stuff.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -16,286 +16,305 @@ import { matchesKey, Text, truncateToWidth } from "@earendil-works/pi-tui";
16
16
  import { Type } from "typebox";
17
17
 
18
18
  interface Todo {
19
- id: number;
20
- text: string;
21
- done: boolean;
19
+ id: number;
20
+ text: string;
21
+ done: boolean;
22
22
  }
23
23
 
24
24
  interface TodoDetails {
25
- action: "list" | "add" | "toggle" | "clear";
26
- todos: Todo[];
27
- nextId: number;
28
- error?: string;
25
+ action: "list" | "add" | "toggle" | "clear";
26
+ todos: Todo[];
27
+ nextId: number;
28
+ error?: string;
29
29
  }
30
30
 
31
31
  const cloneTodos = (items: Todo[]): Todo[] => items.map((todo) => ({ ...todo }));
32
32
 
33
33
  const TodoParams = Type.Object({
34
- action: StringEnum(["list", "add", "toggle", "clear"] as const),
35
- text: Type.Optional(Type.String({ description: "Todo text (for add)" })),
36
- id: Type.Optional(Type.Number({ description: "Todo ID (for toggle)" })),
34
+ action: StringEnum(["list", "add", "toggle", "clear"] as const),
35
+ text: Type.Optional(Type.String({ description: "Todo text (for add)" })),
36
+ id: Type.Optional(Type.Number({ description: "Todo ID (for toggle)" })),
37
37
  });
38
38
 
39
39
  /**
40
40
  * UI component for the /todos command
41
41
  */
42
42
  class TodoListComponent {
43
- private todos: Todo[];
44
- private theme: Theme;
45
- private onClose: () => void;
46
- private cachedWidth?: number;
47
- private cachedLines?: string[];
48
-
49
- constructor(todos: Todo[], theme: Theme, onClose: () => void) {
50
- this.todos = todos;
51
- this.theme = theme;
52
- this.onClose = onClose;
53
- }
54
-
55
- handleInput(data: string): void {
56
- if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
57
- this.onClose();
58
- }
59
- }
60
-
61
- render(width: number): string[] {
62
- if (this.cachedLines && this.cachedWidth === width) {
63
- return this.cachedLines;
64
- }
65
-
66
- const lines: string[] = [];
67
- const th = this.theme;
68
-
69
- lines.push("");
70
- const title = th.fg("accent", " Todos ");
71
- const headerLine =
72
- th.fg("borderMuted", "─".repeat(3)) + title + th.fg("borderMuted", "─".repeat(Math.max(0, width - 10)));
73
- lines.push(truncateToWidth(headerLine, width));
74
- lines.push("");
75
-
76
- if (this.todos.length === 0) {
77
- lines.push(truncateToWidth(` ${th.fg("dim", "No todos yet. Ask the agent to add some!")}`, width));
78
- } else {
79
- const done = this.todos.filter((t) => t.done).length;
80
- const total = this.todos.length;
81
- lines.push(truncateToWidth(` ${th.fg("muted", `${done}/${total} completed`)}`, width));
82
- lines.push("");
83
-
84
- for (const todo of this.todos) {
85
- const check = todo.done ? th.fg("success", "✓") : th.fg("dim", "○");
86
- const id = th.fg("accent", `#${todo.id}`);
87
- const text = todo.done ? th.fg("dim", todo.text) : th.fg("text", todo.text);
88
- lines.push(truncateToWidth(` ${check} ${id} ${text}`, width));
89
- }
90
- }
91
-
92
- lines.push("");
93
- lines.push(truncateToWidth(` ${th.fg("dim", "Press Escape to close")}`, width));
94
- lines.push("");
95
-
96
- this.cachedWidth = width;
97
- this.cachedLines = lines;
98
- return lines;
99
- }
100
-
101
- invalidate(): void {
102
- this.cachedWidth = undefined;
103
- this.cachedLines = undefined;
104
- }
43
+ private todos: Todo[];
44
+ private theme: Theme;
45
+ private onClose: () => void;
46
+ private cachedWidth?: number;
47
+ private cachedLines?: string[];
48
+
49
+ constructor(todos: Todo[], theme: Theme, onClose: () => void) {
50
+ this.todos = todos;
51
+ this.theme = theme;
52
+ this.onClose = onClose;
53
+ }
54
+
55
+ handleInput(data: string): void {
56
+ if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
57
+ this.onClose();
58
+ }
59
+ }
60
+
61
+ render(width: number): string[] {
62
+ if (this.cachedLines && this.cachedWidth === width) {
63
+ return this.cachedLines;
64
+ }
65
+
66
+ const lines: string[] = [];
67
+ const th = this.theme;
68
+
69
+ lines.push("");
70
+ const title = th.fg("accent", " Todos ");
71
+ const headerLine =
72
+ th.fg("borderMuted", "─".repeat(3)) +
73
+ title +
74
+ th.fg("borderMuted", "─".repeat(Math.max(0, width - 10)));
75
+ lines.push(truncateToWidth(headerLine, width));
76
+ lines.push("");
77
+
78
+ if (this.todos.length === 0) {
79
+ lines.push(
80
+ truncateToWidth(` ${th.fg("dim", "No todos yet. Ask the agent to add some!")}`, width),
81
+ );
82
+ } else {
83
+ const done = this.todos.filter((t) => t.done).length;
84
+ const total = this.todos.length;
85
+ lines.push(truncateToWidth(` ${th.fg("muted", `${done}/${total} completed`)}`, width));
86
+ lines.push("");
87
+
88
+ for (const todo of this.todos) {
89
+ const check = todo.done ? th.fg("success", "✓") : th.fg("dim", "○");
90
+ const id = th.fg("accent", `#${todo.id}`);
91
+ const text = todo.done ? th.fg("dim", todo.text) : th.fg("text", todo.text);
92
+ lines.push(truncateToWidth(` ${check} ${id} ${text}`, width));
93
+ }
94
+ }
95
+
96
+ lines.push("");
97
+ lines.push(truncateToWidth(` ${th.fg("dim", "Press Escape to close")}`, width));
98
+ lines.push("");
99
+
100
+ this.cachedWidth = width;
101
+ this.cachedLines = lines;
102
+ return lines;
103
+ }
104
+
105
+ invalidate(): void {
106
+ this.cachedWidth = undefined;
107
+ this.cachedLines = undefined;
108
+ }
105
109
  }
106
110
 
107
111
  export default function (pi: ExtensionAPI) {
108
- // In-memory state (reconstructed from session on load)
109
- let todos: Todo[] = [];
110
- let nextId = 1;
111
-
112
- /**
113
- * Reconstruct state from session entries.
114
- * Scans tool results for this tool and applies them in order.
115
- */
116
- const reconstructState = (ctx: ExtensionContext) => {
117
- todos = [];
118
- nextId = 1;
119
-
120
- for (const entry of ctx.sessionManager.getBranch()) {
121
- if (entry.type !== "message") continue;
122
- const msg = entry.message;
123
- if (msg.role !== "toolResult" || msg.toolName !== "todo") continue;
124
-
125
- const details = msg.details as TodoDetails | undefined;
126
- if (details) {
127
- todos = cloneTodos(details.todos);
128
- nextId = details.nextId;
129
- }
130
- }
131
- };
132
-
133
- // Reconstruct state on session events
134
- pi.on("session_start", async (_event, ctx) => reconstructState(ctx));
135
- pi.on("session_tree", async (_event, ctx) => reconstructState(ctx));
136
-
137
- // Register the todo tool for the LLM
138
- pi.registerTool({
139
- name: "todo",
140
- label: "Todo",
141
- description: "Manage a todo list. Actions: list, add (text), toggle (id), clear",
142
- parameters: TodoParams,
143
-
144
- async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
145
- switch (params.action) {
146
- case "list":
147
- return {
148
- content: [
149
- {
150
- type: "text",
151
- text: todos.length
152
- ? todos.map((t) => `[${t.done ? "x" : " "}] #${t.id}: ${t.text}`).join("\n")
153
- : "No todos",
154
- },
155
- ],
156
- details: { action: "list", todos: cloneTodos(todos), nextId } as TodoDetails,
157
- };
158
-
159
- case "add": {
160
- if (!params.text) {
161
- return {
162
- content: [{ type: "text", text: "Error: text required for add" }],
163
- details: { action: "add", todos: cloneTodos(todos), nextId, error: "text required" } as TodoDetails,
164
- };
165
- }
166
- const newTodo: Todo = { id: nextId++, text: params.text, done: false };
167
- todos = [...todos, newTodo];
168
- return {
169
- content: [{ type: "text", text: `Added todo #${newTodo.id}: ${newTodo.text}` }],
170
- details: { action: "add", todos: cloneTodos(todos), nextId } as TodoDetails,
171
- };
172
- }
173
-
174
- case "toggle": {
175
- if (params.id === undefined) {
176
- return {
177
- content: [{ type: "text", text: "Error: id required for toggle" }],
178
- details: { action: "toggle", todos: cloneTodos(todos), nextId, error: "id required" } as TodoDetails,
179
- };
180
- }
181
- const targetId = params.id;
182
- const todo = todos.find((t) => t.id === targetId);
183
- if (!todo) {
184
- return {
185
- content: [{ type: "text", text: `Todo #${targetId} not found` }],
186
- details: {
187
- action: "toggle",
188
- todos: cloneTodos(todos),
189
- nextId,
190
- error: `#${targetId} not found`,
191
- } as TodoDetails,
192
- };
193
- }
194
- todos = todos.map((t) => (t.id === targetId ? { ...t, done: !t.done } : t));
195
- const updated = todos.find((t) => t.id === targetId)!;
196
- return {
197
- content: [{ type: "text", text: `Todo #${updated.id} ${updated.done ? "completed" : "uncompleted"}` }],
198
- details: { action: "toggle", todos: cloneTodos(todos), nextId } as TodoDetails,
199
- };
200
- }
201
-
202
- case "clear": {
203
- const count = todos.length;
204
- todos = [];
205
- nextId = 1;
206
- return {
207
- content: [{ type: "text", text: `Cleared ${count} todos` }],
208
- details: { action: "clear", todos: [], nextId: 1 } as TodoDetails,
209
- };
210
- }
211
-
212
- default:
213
- return {
214
- content: [{ type: "text", text: `Unknown action: ${params.action}` }],
215
- details: {
216
- action: "list",
217
- todos: cloneTodos(todos),
218
- nextId,
219
- error: `unknown action: ${params.action}`,
220
- } as TodoDetails,
221
- };
222
- }
223
- },
224
-
225
- renderCall(args, theme, _context) {
226
- let text = theme.fg("toolTitle", theme.bold("todo ")) + theme.fg("muted", args.action);
227
- if (args.text) text += ` ${theme.fg("dim", `"${args.text}"`)}`;
228
- if (args.id !== undefined) text += ` ${theme.fg("accent", `#${args.id}`)}`;
229
- return new Text(text, 0, 0);
230
- },
231
-
232
- renderResult(result, { expanded }, theme, _context) {
233
- const details = result.details as TodoDetails | undefined;
234
- if (!details) {
235
- const text = result.content[0];
236
- return new Text(text?.type === "text" ? text.text : "", 0, 0);
237
- }
238
-
239
- if (details.error) {
240
- return new Text(theme.fg("error", `Error: ${details.error}`), 0, 0);
241
- }
242
-
243
- const todoList = details.todos;
244
-
245
- switch (details.action) {
246
- case "list": {
247
- if (todoList.length === 0) {
248
- return new Text(theme.fg("dim", "No todos"), 0, 0);
249
- }
250
- let listText = theme.fg("muted", `${todoList.length} todo(s):`);
251
- const display = expanded ? todoList : todoList.slice(0, 5);
252
- for (const t of display) {
253
- const check = t.done ? theme.fg("success", "✓") : theme.fg("dim", "○");
254
- const itemText = t.done ? theme.fg("dim", t.text) : theme.fg("muted", t.text);
255
- listText += `\n${check} ${theme.fg("accent", `#${t.id}`)} ${itemText}`;
256
- }
257
- if (!expanded && todoList.length > 5) {
258
- listText += `\n${theme.fg("dim", `... ${todoList.length - 5} more`)}`;
259
- }
260
- return new Text(listText, 0, 0);
261
- }
262
-
263
- case "add": {
264
- const added = todoList[todoList.length - 1];
265
- return new Text(
266
- theme.fg("success", "✓ Added ") +
267
- theme.fg("accent", `#${added.id}`) +
268
- " " +
269
- theme.fg("muted", added.text),
270
- 0,
271
- 0,
272
- );
273
- }
274
-
275
- case "toggle": {
276
- const text = result.content[0];
277
- const msg = text?.type === "text" ? text.text : "";
278
- return new Text(theme.fg("success", "✓ ") + theme.fg("muted", msg), 0, 0);
279
- }
280
-
281
- case "clear":
282
- return new Text(theme.fg("success", "✓ ") + theme.fg("muted", "Cleared all todos"), 0, 0);
283
- }
284
- },
285
- });
286
-
287
- // Register the /todos command for users
288
- pi.registerCommand("todos", {
289
- description: "Show all todos on the current branch",
290
- handler: async (_args, ctx) => {
291
- if (ctx.mode !== "tui") {
292
- ctx.ui.notify("/todos requires interactive mode", "error");
293
- return;
294
- }
295
-
296
- await ctx.ui.custom<void>((_tui, theme, _kb, done) => {
297
- return new TodoListComponent(todos, theme, () => done());
298
- });
299
- },
300
- });
112
+ // In-memory state (reconstructed from session on load)
113
+ let todos: Todo[] = [];
114
+ let nextId = 1;
115
+
116
+ /**
117
+ * Reconstruct state from session entries.
118
+ * Scans tool results for this tool and applies them in order.
119
+ */
120
+ const reconstructState = (ctx: ExtensionContext) => {
121
+ todos = [];
122
+ nextId = 1;
123
+
124
+ for (const entry of ctx.sessionManager.getBranch()) {
125
+ if (entry.type !== "message") continue;
126
+ const msg = entry.message;
127
+ if (msg.role !== "toolResult" || msg.toolName !== "todo") continue;
128
+
129
+ const details = msg.details as TodoDetails | undefined;
130
+ if (details) {
131
+ todos = cloneTodos(details.todos);
132
+ nextId = details.nextId;
133
+ }
134
+ }
135
+ };
136
+
137
+ // Reconstruct state on session events
138
+ pi.on("session_start", async (_event, ctx) => reconstructState(ctx));
139
+ pi.on("session_tree", async (_event, ctx) => reconstructState(ctx));
140
+
141
+ // Register the todo tool for the LLM
142
+ pi.registerTool({
143
+ name: "todo",
144
+ label: "Todo",
145
+ description: "Manage a todo list. Actions: list, add (text), toggle (id), clear",
146
+ parameters: TodoParams,
147
+
148
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
149
+ switch (params.action) {
150
+ case "list":
151
+ return {
152
+ content: [
153
+ {
154
+ type: "text",
155
+ text: todos.length
156
+ ? todos.map((t) => `[${t.done ? "x" : " "}] #${t.id}: ${t.text}`).join("\n")
157
+ : "No todos",
158
+ },
159
+ ],
160
+ details: { action: "list", todos: cloneTodos(todos), nextId } as TodoDetails,
161
+ };
162
+
163
+ case "add": {
164
+ if (!params.text) {
165
+ return {
166
+ content: [{ type: "text", text: "Error: text required for add" }],
167
+ details: {
168
+ action: "add",
169
+ todos: cloneTodos(todos),
170
+ nextId,
171
+ error: "text required",
172
+ } as TodoDetails,
173
+ };
174
+ }
175
+ const newTodo: Todo = { id: nextId++, text: params.text, done: false };
176
+ todos = [...todos, newTodo];
177
+ return {
178
+ content: [{ type: "text", text: `Added todo #${newTodo.id}: ${newTodo.text}` }],
179
+ details: { action: "add", todos: cloneTodos(todos), nextId } as TodoDetails,
180
+ };
181
+ }
182
+
183
+ case "toggle": {
184
+ if (params.id === undefined) {
185
+ return {
186
+ content: [{ type: "text", text: "Error: id required for toggle" }],
187
+ details: {
188
+ action: "toggle",
189
+ todos: cloneTodos(todos),
190
+ nextId,
191
+ error: "id required",
192
+ } as TodoDetails,
193
+ };
194
+ }
195
+ const targetId = params.id;
196
+ const todo = todos.find((t) => t.id === targetId);
197
+ if (!todo) {
198
+ return {
199
+ content: [{ type: "text", text: `Todo #${targetId} not found` }],
200
+ details: {
201
+ action: "toggle",
202
+ todos: cloneTodos(todos),
203
+ nextId,
204
+ error: `#${targetId} not found`,
205
+ } as TodoDetails,
206
+ };
207
+ }
208
+ todos = todos.map((t) => (t.id === targetId ? { ...t, done: !t.done } : t));
209
+ const updated = todos.find((t) => t.id === targetId)!;
210
+ return {
211
+ content: [
212
+ {
213
+ type: "text",
214
+ text: `Todo #${updated.id} ${updated.done ? "completed" : "uncompleted"}`,
215
+ },
216
+ ],
217
+ details: { action: "toggle", todos: cloneTodos(todos), nextId } as TodoDetails,
218
+ };
219
+ }
220
+
221
+ case "clear": {
222
+ const count = todos.length;
223
+ todos = [];
224
+ nextId = 1;
225
+ return {
226
+ content: [{ type: "text", text: `Cleared ${count} todos` }],
227
+ details: { action: "clear", todos: [], nextId: 1 } as TodoDetails,
228
+ };
229
+ }
230
+
231
+ default:
232
+ return {
233
+ content: [{ type: "text", text: `Unknown action: ${params.action}` }],
234
+ details: {
235
+ action: "list",
236
+ todos: cloneTodos(todos),
237
+ nextId,
238
+ error: `unknown action: ${params.action}`,
239
+ } as TodoDetails,
240
+ };
241
+ }
242
+ },
243
+
244
+ renderCall(args, theme, _context) {
245
+ let text = theme.fg("toolTitle", theme.bold("todo ")) + theme.fg("muted", args.action);
246
+ if (args.text) text += ` ${theme.fg("dim", `"${args.text}"`)}`;
247
+ if (args.id !== undefined) text += ` ${theme.fg("accent", `#${args.id}`)}`;
248
+ return new Text(text, 0, 0);
249
+ },
250
+
251
+ renderResult(result, { expanded }, theme, _context) {
252
+ const details = result.details as TodoDetails | undefined;
253
+ if (!details) {
254
+ const text = result.content[0];
255
+ return new Text(text?.type === "text" ? text.text : "", 0, 0);
256
+ }
257
+
258
+ if (details.error) {
259
+ return new Text(theme.fg("error", `Error: ${details.error}`), 0, 0);
260
+ }
261
+
262
+ const todoList = details.todos;
263
+
264
+ switch (details.action) {
265
+ case "list": {
266
+ if (todoList.length === 0) {
267
+ return new Text(theme.fg("dim", "No todos"), 0, 0);
268
+ }
269
+ let listText = theme.fg("muted", `${todoList.length} todo(s):`);
270
+ const display = expanded ? todoList : todoList.slice(0, 5);
271
+ for (const t of display) {
272
+ const check = t.done ? theme.fg("success", "✓") : theme.fg("dim", "○");
273
+ const itemText = t.done ? theme.fg("dim", t.text) : theme.fg("muted", t.text);
274
+ listText += `\n${check} ${theme.fg("accent", `#${t.id}`)} ${itemText}`;
275
+ }
276
+ if (!expanded && todoList.length > 5) {
277
+ listText += `\n${theme.fg("dim", `... ${todoList.length - 5} more`)}`;
278
+ }
279
+ return new Text(listText, 0, 0);
280
+ }
281
+
282
+ case "add": {
283
+ const added = todoList[todoList.length - 1];
284
+ return new Text(
285
+ theme.fg("success", "✓ Added ") +
286
+ theme.fg("accent", `#${added.id}`) +
287
+ " " +
288
+ theme.fg("muted", added.text),
289
+ 0,
290
+ 0,
291
+ );
292
+ }
293
+
294
+ case "toggle": {
295
+ const text = result.content[0];
296
+ const msg = text?.type === "text" ? text.text : "";
297
+ return new Text(theme.fg("success", "✓ ") + theme.fg("muted", msg), 0, 0);
298
+ }
299
+
300
+ case "clear":
301
+ return new Text(theme.fg("success", "✓ ") + theme.fg("muted", "Cleared all todos"), 0, 0);
302
+ }
303
+ },
304
+ });
305
+
306
+ // Register the /todos command for users
307
+ pi.registerCommand("todos", {
308
+ description: "Show all todos on the current branch",
309
+ handler: async (_args, ctx) => {
310
+ if (ctx.mode !== "tui") {
311
+ ctx.ui.notify("/todos requires interactive mode", "error");
312
+ return;
313
+ }
314
+
315
+ await ctx.ui.custom<void>((_tui, theme, _kb, done) => {
316
+ return new TodoListComponent(todos, theme, () => done());
317
+ });
318
+ },
319
+ });
301
320
  }
@@ -2,7 +2,7 @@
2
2
 
3
3
  Adds `/triage-comments` plus a read-only `triage_comments` subagent tool for evidence-based review-comment triage.
4
4
 
5
- Use it when you want The Last Harness to inspect selected PR feedback, classify whether each comment is valid, cite local evidence, draft review-thread responses, and propose handling options. It does **not** implement changes; after triage, the main agent should ask which handling option to take before editing anything.
5
+ Use it when you want `/triage-comments` to inspect selected PR feedback, classify whether each comment is valid, cite local evidence, draft review-thread responses, and propose handling options. It does **not** implement changes; after triage, the main agent should ask which handling option to take before editing anything.
6
6
 
7
7
  ## Install
8
8
 
@@ -43,7 +43,7 @@ The extension registers `/triage-comments` as an interactive intake flow.
43
43
  /triage-comments 123
44
44
  ```
45
45
 
46
- - With no arguments, The Last Harness asks whether to paste feedback or fetch PR comments.
46
+ - With no arguments, `/triage-comments` asks whether to paste feedback or fetch PR comments.
47
47
  - `paste` opens an editor for multiline reviewer feedback, then sends one selected feedback item to the main agent.
48
48
  - `pr` with no explicit target first tries to detect an existing PR for the current named non-`main` git branch using read-only `git` and `gh pr view` calls. If the branch is `main`, detached, outside a git repository, `gh` is unavailable or unauthenticated, or no PR is found, it falls back to the PR URL/number prompt.
49
49
  - `pr <PR URL or number>` and a bare PR URL/number fetch that explicit PR directly, display PR review comments, PR issue comments, and review bodies with `gh` as numbered items with stable IDs, and ask whether to investigate all displayed comments or an explicit subset such as `1,3-5`.