@oh-my-pi/pi-coding-agent 14.3.0 → 14.4.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.
Files changed (120) hide show
  1. package/CHANGELOG.md +98 -1
  2. package/package.json +7 -7
  3. package/src/autoresearch/prompt.md +1 -1
  4. package/src/commit/agentic/prompts/analyze-file.md +1 -1
  5. package/src/config/model-registry.ts +67 -15
  6. package/src/config/prompt-templates.ts +5 -5
  7. package/src/config/settings-schema.ts +4 -4
  8. package/src/cursor.ts +3 -8
  9. package/src/discovery/helpers.ts +3 -3
  10. package/src/edit/diff.ts +50 -47
  11. package/src/edit/index.ts +86 -57
  12. package/src/edit/line-hash.ts +743 -24
  13. package/src/edit/modes/apply-patch.ts +0 -9
  14. package/src/edit/modes/atom.ts +893 -0
  15. package/src/edit/modes/chunk.ts +14 -24
  16. package/src/edit/modes/hashline.ts +193 -146
  17. package/src/edit/modes/patch.ts +5 -9
  18. package/src/edit/modes/replace.ts +6 -11
  19. package/src/edit/renderer.ts +14 -10
  20. package/src/edit/streaming.ts +50 -16
  21. package/src/exec/bash-executor.ts +2 -4
  22. package/src/export/html/template.generated.ts +1 -1
  23. package/src/export/html/template.js +4 -12
  24. package/src/extensibility/custom-tools/types.ts +2 -0
  25. package/src/extensibility/custom-tools/wrapper.ts +2 -1
  26. package/src/internal-urls/docs-index.generated.ts +2 -2
  27. package/src/lsp/defaults.json +142 -652
  28. package/src/lsp/index.ts +1 -1
  29. package/src/mcp/render.ts +1 -8
  30. package/src/modes/components/assistant-message.ts +4 -0
  31. package/src/modes/components/diff.ts +23 -14
  32. package/src/modes/components/footer.ts +21 -16
  33. package/src/modes/components/session-selector.ts +3 -3
  34. package/src/modes/components/settings-defs.ts +6 -1
  35. package/src/modes/components/todo-reminder.ts +1 -8
  36. package/src/modes/components/tool-execution.ts +1 -4
  37. package/src/modes/controllers/selector-controller.ts +1 -1
  38. package/src/modes/print-mode.ts +8 -0
  39. package/src/prompts/agents/librarian.md +1 -1
  40. package/src/prompts/agents/reviewer.md +4 -4
  41. package/src/prompts/ci-green-request.md +1 -1
  42. package/src/prompts/review-request.md +1 -1
  43. package/src/prompts/system/subagent-system-prompt.md +3 -3
  44. package/src/prompts/system/subagent-yield-reminder.md +11 -0
  45. package/src/prompts/system/system-prompt.md +3 -0
  46. package/src/prompts/tools/ask.md +3 -2
  47. package/src/prompts/tools/ast-edit.md +16 -20
  48. package/src/prompts/tools/ast-grep.md +19 -24
  49. package/src/prompts/tools/atom.md +87 -0
  50. package/src/prompts/tools/chunk-edit.md +37 -161
  51. package/src/prompts/tools/debug.md +4 -5
  52. package/src/prompts/tools/exit-plan-mode.md +4 -5
  53. package/src/prompts/tools/find.md +4 -8
  54. package/src/prompts/tools/github.md +18 -0
  55. package/src/prompts/tools/grep.md +4 -5
  56. package/src/prompts/tools/hashline.md +22 -89
  57. package/src/prompts/tools/{gemini-image.md → image-gen.md} +1 -1
  58. package/src/prompts/tools/inspect-image.md +6 -6
  59. package/src/prompts/tools/lsp.md +1 -1
  60. package/src/prompts/tools/patch.md +12 -19
  61. package/src/prompts/tools/python.md +3 -2
  62. package/src/prompts/tools/read-chunk.md +2 -3
  63. package/src/prompts/tools/read.md +2 -2
  64. package/src/prompts/tools/ssh.md +8 -17
  65. package/src/prompts/tools/todo-write.md +54 -41
  66. package/src/sdk.ts +14 -9
  67. package/src/session/agent-session.ts +25 -2
  68. package/src/session/session-manager.ts +4 -1
  69. package/src/task/executor.ts +43 -48
  70. package/src/task/render.ts +11 -13
  71. package/src/tools/ask.ts +7 -7
  72. package/src/tools/ast-edit.ts +45 -41
  73. package/src/tools/ast-grep.ts +77 -85
  74. package/src/tools/bash.ts +8 -9
  75. package/src/tools/browser.ts +32 -30
  76. package/src/tools/calculator.ts +4 -4
  77. package/src/tools/cancel-job.ts +1 -1
  78. package/src/tools/checkpoint.ts +2 -2
  79. package/src/tools/debug.ts +41 -37
  80. package/src/tools/exit-plan-mode.ts +1 -1
  81. package/src/tools/find.ts +4 -4
  82. package/src/tools/gh-renderer.ts +12 -4
  83. package/src/tools/gh.ts +509 -697
  84. package/src/tools/grep.ts +116 -131
  85. package/src/tools/{gemini-image.ts → image-gen.ts} +459 -60
  86. package/src/tools/index.ts +14 -32
  87. package/src/tools/inspect-image.ts +3 -3
  88. package/src/tools/json-tree.ts +114 -114
  89. package/src/tools/match-line-format.ts +8 -7
  90. package/src/tools/notebook.ts +8 -7
  91. package/src/tools/poll-tool.ts +2 -1
  92. package/src/tools/python.ts +9 -23
  93. package/src/tools/read.ts +32 -25
  94. package/src/tools/render-mermaid.ts +1 -1
  95. package/src/tools/render-utils.ts +18 -0
  96. package/src/tools/renderers.ts +2 -2
  97. package/src/tools/report-tool-issue.ts +3 -2
  98. package/src/tools/resolve.ts +1 -1
  99. package/src/tools/review.ts +12 -10
  100. package/src/tools/search-tool-bm25.ts +2 -4
  101. package/src/tools/ssh.ts +4 -4
  102. package/src/tools/todo-write.ts +172 -147
  103. package/src/tools/vim.ts +14 -15
  104. package/src/tools/write.ts +4 -4
  105. package/src/tools/{submit-result.ts → yield.ts} +11 -13
  106. package/src/utils/edit-mode.ts +2 -1
  107. package/src/utils/file-display-mode.ts +10 -5
  108. package/src/utils/git.ts +9 -5
  109. package/src/utils/shell-snapshot.ts +2 -3
  110. package/src/vim/render.ts +4 -4
  111. package/src/prompts/system/subagent-submit-reminder.md +0 -11
  112. package/src/prompts/tools/gh-issue-view.md +0 -11
  113. package/src/prompts/tools/gh-pr-checkout.md +0 -12
  114. package/src/prompts/tools/gh-pr-diff.md +0 -12
  115. package/src/prompts/tools/gh-pr-push.md +0 -12
  116. package/src/prompts/tools/gh-pr-view.md +0 -11
  117. package/src/prompts/tools/gh-repo-view.md +0 -11
  118. package/src/prompts/tools/gh-run-watch.md +0 -12
  119. package/src/prompts/tools/gh-search-issues.md +0 -11
  120. package/src/prompts/tools/gh-search-prs.md +0 -11
@@ -23,8 +23,6 @@ export interface TodoItem {
23
23
  id: string;
24
24
  content: string;
25
25
  status: TodoStatus;
26
- notes?: string;
27
- details?: string;
28
26
  }
29
27
 
30
28
  export interface TodoPhase {
@@ -42,46 +40,51 @@ export interface TodoWriteToolDetails {
42
40
  // Schema
43
41
  // =============================================================================
44
42
 
43
+ const TodoOp = StringEnum(["replace", "start", "done", "rm", "drop", "append"] as const, {
44
+ description: "operation to apply",
45
+ });
46
+
45
47
  const InputTask = Type.Object({
46
- content: Type.String({ description: "Task description" }),
48
+ content: Type.String({ description: "task description", examples: ["Add unit tests"] }),
47
49
  status: Type.Optional(
48
50
  StringEnum(["pending", "in_progress", "completed", "abandoned"] as const, {
49
- description: "Task status",
51
+ description: "task status",
50
52
  }),
51
53
  ),
52
- details: Type.Optional(
53
- Type.String({ description: "Implementation details, file paths, and specifics (shown only when active)" }),
54
- ),
55
54
  });
56
55
 
57
56
  const InputPhase = Type.Object({
58
- name: Type.String({ description: "Phase name" }),
57
+ name: Type.String({ description: "phase name", examples: ["Investigation", "Implementation"] }),
59
58
  tasks: Type.Optional(Type.Array(InputTask)),
60
59
  });
61
60
 
62
- const AddNoteEntry = Type.Object({
63
- id: Type.String({ description: "Task ID, e.g. task-3" }),
64
- notes: Type.String({ description: "Notes to append" }),
61
+ const AppendItem = Type.Object({
62
+ id: Type.String({ description: "task id", examples: ["task-3"] }),
63
+ label: Type.String({ description: "task label", examples: ["Run tests"] }),
65
64
  });
66
65
 
67
- const AddTaskEntry = Type.Object({
68
- phase: Type.String({ description: "Phase name or ID" }),
69
- content: Type.String({ description: "Task description" }),
70
- details: Type.Optional(Type.String({ description: "Implementation details, file paths, and specifics" })),
66
+ const TodoOpEntry = Type.Object({
67
+ op: TodoOp,
68
+ phases: Type.Optional(Type.Array(InputPhase, { description: "replacement todo list for op=replace" })),
69
+ task: Type.Optional(Type.String({ description: "task id for start/done/rm/drop", examples: ["task-3"] })),
70
+ phase: Type.Optional(
71
+ Type.String({ description: "phase id for done/rm/drop/append", examples: ["Implementation", "phase-1"] }),
72
+ ),
73
+ items: Type.Optional(Type.Array(AppendItem, { minItems: 1, description: "items to append for op=append" })),
71
74
  });
72
75
 
73
- const todoWriteSchema = Type.Object({
74
- phases: Type.Optional(Type.Array(InputPhase, { description: "Replace entire todo list with these phases" })),
75
- start: Type.Optional(Type.String({ description: "Task ID to start, e.g. task-3" })),
76
- complete: Type.Optional(Type.Array(Type.String(), { description: "Task IDs to mark completed" })),
77
- abandon: Type.Optional(Type.Array(Type.String(), { description: "Task IDs to mark abandoned" })),
78
- remove: Type.Optional(Type.Array(Type.String(), { description: "Task IDs to remove" })),
79
- add_notes: Type.Optional(Type.Array(AddNoteEntry, { description: "Notes to append to tasks" })),
80
- add_tasks: Type.Optional(Type.Array(AddTaskEntry, { description: "Tasks to add" })),
81
- add_phase: Type.Optional(InputPhase),
82
- });
76
+ const todoWriteSchema = Type.Object(
77
+ {
78
+ ops: Type.Array(TodoOpEntry, {
79
+ minItems: 1,
80
+ description: "ordered todo operations",
81
+ }),
82
+ },
83
+ { description: "Apply ordered todo operations" },
84
+ );
83
85
 
84
86
  type TodoWriteParams = Static<typeof todoWriteSchema>;
87
+ type TodoOpEntryValue = TodoWriteParams["ops"][number];
85
88
 
86
89
  // =============================================================================
87
90
  // File format
@@ -109,19 +112,22 @@ function findTask(phases: TodoPhase[], id: string): TodoItem | undefined {
109
112
  return undefined;
110
113
  }
111
114
 
115
+ function findPhase(phases: TodoPhase[], idOrName: string): TodoPhase | undefined {
116
+ return phases.find(phase => phase.id === idOrName || phase.name === idOrName);
117
+ }
118
+
112
119
  function buildPhaseFromInput(
113
- input: { name: string; tasks?: Array<{ content: string; status?: TodoStatus; details?: string }> },
120
+ input: { name: string; tasks?: Array<{ content: string; status?: TodoStatus }> },
114
121
  phaseId: string,
115
122
  nextTaskId: number,
116
123
  ): { phase: TodoPhase; nextTaskId: number } {
117
124
  const tasks: TodoItem[] = [];
118
125
  let tid = nextTaskId;
119
- for (const t of input.tasks ?? []) {
126
+ for (const task of input.tasks ?? []) {
120
127
  tasks.push({
121
128
  id: `task-${tid++}`,
122
- content: t.content,
123
- status: t.status ?? "pending",
124
- details: t.details,
129
+ content: task.content,
130
+ status: task.status ?? "pending",
125
131
  });
126
132
  }
127
133
  return { phase: { id: phaseId, name: input.name, tasks }, nextTaskId: tid };
@@ -192,117 +198,154 @@ export function getLatestTodoPhasesFromEntries(entries: SessionEntry[]): TodoPha
192
198
  return [];
193
199
  }
194
200
 
195
- function resolveTaskOrError(phases: TodoPhase[], id: string, errors: string[]): TodoItem | undefined {
201
+ function resolveTaskOrError(phases: TodoPhase[], id: string | undefined, errors: string[]): TodoItem | undefined {
202
+ if (!id) {
203
+ errors.push("Missing task id");
204
+ return undefined;
205
+ }
196
206
  const task = findTask(phases, id);
197
207
  if (!task) {
198
- const totalTasks = phases.reduce((sum, p) => sum + p.tasks.length, 0);
208
+ const totalTasks = phases.reduce((sum, phase) => sum + phase.tasks.length, 0);
199
209
  const hint = totalTasks === 0 ? " (todo list is empty — was it replaced or not yet created?)" : "";
200
210
  errors.push(`Task "${id}" not found${hint}`);
201
211
  }
202
212
  return task;
203
213
  }
204
214
 
205
- function applyParams(file: TodoFile, params: TodoWriteParams): { file: TodoFile; errors: string[] } {
206
- const errors: string[] = [];
207
-
208
- // Replace (must be first — replaces entire state)
209
- if (params.phases) {
210
- const next = makeEmptyFile();
211
- for (const inputPhase of params.phases) {
212
- const phaseId = `phase-${next.nextPhaseId++}`;
213
- const { phase, nextTaskId } = buildPhaseFromInput(inputPhase, phaseId, next.nextTaskId);
214
- next.phases.push(phase);
215
- next.nextTaskId = nextTaskId;
216
- }
217
- file = next;
215
+ function resolvePhaseOrError(
216
+ phases: TodoPhase[],
217
+ idOrName: string | undefined,
218
+ errors: string[],
219
+ ): TodoPhase | undefined {
220
+ if (!idOrName) {
221
+ errors.push("Missing phase id");
222
+ return undefined;
218
223
  }
224
+ const phase = findPhase(phases, idOrName);
225
+ if (!phase) errors.push(`Phase "${idOrName}" not found`);
226
+ return phase;
227
+ }
219
228
 
220
- if (params.add_phase) {
221
- const phaseId = `phase-${file.nextPhaseId++}`;
222
- const { phase, nextTaskId } = buildPhaseFromInput(params.add_phase, phaseId, file.nextTaskId);
223
- file.phases.push(phase);
224
- file.nextTaskId = nextTaskId;
229
+ function getTaskTargets(file: TodoFile, entry: TodoOpEntryValue, errors: string[]): TodoItem[] {
230
+ if (entry.task) {
231
+ const task = resolveTaskOrError(file.phases, entry.task, errors);
232
+ return task ? [task] : [];
233
+ }
234
+ if (entry.phase) {
235
+ const phase = resolvePhaseOrError(file.phases, entry.phase, errors);
236
+ return phase ? [...phase.tasks] : [];
225
237
  }
238
+ return file.phases.flatMap(phase => phase.tasks);
239
+ }
226
240
 
227
- if (params.add_tasks) {
228
- for (const entry of params.add_tasks) {
229
- const target = file.phases.find(p => p.id === entry.phase || p.name === entry.phase);
230
- if (!target) {
231
- errors.push(`Phase "${entry.phase}" not found`);
232
- continue;
233
- }
234
- target.tasks.push({
235
- id: `task-${file.nextTaskId++}`,
236
- content: entry.content,
237
- status: "pending",
238
- details: entry.details,
239
- });
240
- }
241
+ function replaceFile(entry: TodoOpEntryValue, errors: string[]): TodoFile {
242
+ const next = makeEmptyFile();
243
+ for (const inputPhase of entry.phases ?? []) {
244
+ const phaseId = `phase-${next.nextPhaseId++}`;
245
+ const { phase, nextTaskId } = buildPhaseFromInput(inputPhase, phaseId, next.nextTaskId);
246
+ next.phases.push(phase);
247
+ next.nextTaskId = nextTaskId;
241
248
  }
249
+ if (!entry.phases) errors.push("Missing phases for replace operation");
250
+ return next;
251
+ }
242
252
 
243
- if (params.complete) {
244
- for (const id of params.complete) {
245
- const task = resolveTaskOrError(file.phases, id, errors);
246
- if (task) task.status = "completed";
247
- }
253
+ function appendItems(file: TodoFile, entry: TodoOpEntryValue, errors: string[]): void {
254
+ if (!entry.phase) {
255
+ errors.push("Missing phase id for append operation");
256
+ return;
257
+ }
258
+ if (!entry.items || entry.items.length === 0) {
259
+ errors.push("Missing items for append operation");
260
+ return;
248
261
  }
249
262
 
250
- if (params.abandon) {
251
- for (const id of params.abandon) {
252
- const task = resolveTaskOrError(file.phases, id, errors);
253
- if (task) task.status = "abandoned";
254
- }
263
+ let phase = findPhase(file.phases, entry.phase);
264
+ if (!phase) {
265
+ phase = { id: entry.phase, name: entry.phase, tasks: [] };
266
+ file.phases.push(phase);
255
267
  }
256
268
 
257
- if (params.remove) {
258
- for (const id of params.remove) {
259
- let removed = false;
260
- for (const phase of file.phases) {
261
- const idx = phase.tasks.findIndex(t => t.id === id);
262
- if (idx !== -1) {
263
- phase.tasks.splice(idx, 1);
264
- removed = true;
265
- break;
266
- }
267
- }
268
- if (!removed) {
269
- const totalTasks = file.phases.reduce((sum, p) => sum + p.tasks.length, 0);
270
- const hint = totalTasks === 0 ? " (todo list is empty)" : "";
271
- errors.push(`Task "${id}" not found${hint}`);
272
- }
269
+ for (const item of entry.items) {
270
+ if (findTask(file.phases, item.id)) {
271
+ errors.push(`Task "${item.id}" already exists`);
272
+ continue;
273
273
  }
274
+ phase.tasks.push({ id: item.id, content: item.label, status: "pending" });
274
275
  }
276
+ }
275
277
 
276
- if (params.add_notes) {
277
- for (const entry of params.add_notes) {
278
- const task = resolveTaskOrError(file.phases, entry.id, errors);
279
- if (task) {
280
- task.notes = task.notes ? `${task.notes}\n${entry.notes}` : entry.notes;
281
- }
278
+ function removeTasks(file: TodoFile, entry: TodoOpEntryValue, errors: string[]): void {
279
+ if (entry.task) {
280
+ const task = resolveTaskOrError(file.phases, entry.task, errors);
281
+ if (!task) return;
282
+ for (const phase of file.phases) {
283
+ phase.tasks = phase.tasks.filter(candidate => candidate.id !== task.id);
282
284
  }
285
+ return;
286
+ }
287
+ if (entry.phase) {
288
+ const phase = resolvePhaseOrError(file.phases, entry.phase, errors);
289
+ if (!phase) return;
290
+ phase.tasks = [];
291
+ return;
292
+ }
293
+ for (const phase of file.phases) {
294
+ phase.tasks = [];
283
295
  }
296
+ }
284
297
 
285
- if (params.start) {
286
- const task = resolveTaskOrError(file.phases, params.start, errors);
287
- if (task) {
288
- // Demote any currently in_progress tasks before promoting the target
298
+ function applyEntry(file: TodoFile, entry: TodoOpEntryValue, errors: string[]): TodoFile {
299
+ switch (entry.op) {
300
+ case "replace":
301
+ return replaceFile(entry, errors);
302
+ case "start": {
303
+ const task = resolveTaskOrError(file.phases, entry.task, errors);
304
+ if (!task) return file;
289
305
  for (const phase of file.phases) {
290
- for (const t of phase.tasks) {
291
- if (t.status === "in_progress" && t.id !== task.id) {
292
- t.status = "pending";
306
+ for (const candidate of phase.tasks) {
307
+ if (candidate.status === "in_progress" && candidate.id !== task.id) {
308
+ candidate.status = "pending";
293
309
  }
294
310
  }
295
311
  }
296
312
  task.status = "in_progress";
313
+ return file;
314
+ }
315
+ case "done": {
316
+ for (const task of getTaskTargets(file, entry, errors)) {
317
+ task.status = "completed";
318
+ }
319
+ return file;
320
+ }
321
+ case "drop": {
322
+ for (const task of getTaskTargets(file, entry, errors)) {
323
+ task.status = "abandoned";
324
+ }
325
+ return file;
326
+ }
327
+ case "rm": {
328
+ removeTasks(file, entry, errors);
329
+ return file;
330
+ }
331
+ case "append": {
332
+ appendItems(file, entry, errors);
333
+ return file;
297
334
  }
298
335
  }
336
+ }
299
337
 
338
+ function applyParams(file: TodoFile, params: TodoWriteParams): { file: TodoFile; errors: string[] } {
339
+ const errors: string[] = [];
340
+ for (const entry of params.ops) {
341
+ file = applyEntry(file, entry, errors);
342
+ }
300
343
  normalizeInProgressTask(file.phases);
301
344
  return { file, errors };
302
345
  }
303
346
 
304
347
  function formatSummary(phases: TodoPhase[], errors: string[]): string {
305
- const tasks = phases.flatMap(p => p.tasks);
348
+ const tasks = phases.flatMap(phase => phase.tasks);
306
349
  if (tasks.length === 0) return errors.length > 0 ? `Errors: ${errors.join("; ")}` : "Todo list cleared.";
307
350
 
308
351
  const remainingByPhase = phases
@@ -313,11 +356,12 @@ function formatSummary(phases: TodoPhase[], errors: string[]): string {
313
356
  .filter(phase => phase.tasks.length > 0);
314
357
  const remainingTasks = remainingByPhase.flatMap(phase => phase.tasks.map(task => ({ ...task, phase: phase.name })));
315
358
 
316
- // Find current phase
317
- let currentIdx = phases.findIndex(p => p.tasks.some(t => t.status === "pending" || t.status === "in_progress"));
359
+ let currentIdx = phases.findIndex(phase =>
360
+ phase.tasks.some(task => task.status === "pending" || task.status === "in_progress"),
361
+ );
318
362
  if (currentIdx === -1) currentIdx = phases.length - 1;
319
363
  const current = phases[currentIdx];
320
- const done = current.tasks.filter(t => t.status === "completed" || t.status === "abandoned").length;
364
+ const done = current.tasks.filter(task => task.status === "completed" || task.status === "abandoned").length;
321
365
 
322
366
  const lines: string[] = [];
323
367
  if (errors.length > 0) lines.push(`Errors: ${errors.join("; ")}`);
@@ -327,16 +371,6 @@ function formatSummary(phases: TodoPhase[], errors: string[]): string {
327
371
  lines.push(`Remaining items (${remainingTasks.length}):`);
328
372
  for (const task of remainingTasks) {
329
373
  lines.push(` - ${task.id} ${task.content} [${task.status}] (${task.phase})`);
330
- if (task.status === "in_progress" && task.details) {
331
- for (const line of task.details.split("\n")) {
332
- lines.push(` ${line}`);
333
- }
334
- }
335
- if (task.notes) {
336
- for (const line of task.notes.split("\n")) {
337
- lines.push(` Note: ${line}`);
338
- }
339
- }
340
374
  }
341
375
  }
342
376
  lines.push(
@@ -399,28 +433,22 @@ export class TodoWriteTool implements AgentTool<typeof todoWriteSchema, TodoWrit
399
433
  // TUI Renderer
400
434
  // =============================================================================
401
435
 
402
- interface TodoWriteRenderArgs {
403
- phases?: unknown;
404
- start?: string;
405
- complete?: string[];
406
- abandon?: string[];
407
- remove?: string[];
408
- add_notes?: unknown[];
409
- add_tasks?: unknown[];
410
- add_phase?: unknown;
411
- }
436
+ type TodoWriteRenderArgs = {
437
+ ops?: Array<{
438
+ op?: string;
439
+ task?: string;
440
+ phase?: string;
441
+ items?: Array<{ id?: string; label?: string }>;
442
+ }>;
443
+ };
412
444
 
413
445
  function formatTodoLine(item: TodoItem, uiTheme: Theme, prefix: string): string {
414
446
  const checkbox = uiTheme.checkbox;
415
447
  switch (item.status) {
416
448
  case "completed":
417
449
  return uiTheme.fg("success", `${prefix}${checkbox.checked} ${chalk.strikethrough(item.content)}`);
418
- case "in_progress": {
419
- const main = uiTheme.fg("accent", `${prefix}${checkbox.unchecked} ${item.content}`);
420
- if (!item.details) return main;
421
- const detailLines = item.details.split("\n").map(l => uiTheme.fg("dim", `${prefix} ${l}`));
422
- return [main, ...detailLines].join("\n");
423
- }
450
+ case "in_progress":
451
+ return uiTheme.fg("accent", `${prefix}${checkbox.unchecked} ${item.content}`);
424
452
  case "abandoned":
425
453
  return uiTheme.fg("error", `${prefix}${checkbox.unchecked} ${chalk.strikethrough(item.content)}`);
426
454
  default:
@@ -430,17 +458,14 @@ function formatTodoLine(item: TodoItem, uiTheme: Theme, prefix: string): string
430
458
 
431
459
  export const todoWriteToolRenderer = {
432
460
  renderCall(args: TodoWriteRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
433
- const ops: string[] = [];
434
- if (args.phases) ops.push("replace");
435
- if (args.complete?.length) ops.push(`complete ${args.complete.length}`);
436
- if (args.start) ops.push("start");
437
- if (args.abandon?.length) ops.push("abandon");
438
- if (args.remove?.length) ops.push("remove");
439
- if (args.add_notes?.length) ops.push("add_notes");
440
- if (args.add_tasks?.length) ops.push("add_tasks");
441
- if (args.add_phase) ops.push("add_phase");
442
- const label = ops.length > 0 ? ops.join(", ") : "update";
443
- const text = renderStatusLine({ icon: "pending", title: "Todo Write", meta: [label] }, uiTheme);
461
+ const ops = args?.ops?.map(entry => {
462
+ const parts = [entry.op ?? "update"];
463
+ if (entry.task) parts.push(entry.task);
464
+ if (entry.phase) parts.push(entry.phase);
465
+ if (entry.items?.length) parts.push(`${entry.items.length} item${entry.items.length === 1 ? "" : "s"}`);
466
+ return parts.join(" ");
467
+ }) ?? ["update"];
468
+ const text = renderStatusLine({ icon: "pending", title: "Todo Write", meta: ops }, uiTheme);
444
469
  return new Text(text, 0, 0);
445
470
  },
446
471
 
@@ -450,14 +475,14 @@ export const todoWriteToolRenderer = {
450
475
  uiTheme: Theme,
451
476
  _args?: TodoWriteRenderArgs,
452
477
  ): Component {
453
- const phases = (result.details?.phases ?? []).filter(p => p.tasks.length > 0);
454
- const allTasks = phases.flatMap(p => p.tasks);
478
+ const phases = (result.details?.phases ?? []).filter(phase => phase.tasks.length > 0);
479
+ const allTasks = phases.flatMap(phase => phase.tasks);
455
480
  const header = renderStatusLine(
456
481
  { icon: "success", title: "Todo Write", meta: [`${allTasks.length} tasks`] },
457
482
  uiTheme,
458
483
  );
459
484
  if (allTasks.length === 0) {
460
- const fallback = result.content?.find(c => c.type === "text")?.text ?? "No todos";
485
+ const fallback = result.content?.find(content => content.type === "text")?.text ?? "No todos";
461
486
  return new Text(`${header}\n${uiTheme.fg("dim", fallback)}`, 0, 0);
462
487
  }
463
488
 
package/src/tools/vim.ts CHANGED
@@ -38,28 +38,27 @@ const utf8Decoder = new TextDecoder("utf-8", { fatal: true });
38
38
 
39
39
  const vimStepSchema = Type.Object({
40
40
  kbd: Type.Array(Type.String(), {
41
- description: "Vim key sequences ONLY (e.g. ggdGi, 3Go, dd). NEVER put file content here — use insert for text.",
41
+ description: "vim key sequences",
42
+ examples: [["ggdGi"], ["3Go"], ["dd"]],
42
43
  }),
43
44
  insert: Type.Optional(
44
45
  Type.String({
45
- description:
46
- "Raw text to type into the buffer. kbd must leave INSERT mode active first (e.g. via o, O, i, cc).",
46
+ description: "raw text to insert",
47
+ examples: ["hello world"],
47
48
  }),
48
49
  ),
49
50
  });
50
51
 
51
52
  const vimSchema = Type.Object({
52
- file: Type.String({ description: "File path to edit." }),
53
+ file: Type.String({ description: "file path", examples: ["src/foo.ts"] }),
53
54
  steps: Type.Optional(
54
55
  Type.Array(vimStepSchema, {
55
- description:
56
- "Ordered editing steps. Each step executes kbd sequences, then optionally inserts text. INSERT mode is auto-exited between steps.",
56
+ description: "editing steps",
57
57
  }),
58
58
  ),
59
59
  pause: Type.Optional(
60
60
  Type.Boolean({
61
- description:
62
- "Advanced: skip auto-save after the last step. Rarely needed. Omit or set false for normal use — edits auto-save.",
61
+ description: "skip auto-save",
63
62
  }),
64
63
  ),
65
64
  });
@@ -128,15 +127,15 @@ function renderViewportCursor(line: VimViewportLine, styledText: string, uiTheme
128
127
  }
129
128
 
130
129
  function renderViewportLine(line: VimViewportLine, styledText: string, padWidth: number, uiTheme: Theme): string {
131
- const lineNoStr = String(line.line).padStart(padWidth, " ");
132
- const lineNoStyled = line.isCursor
133
- ? uiTheme.fg("accent", lineNoStr)
130
+ const marker = line.isCursor ? ">" : line.isSelected ? "*" : "";
131
+ const gutterText = `${marker}${line.line}`.padStart(padWidth + 1, " ");
132
+ const gutterStyled = line.isCursor
133
+ ? uiTheme.fg("accent", gutterText)
134
134
  : line.isSelected
135
- ? uiTheme.fg("warning", lineNoStr)
136
- : uiTheme.fg("dim", lineNoStr);
135
+ ? uiTheme.fg("warning", gutterText)
136
+ : uiTheme.fg("dim", gutterText);
137
137
  const separator = uiTheme.fg("dim", "│");
138
- const prefix = line.isCursor ? uiTheme.fg("accent", ">") : line.isSelected ? uiTheme.fg("warning", "*") : " ";
139
- return `${prefix}${lineNoStyled}${separator}${renderViewportCursor(line, styledText, uiTheme)}`;
138
+ return `${gutterStyled}${separator}${renderViewportCursor(line, styledText, uiTheme)}`;
140
139
  }
141
140
 
142
141
  function splitTokensBySequence(kbd: string[]): Array<{ sequence: string; tokens: VimKeyToken[] }> {
@@ -44,8 +44,8 @@ import { ToolError } from "./tool-errors";
44
44
  import { toolResult } from "./tool-result";
45
45
 
46
46
  const writeSchema = Type.Object({
47
- path: Type.String({ description: "Path to the file to write (relative or absolute)" }),
48
- content: Type.String({ description: "Content to write to the file" }),
47
+ path: Type.String({ description: "file path", examples: ["src/new.ts"] }),
48
+ content: Type.String({ description: "file content" }),
49
49
  });
50
50
 
51
51
  export type WriteToolInput = Static<typeof writeSchema>;
@@ -59,7 +59,7 @@ export interface WriteToolDetails {
59
59
  /**
60
60
  * Strip hashline display prefixes from write content.
61
61
  *
62
- * Only active when hashline edit mode is enabled — the model sees `LINE#ID:`
62
+ * Only active when hashline edit mode is enabled — the model sees `LINE+ID|`
63
63
  * prefixes in read output and sometimes copies them into write content.
64
64
  */
65
65
  function stripWriteContent(session: ToolSession, content: string): { text: string; stripped: boolean } {
@@ -418,7 +418,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
418
418
  context?: AgentToolContext,
419
419
  ): Promise<AgentToolResult<WriteToolDetails>> {
420
420
  return untilAborted(signal, async () => {
421
- // Strip hashline display prefixes (LINE#ID:) if the model copied them from read output
421
+ // Strip hashline display prefixes (LINE+ID|) if the model copied them from read output
422
422
  const { text: cleanContent, stripped } = stripWriteContent(this.session, content);
423
423
  const resolvedArchivePath = await this.#resolveArchiveWritePath(path);
424
424
  if (resolvedArchivePath) {
@@ -12,7 +12,7 @@ import { subprocessToolRegistry } from "../task/subprocess-tool-registry";
12
12
  import type { ToolSession } from ".";
13
13
  import { jtdToJsonSchema, normalizeSchema } from "./jtd-to-json-schema";
14
14
 
15
- export interface SubmitResultDetails {
15
+ export interface YieldDetails {
16
16
  data: unknown;
17
17
  status: "success" | "aborted";
18
18
  error?: string;
@@ -40,8 +40,8 @@ function formatAjvErrors(errors: ErrorObject[] | null | undefined): string {
40
40
  .join("; ");
41
41
  }
42
42
 
43
- export class SubmitResultTool implements AgentTool<TSchema, SubmitResultDetails> {
44
- readonly name = "submit_result";
43
+ export class YieldTool implements AgentTool<TSchema, YieldDetails> {
44
+ readonly name = "yield";
45
45
  readonly label = "Submit Result";
46
46
  readonly description =
47
47
  "Finish the task with structured JSON output. Call exactly once at the end of the task.\n\n" +
@@ -59,22 +59,21 @@ export class SubmitResultTool implements AgentTool<TSchema, SubmitResultDetails>
59
59
  Type.Object(
60
60
  {
61
61
  result: Type.Union([
62
- Type.Object({ data: dataSchema }, { description: "Successfully completed the task" }),
62
+ Type.Object({ data: dataSchema }, { description: "task succeeded" }),
63
63
  Type.Object({
64
- error: Type.String({ description: "Error message when the task cannot be completed" }),
64
+ error: Type.String({ description: "error message" }),
65
65
  }),
66
66
  ]),
67
67
  },
68
68
  {
69
69
  additionalProperties: false,
70
- description: "Submit either `data` for success or `error` for failure",
70
+ description: "submit data or error",
71
71
  },
72
72
  ) as TSchema;
73
73
 
74
74
  let validate: ValidateFunction | undefined;
75
75
  let dataSchema: TSchema;
76
76
  let parameters: TSchema;
77
- let strict = true;
78
77
 
79
78
  try {
80
79
  const schemaResult = normalizeSchema(session.outputSchema);
@@ -131,21 +130,20 @@ export class SubmitResultTool implements AgentTool<TSchema, SubmitResultDetails>
131
130
  });
132
131
  parameters = createParameters(dataSchema);
133
132
  validate = undefined;
134
- strict = false;
133
+ this.strict = false;
135
134
  }
136
135
 
137
136
  this.#validate = validate;
138
137
  this.parameters = parameters;
139
- this.strict = strict;
140
138
  }
141
139
 
142
140
  async execute(
143
141
  _toolCallId: string,
144
142
  params: Static<TSchema>,
145
143
  _signal?: AbortSignal,
146
- _onUpdate?: AgentToolUpdateCallback<SubmitResultDetails>,
144
+ _onUpdate?: AgentToolUpdateCallback<YieldDetails>,
147
145
  _context?: AgentToolContext,
148
- ): Promise<AgentToolResult<SubmitResultDetails>> {
146
+ ): Promise<AgentToolResult<YieldDetails>> {
149
147
  const raw = params as Record<string, unknown>;
150
148
  const rawResult = raw.result;
151
149
  if (!rawResult || typeof rawResult !== "object" || Array.isArray(rawResult)) {
@@ -169,7 +167,7 @@ export class SubmitResultTool implements AgentTool<TSchema, SubmitResultDetails>
169
167
  let schemaValidationOverridden = false;
170
168
  if (status === "success") {
171
169
  if (data === undefined || data === null) {
172
- throw new Error("data is required when submit_result indicates success");
170
+ throw new Error("data is required when yield indicates success");
173
171
  }
174
172
  if (this.#validate && !this.#validate(data)) {
175
173
  this.#schemaValidationFailures++;
@@ -194,7 +192,7 @@ export class SubmitResultTool implements AgentTool<TSchema, SubmitResultDetails>
194
192
  }
195
193
 
196
194
  // Register subprocess tool handler for extraction + termination.
197
- subprocessToolRegistry.register<SubmitResultDetails>("submit_result", {
195
+ subprocessToolRegistry.register<YieldDetails>("yield", {
198
196
  extractData: event => {
199
197
  const details = event.result?.details;
200
198
  if (!details || typeof details !== "object") return undefined;
@@ -1,11 +1,12 @@
1
1
  import { $env, $flag } from "@oh-my-pi/pi-utils";
2
2
 
3
- export type EditMode = "replace" | "patch" | "hashline" | "chunk" | "vim" | "apply_patch";
3
+ export type EditMode = "replace" | "patch" | "hashline" | "chunk" | "vim" | "apply_patch" | "atom";
4
4
 
5
5
  export const DEFAULT_EDIT_MODE: EditMode = "hashline";
6
6
 
7
7
  const EDIT_MODE_IDS = {
8
8
  apply_patch: "apply_patch",
9
+ atom: "atom",
9
10
  chunk: "chunk",
10
11
  hashline: "hashline",
11
12
  patch: "patch",