@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.
- package/CHANGELOG.md +98 -1
- package/package.json +7 -7
- package/src/autoresearch/prompt.md +1 -1
- package/src/commit/agentic/prompts/analyze-file.md +1 -1
- package/src/config/model-registry.ts +67 -15
- package/src/config/prompt-templates.ts +5 -5
- package/src/config/settings-schema.ts +4 -4
- package/src/cursor.ts +3 -8
- package/src/discovery/helpers.ts +3 -3
- package/src/edit/diff.ts +50 -47
- package/src/edit/index.ts +86 -57
- package/src/edit/line-hash.ts +743 -24
- package/src/edit/modes/apply-patch.ts +0 -9
- package/src/edit/modes/atom.ts +893 -0
- package/src/edit/modes/chunk.ts +14 -24
- package/src/edit/modes/hashline.ts +193 -146
- package/src/edit/modes/patch.ts +5 -9
- package/src/edit/modes/replace.ts +6 -11
- package/src/edit/renderer.ts +14 -10
- package/src/edit/streaming.ts +50 -16
- package/src/exec/bash-executor.ts +2 -4
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +4 -12
- package/src/extensibility/custom-tools/types.ts +2 -0
- package/src/extensibility/custom-tools/wrapper.ts +2 -1
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/lsp/defaults.json +142 -652
- package/src/lsp/index.ts +1 -1
- package/src/mcp/render.ts +1 -8
- package/src/modes/components/assistant-message.ts +4 -0
- package/src/modes/components/diff.ts +23 -14
- package/src/modes/components/footer.ts +21 -16
- package/src/modes/components/session-selector.ts +3 -3
- package/src/modes/components/settings-defs.ts +6 -1
- package/src/modes/components/todo-reminder.ts +1 -8
- package/src/modes/components/tool-execution.ts +1 -4
- package/src/modes/controllers/selector-controller.ts +1 -1
- package/src/modes/print-mode.ts +8 -0
- package/src/prompts/agents/librarian.md +1 -1
- package/src/prompts/agents/reviewer.md +4 -4
- package/src/prompts/ci-green-request.md +1 -1
- package/src/prompts/review-request.md +1 -1
- package/src/prompts/system/subagent-system-prompt.md +3 -3
- package/src/prompts/system/subagent-yield-reminder.md +11 -0
- package/src/prompts/system/system-prompt.md +3 -0
- package/src/prompts/tools/ask.md +3 -2
- package/src/prompts/tools/ast-edit.md +16 -20
- package/src/prompts/tools/ast-grep.md +19 -24
- package/src/prompts/tools/atom.md +87 -0
- package/src/prompts/tools/chunk-edit.md +37 -161
- package/src/prompts/tools/debug.md +4 -5
- package/src/prompts/tools/exit-plan-mode.md +4 -5
- package/src/prompts/tools/find.md +4 -8
- package/src/prompts/tools/github.md +18 -0
- package/src/prompts/tools/grep.md +4 -5
- package/src/prompts/tools/hashline.md +22 -89
- package/src/prompts/tools/{gemini-image.md → image-gen.md} +1 -1
- package/src/prompts/tools/inspect-image.md +6 -6
- package/src/prompts/tools/lsp.md +1 -1
- package/src/prompts/tools/patch.md +12 -19
- package/src/prompts/tools/python.md +3 -2
- package/src/prompts/tools/read-chunk.md +2 -3
- package/src/prompts/tools/read.md +2 -2
- package/src/prompts/tools/ssh.md +8 -17
- package/src/prompts/tools/todo-write.md +54 -41
- package/src/sdk.ts +14 -9
- package/src/session/agent-session.ts +25 -2
- package/src/session/session-manager.ts +4 -1
- package/src/task/executor.ts +43 -48
- package/src/task/render.ts +11 -13
- package/src/tools/ask.ts +7 -7
- package/src/tools/ast-edit.ts +45 -41
- package/src/tools/ast-grep.ts +77 -85
- package/src/tools/bash.ts +8 -9
- package/src/tools/browser.ts +32 -30
- package/src/tools/calculator.ts +4 -4
- package/src/tools/cancel-job.ts +1 -1
- package/src/tools/checkpoint.ts +2 -2
- package/src/tools/debug.ts +41 -37
- package/src/tools/exit-plan-mode.ts +1 -1
- package/src/tools/find.ts +4 -4
- package/src/tools/gh-renderer.ts +12 -4
- package/src/tools/gh.ts +509 -697
- package/src/tools/grep.ts +116 -131
- package/src/tools/{gemini-image.ts → image-gen.ts} +459 -60
- package/src/tools/index.ts +14 -32
- package/src/tools/inspect-image.ts +3 -3
- package/src/tools/json-tree.ts +114 -114
- package/src/tools/match-line-format.ts +8 -7
- package/src/tools/notebook.ts +8 -7
- package/src/tools/poll-tool.ts +2 -1
- package/src/tools/python.ts +9 -23
- package/src/tools/read.ts +32 -25
- package/src/tools/render-mermaid.ts +1 -1
- package/src/tools/render-utils.ts +18 -0
- package/src/tools/renderers.ts +2 -2
- package/src/tools/report-tool-issue.ts +3 -2
- package/src/tools/resolve.ts +1 -1
- package/src/tools/review.ts +12 -10
- package/src/tools/search-tool-bm25.ts +2 -4
- package/src/tools/ssh.ts +4 -4
- package/src/tools/todo-write.ts +172 -147
- package/src/tools/vim.ts +14 -15
- package/src/tools/write.ts +4 -4
- package/src/tools/{submit-result.ts → yield.ts} +11 -13
- package/src/utils/edit-mode.ts +2 -1
- package/src/utils/file-display-mode.ts +10 -5
- package/src/utils/git.ts +9 -5
- package/src/utils/shell-snapshot.ts +2 -3
- package/src/vim/render.ts +4 -4
- package/src/prompts/system/subagent-submit-reminder.md +0 -11
- package/src/prompts/tools/gh-issue-view.md +0 -11
- package/src/prompts/tools/gh-pr-checkout.md +0 -12
- package/src/prompts/tools/gh-pr-diff.md +0 -12
- package/src/prompts/tools/gh-pr-push.md +0 -12
- package/src/prompts/tools/gh-pr-view.md +0 -11
- package/src/prompts/tools/gh-repo-view.md +0 -11
- package/src/prompts/tools/gh-run-watch.md +0 -12
- package/src/prompts/tools/gh-search-issues.md +0 -11
- package/src/prompts/tools/gh-search-prs.md +0 -11
package/src/tools/todo-write.ts
CHANGED
|
@@ -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: "
|
|
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: "
|
|
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: "
|
|
57
|
+
name: Type.String({ description: "phase name", examples: ["Investigation", "Implementation"] }),
|
|
59
58
|
tasks: Type.Optional(Type.Array(InputTask)),
|
|
60
59
|
});
|
|
61
60
|
|
|
62
|
-
const
|
|
63
|
-
id: Type.String({ description: "
|
|
64
|
-
|
|
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
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
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
|
|
126
|
+
for (const task of input.tasks ?? []) {
|
|
120
127
|
tasks.push({
|
|
121
128
|
id: `task-${tid++}`,
|
|
122
|
-
content:
|
|
123
|
-
status:
|
|
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,
|
|
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
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
|
291
|
-
if (
|
|
292
|
-
|
|
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(
|
|
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
|
-
|
|
317
|
-
|
|
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(
|
|
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
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
|
|
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
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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(
|
|
454
|
-
const allTasks = phases.flatMap(
|
|
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(
|
|
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: "
|
|
41
|
+
description: "vim key sequences",
|
|
42
|
+
examples: [["ggdGi"], ["3Go"], ["dd"]],
|
|
42
43
|
}),
|
|
43
44
|
insert: Type.Optional(
|
|
44
45
|
Type.String({
|
|
45
|
-
description:
|
|
46
|
-
|
|
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: "
|
|
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
|
|
132
|
-
const
|
|
133
|
-
|
|
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",
|
|
136
|
-
: uiTheme.fg("dim",
|
|
135
|
+
? uiTheme.fg("warning", gutterText)
|
|
136
|
+
: uiTheme.fg("dim", gutterText);
|
|
137
137
|
const separator = uiTheme.fg("dim", "│");
|
|
138
|
-
|
|
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[] }> {
|
package/src/tools/write.ts
CHANGED
|
@@ -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: "
|
|
48
|
-
content: Type.String({ description: "
|
|
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
|
|
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
|
|
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
|
|
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
|
|
44
|
-
readonly name = "
|
|
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: "
|
|
62
|
+
Type.Object({ data: dataSchema }, { description: "task succeeded" }),
|
|
63
63
|
Type.Object({
|
|
64
|
-
error: Type.String({ description: "
|
|
64
|
+
error: Type.String({ description: "error message" }),
|
|
65
65
|
}),
|
|
66
66
|
]),
|
|
67
67
|
},
|
|
68
68
|
{
|
|
69
69
|
additionalProperties: false,
|
|
70
|
-
description: "
|
|
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<
|
|
144
|
+
_onUpdate?: AgentToolUpdateCallback<YieldDetails>,
|
|
147
145
|
_context?: AgentToolContext,
|
|
148
|
-
): Promise<AgentToolResult<
|
|
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
|
|
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<
|
|
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;
|
package/src/utils/edit-mode.ts
CHANGED
|
@@ -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",
|