@oh-my-pi/pi-coding-agent 14.1.2 → 14.2.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 +47 -2
- package/package.json +8 -8
- package/scripts/build-binary.ts +61 -0
- package/src/autoresearch/helpers.ts +10 -0
- package/src/autoresearch/index.ts +1 -11
- package/src/autoresearch/tools/init-experiment.ts +1 -10
- package/src/autoresearch/tools/log-experiment.ts +1 -11
- package/src/autoresearch/tools/run-experiment.ts +1 -10
- package/src/bun-imports.d.ts +6 -0
- package/src/cli/plugin-cli.ts +23 -45
- package/src/commit/agentic/tools/propose-commit.ts +1 -14
- package/src/commit/agentic/tools/split-commit.ts +1 -15
- package/src/commit/utils.ts +15 -1
- package/src/config/model-registry.ts +3 -3
- package/src/config/prompt-templates.ts +4 -12
- package/src/config/settings-schema.ts +27 -2
- package/src/config/settings.ts +1 -1
- package/src/dap/session.ts +8 -2
- package/src/discovery/claude-plugins.ts +61 -6
- package/src/discovery/codex.ts +2 -15
- package/src/discovery/gemini.ts +2 -15
- package/src/discovery/helpers.ts +40 -1
- package/src/discovery/opencode.ts +2 -15
- package/src/edit/apply-patch/index.ts +87 -0
- package/src/edit/apply-patch/parser.ts +174 -0
- package/src/edit/diff.ts +3 -14
- package/src/edit/index.ts +67 -3
- package/src/edit/modes/apply-patch.lark +19 -0
- package/src/edit/modes/apply-patch.ts +63 -0
- package/src/edit/modes/chunk.ts +6 -2
- package/src/edit/modes/hashline.ts +3 -3
- package/src/edit/modes/replace.ts +2 -13
- package/src/edit/read-file.ts +18 -0
- package/src/edit/renderer.ts +61 -33
- package/src/extensibility/extensions/compact-handler.ts +40 -0
- package/src/extensibility/extensions/runner.ts +11 -29
- package/src/extensibility/utils.ts +7 -1
- package/src/internal-urls/docs-index.generated.ts +9 -2
- package/src/lsp/client.ts +14 -5
- package/src/lsp/index.ts +53 -10
- package/src/lsp/render.ts +14 -2
- package/src/lsp/types.ts +2 -0
- package/src/main.ts +1 -0
- package/src/mcp/manager.ts +29 -48
- package/src/memories/index.ts +7 -1
- package/src/modes/acp/acp-agent.ts +3 -16
- package/src/modes/components/model-selector.ts +15 -24
- package/src/modes/components/plugin-settings.ts +16 -5
- package/src/modes/components/read-tool-group.ts +92 -9
- package/src/modes/components/settings-defs.ts +18 -0
- package/src/modes/components/settings-selector.ts +2 -6
- package/src/modes/components/tool-execution.ts +61 -28
- package/src/modes/controllers/event-controller.ts +3 -1
- package/src/modes/controllers/extension-ui-controller.ts +99 -150
- package/src/modes/controllers/selector-controller.ts +3 -12
- package/src/modes/interactive-mode.ts +4 -2
- package/src/modes/print-mode.ts +4 -22
- package/src/modes/rpc/rpc-mode.ts +18 -38
- package/src/modes/shared.ts +10 -1
- package/src/modes/utils/ui-helpers.ts +6 -2
- package/src/plan-mode/approved-plan.ts +5 -4
- package/src/prompts/system/subagent-system-prompt.md +4 -4
- package/src/prompts/system/subagent-user-prompt.md +2 -2
- package/src/prompts/system/system-prompt.md +208 -243
- package/src/prompts/tools/apply-patch.md +67 -0
- package/src/prompts/tools/ast-edit.md +18 -23
- package/src/prompts/tools/ast-grep.md +25 -32
- package/src/prompts/tools/bash.md +11 -23
- package/src/prompts/tools/debug.md +8 -22
- package/src/prompts/tools/find.md +0 -4
- package/src/prompts/tools/grep.md +3 -5
- package/src/prompts/tools/hashline.md +16 -10
- package/src/prompts/tools/python.md +10 -14
- package/src/prompts/tools/read.md +17 -24
- package/src/prompts/tools/task.md +57 -21
- package/src/prompts/tools/todo-write.md +45 -67
- package/src/session/agent-session.ts +4 -4
- package/src/session/session-manager.ts +15 -7
- package/src/session/streaming-output.ts +24 -0
- package/src/slash-commands/builtin-registry.ts +3 -14
- package/src/task/executor.ts +13 -34
- package/src/task/index.ts +82 -18
- package/src/task/simple-mode.ts +27 -0
- package/src/task/template.ts +17 -3
- package/src/task/types.ts +77 -30
- package/src/tools/ask.ts +2 -4
- package/src/tools/ast-edit.ts +41 -17
- package/src/tools/ast-grep.ts +8 -27
- package/src/tools/bash-skill-urls.ts +9 -7
- package/src/tools/bash.ts +66 -24
- package/src/tools/browser.ts +1 -1
- package/src/tools/fetch.ts +1 -14
- package/src/tools/file-recorder.ts +35 -0
- package/src/tools/find.ts +25 -29
- package/src/tools/gh-format.ts +12 -0
- package/src/tools/gh-renderer.ts +1 -8
- package/src/tools/gh.ts +6 -13
- package/src/tools/grep.ts +103 -59
- package/src/tools/jtd-to-json-schema.ts +16 -0
- package/src/tools/match-line-format.ts +20 -0
- package/src/tools/path-utils.ts +61 -5
- package/src/tools/plan-mode-guard.ts +6 -5
- package/src/tools/python.ts +1 -1
- package/src/tools/read.ts +1 -1
- package/src/tools/render-utils.ts +38 -6
- package/src/tools/renderers.ts +1 -0
- package/src/tools/resolve.ts +12 -3
- package/src/tools/ssh.ts +3 -11
- package/src/tools/submit-result.ts +1 -13
- package/src/tools/todo-write.ts +137 -103
- package/src/tools/vim.ts +1 -1
- package/src/tools/write.ts +2 -23
- package/src/tui/code-cell.ts +12 -7
- package/src/utils/edit-mode.ts +3 -2
- package/src/utils/git.ts +1 -1
- package/src/vim/engine.ts +41 -58
- package/src/web/scrapers/crates-io.ts +1 -14
- package/src/web/scrapers/types.ts +13 -0
- package/src/web/search/providers/base.ts +13 -0
- package/src/web/search/providers/brave.ts +2 -5
- package/src/web/search/providers/codex.ts +20 -24
- package/src/web/search/providers/gemini.ts +39 -1
- package/src/web/search/providers/jina.ts +2 -5
- package/src/web/search/providers/kagi.ts +3 -8
- package/src/web/search/providers/kimi.ts +3 -7
- package/src/web/search/providers/parallel.ts +3 -8
- package/src/web/search/providers/synthetic.ts +3 -7
- package/src/web/search/providers/tavily.ts +15 -11
- package/src/web/search/providers/utils.ts +36 -0
- package/src/web/search/providers/zai.ts +3 -7
package/src/tools/ssh.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { loadCapability } from "../discovery";
|
|
|
9
9
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
10
10
|
import type { Theme } from "../modes/theme/theme";
|
|
11
11
|
import sshDescriptionBase from "../prompts/tools/ssh.md" with { type: "text" };
|
|
12
|
-
import { DEFAULT_MAX_BYTES, TailBuffer } from "../session/streaming-output";
|
|
12
|
+
import { DEFAULT_MAX_BYTES, streamTailUpdates, TailBuffer } from "../session/streaming-output";
|
|
13
13
|
import type { SSHHostInfo } from "../ssh/connection-manager";
|
|
14
14
|
import { ensureHostInfo, getHostInfoForHost } from "../ssh/connection-manager";
|
|
15
15
|
import { executeSSH } from "../ssh/ssh-executor";
|
|
@@ -25,7 +25,7 @@ const sshSchema = Type.Object({
|
|
|
25
25
|
host: Type.String({ description: "Host name from managed SSH config or discovered ssh.json files" }),
|
|
26
26
|
command: Type.String({ description: "Command to execute on the remote host" }),
|
|
27
27
|
cwd: Type.Optional(Type.String({ description: "Remote working directory (optional)" })),
|
|
28
|
-
timeout: Type.Optional(Type.Number({ description: "Timeout in seconds
|
|
28
|
+
timeout: Type.Optional(Type.Number({ description: "Timeout in seconds", default: 60 })),
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
export interface SSHToolDetails {
|
|
@@ -168,15 +168,7 @@ export class SshTool implements AgentTool<typeof sshSchema, SSHToolDetails> {
|
|
|
168
168
|
compatEnabled: hostInfo.compatEnabled,
|
|
169
169
|
artifactPath,
|
|
170
170
|
artifactId,
|
|
171
|
-
onChunk:
|
|
172
|
-
tailBuffer.append(chunk);
|
|
173
|
-
if (onUpdate) {
|
|
174
|
-
onUpdate({
|
|
175
|
-
content: [{ type: "text", text: tailBuffer.text() }],
|
|
176
|
-
details: {},
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
},
|
|
171
|
+
onChunk: streamTailUpdates(tailBuffer, onUpdate),
|
|
180
172
|
});
|
|
181
173
|
|
|
182
174
|
if (result.cancelled) {
|
|
@@ -10,7 +10,7 @@ import { Type } from "@sinclair/typebox";
|
|
|
10
10
|
import Ajv, { type ErrorObject, type ValidateFunction } from "ajv";
|
|
11
11
|
import { subprocessToolRegistry } from "../task/subprocess-tool-registry";
|
|
12
12
|
import type { ToolSession } from ".";
|
|
13
|
-
import { jtdToJsonSchema } from "./jtd-to-json-schema";
|
|
13
|
+
import { jtdToJsonSchema, normalizeSchema } from "./jtd-to-json-schema";
|
|
14
14
|
|
|
15
15
|
export interface SubmitResultDetails {
|
|
16
16
|
data: unknown;
|
|
@@ -20,18 +20,6 @@ export interface SubmitResultDetails {
|
|
|
20
20
|
|
|
21
21
|
const ajv = new Ajv({ allErrors: true, strict: false, logger: false });
|
|
22
22
|
|
|
23
|
-
function normalizeSchema(schema: unknown): { normalized?: unknown; error?: string } {
|
|
24
|
-
if (schema === undefined || schema === null) return {};
|
|
25
|
-
if (typeof schema === "string") {
|
|
26
|
-
try {
|
|
27
|
-
return { normalized: JSON.parse(schema) };
|
|
28
|
-
} catch (err) {
|
|
29
|
-
return { error: err instanceof Error ? err.message : String(err) };
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return { normalized: schema };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
23
|
function formatSchema(schema: unknown): string {
|
|
36
24
|
if (schema === undefined) return "No schema provided.";
|
|
37
25
|
if (typeof schema === "string") return schema;
|
package/src/tools/todo-write.ts
CHANGED
|
@@ -42,14 +42,13 @@ export interface TodoWriteToolDetails {
|
|
|
42
42
|
// Schema
|
|
43
43
|
// =============================================================================
|
|
44
44
|
|
|
45
|
-
const StatusEnum = StringEnum(["pending", "in_progress", "completed", "abandoned"] as const, {
|
|
46
|
-
description: "Task status",
|
|
47
|
-
});
|
|
48
|
-
|
|
49
45
|
const InputTask = Type.Object({
|
|
50
46
|
content: Type.String({ description: "Task description" }),
|
|
51
|
-
status: Type.Optional(
|
|
52
|
-
|
|
47
|
+
status: Type.Optional(
|
|
48
|
+
StringEnum(["pending", "in_progress", "completed", "abandoned"] as const, {
|
|
49
|
+
description: "Task status",
|
|
50
|
+
}),
|
|
51
|
+
),
|
|
53
52
|
details: Type.Optional(
|
|
54
53
|
Type.String({ description: "Implementation details, file paths, and specifics (shown only when active)" }),
|
|
55
54
|
),
|
|
@@ -60,39 +59,26 @@ const InputPhase = Type.Object({
|
|
|
60
59
|
tasks: Type.Optional(Type.Array(InputTask)),
|
|
61
60
|
});
|
|
62
61
|
|
|
62
|
+
const AddNoteEntry = Type.Object({
|
|
63
|
+
id: Type.String({ description: "Task ID, e.g. task-3" }),
|
|
64
|
+
notes: Type.String({ description: "Notes to append" }),
|
|
65
|
+
});
|
|
66
|
+
|
|
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" })),
|
|
71
|
+
});
|
|
72
|
+
|
|
63
73
|
const todoWriteSchema = Type.Object({
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
name: Type.String({ description: "Phase name" }),
|
|
73
|
-
tasks: Type.Optional(Type.Array(InputTask)),
|
|
74
|
-
}),
|
|
75
|
-
Type.Object({
|
|
76
|
-
op: Type.Literal("add_task"),
|
|
77
|
-
phase: Type.String({ description: "Phase ID, e.g. phase-1" }),
|
|
78
|
-
content: Type.String({ description: "Task description" }),
|
|
79
|
-
notes: Type.Optional(Type.String({ description: "Additional context or notes" })),
|
|
80
|
-
details: Type.Optional(Type.String({ description: "Implementation details, file paths, and specifics" })),
|
|
81
|
-
}),
|
|
82
|
-
Type.Object({
|
|
83
|
-
op: Type.Literal("update"),
|
|
84
|
-
id: Type.String({ description: "Task ID, e.g. task-3" }),
|
|
85
|
-
status: Type.Optional(StatusEnum),
|
|
86
|
-
content: Type.Optional(Type.String({ description: "Updated task description" })),
|
|
87
|
-
notes: Type.Optional(Type.String({ description: "Additional context or notes" })),
|
|
88
|
-
details: Type.Optional(Type.String({ description: "Updated details" })),
|
|
89
|
-
}),
|
|
90
|
-
Type.Object({
|
|
91
|
-
op: Type.Literal("remove_task"),
|
|
92
|
-
id: Type.String({ description: "Task ID, e.g. task-3" }),
|
|
93
|
-
}),
|
|
94
|
-
]),
|
|
95
|
-
),
|
|
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),
|
|
96
82
|
});
|
|
97
83
|
|
|
98
84
|
type TodoWriteParams = Static<typeof todoWriteSchema>;
|
|
@@ -124,7 +110,7 @@ function findTask(phases: TodoPhase[], id: string): TodoItem | undefined {
|
|
|
124
110
|
}
|
|
125
111
|
|
|
126
112
|
function buildPhaseFromInput(
|
|
127
|
-
input: { name: string; tasks?: Array<{ content: string; status?: TodoStatus;
|
|
113
|
+
input: { name: string; tasks?: Array<{ content: string; status?: TodoStatus; details?: string }> },
|
|
128
114
|
phaseId: string,
|
|
129
115
|
nextTaskId: number,
|
|
130
116
|
): { phase: TodoPhase; nextTaskId: number } {
|
|
@@ -135,7 +121,6 @@ function buildPhaseFromInput(
|
|
|
135
121
|
id: `task-${tid++}`,
|
|
136
122
|
content: t.content,
|
|
137
123
|
status: t.status ?? "pending",
|
|
138
|
-
notes: t.notes,
|
|
139
124
|
details: t.details,
|
|
140
125
|
});
|
|
141
126
|
}
|
|
@@ -207,79 +192,108 @@ export function getLatestTodoPhasesFromEntries(entries: SessionEntry[]): TodoPha
|
|
|
207
192
|
return [];
|
|
208
193
|
}
|
|
209
194
|
|
|
210
|
-
function
|
|
195
|
+
function resolveTaskOrError(phases: TodoPhase[], id: string, errors: string[]): TodoItem | undefined {
|
|
196
|
+
const task = findTask(phases, id);
|
|
197
|
+
if (!task) {
|
|
198
|
+
const totalTasks = phases.reduce((sum, p) => sum + p.tasks.length, 0);
|
|
199
|
+
const hint = totalTasks === 0 ? " (todo list is empty — was it replaced or not yet created?)" : "";
|
|
200
|
+
errors.push(`Task "${id}" not found${hint}`);
|
|
201
|
+
}
|
|
202
|
+
return task;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function applyParams(file: TodoFile, params: TodoWriteParams): { file: TodoFile; errors: string[] } {
|
|
211
206
|
const errors: string[] = [];
|
|
212
207
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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;
|
|
218
|
+
}
|
|
219
|
+
|
|
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;
|
|
225
|
+
}
|
|
226
226
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
233
|
}
|
|
234
|
+
target.tasks.push({
|
|
235
|
+
id: `task-${file.nextTaskId++}`,
|
|
236
|
+
content: entry.content,
|
|
237
|
+
status: "pending",
|
|
238
|
+
details: entry.details,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
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
|
+
}
|
|
248
|
+
}
|
|
234
249
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
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;
|
|
239
265
|
break;
|
|
240
266
|
}
|
|
241
|
-
target.tasks.push({
|
|
242
|
-
id: `task-${file.nextTaskId++}`,
|
|
243
|
-
content: op.content,
|
|
244
|
-
status: "pending",
|
|
245
|
-
notes: op.notes,
|
|
246
|
-
details: op.details,
|
|
247
|
-
});
|
|
248
|
-
break;
|
|
249
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
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
250
275
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
errors.push(`Task "${op.id}" not found${hint}`);
|
|
257
|
-
break;
|
|
258
|
-
}
|
|
259
|
-
if (op.status !== undefined) task.status = op.status;
|
|
260
|
-
if (op.content !== undefined) task.content = op.content;
|
|
261
|
-
if (op.notes !== undefined) task.notes = op.notes;
|
|
262
|
-
if (op.details !== undefined) task.details = op.details;
|
|
263
|
-
break;
|
|
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;
|
|
264
281
|
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
265
284
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
|
289
|
+
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";
|
|
274
293
|
}
|
|
275
294
|
}
|
|
276
|
-
if (!removed) {
|
|
277
|
-
const totalTasks = file.phases.reduce((sum, p) => sum + p.tasks.length, 0);
|
|
278
|
-
const hint = totalTasks === 0 ? " (todo list is empty)" : "";
|
|
279
|
-
errors.push(`Task "${op.id}" not found${hint}`);
|
|
280
|
-
}
|
|
281
|
-
break;
|
|
282
295
|
}
|
|
296
|
+
task.status = "in_progress";
|
|
283
297
|
}
|
|
284
298
|
}
|
|
285
299
|
|
|
@@ -318,6 +332,11 @@ function formatSummary(phases: TodoPhase[], errors: string[]): string {
|
|
|
318
332
|
lines.push(` ${line}`);
|
|
319
333
|
}
|
|
320
334
|
}
|
|
335
|
+
if (task.notes) {
|
|
336
|
+
for (const line of task.notes.split("\n")) {
|
|
337
|
+
lines.push(` Note: ${line}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
321
340
|
}
|
|
322
341
|
}
|
|
323
342
|
lines.push(
|
|
@@ -365,7 +384,7 @@ export class TodoWriteTool implements AgentTool<typeof todoWriteSchema, TodoWrit
|
|
|
365
384
|
): Promise<AgentToolResult<TodoWriteToolDetails>> {
|
|
366
385
|
const previousPhases = this.session.getTodoPhases?.() ?? [];
|
|
367
386
|
const current = fileFromPhases(previousPhases);
|
|
368
|
-
const { file: updated, errors } =
|
|
387
|
+
const { file: updated, errors } = applyParams(current, params);
|
|
369
388
|
this.session.setTodoPhases?.(updated.phases);
|
|
370
389
|
const storage = this.session.getSessionFile() ? "session" : "memory";
|
|
371
390
|
|
|
@@ -381,7 +400,14 @@ export class TodoWriteTool implements AgentTool<typeof todoWriteSchema, TodoWrit
|
|
|
381
400
|
// =============================================================================
|
|
382
401
|
|
|
383
402
|
interface TodoWriteRenderArgs {
|
|
384
|
-
|
|
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;
|
|
385
411
|
}
|
|
386
412
|
|
|
387
413
|
function formatTodoLine(item: TodoItem, uiTheme: Theme, prefix: string): string {
|
|
@@ -404,8 +430,16 @@ function formatTodoLine(item: TodoItem, uiTheme: Theme, prefix: string): string
|
|
|
404
430
|
|
|
405
431
|
export const todoWriteToolRenderer = {
|
|
406
432
|
renderCall(args: TodoWriteRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
407
|
-
const
|
|
408
|
-
|
|
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";
|
|
409
443
|
const text = renderStatusLine({ icon: "pending", title: "Todo Write", meta: [label] }, uiTheme);
|
|
410
444
|
return new Text(text, 0, 0);
|
|
411
445
|
},
|
package/src/tools/vim.ts
CHANGED
|
@@ -639,7 +639,7 @@ export class VimTool implements AgentTool<typeof vimSchema, VimToolDetails> {
|
|
|
639
639
|
|
|
640
640
|
await executeVimSteps(engine, steps, {
|
|
641
641
|
pauseLastStep: params.pause === true,
|
|
642
|
-
onKbdStep: emitUpdate ? () => emitUpdate() : undefined,
|
|
642
|
+
onKbdStep: emitUpdate ? () => emitUpdate(true) : undefined,
|
|
643
643
|
onInsertStep: emitUpdate ? () => emitUpdate(true) : undefined,
|
|
644
644
|
});
|
|
645
645
|
|
package/src/tools/write.ts
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
import { Database } from "bun:sqlite";
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import type {
|
|
5
|
-
AgentTool,
|
|
6
|
-
AgentToolContext,
|
|
7
|
-
AgentToolResult,
|
|
8
|
-
AgentToolUpdateCallback,
|
|
9
|
-
ToolCallContext,
|
|
10
|
-
} from "@oh-my-pi/pi-agent-core";
|
|
4
|
+
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
11
5
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
12
6
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
13
7
|
import { isEnoent, isRecord, prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
@@ -32,6 +26,7 @@ import {
|
|
|
32
26
|
formatMoreItems,
|
|
33
27
|
formatStatusIcon,
|
|
34
28
|
formatTitle,
|
|
29
|
+
getLspBatchRequest,
|
|
35
30
|
replaceTabs,
|
|
36
31
|
shortenPath,
|
|
37
32
|
} from "./render-utils";
|
|
@@ -61,22 +56,6 @@ export interface WriteToolDetails {
|
|
|
61
56
|
meta?: OutputMeta;
|
|
62
57
|
}
|
|
63
58
|
|
|
64
|
-
const LSP_BATCH_TOOLS = new Set(["edit", "write"]);
|
|
65
|
-
|
|
66
|
-
function getLspBatchRequest(toolCall: ToolCallContext | undefined): { id: string; flush: boolean } | undefined {
|
|
67
|
-
if (!toolCall) {
|
|
68
|
-
return undefined;
|
|
69
|
-
}
|
|
70
|
-
const hasOtherWrites = toolCall.toolCalls.some(
|
|
71
|
-
(call, index) => index !== toolCall.index && LSP_BATCH_TOOLS.has(call.name),
|
|
72
|
-
);
|
|
73
|
-
if (!hasOtherWrites) {
|
|
74
|
-
return undefined;
|
|
75
|
-
}
|
|
76
|
-
const hasLaterWrites = toolCall.toolCalls.slice(toolCall.index + 1).some(call => LSP_BATCH_TOOLS.has(call.name));
|
|
77
|
-
return { id: toolCall.batchId, flush: !hasLaterWrites };
|
|
78
|
-
}
|
|
79
|
-
|
|
80
59
|
/**
|
|
81
60
|
* Strip hashline display prefixes from write content.
|
|
82
61
|
*
|
package/src/tui/code-cell.ts
CHANGED
|
@@ -18,7 +18,7 @@ export interface CodeCellOptions {
|
|
|
18
18
|
index?: number;
|
|
19
19
|
total?: number;
|
|
20
20
|
title?: string;
|
|
21
|
-
status?: "pending" | "running" | "complete" | "error";
|
|
21
|
+
status?: "pending" | "running" | "warning" | "complete" | "error";
|
|
22
22
|
spinnerFrame?: number;
|
|
23
23
|
duration?: number;
|
|
24
24
|
output?: string;
|
|
@@ -32,6 +32,7 @@ function getState(status?: CodeCellOptions["status"]): State | undefined {
|
|
|
32
32
|
if (!status) return undefined;
|
|
33
33
|
if (status === "complete") return "success";
|
|
34
34
|
if (status === "error") return "error";
|
|
35
|
+
if (status === "warning") return "warning";
|
|
35
36
|
if (status === "running") return "running";
|
|
36
37
|
return "pending";
|
|
37
38
|
}
|
|
@@ -45,9 +46,11 @@ function formatHeader(options: CodeCellOptions, theme: Theme): { title: string;
|
|
|
45
46
|
? "success"
|
|
46
47
|
: status === "error"
|
|
47
48
|
? "error"
|
|
48
|
-
: status === "
|
|
49
|
-
? "
|
|
50
|
-
: "
|
|
49
|
+
: status === "warning"
|
|
50
|
+
? "warning"
|
|
51
|
+
: status === "running"
|
|
52
|
+
? "running"
|
|
53
|
+
: "pending",
|
|
51
54
|
theme,
|
|
52
55
|
spinnerFrame,
|
|
53
56
|
);
|
|
@@ -78,10 +81,12 @@ export function renderCodeCell(options: CodeCellOptions, theme: Theme): string[]
|
|
|
78
81
|
const { title, meta } = formatHeader(options, theme);
|
|
79
82
|
const state = getState(options.status);
|
|
80
83
|
|
|
81
|
-
const
|
|
84
|
+
const normalizedCode = replaceTabs(code ?? "");
|
|
85
|
+
const rawCodeLines = normalizedCode.split("\n");
|
|
82
86
|
const maxCodeLines = expanded ? rawCodeLines.length : Math.min(rawCodeLines.length, codeMaxLines);
|
|
83
|
-
const
|
|
84
|
-
const
|
|
87
|
+
const visibleCode = rawCodeLines.slice(0, maxCodeLines).join("\n");
|
|
88
|
+
const codeLines = highlightCode(visibleCode, language);
|
|
89
|
+
const hiddenCodeLines = rawCodeLines.length - maxCodeLines;
|
|
85
90
|
if (hiddenCodeLines > 0) {
|
|
86
91
|
const hint = formatExpandHint(theme, expanded, hiddenCodeLines > 0);
|
|
87
92
|
const moreLine = `${formatMoreItems(hiddenCodeLines, "line")}${hint ? ` ${hint}` : ""}`;
|
package/src/utils/edit-mode.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { $env, $flag } from "@oh-my-pi/pi-utils";
|
|
2
2
|
|
|
3
|
-
export type EditMode = "replace" | "patch" | "hashline" | "chunk" | "vim";
|
|
3
|
+
export type EditMode = "replace" | "patch" | "hashline" | "chunk" | "vim" | "apply_patch";
|
|
4
4
|
|
|
5
5
|
export const DEFAULT_EDIT_MODE: EditMode = "hashline";
|
|
6
6
|
|
|
7
7
|
const EDIT_MODE_IDS = {
|
|
8
|
+
apply_patch: "apply_patch",
|
|
8
9
|
chunk: "chunk",
|
|
9
10
|
hashline: "hashline",
|
|
10
11
|
patch: "patch",
|
|
@@ -38,7 +39,7 @@ export function resolveEditMode(session: EditModeSessionLike): EditMode {
|
|
|
38
39
|
if (envMode) return envMode;
|
|
39
40
|
|
|
40
41
|
if (!$flag("PI_STRICT_EDIT_MODE")) {
|
|
41
|
-
if (activeModel?.includes("spark")) return "
|
|
42
|
+
if (activeModel?.includes("spark")) return "apply_patch";
|
|
42
43
|
if (activeModel?.includes("nano")) return "replace";
|
|
43
44
|
if (activeModel?.includes("mini")) return "replace";
|
|
44
45
|
if (activeModel?.includes("haiku")) return "replace";
|
package/src/utils/git.ts
CHANGED
|
@@ -190,7 +190,7 @@ async function runCommand(
|
|
|
190
190
|
const commandArgs = withShortLivedGitConfig(options.readOnly ? withNoOptionalLocks(args) : [...args]);
|
|
191
191
|
const child = Bun.spawn(["git", ...commandArgs], {
|
|
192
192
|
cwd,
|
|
193
|
-
env: options.env ? { ...process.env, ...options.env } : undefined,
|
|
193
|
+
env: options.env ? { ...process.env, GIT_OPTIONAL_LOCKS: "0", ...options.env } : undefined,
|
|
194
194
|
signal: options.signal,
|
|
195
195
|
stdin: normalizeStdin(options.stdin),
|
|
196
196
|
stdout: "pipe",
|
package/src/vim/engine.ts
CHANGED
|
@@ -294,6 +294,23 @@ function findParagraphEnd(lines: string[], line: number): number {
|
|
|
294
294
|
return index;
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
+
function cloneUndoStack(stack: VimUndoEntry[]): VimUndoEntry[] {
|
|
298
|
+
return stack.map(entry => ({
|
|
299
|
+
before: {
|
|
300
|
+
...entry.before,
|
|
301
|
+
lines: [...entry.before.lines],
|
|
302
|
+
cursor: clonePosition(entry.before.cursor),
|
|
303
|
+
baseFingerprint: entry.before.baseFingerprint ? { ...entry.before.baseFingerprint } : null,
|
|
304
|
+
},
|
|
305
|
+
after: {
|
|
306
|
+
...entry.after,
|
|
307
|
+
lines: [...entry.after.lines],
|
|
308
|
+
cursor: clonePosition(entry.after.cursor),
|
|
309
|
+
baseFingerprint: entry.after.baseFingerprint ? { ...entry.after.baseFingerprint } : null,
|
|
310
|
+
},
|
|
311
|
+
}));
|
|
312
|
+
}
|
|
313
|
+
|
|
297
314
|
export class VimEngine {
|
|
298
315
|
buffer: VimBuffer;
|
|
299
316
|
inputMode: VimInputMode = "normal";
|
|
@@ -361,34 +378,8 @@ export class VimEngine {
|
|
|
361
378
|
inserted: this.#pendingChange.inserted,
|
|
362
379
|
}
|
|
363
380
|
: null;
|
|
364
|
-
next.#undoStack = this.#undoStack
|
|
365
|
-
|
|
366
|
-
...entry.before,
|
|
367
|
-
lines: [...entry.before.lines],
|
|
368
|
-
cursor: clonePosition(entry.before.cursor),
|
|
369
|
-
baseFingerprint: entry.before.baseFingerprint ? { ...entry.before.baseFingerprint } : null,
|
|
370
|
-
},
|
|
371
|
-
after: {
|
|
372
|
-
...entry.after,
|
|
373
|
-
lines: [...entry.after.lines],
|
|
374
|
-
cursor: clonePosition(entry.after.cursor),
|
|
375
|
-
baseFingerprint: entry.after.baseFingerprint ? { ...entry.after.baseFingerprint } : null,
|
|
376
|
-
},
|
|
377
|
-
}));
|
|
378
|
-
next.#redoStack = this.#redoStack.map(entry => ({
|
|
379
|
-
before: {
|
|
380
|
-
...entry.before,
|
|
381
|
-
lines: [...entry.before.lines],
|
|
382
|
-
cursor: clonePosition(entry.before.cursor),
|
|
383
|
-
baseFingerprint: entry.before.baseFingerprint ? { ...entry.before.baseFingerprint } : null,
|
|
384
|
-
},
|
|
385
|
-
after: {
|
|
386
|
-
...entry.after,
|
|
387
|
-
lines: [...entry.after.lines],
|
|
388
|
-
cursor: clonePosition(entry.after.cursor),
|
|
389
|
-
baseFingerprint: entry.after.baseFingerprint ? { ...entry.after.baseFingerprint } : null,
|
|
390
|
-
},
|
|
391
|
-
}));
|
|
381
|
+
next.#undoStack = cloneUndoStack(this.#undoStack);
|
|
382
|
+
next.#redoStack = cloneUndoStack(this.#redoStack);
|
|
392
383
|
return next;
|
|
393
384
|
}
|
|
394
385
|
|
|
@@ -913,22 +904,14 @@ export class VimEngine {
|
|
|
913
904
|
}
|
|
914
905
|
case "d": {
|
|
915
906
|
await this.#applyAtomicChange(tokens, () => {
|
|
916
|
-
this
|
|
917
|
-
kind: visual.linewise ? "line" : "char",
|
|
918
|
-
text: this.buffer.getText().slice(visual.start, visual.end),
|
|
919
|
-
};
|
|
920
|
-
this.buffer.deleteOffsets(visual.start, visual.end);
|
|
907
|
+
this.#yankAndDeleteRange(visual);
|
|
921
908
|
});
|
|
922
909
|
this.#clearSelection();
|
|
923
910
|
return;
|
|
924
911
|
}
|
|
925
912
|
case "c": {
|
|
926
913
|
await this.#startInsertChange(tokens, () => {
|
|
927
|
-
this
|
|
928
|
-
kind: visual.linewise ? "line" : "char",
|
|
929
|
-
text: this.buffer.getText().slice(visual.start, visual.end),
|
|
930
|
-
};
|
|
931
|
-
this.buffer.deleteOffsets(visual.start, visual.end);
|
|
914
|
+
this.#yankAndDeleteRange(visual);
|
|
932
915
|
});
|
|
933
916
|
this.#clearSelection();
|
|
934
917
|
return;
|
|
@@ -1106,11 +1089,7 @@ export class VimEngine {
|
|
|
1106
1089
|
return nextIndex + 1;
|
|
1107
1090
|
case "s":
|
|
1108
1091
|
await this.#startInsertChange(["s"], () => {
|
|
1109
|
-
|
|
1110
|
-
this.register = {
|
|
1111
|
-
kind: "char",
|
|
1112
|
-
text: this.buffer.deleteOffsets(start, Math.min(this.buffer.getText().length, start + count)),
|
|
1113
|
-
};
|
|
1092
|
+
this.#deleteCharsForward(count);
|
|
1114
1093
|
});
|
|
1115
1094
|
return nextIndex + 1;
|
|
1116
1095
|
case "S":
|
|
@@ -1118,11 +1097,7 @@ export class VimEngine {
|
|
|
1118
1097
|
return nextIndex + 1;
|
|
1119
1098
|
case "x":
|
|
1120
1099
|
await this.#applyAtomicChange(["x"], () => {
|
|
1121
|
-
|
|
1122
|
-
this.register = {
|
|
1123
|
-
kind: "char",
|
|
1124
|
-
text: this.buffer.deleteOffsets(start, Math.min(this.buffer.getText().length, start + count)),
|
|
1125
|
-
};
|
|
1100
|
+
this.#deleteCharsForward(count);
|
|
1126
1101
|
});
|
|
1127
1102
|
return nextIndex + 1;
|
|
1128
1103
|
case "X":
|
|
@@ -1559,11 +1534,7 @@ export class VimEngine {
|
|
|
1559
1534
|
if (operator === "d") {
|
|
1560
1535
|
const range = this.#resolveMotionRange(motion);
|
|
1561
1536
|
await this.#applyAtomicChange(tokens, () => {
|
|
1562
|
-
this
|
|
1563
|
-
kind: range.linewise ? "line" : "char",
|
|
1564
|
-
text: this.buffer.getText().slice(range.start, range.end),
|
|
1565
|
-
};
|
|
1566
|
-
this.buffer.deleteOffsets(range.start, range.end);
|
|
1537
|
+
this.#yankAndDeleteRange(range);
|
|
1567
1538
|
});
|
|
1568
1539
|
return;
|
|
1569
1540
|
}
|
|
@@ -1571,16 +1542,28 @@ export class VimEngine {
|
|
|
1571
1542
|
if (operator === "c") {
|
|
1572
1543
|
const range = this.#resolveMotionRange(motion);
|
|
1573
1544
|
await this.#startInsertChange(tokens, () => {
|
|
1574
|
-
this
|
|
1575
|
-
kind: range.linewise ? "line" : "char",
|
|
1576
|
-
text: this.buffer.getText().slice(range.start, range.end),
|
|
1577
|
-
};
|
|
1578
|
-
this.buffer.deleteOffsets(range.start, range.end);
|
|
1545
|
+
this.#yankAndDeleteRange(range);
|
|
1579
1546
|
});
|
|
1580
1547
|
return;
|
|
1581
1548
|
}
|
|
1582
1549
|
}
|
|
1583
1550
|
|
|
1551
|
+
#yankAndDeleteRange(range: { start: number; end: number; linewise: boolean }): void {
|
|
1552
|
+
this.register = {
|
|
1553
|
+
kind: range.linewise ? "line" : "char",
|
|
1554
|
+
text: this.buffer.getText().slice(range.start, range.end),
|
|
1555
|
+
};
|
|
1556
|
+
this.buffer.deleteOffsets(range.start, range.end);
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
#deleteCharsForward(count: number): void {
|
|
1560
|
+
const start = this.buffer.currentOffset();
|
|
1561
|
+
this.register = {
|
|
1562
|
+
kind: "char",
|
|
1563
|
+
text: this.buffer.deleteOffsets(start, Math.min(this.buffer.getText().length, start + count)),
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1584
1567
|
async #changeWholeLines(count: number, tokens: readonly string[]): Promise<void> {
|
|
1585
1568
|
await this.#startInsertChange(tokens, () => {
|
|
1586
1569
|
const start = this.buffer.cursor.line;
|