@oh-my-pi/pi-coding-agent 3.20.1 → 3.21.0
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 +69 -9
- package/docs/custom-tools.md +3 -3
- package/docs/extensions.md +226 -220
- package/docs/hooks.md +2 -2
- package/docs/sdk.md +3 -3
- package/examples/custom-tools/README.md +2 -2
- package/examples/custom-tools/subagent/index.ts +1 -1
- package/examples/extensions/README.md +76 -74
- package/examples/extensions/todo.ts +2 -5
- package/examples/hooks/custom-compaction.ts +1 -1
- package/examples/hooks/handoff.ts +1 -1
- package/examples/hooks/qna.ts +1 -1
- package/examples/sdk/02-custom-model.ts +1 -1
- package/examples/sdk/12-full-control.ts +1 -1
- package/examples/sdk/README.md +1 -1
- package/package.json +5 -5
- package/src/cli/file-processor.ts +1 -1
- package/src/cli/list-models.ts +1 -1
- package/src/core/agent-session.ts +13 -2
- package/src/core/auth-storage.ts +1 -1
- package/src/core/compaction/branch-summarization.ts +2 -2
- package/src/core/compaction/compaction.ts +2 -2
- package/src/core/compaction/utils.ts +1 -1
- package/src/core/custom-tools/types.ts +1 -1
- package/src/core/extensions/runner.ts +1 -1
- package/src/core/extensions/types.ts +1 -1
- package/src/core/extensions/wrapper.ts +1 -1
- package/src/core/hooks/runner.ts +2 -2
- package/src/core/hooks/types.ts +1 -1
- package/src/core/messages.ts +1 -1
- package/src/core/model-registry.ts +1 -1
- package/src/core/model-resolver.ts +1 -1
- package/src/core/sdk.ts +33 -4
- package/src/core/session-manager.ts +11 -22
- package/src/core/settings-manager.ts +66 -1
- package/src/core/slash-commands.ts +12 -5
- package/src/core/system-prompt.ts +27 -3
- package/src/core/title-generator.ts +2 -2
- package/src/core/tools/ask.ts +88 -1
- package/src/core/tools/bash-interceptor.ts +7 -0
- package/src/core/tools/bash.ts +106 -0
- package/src/core/tools/edit-diff.ts +73 -24
- package/src/core/tools/edit.ts +214 -20
- package/src/core/tools/find.ts +155 -0
- package/src/core/tools/gemini-image.ts +279 -56
- package/src/core/tools/git.ts +4 -0
- package/src/core/tools/grep.ts +191 -0
- package/src/core/tools/index.ts +3 -6
- package/src/core/tools/ls.ts +134 -1
- package/src/core/tools/lsp/render.ts +34 -14
- package/src/core/tools/notebook.ts +110 -0
- package/src/core/tools/output.ts +179 -7
- package/src/core/tools/read.ts +122 -9
- package/src/core/tools/render-utils.ts +241 -0
- package/src/core/tools/renderers.ts +40 -828
- package/src/core/tools/review.ts +26 -7
- package/src/core/tools/rulebook.ts +3 -1
- package/src/core/tools/task/index.ts +18 -3
- package/src/core/tools/task/render.ts +5 -0
- package/src/core/tools/task/types.ts +1 -1
- package/src/core/tools/truncate.ts +27 -1
- package/src/core/tools/web-fetch.ts +23 -15
- package/src/core/tools/web-search/index.ts +130 -45
- package/src/core/tools/web-search/providers/anthropic.ts +7 -2
- package/src/core/tools/web-search/providers/exa.ts +2 -1
- package/src/core/tools/web-search/providers/perplexity.ts +6 -1
- package/src/core/tools/web-search/render.ts +5 -0
- package/src/core/tools/web-search/types.ts +13 -0
- package/src/core/tools/write.ts +90 -0
- package/src/core/voice.ts +1 -1
- package/src/main.ts +1 -1
- package/src/modes/interactive/components/assistant-message.ts +1 -1
- package/src/modes/interactive/components/custom-message.ts +1 -1
- package/src/modes/interactive/components/extensions/inspector-panel.ts +25 -22
- package/src/modes/interactive/components/extensions/state-manager.ts +12 -0
- package/src/modes/interactive/components/footer.ts +1 -1
- package/src/modes/interactive/components/hook-message.ts +1 -1
- package/src/modes/interactive/components/model-selector.ts +1 -1
- package/src/modes/interactive/components/oauth-selector.ts +1 -1
- package/src/modes/interactive/components/settings-defs.ts +49 -0
- package/src/modes/interactive/components/status-line.ts +1 -1
- package/src/modes/interactive/components/tool-execution.ts +93 -538
- package/src/modes/interactive/interactive-mode.ts +19 -7
- package/src/modes/print-mode.ts +1 -1
- package/src/modes/rpc/rpc-client.ts +1 -1
- package/src/modes/rpc/rpc-types.ts +1 -1
- package/src/prompts/system-prompt.md +4 -0
- package/src/prompts/tools/gemini-image.md +5 -1
- package/src/prompts/tools/output.md +4 -0
- package/src/prompts/tools/web-fetch.md +1 -0
- package/src/prompts/tools/web-search.md +2 -0
- package/src/utils/image-convert.ts +8 -2
- package/src/utils/image-magick.ts +247 -0
- package/src/utils/image-resize.ts +53 -13
|
@@ -422,6 +422,7 @@ export async function computeEditDiff(
|
|
|
422
422
|
newText: string,
|
|
423
423
|
cwd: string,
|
|
424
424
|
fuzzy = true,
|
|
425
|
+
all = false,
|
|
425
426
|
): Promise<EditDiffResult | EditDiffError> {
|
|
426
427
|
const absolutePath = resolveToCwd(path, cwd);
|
|
427
428
|
|
|
@@ -443,34 +444,82 @@ export async function computeEditDiff(
|
|
|
443
444
|
const normalizedOldText = normalizeToLF(oldText);
|
|
444
445
|
const normalizedNewText = normalizeToLF(newText);
|
|
445
446
|
|
|
446
|
-
|
|
447
|
-
allowFuzzy: fuzzy,
|
|
448
|
-
similarityThreshold: DEFAULT_FUZZY_THRESHOLD,
|
|
449
|
-
});
|
|
447
|
+
let normalizedNewContent: string;
|
|
450
448
|
|
|
451
|
-
if (
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
}
|
|
449
|
+
if (all) {
|
|
450
|
+
// Replace all occurrences mode with fuzzy matching
|
|
451
|
+
normalizedNewContent = normalizedContent;
|
|
452
|
+
let replacementCount = 0;
|
|
456
453
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
454
|
+
// First check: if exact matches exist, use simple replaceAll
|
|
455
|
+
const exactCount = normalizedContent.split(normalizedOldText).length - 1;
|
|
456
|
+
if (exactCount > 0) {
|
|
457
|
+
normalizedNewContent = normalizedContent.split(normalizedOldText).join(normalizedNewText);
|
|
458
|
+
replacementCount = exactCount;
|
|
459
|
+
} else {
|
|
460
|
+
// No exact matches - try fuzzy matching iteratively
|
|
461
|
+
while (true) {
|
|
462
|
+
const matchOutcome = findEditMatch(normalizedNewContent, normalizedOldText, {
|
|
463
|
+
allowFuzzy: fuzzy,
|
|
464
|
+
similarityThreshold: DEFAULT_FUZZY_THRESHOLD,
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// In all mode, use closest match if it passes threshold (even with multiple matches)
|
|
468
|
+
const match =
|
|
469
|
+
matchOutcome.match ||
|
|
470
|
+
(fuzzy && matchOutcome.closest && matchOutcome.closest.confidence >= DEFAULT_FUZZY_THRESHOLD
|
|
471
|
+
? matchOutcome.closest
|
|
472
|
+
: undefined);
|
|
473
|
+
|
|
474
|
+
if (!match) {
|
|
475
|
+
if (replacementCount === 0) {
|
|
476
|
+
return {
|
|
477
|
+
error: EditMatchError.formatMessage(path, normalizedOldText, matchOutcome.closest, {
|
|
478
|
+
allowFuzzy: fuzzy,
|
|
479
|
+
similarityThreshold: DEFAULT_FUZZY_THRESHOLD,
|
|
480
|
+
fuzzyMatches: matchOutcome.fuzzyMatches,
|
|
481
|
+
}),
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
break;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
normalizedNewContent =
|
|
488
|
+
normalizedNewContent.substring(0, match.startIndex) +
|
|
489
|
+
normalizedNewText +
|
|
490
|
+
normalizedNewContent.substring(match.startIndex + match.actualText.length);
|
|
491
|
+
replacementCount++;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
} else {
|
|
495
|
+
// Single replacement mode with fuzzy matching
|
|
496
|
+
const matchOutcome = findEditMatch(normalizedContent, normalizedOldText, {
|
|
497
|
+
allowFuzzy: fuzzy,
|
|
498
|
+
similarityThreshold: DEFAULT_FUZZY_THRESHOLD,
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
if (matchOutcome.occurrences && matchOutcome.occurrences > 1) {
|
|
502
|
+
return {
|
|
503
|
+
error: `Found ${matchOutcome.occurrences} occurrences of the text in ${path}. The text must be unique. Please provide more context to make it unique, or use all: true to replace all.`,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
466
506
|
|
|
467
|
-
|
|
507
|
+
if (!matchOutcome.match) {
|
|
508
|
+
return {
|
|
509
|
+
error: EditMatchError.formatMessage(path, normalizedOldText, matchOutcome.closest, {
|
|
510
|
+
allowFuzzy: fuzzy,
|
|
511
|
+
similarityThreshold: DEFAULT_FUZZY_THRESHOLD,
|
|
512
|
+
fuzzyMatches: matchOutcome.fuzzyMatches,
|
|
513
|
+
}),
|
|
514
|
+
};
|
|
515
|
+
}
|
|
468
516
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
517
|
+
const match = matchOutcome.match;
|
|
518
|
+
normalizedNewContent =
|
|
519
|
+
normalizedContent.substring(0, match.startIndex) +
|
|
520
|
+
normalizedNewText +
|
|
521
|
+
normalizedContent.substring(match.startIndex + match.actualText.length);
|
|
522
|
+
}
|
|
474
523
|
|
|
475
524
|
// Check if it would actually change anything
|
|
476
525
|
if (normalizedContent === normalizedNewContent) {
|
package/src/core/tools/edit.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
2
|
+
import type { Component } from "@oh-my-pi/pi-tui";
|
|
3
|
+
import { Text } from "@oh-my-pi/pi-tui";
|
|
2
4
|
import { Type } from "@sinclair/typebox";
|
|
5
|
+
import { getLanguageFromPath, type Theme } from "../../modes/interactive/theme/theme";
|
|
3
6
|
import editDescription from "../../prompts/tools/edit.md" with { type: "text" };
|
|
7
|
+
import type { RenderResultOptions } from "../custom-tools/types";
|
|
4
8
|
import {
|
|
5
9
|
DEFAULT_FUZZY_THRESHOLD,
|
|
6
10
|
detectLineEnding,
|
|
11
|
+
type EditDiffError,
|
|
12
|
+
type EditDiffResult,
|
|
7
13
|
EditMatchError,
|
|
8
14
|
findEditMatch,
|
|
9
15
|
generateDiffString,
|
|
@@ -13,6 +19,14 @@ import {
|
|
|
13
19
|
} from "./edit-diff";
|
|
14
20
|
import { type FileDiagnosticsResult, type WritethroughCallback, writethroughNoop } from "./lsp/index";
|
|
15
21
|
import { resolveToCwd } from "./path-utils";
|
|
22
|
+
import {
|
|
23
|
+
formatDiagnostics,
|
|
24
|
+
formatDiffStats,
|
|
25
|
+
getDiffStats,
|
|
26
|
+
shortenPath,
|
|
27
|
+
truncateDiffByHunk,
|
|
28
|
+
wrapBrackets,
|
|
29
|
+
} from "./render-utils";
|
|
16
30
|
|
|
17
31
|
const editSchema = Type.Object({
|
|
18
32
|
path: Type.String({ description: "Path to the file to edit (relative or absolute)" }),
|
|
@@ -20,6 +34,7 @@ const editSchema = Type.Object({
|
|
|
20
34
|
description: "Text to find and replace (high-confidence fuzzy matching for whitespace/indentation is always on)",
|
|
21
35
|
}),
|
|
22
36
|
newText: Type.String({ description: "New text to replace the old text with" }),
|
|
37
|
+
all: Type.Optional(Type.Boolean({ description: "Replace all occurrences instead of requiring unique match" })),
|
|
23
38
|
});
|
|
24
39
|
|
|
25
40
|
export interface EditToolDetails {
|
|
@@ -48,7 +63,7 @@ export function createEditTool(cwd: string, options: EditToolOptions = {}): Agen
|
|
|
48
63
|
parameters: editSchema,
|
|
49
64
|
execute: async (
|
|
50
65
|
_toolCallId: string,
|
|
51
|
-
{ path, oldText, newText }: { path: string; oldText: string; newText: string },
|
|
66
|
+
{ path, oldText, newText, all }: { path: string; oldText: string; newText: string; all?: boolean },
|
|
52
67
|
signal?: AbortSignal,
|
|
53
68
|
) => {
|
|
54
69
|
// Reject .ipynb files - use NotebookEdit tool instead
|
|
@@ -73,30 +88,79 @@ export function createEditTool(cwd: string, options: EditToolOptions = {}): Agen
|
|
|
73
88
|
const normalizedOldText = normalizeToLF(oldText);
|
|
74
89
|
const normalizedNewText = normalizeToLF(newText);
|
|
75
90
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
similarityThreshold: DEFAULT_FUZZY_THRESHOLD,
|
|
79
|
-
});
|
|
91
|
+
let normalizedNewContent: string;
|
|
92
|
+
let replacementCount = 0;
|
|
80
93
|
|
|
81
|
-
if (
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
94
|
+
if (all) {
|
|
95
|
+
// Replace all occurrences mode with fuzzy matching
|
|
96
|
+
normalizedNewContent = normalizedContent;
|
|
97
|
+
|
|
98
|
+
// First check: if exact matches exist, use simple replaceAll
|
|
99
|
+
const exactCount = normalizedContent.split(normalizedOldText).length - 1;
|
|
100
|
+
if (exactCount > 0) {
|
|
101
|
+
normalizedNewContent = normalizedContent.split(normalizedOldText).join(normalizedNewText);
|
|
102
|
+
replacementCount = exactCount;
|
|
103
|
+
} else {
|
|
104
|
+
// No exact matches - try fuzzy matching iteratively
|
|
105
|
+
while (true) {
|
|
106
|
+
const matchOutcome = findEditMatch(normalizedNewContent, normalizedOldText, {
|
|
107
|
+
allowFuzzy,
|
|
108
|
+
similarityThreshold: DEFAULT_FUZZY_THRESHOLD,
|
|
109
|
+
});
|
|
86
110
|
|
|
87
|
-
|
|
88
|
-
|
|
111
|
+
// In all mode, use closest match if it passes threshold (even with multiple matches)
|
|
112
|
+
const match =
|
|
113
|
+
matchOutcome.match ||
|
|
114
|
+
(allowFuzzy && matchOutcome.closest && matchOutcome.closest.confidence >= DEFAULT_FUZZY_THRESHOLD
|
|
115
|
+
? matchOutcome.closest
|
|
116
|
+
: undefined);
|
|
117
|
+
|
|
118
|
+
if (!match) {
|
|
119
|
+
if (replacementCount === 0) {
|
|
120
|
+
throw new EditMatchError(path, normalizedOldText, matchOutcome.closest, {
|
|
121
|
+
allowFuzzy,
|
|
122
|
+
similarityThreshold: DEFAULT_FUZZY_THRESHOLD,
|
|
123
|
+
fuzzyMatches: matchOutcome.fuzzyMatches,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
normalizedNewContent =
|
|
130
|
+
normalizedNewContent.substring(0, match.startIndex) +
|
|
131
|
+
normalizedNewText +
|
|
132
|
+
normalizedNewContent.substring(match.startIndex + match.actualText.length);
|
|
133
|
+
replacementCount++;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
} else {
|
|
137
|
+
// Single replacement mode with fuzzy matching
|
|
138
|
+
const matchOutcome = findEditMatch(normalizedContent, normalizedOldText, {
|
|
89
139
|
allowFuzzy,
|
|
90
140
|
similarityThreshold: DEFAULT_FUZZY_THRESHOLD,
|
|
91
|
-
fuzzyMatches: matchOutcome.fuzzyMatches,
|
|
92
141
|
});
|
|
93
|
-
}
|
|
94
142
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
143
|
+
if (matchOutcome.occurrences && matchOutcome.occurrences > 1) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`Found ${matchOutcome.occurrences} occurrences of the text in ${path}. The text must be unique. Please provide more context to make it unique, or use all: true to replace all.`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!matchOutcome.match) {
|
|
150
|
+
throw new EditMatchError(path, normalizedOldText, matchOutcome.closest, {
|
|
151
|
+
allowFuzzy,
|
|
152
|
+
similarityThreshold: DEFAULT_FUZZY_THRESHOLD,
|
|
153
|
+
fuzzyMatches: matchOutcome.fuzzyMatches,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const match = matchOutcome.match;
|
|
158
|
+
normalizedNewContent =
|
|
159
|
+
normalizedContent.substring(0, match.startIndex) +
|
|
160
|
+
normalizedNewText +
|
|
161
|
+
normalizedContent.substring(match.startIndex + match.actualText.length);
|
|
162
|
+
replacementCount = 1;
|
|
163
|
+
}
|
|
100
164
|
|
|
101
165
|
// Verify the replacement actually changed something
|
|
102
166
|
if (normalizedContent === normalizedNewContent) {
|
|
@@ -111,7 +175,10 @@ export function createEditTool(cwd: string, options: EditToolOptions = {}): Agen
|
|
|
111
175
|
const diffResult = generateDiffString(normalizedContent, normalizedNewContent);
|
|
112
176
|
|
|
113
177
|
// Build result text
|
|
114
|
-
let resultText =
|
|
178
|
+
let resultText =
|
|
179
|
+
replacementCount > 1
|
|
180
|
+
? `Successfully replaced ${replacementCount} occurrences in ${path}.`
|
|
181
|
+
: `Successfully replaced text in ${path}.`;
|
|
115
182
|
|
|
116
183
|
const messages = diagnostics?.messages;
|
|
117
184
|
if (messages && messages.length > 0) {
|
|
@@ -138,3 +205,130 @@ export function createEditTool(cwd: string, options: EditToolOptions = {}): Agen
|
|
|
138
205
|
|
|
139
206
|
/** Default edit tool using process.cwd() - for backwards compatibility */
|
|
140
207
|
export const editTool = createEditTool(process.cwd());
|
|
208
|
+
|
|
209
|
+
// =============================================================================
|
|
210
|
+
// TUI Renderer
|
|
211
|
+
// =============================================================================
|
|
212
|
+
|
|
213
|
+
interface EditRenderArgs {
|
|
214
|
+
path?: string;
|
|
215
|
+
file_path?: string;
|
|
216
|
+
oldText?: string;
|
|
217
|
+
newText?: string;
|
|
218
|
+
all?: boolean;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Extended context for edit tool rendering */
|
|
222
|
+
export interface EditRenderContext {
|
|
223
|
+
/** Pre-computed diff preview (computed before tool executes) */
|
|
224
|
+
editDiffPreview?: EditDiffResult | EditDiffError;
|
|
225
|
+
/** Function to render diff text with syntax highlighting */
|
|
226
|
+
renderDiff?: (diffText: string, options?: { filePath?: string }) => string;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const EDIT_DIFF_PREVIEW_HUNKS = 2;
|
|
230
|
+
const EDIT_DIFF_PREVIEW_LINES = 24;
|
|
231
|
+
|
|
232
|
+
function countLines(text: string): number {
|
|
233
|
+
if (!text) return 0;
|
|
234
|
+
return text.split("\n").length;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function formatMetadataLine(lineCount: number | null, language: string | undefined, uiTheme: Theme): string {
|
|
238
|
+
const icon = uiTheme.getLangIcon(language);
|
|
239
|
+
if (lineCount !== null) {
|
|
240
|
+
return uiTheme.fg("dim", `${icon} ${lineCount} lines`);
|
|
241
|
+
}
|
|
242
|
+
return uiTheme.fg("dim", `${icon}`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export const editToolRenderer = {
|
|
246
|
+
renderCall(args: EditRenderArgs, uiTheme: Theme): Component {
|
|
247
|
+
const rawPath = args.file_path || args.path || "";
|
|
248
|
+
const filePath = shortenPath(rawPath);
|
|
249
|
+
const editLanguage = getLanguageFromPath(rawPath) ?? "text";
|
|
250
|
+
const editIcon = uiTheme.fg("muted", uiTheme.getLangIcon(editLanguage));
|
|
251
|
+
const pathDisplay = filePath ? uiTheme.fg("accent", filePath) : uiTheme.fg("toolOutput", uiTheme.format.ellipsis);
|
|
252
|
+
|
|
253
|
+
const text = `${uiTheme.fg("toolTitle", uiTheme.bold("Edit"))} ${editIcon} ${pathDisplay}`;
|
|
254
|
+
return new Text(text, 0, 0);
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
renderResult(
|
|
258
|
+
result: { content: Array<{ type: string; text?: string }>; details?: EditToolDetails; isError?: boolean },
|
|
259
|
+
options: RenderResultOptions & { renderContext?: EditRenderContext },
|
|
260
|
+
uiTheme: Theme,
|
|
261
|
+
args?: EditRenderArgs,
|
|
262
|
+
): Component {
|
|
263
|
+
const { expanded, renderContext } = options;
|
|
264
|
+
const rawPath = args?.file_path || args?.path || "";
|
|
265
|
+
const filePath = shortenPath(rawPath);
|
|
266
|
+
const editLanguage = getLanguageFromPath(rawPath) ?? "text";
|
|
267
|
+
const editIcon = uiTheme.fg("muted", uiTheme.getLangIcon(editLanguage));
|
|
268
|
+
const editDiffPreview = renderContext?.editDiffPreview;
|
|
269
|
+
const renderDiffFn = renderContext?.renderDiff ?? ((t: string) => t);
|
|
270
|
+
|
|
271
|
+
// Build path display with line number if available
|
|
272
|
+
let pathDisplay = filePath ? uiTheme.fg("accent", filePath) : uiTheme.fg("toolOutput", uiTheme.format.ellipsis);
|
|
273
|
+
const firstChangedLine =
|
|
274
|
+
(editDiffPreview && "firstChangedLine" in editDiffPreview ? editDiffPreview.firstChangedLine : undefined) ||
|
|
275
|
+
(result.details && !result.isError ? result.details.firstChangedLine : undefined);
|
|
276
|
+
if (firstChangedLine) {
|
|
277
|
+
pathDisplay += uiTheme.fg("warning", `:${firstChangedLine}`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
let text = `${uiTheme.fg("toolTitle", uiTheme.bold("Edit"))} ${editIcon} ${pathDisplay}`;
|
|
281
|
+
|
|
282
|
+
const editLineCount = countLines(args?.newText ?? args?.oldText ?? "");
|
|
283
|
+
text += `\n${formatMetadataLine(editLineCount, editLanguage, uiTheme)}`;
|
|
284
|
+
|
|
285
|
+
if (result.isError) {
|
|
286
|
+
// Show error from result
|
|
287
|
+
const errorText = result.content?.find((c) => c.type === "text")?.text ?? "";
|
|
288
|
+
if (errorText) {
|
|
289
|
+
text += `\n\n${uiTheme.fg("error", errorText)}`;
|
|
290
|
+
}
|
|
291
|
+
} else if (editDiffPreview) {
|
|
292
|
+
// Use cached diff preview (works both before and after execution)
|
|
293
|
+
if ("error" in editDiffPreview) {
|
|
294
|
+
text += `\n\n${uiTheme.fg("error", editDiffPreview.error)}`;
|
|
295
|
+
} else if (editDiffPreview.diff) {
|
|
296
|
+
const diffStats = getDiffStats(editDiffPreview.diff);
|
|
297
|
+
text += `\n${uiTheme.fg("dim", uiTheme.format.bracketLeft)}${formatDiffStats(
|
|
298
|
+
diffStats.added,
|
|
299
|
+
diffStats.removed,
|
|
300
|
+
diffStats.hunks,
|
|
301
|
+
uiTheme,
|
|
302
|
+
)}${uiTheme.fg("dim", uiTheme.format.bracketRight)}`;
|
|
303
|
+
|
|
304
|
+
const {
|
|
305
|
+
text: diffText,
|
|
306
|
+
hiddenHunks,
|
|
307
|
+
hiddenLines,
|
|
308
|
+
} = expanded
|
|
309
|
+
? { text: editDiffPreview.diff, hiddenHunks: 0, hiddenLines: 0 }
|
|
310
|
+
: truncateDiffByHunk(editDiffPreview.diff, EDIT_DIFF_PREVIEW_HUNKS, EDIT_DIFF_PREVIEW_LINES);
|
|
311
|
+
|
|
312
|
+
text += `\n\n${renderDiffFn(diffText, { filePath: rawPath })}`;
|
|
313
|
+
if (!expanded && (hiddenHunks > 0 || hiddenLines > 0)) {
|
|
314
|
+
const remainder: string[] = [];
|
|
315
|
+
if (hiddenHunks > 0) remainder.push(`${hiddenHunks} more hunks`);
|
|
316
|
+
if (hiddenLines > 0) remainder.push(`${hiddenLines} more lines`);
|
|
317
|
+
text += uiTheme.fg(
|
|
318
|
+
"toolOutput",
|
|
319
|
+
`\n${uiTheme.format.ellipsis} (${remainder.join(", ")}) ${wrapBrackets("Ctrl+O to expand", uiTheme)}`,
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Show LSP diagnostics if available
|
|
326
|
+
if (result.details?.diagnostics) {
|
|
327
|
+
text += formatDiagnostics(result.details.diagnostics, expanded, uiTheme, (fp) =>
|
|
328
|
+
uiTheme.getLangIcon(getLanguageFromPath(fp)),
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return new Text(text, 0, 0);
|
|
333
|
+
},
|
|
334
|
+
};
|
package/src/core/tools/find.ts
CHANGED
|
@@ -1,12 +1,27 @@
|
|
|
1
1
|
import { existsSync, type Stats, statSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
4
|
+
import type { Component } from "@oh-my-pi/pi-tui";
|
|
5
|
+
import { Text } from "@oh-my-pi/pi-tui";
|
|
4
6
|
import { Type } from "@sinclair/typebox";
|
|
5
7
|
import { globSync } from "glob";
|
|
8
|
+
import { getLanguageFromPath, type Theme } from "../../modes/interactive/theme/theme";
|
|
6
9
|
import findDescription from "../../prompts/tools/find.md" with { type: "text" };
|
|
7
10
|
import { ensureTool } from "../../utils/tools-manager";
|
|
11
|
+
import type { RenderResultOptions } from "../custom-tools/types";
|
|
8
12
|
import { untilAborted } from "../utils";
|
|
9
13
|
import { resolveToCwd } from "./path-utils";
|
|
14
|
+
import {
|
|
15
|
+
formatCount,
|
|
16
|
+
formatEmptyMessage,
|
|
17
|
+
formatErrorMessage,
|
|
18
|
+
formatExpandHint,
|
|
19
|
+
formatMeta,
|
|
20
|
+
formatMoreItems,
|
|
21
|
+
formatScope,
|
|
22
|
+
formatTruncationSuffix,
|
|
23
|
+
PREVIEW_LIMITS,
|
|
24
|
+
} from "./render-utils";
|
|
10
25
|
import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from "./truncate";
|
|
11
26
|
|
|
12
27
|
const findSchema = Type.Object({
|
|
@@ -248,3 +263,143 @@ export function createFindTool(cwd: string): AgentTool<typeof findSchema> {
|
|
|
248
263
|
|
|
249
264
|
/** Default find tool using process.cwd() - for backwards compatibility */
|
|
250
265
|
export const findTool = createFindTool(process.cwd());
|
|
266
|
+
|
|
267
|
+
// =============================================================================
|
|
268
|
+
// TUI Renderer
|
|
269
|
+
// =============================================================================
|
|
270
|
+
|
|
271
|
+
interface FindRenderArgs {
|
|
272
|
+
pattern: string;
|
|
273
|
+
path?: string;
|
|
274
|
+
type?: string;
|
|
275
|
+
hidden?: boolean;
|
|
276
|
+
sortByMtime?: boolean;
|
|
277
|
+
limit?: number;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const COLLAPSED_LIST_LIMIT = PREVIEW_LIMITS.COLLAPSED_ITEMS;
|
|
281
|
+
|
|
282
|
+
export const findToolRenderer = {
|
|
283
|
+
renderCall(args: FindRenderArgs, uiTheme: Theme): Component {
|
|
284
|
+
const label = uiTheme.fg("toolTitle", uiTheme.bold("Find"));
|
|
285
|
+
let text = `${label} ${uiTheme.fg("accent", args.pattern || "*")}`;
|
|
286
|
+
|
|
287
|
+
const meta: string[] = [];
|
|
288
|
+
if (args.path) meta.push(`in ${args.path}`);
|
|
289
|
+
if (args.type && args.type !== "all") meta.push(`type:${args.type}`);
|
|
290
|
+
if (args.hidden) meta.push("hidden");
|
|
291
|
+
if (args.sortByMtime) meta.push("sort:mtime");
|
|
292
|
+
if (args.limit !== undefined) meta.push(`limit:${args.limit}`);
|
|
293
|
+
|
|
294
|
+
text += formatMeta(meta, uiTheme);
|
|
295
|
+
|
|
296
|
+
return new Text(text, 0, 0);
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
renderResult(
|
|
300
|
+
result: { content: Array<{ type: string; text?: string }>; details?: FindToolDetails },
|
|
301
|
+
{ expanded }: RenderResultOptions,
|
|
302
|
+
uiTheme: Theme,
|
|
303
|
+
): Component {
|
|
304
|
+
const details = result.details;
|
|
305
|
+
|
|
306
|
+
if (details?.error) {
|
|
307
|
+
return new Text(formatErrorMessage(details.error, uiTheme), 0, 0);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const hasDetailedData = details?.fileCount !== undefined;
|
|
311
|
+
const textContent = result.content?.find((c) => c.type === "text")?.text;
|
|
312
|
+
|
|
313
|
+
if (!hasDetailedData) {
|
|
314
|
+
if (!textContent || textContent.includes("No files matching") || textContent.trim() === "") {
|
|
315
|
+
return new Text(formatEmptyMessage("No files found", uiTheme), 0, 0);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const lines = textContent.split("\n").filter((l) => l.trim());
|
|
319
|
+
const maxLines = expanded ? lines.length : Math.min(lines.length, COLLAPSED_LIST_LIMIT);
|
|
320
|
+
const displayLines = lines.slice(0, maxLines);
|
|
321
|
+
const remaining = lines.length - maxLines;
|
|
322
|
+
const hasMore = remaining > 0;
|
|
323
|
+
|
|
324
|
+
const icon = uiTheme.styledSymbol("status.success", "success");
|
|
325
|
+
const summary = formatCount("file", lines.length);
|
|
326
|
+
const expandHint = formatExpandHint(expanded, hasMore, uiTheme);
|
|
327
|
+
let text = `${icon} ${uiTheme.fg("dim", summary)}${expandHint}`;
|
|
328
|
+
|
|
329
|
+
for (let i = 0; i < displayLines.length; i++) {
|
|
330
|
+
const isLast = i === displayLines.length - 1 && remaining === 0;
|
|
331
|
+
const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
|
|
332
|
+
text += `\n ${uiTheme.fg("dim", branch)} ${uiTheme.fg("accent", displayLines[i])}`;
|
|
333
|
+
}
|
|
334
|
+
if (remaining > 0) {
|
|
335
|
+
text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg(
|
|
336
|
+
"muted",
|
|
337
|
+
formatMoreItems(remaining, "file", uiTheme),
|
|
338
|
+
)}`;
|
|
339
|
+
}
|
|
340
|
+
return new Text(text, 0, 0);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const fileCount = details?.fileCount ?? 0;
|
|
344
|
+
const truncated = details?.truncated ?? details?.truncation?.truncated ?? false;
|
|
345
|
+
const files = details?.files ?? [];
|
|
346
|
+
|
|
347
|
+
if (fileCount === 0) {
|
|
348
|
+
return new Text(formatEmptyMessage("No files found", uiTheme), 0, 0);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const icon = uiTheme.styledSymbol("status.success", "success");
|
|
352
|
+
const summaryText = formatCount("file", fileCount);
|
|
353
|
+
const scopeLabel = formatScope(details?.scopePath, uiTheme);
|
|
354
|
+
const maxFiles = expanded ? files.length : Math.min(files.length, COLLAPSED_LIST_LIMIT);
|
|
355
|
+
const hasMoreFiles = files.length > maxFiles;
|
|
356
|
+
const expandHint = formatExpandHint(expanded, hasMoreFiles, uiTheme);
|
|
357
|
+
|
|
358
|
+
let text = `${icon} ${uiTheme.fg("dim", summaryText)}${formatTruncationSuffix(
|
|
359
|
+
truncated,
|
|
360
|
+
uiTheme,
|
|
361
|
+
)}${scopeLabel}${expandHint}`;
|
|
362
|
+
|
|
363
|
+
const truncationReasons: string[] = [];
|
|
364
|
+
if (details?.resultLimitReached) {
|
|
365
|
+
truncationReasons.push(`limit ${details.resultLimitReached} results`);
|
|
366
|
+
}
|
|
367
|
+
if (details?.truncation?.truncated) {
|
|
368
|
+
truncationReasons.push("size limit");
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const hasTruncation = truncationReasons.length > 0;
|
|
372
|
+
|
|
373
|
+
if (files.length > 0) {
|
|
374
|
+
for (let i = 0; i < maxFiles; i++) {
|
|
375
|
+
const isLast = i === maxFiles - 1 && !hasMoreFiles && !hasTruncation;
|
|
376
|
+
const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
|
|
377
|
+
const entry = files[i];
|
|
378
|
+
const isDir = entry.endsWith("/");
|
|
379
|
+
const entryPath = isDir ? entry.slice(0, -1) : entry;
|
|
380
|
+
const lang = isDir ? undefined : getLanguageFromPath(entryPath);
|
|
381
|
+
const entryIcon = isDir
|
|
382
|
+
? uiTheme.fg("accent", uiTheme.icon.folder)
|
|
383
|
+
: uiTheme.fg("muted", uiTheme.getLangIcon(lang));
|
|
384
|
+
text += `\n ${uiTheme.fg("dim", branch)} ${entryIcon} ${uiTheme.fg("accent", entry)}`;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (hasMoreFiles) {
|
|
388
|
+
const moreFilesBranch = hasTruncation ? uiTheme.tree.branch : uiTheme.tree.last;
|
|
389
|
+
text += `\n ${uiTheme.fg("dim", moreFilesBranch)} ${uiTheme.fg(
|
|
390
|
+
"muted",
|
|
391
|
+
formatMoreItems(files.length - maxFiles, "file", uiTheme),
|
|
392
|
+
)}`;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (hasTruncation) {
|
|
397
|
+
text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg(
|
|
398
|
+
"warning",
|
|
399
|
+
`truncated: ${truncationReasons.join(", ")}`,
|
|
400
|
+
)}`;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return new Text(text, 0, 0);
|
|
404
|
+
},
|
|
405
|
+
};
|