@oh-my-pi/pi-coding-agent 6.2.0 → 6.7.67
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 +60 -0
- package/docs/sdk.md +1 -1
- package/package.json +5 -5
- package/scripts/generate-template.ts +6 -6
- package/src/cli/args.ts +3 -0
- package/src/core/agent-session.ts +39 -0
- package/src/core/bash-executor.ts +3 -3
- package/src/core/cursor/exec-bridge.ts +95 -88
- package/src/core/custom-commands/bundled/review/index.ts +142 -145
- package/src/core/custom-commands/bundled/wt/index.ts +68 -66
- package/src/core/custom-commands/loader.ts +4 -6
- package/src/core/custom-tools/index.ts +2 -2
- package/src/core/custom-tools/loader.ts +66 -61
- package/src/core/custom-tools/types.ts +4 -4
- package/src/core/custom-tools/wrapper.ts +61 -25
- package/src/core/event-bus.ts +19 -47
- package/src/core/extensions/index.ts +8 -4
- package/src/core/extensions/loader.ts +160 -120
- package/src/core/extensions/types.ts +4 -4
- package/src/core/extensions/wrapper.ts +149 -100
- package/src/core/hooks/index.ts +1 -1
- package/src/core/hooks/tool-wrapper.ts +96 -70
- package/src/core/hooks/types.ts +1 -2
- package/src/core/index.ts +1 -0
- package/src/core/mcp/index.ts +6 -2
- package/src/core/mcp/json-rpc.ts +88 -0
- package/src/core/mcp/loader.ts +22 -4
- package/src/core/mcp/manager.ts +202 -48
- package/src/core/mcp/tool-bridge.ts +143 -55
- package/src/core/mcp/tool-cache.ts +122 -0
- package/src/core/python-executor.ts +3 -9
- package/src/core/sdk.ts +33 -32
- package/src/core/session-manager.ts +30 -0
- package/src/core/settings-manager.ts +54 -1
- package/src/core/ssh/ssh-executor.ts +6 -84
- package/src/core/streaming-output.ts +107 -53
- package/src/core/tools/ask.ts +92 -93
- package/src/core/tools/bash.ts +103 -94
- package/src/core/tools/calculator.ts +41 -26
- package/src/core/tools/complete.ts +76 -66
- package/src/core/tools/context.ts +22 -24
- package/src/core/tools/exa/index.ts +1 -1
- package/src/core/tools/exa/mcp-client.ts +56 -101
- package/src/core/tools/find.ts +250 -253
- package/src/core/tools/git.ts +39 -33
- package/src/core/tools/grep.ts +440 -427
- package/src/core/tools/index.ts +63 -61
- package/src/core/tools/ls.ts +119 -114
- package/src/core/tools/lsp/clients/biome-client.ts +5 -7
- package/src/core/tools/lsp/clients/index.ts +4 -4
- package/src/core/tools/lsp/clients/lsp-linter-client.ts +5 -7
- package/src/core/tools/lsp/config.ts +2 -2
- package/src/core/tools/lsp/index.ts +604 -578
- package/src/core/tools/notebook.ts +121 -119
- package/src/core/tools/output.ts +163 -147
- package/src/core/tools/patch/applicator.ts +1100 -0
- package/src/core/tools/patch/diff.ts +362 -0
- package/src/core/tools/patch/fuzzy.ts +647 -0
- package/src/core/tools/patch/index.ts +430 -0
- package/src/core/tools/patch/normalize.ts +220 -0
- package/src/core/tools/patch/normative.ts +73 -0
- package/src/core/tools/patch/parser.ts +528 -0
- package/src/core/tools/patch/shared.ts +257 -0
- package/src/core/tools/patch/types.ts +244 -0
- package/src/core/tools/python.ts +139 -136
- package/src/core/tools/read.ts +239 -216
- package/src/core/tools/render-utils.ts +196 -77
- package/src/core/tools/renderers.ts +6 -2
- package/src/core/tools/ssh.ts +99 -80
- package/src/core/tools/task/executor.ts +11 -7
- package/src/core/tools/task/index.ts +352 -343
- package/src/core/tools/task/worker.ts +13 -23
- package/src/core/tools/todo-write.ts +74 -59
- package/src/core/tools/web-fetch.ts +54 -47
- package/src/core/tools/web-search/index.ts +27 -16
- package/src/core/tools/write.ts +108 -47
- package/src/core/ttsr.ts +106 -152
- package/src/core/voice.ts +49 -39
- package/src/index.ts +16 -12
- package/src/lib/worktree/index.ts +1 -9
- package/src/modes/interactive/components/diff.ts +15 -8
- package/src/modes/interactive/components/settings-defs.ts +42 -0
- package/src/modes/interactive/components/tool-execution.ts +46 -8
- package/src/modes/interactive/controllers/event-controller.ts +6 -19
- package/src/modes/interactive/controllers/input-controller.ts +1 -1
- package/src/modes/interactive/utils/ui-helpers.ts +5 -1
- package/src/modes/rpc/rpc-mode.ts +99 -81
- package/src/prompts/tools/patch.md +76 -0
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/{edit.md → replace.md} +1 -0
- package/src/utils/shell.ts +0 -40
- package/src/core/tools/edit-diff.ts +0 -574
- package/src/core/tools/edit.ts +0 -345
|
@@ -443,30 +443,20 @@ const isAgentEvent = (event: AgentSessionEvent): event is AgentEvent => {
|
|
|
443
443
|
return agentEventTypes.has(event.type as AgentEvent["type"]);
|
|
444
444
|
};
|
|
445
445
|
|
|
446
|
-
|
|
447
|
-
abortController
|
|
448
|
-
startTime
|
|
449
|
-
session: { abort: () => Promise<void>; dispose: () => Promise<void> } | null;
|
|
450
|
-
unsubscribe: (() => void) | null;
|
|
451
|
-
sendDoneOnce: (message: Extract<SubagentWorkerResponse, { type: "done" }>) => void;
|
|
452
|
-
}
|
|
446
|
+
class RunState {
|
|
447
|
+
abortController = new AbortController();
|
|
448
|
+
startTime = Date.now();
|
|
449
|
+
session: { abort: () => Promise<void>; dispose: () => Promise<void> } | null = null;
|
|
450
|
+
unsubscribe: (() => void) | null = null;
|
|
453
451
|
|
|
454
|
-
|
|
455
|
-
let sent = false;
|
|
456
|
-
return (message) => {
|
|
457
|
-
if (sent) return;
|
|
458
|
-
sent = true;
|
|
459
|
-
postMessageSafe(message);
|
|
460
|
-
};
|
|
461
|
-
};
|
|
452
|
+
private doneSent = false;
|
|
462
453
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
});
|
|
454
|
+
sendDoneOnce(message: Extract<SubagentWorkerResponse, { type: "done" }>): void {
|
|
455
|
+
if (this.doneSent) return;
|
|
456
|
+
this.doneSent = true;
|
|
457
|
+
postMessageSafe(message);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
470
460
|
|
|
471
461
|
let activeRun: RunState | null = null;
|
|
472
462
|
let pendingAbort = false;
|
|
@@ -861,7 +851,7 @@ globalThis.addEventListener("message", (event: WorkerMessageEvent<SubagentWorker
|
|
|
861
851
|
if (message.type === "start") {
|
|
862
852
|
// Only allow one task per worker
|
|
863
853
|
if (activeRun) return;
|
|
864
|
-
const runState =
|
|
854
|
+
const runState = new RunState();
|
|
865
855
|
if (pendingAbort) {
|
|
866
856
|
pendingAbort = false;
|
|
867
857
|
runState.abortController.abort();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { mkdirSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
4
|
+
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
5
5
|
import { StringEnum } from "@oh-my-pi/pi-ai";
|
|
6
6
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
7
7
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
@@ -49,6 +49,8 @@ export interface TodoWriteToolDetails {
|
|
|
49
49
|
|
|
50
50
|
const TODO_FILE_NAME = "todos.json";
|
|
51
51
|
|
|
52
|
+
type TodoWriteParams = { todos: Array<{ id?: string; content?: string; activeForm?: string; status?: string }> };
|
|
53
|
+
|
|
52
54
|
function normalizeTodoStatus(status?: string): TodoStatus {
|
|
53
55
|
switch (status) {
|
|
54
56
|
case "in_progress":
|
|
@@ -137,6 +139,14 @@ async function saveTodoFile(filePath: string, data: TodoFile): Promise<void> {
|
|
|
137
139
|
await Bun.write(filePath, JSON.stringify(data, null, 2));
|
|
138
140
|
}
|
|
139
141
|
|
|
142
|
+
function formatTodoSummary(todos: TodoItem[]): string {
|
|
143
|
+
if (todos.length === 0) return "Todo list cleared.";
|
|
144
|
+
const completed = todos.filter((t) => t.status === "completed").length;
|
|
145
|
+
const inProgress = todos.filter((t) => t.status === "in_progress").length;
|
|
146
|
+
const pending = todos.filter((t) => t.status === "pending").length;
|
|
147
|
+
return `Saved ${todos.length} todos (${pending} pending, ${inProgress} in progress, ${completed} completed).`;
|
|
148
|
+
}
|
|
149
|
+
|
|
140
150
|
function formatTodoLine(item: TodoItem, uiTheme: Theme, prefix: string): string {
|
|
141
151
|
const checkbox = uiTheme.checkbox;
|
|
142
152
|
const displayText =
|
|
@@ -151,71 +161,76 @@ function formatTodoLine(item: TodoItem, uiTheme: Theme, prefix: string): string
|
|
|
151
161
|
}
|
|
152
162
|
}
|
|
153
163
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const inProgress = todos.filter((t) => t.status === "in_progress").length;
|
|
158
|
-
const pending = todos.filter((t) => t.status === "pending").length;
|
|
159
|
-
return `Saved ${todos.length} todos (${pending} pending, ${inProgress} in progress, ${completed} completed).`;
|
|
160
|
-
}
|
|
164
|
+
// =============================================================================
|
|
165
|
+
// Tool Class
|
|
166
|
+
// =============================================================================
|
|
161
167
|
|
|
162
|
-
export
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
parameters: todoWriteSchema,
|
|
168
|
-
execute: async (
|
|
169
|
-
_toolCallId: string,
|
|
170
|
-
params: { todos: Array<{ id?: string; content?: string; activeForm?: string; status?: string }> },
|
|
171
|
-
) => {
|
|
172
|
-
const todos = normalizeTodos(params.todos ?? []);
|
|
173
|
-
const validation = validateSequentialTodos(todos);
|
|
174
|
-
if (!validation.valid) {
|
|
175
|
-
throw new Error(validation.error ?? "Todos must be completed sequentially.");
|
|
176
|
-
}
|
|
177
|
-
const updatedAt = Date.now();
|
|
168
|
+
export class TodoWriteTool implements AgentTool<typeof todoWriteSchema, TodoWriteToolDetails> {
|
|
169
|
+
public readonly name = "todo_write";
|
|
170
|
+
public readonly label = "Todo Write";
|
|
171
|
+
public readonly description: string;
|
|
172
|
+
public readonly parameters = todoWriteSchema;
|
|
178
173
|
|
|
179
|
-
|
|
180
|
-
if (!sessionFile) {
|
|
181
|
-
return {
|
|
182
|
-
content: [{ type: "text", text: formatTodoSummary(todos) }],
|
|
183
|
-
details: { todos, updatedAt, storage: "memory" },
|
|
184
|
-
};
|
|
185
|
-
}
|
|
174
|
+
private readonly session: ToolSession;
|
|
186
175
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
details: { todos, updatedAt, storage: "memory" },
|
|
192
|
-
};
|
|
193
|
-
}
|
|
176
|
+
constructor(session: ToolSession) {
|
|
177
|
+
this.session = session;
|
|
178
|
+
this.description = renderPromptTemplate(todoWriteDescription);
|
|
179
|
+
}
|
|
194
180
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
181
|
+
public async execute(
|
|
182
|
+
_toolCallId: string,
|
|
183
|
+
params: TodoWriteParams,
|
|
184
|
+
_signal?: AbortSignal,
|
|
185
|
+
_onUpdate?: AgentToolUpdateCallback<TodoWriteToolDetails>,
|
|
186
|
+
_context?: AgentToolContext,
|
|
187
|
+
): Promise<AgentToolResult<TodoWriteToolDetails>> {
|
|
188
|
+
const todos = normalizeTodos(params.todos ?? []);
|
|
189
|
+
const validation = validateSequentialTodos(todos);
|
|
190
|
+
if (!validation.valid) {
|
|
191
|
+
throw new Error(validation.error ?? "Todos must be completed sequentially.");
|
|
192
|
+
}
|
|
193
|
+
const updatedAt = Date.now();
|
|
194
|
+
|
|
195
|
+
const sessionFile = this.session.getSessionFile();
|
|
196
|
+
if (!sessionFile) {
|
|
197
|
+
return {
|
|
198
|
+
content: [{ type: "text", text: formatTodoSummary(todos) }],
|
|
199
|
+
details: { todos, updatedAt, storage: "memory" },
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const artifactsDir = getArtifactsDir(sessionFile);
|
|
204
|
+
if (!artifactsDir) {
|
|
205
|
+
return {
|
|
206
|
+
content: [{ type: "text", text: formatTodoSummary(todos) }],
|
|
207
|
+
details: { todos, updatedAt, storage: "memory" },
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
ensureArtifactsDir(artifactsDir);
|
|
212
|
+
const todoPath = path.join(artifactsDir, TODO_FILE_NAME);
|
|
213
|
+
const existing = await loadTodoFile(todoPath);
|
|
214
|
+
const storedTodos = existing?.todos ?? [];
|
|
215
|
+
const merged = todos.length > 0 ? todos : [];
|
|
216
|
+
const fileData: TodoFile = { updatedAt, todos: merged };
|
|
212
217
|
|
|
218
|
+
try {
|
|
219
|
+
mkdirSync(artifactsDir, { recursive: true });
|
|
220
|
+
await saveTodoFile(todoPath, fileData);
|
|
221
|
+
} catch (error) {
|
|
222
|
+
logger.error("Failed to write todo file", { path: todoPath, error: String(error) });
|
|
213
223
|
return {
|
|
214
|
-
content: [{ type: "text", text:
|
|
215
|
-
details: { todos:
|
|
224
|
+
content: [{ type: "text", text: "Failed to save todos." }],
|
|
225
|
+
details: { todos: storedTodos, updatedAt, storage: "session" },
|
|
216
226
|
};
|
|
217
|
-
}
|
|
218
|
-
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
content: [{ type: "text", text: formatTodoSummary(merged) }],
|
|
231
|
+
details: { todos: merged, updatedAt, storage: "session" },
|
|
232
|
+
};
|
|
233
|
+
}
|
|
219
234
|
}
|
|
220
235
|
|
|
221
236
|
// =============================================================================
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { tmpdir } from "node:os";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
3
|
+
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
4
4
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
5
5
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
6
|
-
import { Type } from "@sinclair/typebox";
|
|
6
|
+
import { type Static, Type } from "@sinclair/typebox";
|
|
7
7
|
import { nanoid } from "nanoid";
|
|
8
8
|
import { parse as parseHtml } from "node-html-parser";
|
|
9
9
|
import { type Theme, theme } from "../../modes/interactive/theme/theme";
|
|
@@ -848,55 +848,62 @@ export interface WebFetchToolDetails {
|
|
|
848
848
|
notes: string[];
|
|
849
849
|
}
|
|
850
850
|
|
|
851
|
-
export
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
parameters: webFetchSchema,
|
|
857
|
-
execute: async (
|
|
858
|
-
_toolCallId: string,
|
|
859
|
-
{ url, timeout = DEFAULT_TIMEOUT, raw = false }: { url: string; timeout?: number; raw?: boolean },
|
|
860
|
-
signal?: AbortSignal,
|
|
861
|
-
) => {
|
|
862
|
-
if (signal?.aborted) {
|
|
863
|
-
throw new Error("Operation aborted");
|
|
864
|
-
}
|
|
851
|
+
export class WebFetchTool implements AgentTool<typeof webFetchSchema, WebFetchToolDetails> {
|
|
852
|
+
public readonly name = "web_fetch";
|
|
853
|
+
public readonly label = "Web Fetch";
|
|
854
|
+
public readonly description: string;
|
|
855
|
+
public readonly parameters = webFetchSchema;
|
|
865
856
|
|
|
866
|
-
|
|
867
|
-
|
|
857
|
+
constructor(_session: ToolSession) {
|
|
858
|
+
this.description = renderPromptTemplate(webFetchDescription);
|
|
859
|
+
}
|
|
868
860
|
|
|
869
|
-
|
|
861
|
+
public async execute(
|
|
862
|
+
_toolCallId: string,
|
|
863
|
+
params: Static<typeof webFetchSchema>,
|
|
864
|
+
signal?: AbortSignal,
|
|
865
|
+
_onUpdate?: AgentToolUpdateCallback<WebFetchToolDetails>,
|
|
866
|
+
_context?: AgentToolContext,
|
|
867
|
+
): Promise<AgentToolResult<WebFetchToolDetails>> {
|
|
868
|
+
const { url, timeout = DEFAULT_TIMEOUT, raw = false } = params;
|
|
870
869
|
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
output += `Content-Type: ${result.contentType}\n`;
|
|
875
|
-
output += `Method: ${result.method}\n`;
|
|
876
|
-
if (result.truncated) {
|
|
877
|
-
output += `Warning: Output was truncated\n`;
|
|
878
|
-
}
|
|
879
|
-
if (result.notes.length > 0) {
|
|
880
|
-
output += `Notes: ${result.notes.join("; ")}\n`;
|
|
881
|
-
}
|
|
882
|
-
output += `\n---\n\n`;
|
|
883
|
-
output += result.content;
|
|
884
|
-
|
|
885
|
-
const details: WebFetchToolDetails = {
|
|
886
|
-
url: result.url,
|
|
887
|
-
finalUrl: result.finalUrl,
|
|
888
|
-
contentType: result.contentType,
|
|
889
|
-
method: result.method,
|
|
890
|
-
truncated: result.truncated,
|
|
891
|
-
notes: result.notes,
|
|
892
|
-
};
|
|
870
|
+
if (signal?.aborted) {
|
|
871
|
+
throw new Error("Operation aborted");
|
|
872
|
+
}
|
|
893
873
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
874
|
+
// Clamp timeout
|
|
875
|
+
const effectiveTimeout = Math.min(Math.max(timeout, 1), 120);
|
|
876
|
+
|
|
877
|
+
const result = await renderUrl(url, effectiveTimeout, raw, signal);
|
|
878
|
+
|
|
879
|
+
// Format output
|
|
880
|
+
let output = "";
|
|
881
|
+
output += `URL: ${result.finalUrl}\n`;
|
|
882
|
+
output += `Content-Type: ${result.contentType}\n`;
|
|
883
|
+
output += `Method: ${result.method}\n`;
|
|
884
|
+
if (result.truncated) {
|
|
885
|
+
output += `Warning: Output was truncated\n`;
|
|
886
|
+
}
|
|
887
|
+
if (result.notes.length > 0) {
|
|
888
|
+
output += `Notes: ${result.notes.join("; ")}\n`;
|
|
889
|
+
}
|
|
890
|
+
output += `\n---\n\n`;
|
|
891
|
+
output += result.content;
|
|
892
|
+
|
|
893
|
+
const details: WebFetchToolDetails = {
|
|
894
|
+
url: result.url,
|
|
895
|
+
finalUrl: result.finalUrl,
|
|
896
|
+
contentType: result.contentType,
|
|
897
|
+
method: result.method,
|
|
898
|
+
truncated: result.truncated,
|
|
899
|
+
notes: result.notes,
|
|
900
|
+
};
|
|
901
|
+
|
|
902
|
+
return {
|
|
903
|
+
content: [{ type: "text", text: output }],
|
|
904
|
+
details,
|
|
905
|
+
};
|
|
906
|
+
}
|
|
900
907
|
}
|
|
901
908
|
|
|
902
909
|
// =============================================================================
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* - web_search_company: Comprehensive company research
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
15
|
+
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
16
16
|
import { StringEnum } from "@oh-my-pi/pi-ai";
|
|
17
17
|
import { Type } from "@sinclair/typebox";
|
|
18
18
|
import type { Theme } from "../../../modes/interactive/theme/theme";
|
|
@@ -330,16 +330,32 @@ async function executeWebSearch(
|
|
|
330
330
|
};
|
|
331
331
|
}
|
|
332
332
|
|
|
333
|
-
/**
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
333
|
+
/**
|
|
334
|
+
* Web search tool implementation.
|
|
335
|
+
*
|
|
336
|
+
* Supports Anthropic, Perplexity, and Exa providers with automatic fallback.
|
|
337
|
+
* Session is accepted for interface consistency but not used.
|
|
338
|
+
*/
|
|
339
|
+
export class WebSearchTool implements AgentTool<typeof webSearchSchema, WebSearchRenderDetails> {
|
|
340
|
+
public readonly name = "web_search";
|
|
341
|
+
public readonly label = "Web Search";
|
|
342
|
+
public readonly description: string;
|
|
343
|
+
public readonly parameters = webSearchSchema;
|
|
344
|
+
|
|
345
|
+
constructor(_session: ToolSession) {
|
|
346
|
+
this.description = renderPromptTemplate(webSearchDescription);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
public async execute(
|
|
350
|
+
_toolCallId: string,
|
|
351
|
+
params: WebSearchParams,
|
|
352
|
+
_signal?: AbortSignal,
|
|
353
|
+
_onUpdate?: AgentToolUpdateCallback<WebSearchRenderDetails>,
|
|
354
|
+
_context?: AgentToolContext,
|
|
355
|
+
): Promise<AgentToolResult<WebSearchRenderDetails>> {
|
|
356
|
+
return executeWebSearch(_toolCallId, params);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
343
359
|
|
|
344
360
|
/** Web search tool as CustomTool (for TUI rendering support) */
|
|
345
361
|
export const webSearchCustomTool: CustomTool<typeof webSearchSchema, WebSearchRenderDetails> = {
|
|
@@ -367,11 +383,6 @@ export const webSearchCustomTool: CustomTool<typeof webSearchSchema, WebSearchRe
|
|
|
367
383
|
},
|
|
368
384
|
};
|
|
369
385
|
|
|
370
|
-
/** Factory function for backward compatibility */
|
|
371
|
-
export function createWebSearchTool(_session: ToolSession): AgentTool<typeof webSearchSchema> {
|
|
372
|
-
return webSearchTool;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
386
|
// ============================================================================
|
|
376
387
|
// Exa-specific tools (available when EXA_API_KEY is present)
|
|
377
388
|
// ============================================================================
|
package/src/core/tools/write.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
AgentTool,
|
|
3
|
+
AgentToolContext,
|
|
4
|
+
AgentToolResult,
|
|
5
|
+
AgentToolUpdateCallback,
|
|
6
|
+
ToolCallContext,
|
|
7
|
+
} from "@oh-my-pi/pi-agent-core";
|
|
2
8
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
3
9
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
4
10
|
import { Type } from "@sinclair/typebox";
|
|
@@ -8,9 +14,15 @@ import type { RenderResultOptions } from "../custom-tools/types";
|
|
|
8
14
|
import { renderPromptTemplate } from "../prompt-templates";
|
|
9
15
|
import type { ToolSession } from "../sdk";
|
|
10
16
|
import { untilAborted } from "../utils";
|
|
11
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
createLspWritethrough,
|
|
19
|
+
type FileDiagnosticsResult,
|
|
20
|
+
type WritethroughCallback,
|
|
21
|
+
writethroughNoop,
|
|
22
|
+
} from "./lsp/index";
|
|
12
23
|
import { resolveToCwd } from "./path-utils";
|
|
13
|
-
import { formatDiagnostics, formatExpandHint, replaceTabs, shortenPath } from "./render-utils";
|
|
24
|
+
import { formatDiagnostics, formatExpandHint, formatStatusIcon, replaceTabs, shortenPath } from "./render-utils";
|
|
25
|
+
import type { RenderCallOptions } from "./renderers";
|
|
14
26
|
|
|
15
27
|
const writeSchema = Type.Object({
|
|
16
28
|
path: Type.String({ description: "Path to the file to write (relative or absolute)" }),
|
|
@@ -38,51 +50,69 @@ function getLspBatchRequest(toolCall: ToolCallContext | undefined): { id: string
|
|
|
38
50
|
return { id: toolCall.batchId, flush: !hasLaterWrites };
|
|
39
51
|
}
|
|
40
52
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
53
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
54
|
+
// Tool Class
|
|
55
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
56
|
+
|
|
57
|
+
type WriteParams = { path: string; content: string };
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Write tool implementation.
|
|
61
|
+
*
|
|
62
|
+
* Creates or overwrites files with optional LSP formatting and diagnostics.
|
|
63
|
+
*/
|
|
64
|
+
export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails> {
|
|
65
|
+
public readonly name = "write";
|
|
66
|
+
public readonly label = "Write";
|
|
67
|
+
public readonly description: string;
|
|
68
|
+
public readonly parameters = writeSchema;
|
|
69
|
+
|
|
70
|
+
private readonly session: ToolSession;
|
|
71
|
+
private readonly writethrough: WritethroughCallback;
|
|
72
|
+
|
|
73
|
+
constructor(session: ToolSession) {
|
|
74
|
+
this.session = session;
|
|
75
|
+
const enableLsp = session.enableLsp ?? true;
|
|
76
|
+
const enableFormat = enableLsp ? (session.settings?.getLspFormatOnWrite() ?? true) : false;
|
|
77
|
+
const enableDiagnostics = enableLsp ? (session.settings?.getLspDiagnosticsOnWrite() ?? true) : false;
|
|
78
|
+
this.writethrough = enableLsp
|
|
79
|
+
? createLspWritethrough(session.cwd, { enableFormat, enableDiagnostics })
|
|
80
|
+
: writethroughNoop;
|
|
81
|
+
this.description = renderPromptTemplate(writeDescription);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
public async execute(
|
|
85
|
+
_toolCallId: string,
|
|
86
|
+
{ path, content }: WriteParams,
|
|
87
|
+
signal?: AbortSignal,
|
|
88
|
+
_onUpdate?: AgentToolUpdateCallback<WriteToolDetails>,
|
|
89
|
+
context?: AgentToolContext,
|
|
90
|
+
): Promise<AgentToolResult<WriteToolDetails>> {
|
|
91
|
+
return untilAborted(signal, async () => {
|
|
92
|
+
const absolutePath = resolveToCwd(path, this.session.cwd);
|
|
93
|
+
const batchRequest = getLspBatchRequest(context?.toolCall);
|
|
94
|
+
|
|
95
|
+
const diagnostics = await this.writethrough(absolutePath, content, signal, undefined, batchRequest);
|
|
96
|
+
|
|
97
|
+
let resultText = `Successfully wrote ${content.length} bytes to ${path}`;
|
|
98
|
+
if (!diagnostics) {
|
|
79
99
|
return {
|
|
80
100
|
content: [{ type: "text", text: resultText }],
|
|
81
|
-
details: {
|
|
101
|
+
details: {},
|
|
82
102
|
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const messages = diagnostics?.messages;
|
|
106
|
+
if (messages && messages.length > 0) {
|
|
107
|
+
resultText += `\n\nLSP Diagnostics (${diagnostics.summary}):\n`;
|
|
108
|
+
resultText += messages.map((d) => ` ${d}`).join("\n");
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
content: [{ type: "text", text: resultText }],
|
|
112
|
+
details: { diagnostics },
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
}
|
|
86
116
|
}
|
|
87
117
|
|
|
88
118
|
// =============================================================================
|
|
@@ -95,11 +125,34 @@ interface WriteRenderArgs {
|
|
|
95
125
|
content?: string;
|
|
96
126
|
}
|
|
97
127
|
|
|
128
|
+
const WRITE_STREAMING_PREVIEW_LINES = 12;
|
|
129
|
+
|
|
98
130
|
function countLines(text: string): number {
|
|
99
131
|
if (!text) return 0;
|
|
100
132
|
return text.split("\n").length;
|
|
101
133
|
}
|
|
102
134
|
|
|
135
|
+
function formatStreamingContent(content: string, rawPath: string, uiTheme: Theme): string {
|
|
136
|
+
if (!content) return "";
|
|
137
|
+
const lang = getLanguageFromPath(rawPath);
|
|
138
|
+
const lines = content.split("\n");
|
|
139
|
+
const total = lines.length;
|
|
140
|
+
const displayLines = lines.slice(-WRITE_STREAMING_PREVIEW_LINES);
|
|
141
|
+
const hidden = total - displayLines.length;
|
|
142
|
+
|
|
143
|
+
const formattedLines = lang
|
|
144
|
+
? highlightCode(replaceTabs(displayLines.join("\n")), lang)
|
|
145
|
+
: displayLines.map((line: string) => uiTheme.fg("toolOutput", replaceTabs(line)));
|
|
146
|
+
|
|
147
|
+
let text = "\n\n";
|
|
148
|
+
if (hidden > 0) {
|
|
149
|
+
text += uiTheme.fg("dim", `${uiTheme.format.ellipsis} (${hidden} earlier lines)\n`);
|
|
150
|
+
}
|
|
151
|
+
text += formattedLines.join("\n");
|
|
152
|
+
text += uiTheme.fg("dim", `\n${uiTheme.format.ellipsis} (streaming)`);
|
|
153
|
+
return text;
|
|
154
|
+
}
|
|
155
|
+
|
|
103
156
|
function formatMetadataLine(lineCount: number | null, language: string | undefined, uiTheme: Theme): string {
|
|
104
157
|
const icon = uiTheme.getLangIcon(language);
|
|
105
158
|
if (lineCount !== null) {
|
|
@@ -109,11 +162,19 @@ function formatMetadataLine(lineCount: number | null, language: string | undefin
|
|
|
109
162
|
}
|
|
110
163
|
|
|
111
164
|
export const writeToolRenderer = {
|
|
112
|
-
renderCall(args: WriteRenderArgs, uiTheme: Theme): Component {
|
|
165
|
+
renderCall(args: WriteRenderArgs, uiTheme: Theme, options?: RenderCallOptions): Component {
|
|
113
166
|
const rawPath = args.file_path || args.path || "";
|
|
114
167
|
const filePath = shortenPath(rawPath);
|
|
115
168
|
const pathDisplay = filePath ? uiTheme.fg("accent", filePath) : uiTheme.fg("toolOutput", uiTheme.format.ellipsis);
|
|
116
|
-
const
|
|
169
|
+
const spinner =
|
|
170
|
+
options?.spinnerFrame !== undefined ? formatStatusIcon("running", uiTheme, options.spinnerFrame) : "";
|
|
171
|
+
let text = `${uiTheme.fg("toolTitle", uiTheme.bold("Write"))} ${spinner ? `${spinner} ` : ""}${pathDisplay}`;
|
|
172
|
+
|
|
173
|
+
// Show streaming preview of content
|
|
174
|
+
if (args.content) {
|
|
175
|
+
text += formatStreamingContent(args.content, rawPath, uiTheme);
|
|
176
|
+
}
|
|
177
|
+
|
|
117
178
|
return new Text(text, 0, 0);
|
|
118
179
|
},
|
|
119
180
|
|