@oh-my-pi/pi-coding-agent 15.0.0 → 15.0.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 +41 -0
- package/examples/extensions/plan-mode.ts +0 -1
- package/package.json +9 -9
- package/scripts/build-binary.ts +5 -0
- package/src/autoresearch/helpers.ts +17 -0
- package/src/autoresearch/tools/log-experiment.ts +9 -17
- package/src/autoresearch/tools/run-experiment.ts +2 -17
- package/src/capability/skill.ts +7 -0
- package/src/cli/list-models.ts +1 -1
- package/src/cli/shell-cli.ts +3 -13
- package/src/cli/update-cli.ts +1 -1
- package/src/cli.ts +10 -29
- package/src/commit/agentic/tools/propose-changelog.ts +8 -1
- package/src/commit/analysis/conventional.ts +8 -66
- package/src/commit/map-reduce/reduce-phase.ts +6 -65
- package/src/commit/pipeline.ts +2 -2
- package/src/commit/shared-llm.ts +89 -0
- package/src/config/config-file.ts +210 -0
- package/src/config/model-equivalence.ts +8 -11
- package/src/config/model-registry.ts +13 -2
- package/src/config/model-resolver.ts +1 -4
- package/src/config/settings-schema.ts +71 -1
- package/src/config/settings.ts +1 -1
- package/src/config.ts +3 -219
- package/src/edit/renderer.ts +7 -1
- package/src/eval/js/executor.ts +3 -0
- package/src/eval/js/shared/rewrite-imports.ts +2 -2
- package/src/eval/py/executor.ts +5 -0
- package/src/exa/factory.ts +2 -2
- package/src/exa/mcp-client.ts +74 -1
- package/src/exec/bash-executor.ts +5 -1
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +0 -11
- package/src/extensibility/extensions/runner.ts +1 -1
- package/src/extensibility/extensions/types.ts +89 -223
- package/src/extensibility/hooks/types.ts +89 -314
- package/src/extensibility/shared-events.ts +343 -0
- package/src/extensibility/skills.ts +9 -0
- package/src/goals/index.ts +3 -0
- package/src/goals/runtime.ts +500 -0
- package/src/goals/state.ts +37 -0
- package/src/goals/tools/goal-tool.ts +237 -0
- package/src/hashline/anchors.ts +2 -2
- package/src/hindsight/mental-models.ts +1 -1
- package/src/internal-urls/agent-protocol.ts +1 -20
- package/src/internal-urls/artifact-protocol.ts +1 -19
- package/src/internal-urls/docs-index.generated.ts +5 -6
- package/src/internal-urls/registry-helpers.ts +25 -0
- package/src/main.ts +11 -2
- package/src/mcp/oauth-flow.ts +20 -0
- package/src/modes/acp/acp-agent.ts +79 -45
- package/src/modes/components/assistant-message.ts +14 -8
- package/src/modes/components/bash-execution.ts +24 -63
- package/src/modes/components/custom-message.ts +14 -40
- package/src/modes/components/eval-execution.ts +27 -57
- package/src/modes/components/execution-shared.ts +102 -0
- package/src/modes/components/hook-message.ts +17 -49
- package/src/modes/components/mcp-add-wizard.ts +26 -5
- package/src/modes/components/message-frame.ts +88 -0
- package/src/modes/components/model-selector.ts +1 -1
- package/src/modes/components/session-observer-overlay.ts +6 -2
- package/src/modes/components/session-selector.ts +1 -1
- package/src/modes/components/status-line/segments.ts +55 -4
- package/src/modes/components/status-line/types.ts +4 -0
- package/src/modes/components/status-line.ts +28 -10
- package/src/modes/components/tool-execution.ts +7 -8
- package/src/modes/controllers/command-controller-shared.ts +108 -0
- package/src/modes/controllers/command-controller.ts +13 -4
- package/src/modes/controllers/event-controller.ts +36 -7
- package/src/modes/controllers/input-controller.ts +13 -0
- package/src/modes/controllers/mcp-command-controller.ts +56 -61
- package/src/modes/controllers/ssh-command-controller.ts +18 -57
- package/src/modes/interactive-mode.ts +624 -52
- package/src/modes/print-mode.ts +16 -86
- package/src/modes/rpc/rpc-mode.ts +14 -87
- package/src/modes/runtime-init.ts +115 -0
- package/src/modes/theme/defaults/dark-poimandres.json +2 -0
- package/src/modes/theme/defaults/light-poimandres.json +2 -0
- package/src/modes/theme/theme.ts +18 -6
- package/src/modes/types.ts +14 -3
- package/src/modes/utils/context-usage.ts +13 -13
- package/src/modes/utils/ui-helpers.ts +10 -3
- package/src/plan-mode/approved-plan.ts +35 -1
- package/src/prompts/goals/goal-budget-limit.md +16 -0
- package/src/prompts/goals/goal-continuation.md +28 -0
- package/src/prompts/goals/goal-mode-active.md +23 -0
- package/src/prompts/system/plan-mode-active.md +5 -5
- package/src/prompts/system/plan-mode-tool-decision-reminder.md +1 -1
- package/src/prompts/tools/bash.md +6 -0
- package/src/prompts/tools/goal.md +13 -0
- package/src/prompts/tools/hashline.md +102 -114
- package/src/prompts/tools/read.md +1 -0
- package/src/prompts/tools/resolve.md +6 -5
- package/src/sdk.ts +12 -5
- package/src/session/agent-session.ts +428 -106
- package/src/session/blob-store.ts +36 -3
- package/src/session/messages.ts +67 -2
- package/src/session/session-manager.ts +131 -12
- package/src/session/session-storage.ts +33 -15
- package/src/session/streaming-output.ts +309 -13
- package/src/slash-commands/builtin-registry.ts +18 -0
- package/src/ssh/ssh-executor.ts +5 -0
- package/src/system-prompt.ts +4 -2
- package/src/task/executor.ts +17 -7
- package/src/task/index.ts +3 -0
- package/src/task/render.ts +21 -15
- package/src/task/types.ts +4 -0
- package/src/tools/ast-edit.ts +21 -120
- package/src/tools/ast-grep.ts +21 -119
- package/src/tools/bash-interactive.ts +9 -1
- package/src/tools/bash.ts +27 -4
- package/src/tools/browser/attach.ts +3 -3
- package/src/tools/browser/launch.ts +81 -18
- package/src/tools/browser/registry.ts +1 -5
- package/src/tools/browser/tab-supervisor.ts +51 -14
- package/src/tools/conflict-detect.ts +15 -4
- package/src/tools/eval.ts +3 -1
- package/src/tools/find.ts +20 -38
- package/src/tools/gh.ts +7 -6
- package/src/tools/index.ts +22 -11
- package/src/tools/inspect-image.ts +3 -10
- package/src/tools/output-meta.ts +176 -37
- package/src/tools/path-utils.ts +125 -2
- package/src/tools/read.ts +516 -233
- package/src/tools/render-utils.ts +92 -0
- package/src/tools/renderers.ts +2 -0
- package/src/tools/resolve.ts +72 -44
- package/src/tools/search.ts +120 -186
- package/src/tools/write.ts +44 -9
- package/src/utils/file-mentions.ts +1 -1
- package/src/utils/image-loading.ts +7 -3
- package/src/utils/image-resize.ts +32 -43
- package/src/vim/parser.ts +0 -17
- package/src/vim/render.ts +1 -1
- package/src/vim/types.ts +1 -1
- package/src/web/search/providers/gemini.ts +35 -95
- package/src/prompts/tools/exit-plan-mode.md +0 -6
- package/src/tools/exit-plan-mode.ts +0 -97
- package/src/utils/fuzzy.ts +0 -108
- package/src/utils/image-convert.ts +0 -27
package/src/tools/search.ts
CHANGED
|
@@ -8,32 +8,25 @@ import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
|
8
8
|
import { type Static, Type } from "@sinclair/typebox";
|
|
9
9
|
import { getFileReadCache } from "../edit/file-read-cache";
|
|
10
10
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
11
|
-
import { InternalUrlRouter } from "../internal-urls";
|
|
12
11
|
import type { Theme } from "../modes/theme/theme";
|
|
13
12
|
import searchDescription from "../prompts/tools/search.md" with { type: "text" };
|
|
14
13
|
import { DEFAULT_MAX_COLUMN, type TruncationResult, truncateHead } from "../session/streaming-output";
|
|
15
|
-
import { Ellipsis,
|
|
14
|
+
import { Ellipsis, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
|
|
16
15
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
17
16
|
import type { ToolSession } from ".";
|
|
18
17
|
import { createFileRecorder, formatResultPath } from "./file-recorder";
|
|
19
18
|
import { formatGroupedFiles } from "./grouped-file-output";
|
|
20
19
|
import { formatMatchLine } from "./match-line-format";
|
|
21
20
|
import { formatFullOutputReference, type OutputMeta } from "./output-meta";
|
|
21
|
+
import { resolveToolSearchScope } from "./path-utils";
|
|
22
22
|
import {
|
|
23
|
-
|
|
24
|
-
hasGlobPathChars,
|
|
25
|
-
normalizePathLikeInput,
|
|
26
|
-
parseSearchPath,
|
|
27
|
-
partitionExistingPaths,
|
|
28
|
-
resolveExplicitSearchPaths,
|
|
29
|
-
resolveToCwd,
|
|
30
|
-
} from "./path-utils";
|
|
31
|
-
import {
|
|
23
|
+
createCachedComponent,
|
|
32
24
|
formatCodeFrameLine,
|
|
33
25
|
formatCount,
|
|
34
26
|
formatEmptyMessage,
|
|
35
27
|
formatErrorMessage,
|
|
36
28
|
PREVIEW_LIMITS,
|
|
29
|
+
splitGroupsByBlankLine,
|
|
37
30
|
} from "./render-utils";
|
|
38
31
|
import { ToolError } from "./tool-errors";
|
|
39
32
|
import { toolResult } from "./tool-result";
|
|
@@ -47,17 +40,36 @@ const searchSchema = Type.Object({
|
|
|
47
40
|
}),
|
|
48
41
|
i: Type.Optional(Type.Boolean({ description: "case-insensitive search", default: false })),
|
|
49
42
|
gitignore: Type.Optional(Type.Boolean({ description: "respect gitignore", default: true })),
|
|
50
|
-
skip: Type.Optional(
|
|
43
|
+
skip: Type.Optional(
|
|
44
|
+
Type.Number({
|
|
45
|
+
description:
|
|
46
|
+
"files to skip before collecting results — use to paginate when the prior call hit the file limit",
|
|
47
|
+
default: 0,
|
|
48
|
+
}),
|
|
49
|
+
),
|
|
51
50
|
});
|
|
52
51
|
|
|
53
52
|
export type SearchToolInput = Static<typeof searchSchema>;
|
|
54
53
|
|
|
55
|
-
|
|
54
|
+
/** Maximum number of distinct files surfaced in a single response. The
|
|
55
|
+
* agent paginates further pages via `skip`. */
|
|
56
|
+
export const DEFAULT_FILE_LIMIT = 20;
|
|
57
|
+
/** Per-file match cap for multi-file searches — keeps a single hot file
|
|
58
|
+
* from crowding out diverse hits. Applied in JS after grep returns. */
|
|
59
|
+
export const MULTI_FILE_PER_FILE_MATCHES = 20;
|
|
60
|
+
/** Per-file match cap for single-file searches — there's no diversity
|
|
61
|
+
* concern when the scope is one file. */
|
|
62
|
+
export const SINGLE_FILE_MATCHES = 200;
|
|
63
|
+
/** Hard safety ceiling on how many matches we fetch from native grep
|
|
64
|
+
* before JS-side grouping. Sized to comfortably cover the file window
|
|
65
|
+
* (DEFAULT_FILE_LIMIT files × MULTI_FILE_PER_FILE_MATCHES matches) plus
|
|
66
|
+
* pagination headroom so the caller can see total file count. */
|
|
67
|
+
const INTERNAL_TOTAL_CAP = 2000;
|
|
56
68
|
|
|
57
69
|
export interface SearchToolDetails {
|
|
58
70
|
truncation?: TruncationResult;
|
|
59
|
-
|
|
60
|
-
|
|
71
|
+
fileLimitReached?: number;
|
|
72
|
+
perFileLimitReached?: number;
|
|
61
73
|
linesTruncated?: boolean;
|
|
62
74
|
meta?: OutputMeta;
|
|
63
75
|
scopePath?: string;
|
|
@@ -122,82 +134,34 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
122
134
|
const patternHasNewline = normalizedPattern.includes("\n") || normalizedPattern.includes("\\n");
|
|
123
135
|
const effectiveMultiline = patternHasNewline;
|
|
124
136
|
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
resolvedPathInputs.push(rawPath);
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
if (hasGlobPathChars(rawPath)) {
|
|
147
|
-
throw new ToolError(`Glob patterns are not supported for internal URLs: ${rawPath}`);
|
|
148
|
-
}
|
|
149
|
-
const resource = await internalRouter.resolve(rawPath);
|
|
150
|
-
if (!resource.sourcePath) {
|
|
151
|
-
throw new ToolError(`Cannot search internal URL without a backing file: ${rawPath}`);
|
|
152
|
-
}
|
|
153
|
-
if (resource.immutable) {
|
|
154
|
-
immutableSourcePaths.add(path.resolve(resource.sourcePath));
|
|
155
|
-
}
|
|
156
|
-
resolvedPathInputs.push(resource.sourcePath);
|
|
157
|
-
}
|
|
137
|
+
const scope = await resolveToolSearchScope({
|
|
138
|
+
rawPaths: paths,
|
|
139
|
+
cwd: this.session.cwd,
|
|
140
|
+
internalUrlAction: "search",
|
|
141
|
+
trackImmutableSources: true,
|
|
142
|
+
surfaceExactFilePaths: true,
|
|
143
|
+
multipathStatHint: " (`paths` entries must each exist relative to cwd)",
|
|
144
|
+
});
|
|
145
|
+
const {
|
|
146
|
+
searchPath,
|
|
147
|
+
scopePath,
|
|
148
|
+
isDirectory,
|
|
149
|
+
multiTargets,
|
|
150
|
+
exactFilePaths,
|
|
151
|
+
missingPaths,
|
|
152
|
+
immutableSourcePaths,
|
|
153
|
+
} = scope;
|
|
154
|
+
const { globFilter } = scope;
|
|
158
155
|
const baseDisplayMode = resolveFileDisplayMode(this.session);
|
|
159
156
|
const immutableDisplayMode = resolveFileDisplayMode(this.session, { immutable: true });
|
|
160
|
-
// Tolerate missing entries in a multi-path call: skip ones whose base
|
|
161
|
-
// directory is gone, and only error if every entry is missing. Single
|
|
162
|
-
// missing path keeps the original ENOENT semantics.
|
|
163
|
-
let missingPaths: string[] = [];
|
|
164
|
-
let effectivePaths = resolvedPathInputs;
|
|
165
|
-
if (resolvedPathInputs.length > 1) {
|
|
166
|
-
const partition = await partitionExistingPaths(resolvedPathInputs, this.session.cwd, parseSearchPath);
|
|
167
|
-
if (partition.valid.length === 0) {
|
|
168
|
-
throw new ToolError(`Path not found: ${partition.missing.join(", ")}`);
|
|
169
|
-
}
|
|
170
|
-
effectivePaths = partition.valid;
|
|
171
|
-
missingPaths = partition.missing;
|
|
172
|
-
}
|
|
173
|
-
if (effectivePaths.length === 1) {
|
|
174
|
-
const parsedPath = parseSearchPath(effectivePaths[0] ?? ".");
|
|
175
|
-
searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
|
|
176
|
-
globFilter = parsedPath.glob;
|
|
177
|
-
scopePath = formatScopePath(searchPath);
|
|
178
|
-
} else {
|
|
179
|
-
const multiSearchPath = await resolveExplicitSearchPaths(effectivePaths, this.session.cwd, globFilter);
|
|
180
|
-
if (!multiSearchPath) {
|
|
181
|
-
throw new ToolError("`paths` must contain at least one path or glob");
|
|
182
|
-
}
|
|
183
|
-
searchPath = multiSearchPath.basePath;
|
|
184
|
-
exactFilePaths = multiSearchPath.exactFilePaths;
|
|
185
|
-
multiTargets = multiSearchPath.targets;
|
|
186
|
-
globFilter = exactFilePaths || multiTargets ? undefined : multiSearchPath.glob;
|
|
187
|
-
scopePath = multiSearchPath.scopePath;
|
|
188
|
-
}
|
|
189
|
-
let isDirectory: boolean;
|
|
190
|
-
try {
|
|
191
|
-
const stat = await Bun.file(searchPath).stat();
|
|
192
|
-
isDirectory = stat.isDirectory();
|
|
193
|
-
} catch {
|
|
194
|
-
const hint = rawPaths.length > 1 ? " (`paths` entries must each exist relative to cwd)" : "";
|
|
195
|
-
throw new ToolError(`Path not found: ${scopePath}${hint}`);
|
|
196
|
-
}
|
|
197
157
|
|
|
198
158
|
const effectiveOutputMode = GrepOutputMode.Content;
|
|
199
|
-
|
|
200
|
-
|
|
159
|
+
// Multi-scope = more than one file may match. We fetch up to
|
|
160
|
+
// INTERNAL_TOTAL_CAP matches from native grep, then in JS group by
|
|
161
|
+
// file, apply a per-file cap (so one hot file doesn't crowd the
|
|
162
|
+
// window), and round-robin emit from up to DEFAULT_FILE_LIMIT files.
|
|
163
|
+
const isMultiScope = isDirectory || Boolean(exactFilePaths) || Boolean(multiTargets);
|
|
164
|
+
const perFileMatchCap = isMultiScope ? MULTI_FILE_PER_FILE_MATCHES : SINGLE_FILE_MATCHES;
|
|
201
165
|
|
|
202
166
|
// Run grep
|
|
203
167
|
let result: GrepResult;
|
|
@@ -221,7 +185,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
221
185
|
hidden: true,
|
|
222
186
|
gitignore: useGitignore,
|
|
223
187
|
cache: false,
|
|
224
|
-
maxCount:
|
|
188
|
+
maxCount: INTERNAL_TOTAL_CAP,
|
|
225
189
|
contextBefore: normalizedContextBefore,
|
|
226
190
|
contextAfter: normalizedContextAfter,
|
|
227
191
|
maxColumns: DEFAULT_MAX_COLUMN,
|
|
@@ -238,11 +202,10 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
238
202
|
matches.push({ ...match, path: rebased });
|
|
239
203
|
}
|
|
240
204
|
}
|
|
241
|
-
const offsetMatches = matches.slice(normalizedSkip);
|
|
242
205
|
result = {
|
|
243
|
-
matches
|
|
244
|
-
totalMatches: exactFilePaths ?
|
|
245
|
-
filesWithMatches: new Set(
|
|
206
|
+
matches,
|
|
207
|
+
totalMatches: exactFilePaths ? matches.length : totalMatches,
|
|
208
|
+
filesWithMatches: new Set(matches.map(match => match.path)).size,
|
|
246
209
|
filesSearched: exactFilePaths ? exactFilePaths.length : filesSearched,
|
|
247
210
|
limitReached,
|
|
248
211
|
};
|
|
@@ -257,8 +220,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
257
220
|
hidden: true,
|
|
258
221
|
gitignore: useGitignore,
|
|
259
222
|
cache: false,
|
|
260
|
-
maxCount:
|
|
261
|
-
offset: normalizedSkip > 0 ? normalizedSkip : undefined,
|
|
223
|
+
maxCount: INTERNAL_TOTAL_CAP,
|
|
262
224
|
contextBefore: normalizedContextBefore,
|
|
263
225
|
contextAfter: normalizedContextAfter,
|
|
264
226
|
maxColumns: DEFAULT_MAX_COLUMN,
|
|
@@ -277,42 +239,51 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
277
239
|
const formatPath = (filePath: string): string =>
|
|
278
240
|
formatResultPath(filePath, isDirectory, searchPath, this.session.cwd);
|
|
279
241
|
|
|
280
|
-
//
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
}
|
|
290
|
-
byFile.get(match.path)!.push(match);
|
|
242
|
+
// Group matches by file in encounter order. Detect per-file overflow
|
|
243
|
+
// BEFORE truncation so the renderer can surface that a hot file was
|
|
244
|
+
// trimmed for diversity.
|
|
245
|
+
const fileOrder: string[] = [];
|
|
246
|
+
const matchesByPath = new Map<string, GrepMatch[]>();
|
|
247
|
+
for (const match of result.matches) {
|
|
248
|
+
if (!matchesByPath.has(match.path)) {
|
|
249
|
+
fileOrder.push(match.path);
|
|
250
|
+
matchesByPath.set(match.path, []);
|
|
291
251
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
252
|
+
matchesByPath.get(match.path)!.push(match);
|
|
253
|
+
}
|
|
254
|
+
let perFileLimitReached = false;
|
|
255
|
+
for (const file of fileOrder) {
|
|
256
|
+
const list = matchesByPath.get(file)!;
|
|
257
|
+
if (list.length > perFileMatchCap) {
|
|
258
|
+
perFileLimitReached = true;
|
|
259
|
+
list.length = perFileMatchCap;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const totalFiles = fileOrder.length;
|
|
263
|
+
// Single-file scopes can't paginate — there is one file by definition.
|
|
264
|
+
const canPaginate = isMultiScope;
|
|
265
|
+
const skipFiles = canPaginate ? Math.min(normalizedSkip, totalFiles) : 0;
|
|
266
|
+
const windowFiles = canPaginate ? fileOrder.slice(skipFiles, skipFiles + DEFAULT_FILE_LIMIT) : fileOrder;
|
|
267
|
+
const fileLimitReached = canPaginate && totalFiles > skipFiles + DEFAULT_FILE_LIMIT;
|
|
268
|
+
const selectedMatches: GrepMatch[] = [];
|
|
269
|
+
if (windowFiles.length > 0) {
|
|
270
|
+
const lists = windowFiles.map(file => matchesByPath.get(file) ?? []);
|
|
271
|
+
const cursors = new Array<number>(lists.length).fill(0);
|
|
272
|
+
let anyAdded = true;
|
|
273
|
+
while (anyAdded) {
|
|
274
|
+
anyAdded = false;
|
|
275
|
+
for (let i = 0; i < lists.length; i++) {
|
|
276
|
+
if (cursors[i] < lists[i].length) {
|
|
277
|
+
selectedMatches.push(lists[i][cursors[i]++]);
|
|
303
278
|
anyAdded = true;
|
|
304
279
|
}
|
|
305
280
|
}
|
|
306
|
-
if (!anyAdded) break;
|
|
307
281
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
const
|
|
311
|
-
?
|
|
312
|
-
:
|
|
313
|
-
const matchLimitReached = result.matches.length > effectiveLimit;
|
|
314
|
-
const nextSkip = normalizedSkip + selectedMatches.length;
|
|
315
|
-
const limitMessage = `Result limit reached; narrow paths or use skip=${nextSkip}.`;
|
|
282
|
+
}
|
|
283
|
+
const nextSkip = skipFiles + windowFiles.length;
|
|
284
|
+
const limitMessage = fileLimitReached
|
|
285
|
+
? `Showing files ${skipFiles + 1}-${nextSkip} of ${totalFiles}. Use skip=${nextSkip} for the next page, or narrow paths/pattern.`
|
|
286
|
+
: "";
|
|
316
287
|
const { record: recordFile, list: fileList } = createFileRecorder();
|
|
317
288
|
const fileMatchCounts = new Map<string, number>();
|
|
318
289
|
const missingPathsNote =
|
|
@@ -405,7 +376,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
405
376
|
displayLines.push(...rendered.display);
|
|
406
377
|
}
|
|
407
378
|
}
|
|
408
|
-
if (
|
|
379
|
+
if (limitMessage) {
|
|
409
380
|
outputLines.push("", limitMessage);
|
|
410
381
|
}
|
|
411
382
|
if (missingPathsNote) {
|
|
@@ -414,7 +385,9 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
414
385
|
const rawOutput = outputLines.join("\n");
|
|
415
386
|
const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
|
|
416
387
|
const output = truncation.content;
|
|
417
|
-
const truncated = Boolean(
|
|
388
|
+
const truncated = Boolean(
|
|
389
|
+
fileLimitReached || perFileLimitReached || result.limitReached || truncation.truncated || linesTruncated,
|
|
390
|
+
);
|
|
418
391
|
const details: SearchToolDetails = {
|
|
419
392
|
scopePath,
|
|
420
393
|
matchCount: selectedMatches.length,
|
|
@@ -425,8 +398,8 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
425
398
|
count: fileMatchCounts.get(path) ?? 0,
|
|
426
399
|
})),
|
|
427
400
|
truncated,
|
|
428
|
-
|
|
429
|
-
|
|
401
|
+
fileLimitReached: fileLimitReached ? DEFAULT_FILE_LIMIT : undefined,
|
|
402
|
+
perFileLimitReached: perFileLimitReached ? perFileMatchCap : undefined,
|
|
430
403
|
displayContent: displayLines.join("\n"),
|
|
431
404
|
missingPaths: missingPaths.length > 0 ? missingPaths : undefined,
|
|
432
405
|
};
|
|
@@ -499,16 +472,13 @@ export const searchToolRenderer = {
|
|
|
499
472
|
{ icon: "success", title: "Search", description, meta: [formatCount("item", lines.length)] },
|
|
500
473
|
uiTheme,
|
|
501
474
|
);
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
const { expanded } = options;
|
|
506
|
-
const key = new Hasher().bool(expanded).u32(width).digest();
|
|
507
|
-
if (cached?.key === key) return cached.lines;
|
|
475
|
+
return createCachedComponent(
|
|
476
|
+
() => options.expanded,
|
|
477
|
+
width => {
|
|
508
478
|
const listLines = renderTreeList(
|
|
509
479
|
{
|
|
510
480
|
items: lines,
|
|
511
|
-
expanded,
|
|
481
|
+
expanded: options.expanded,
|
|
512
482
|
maxCollapsed: COLLAPSED_TEXT_LIMIT,
|
|
513
483
|
maxCollapsedLines: COLLAPSED_TEXT_LIMIT,
|
|
514
484
|
itemType: "item",
|
|
@@ -516,23 +486,16 @@ export const searchToolRenderer = {
|
|
|
516
486
|
},
|
|
517
487
|
uiTheme,
|
|
518
488
|
);
|
|
519
|
-
|
|
520
|
-
cached = { key, lines: result };
|
|
521
|
-
return result;
|
|
522
|
-
},
|
|
523
|
-
invalidate() {
|
|
524
|
-
cached = undefined;
|
|
489
|
+
return [header, ...listLines].map(l => truncateToWidth(l, width, Ellipsis.Omit));
|
|
525
490
|
},
|
|
526
|
-
|
|
491
|
+
);
|
|
527
492
|
}
|
|
528
493
|
|
|
529
494
|
const matchCount = details?.matchCount ?? 0;
|
|
530
495
|
const fileCount = details?.fileCount ?? 0;
|
|
531
496
|
const truncation = details?.meta?.truncation;
|
|
532
497
|
const limits = details?.meta?.limits;
|
|
533
|
-
const truncated = Boolean(
|
|
534
|
-
details?.truncated || truncation || limits?.matchLimit || limits?.resultLimit || limits?.columnTruncated,
|
|
535
|
-
);
|
|
498
|
+
const truncated = Boolean(details?.truncated || truncation || limits?.columnTruncated);
|
|
536
499
|
|
|
537
500
|
const missingPathsList = details?.missingPaths ?? [];
|
|
538
501
|
const missingNote =
|
|
@@ -561,34 +524,13 @@ export const searchToolRenderer = {
|
|
|
561
524
|
);
|
|
562
525
|
|
|
563
526
|
const textContent = result.details?.displayContent ?? result.content?.find(c => c.type === "text")?.text ?? "";
|
|
564
|
-
const
|
|
565
|
-
const hasSeparators = rawLines.some(line => line.trim().length === 0);
|
|
566
|
-
const matchGroups: string[][] = [];
|
|
567
|
-
if (hasSeparators) {
|
|
568
|
-
let current: string[] = [];
|
|
569
|
-
for (const line of rawLines) {
|
|
570
|
-
if (line.trim().length === 0) {
|
|
571
|
-
if (current.length > 0) {
|
|
572
|
-
matchGroups.push(current);
|
|
573
|
-
current = [];
|
|
574
|
-
}
|
|
575
|
-
continue;
|
|
576
|
-
}
|
|
577
|
-
current.push(line);
|
|
578
|
-
}
|
|
579
|
-
if (current.length > 0) matchGroups.push(current);
|
|
580
|
-
} else {
|
|
581
|
-
const nonEmpty = rawLines.filter(line => line.trim().length > 0);
|
|
582
|
-
if (nonEmpty.length > 0) {
|
|
583
|
-
matchGroups.push(nonEmpty);
|
|
584
|
-
}
|
|
585
|
-
}
|
|
527
|
+
const matchGroups = splitGroupsByBlankLine(textContent.split("\n"));
|
|
586
528
|
|
|
587
|
-
const
|
|
588
|
-
const
|
|
529
|
+
const renderedFileLimit = details?.fileLimitReached;
|
|
530
|
+
const renderedPerFileLimit = details?.perFileLimitReached;
|
|
589
531
|
const truncationReasons: string[] = [];
|
|
590
|
-
if (
|
|
591
|
-
if (
|
|
532
|
+
if (renderedFileLimit) truncationReasons.push(`first ${renderedFileLimit} files (skip to paginate)`);
|
|
533
|
+
if (renderedPerFileLimit) truncationReasons.push(`first ${renderedPerFileLimit} matches per file`);
|
|
592
534
|
if (truncation) truncationReasons.push(truncation.truncatedBy === "lines" ? "line limit" : "size limit");
|
|
593
535
|
if (limits?.columnTruncated) truncationReasons.push(`line length ${limits.columnTruncated.maxColumn}`);
|
|
594
536
|
if (truncation?.artifactId) truncationReasons.push(formatFullOutputReference(truncation.artifactId));
|
|
@@ -599,17 +541,14 @@ export const searchToolRenderer = {
|
|
|
599
541
|
}
|
|
600
542
|
if (missingNote) extraLines.push(missingNote);
|
|
601
543
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
const { expanded } = options;
|
|
606
|
-
const key = new Hasher().bool(expanded).u32(width).digest();
|
|
607
|
-
if (cached?.key === key) return cached.lines;
|
|
544
|
+
return createCachedComponent(
|
|
545
|
+
() => options.expanded,
|
|
546
|
+
width => {
|
|
608
547
|
const collapsedMatchLineBudget = Math.max(COLLAPSED_TEXT_LIMIT - extraLines.length, 0);
|
|
609
548
|
const matchLines = renderTreeList(
|
|
610
549
|
{
|
|
611
550
|
items: matchGroups,
|
|
612
|
-
expanded,
|
|
551
|
+
expanded: options.expanded,
|
|
613
552
|
maxCollapsed: matchGroups.length,
|
|
614
553
|
maxCollapsedLines: collapsedMatchLineBudget,
|
|
615
554
|
itemType: "match",
|
|
@@ -622,14 +561,9 @@ export const searchToolRenderer = {
|
|
|
622
561
|
},
|
|
623
562
|
uiTheme,
|
|
624
563
|
);
|
|
625
|
-
|
|
626
|
-
cached = { key, lines: result };
|
|
627
|
-
return result;
|
|
564
|
+
return [header, ...matchLines, ...extraLines].map(l => truncateToWidth(l, width, Ellipsis.Omit));
|
|
628
565
|
},
|
|
629
|
-
|
|
630
|
-
cached = undefined;
|
|
631
|
-
},
|
|
632
|
-
};
|
|
566
|
+
);
|
|
633
567
|
},
|
|
634
568
|
mergeCallAndResult: true,
|
|
635
569
|
};
|
package/src/tools/write.ts
CHANGED
|
@@ -85,6 +85,21 @@ function stripWriteContent(session: ToolSession, content: string): { text: strin
|
|
|
85
85
|
return { text: cleaned.join("\n"), stripped: true };
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Append a trailing note line to the first text block of a tool result.
|
|
90
|
+
* Mutates `result` in place (the result object is owned by this call).
|
|
91
|
+
*/
|
|
92
|
+
function appendNoteToResult(result: AgentToolResult<WriteToolDetails>, note: string): void {
|
|
93
|
+
const firstText = result.content.find(
|
|
94
|
+
(block): block is { type: "text"; text: string } => block.type === "text" && typeof block.text === "string",
|
|
95
|
+
);
|
|
96
|
+
if (firstText) {
|
|
97
|
+
firstText.text = firstText.text.length > 0 ? `${firstText.text}\n${note}` : note;
|
|
98
|
+
} else {
|
|
99
|
+
result.content.push({ type: "text", text: note });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
88
103
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
89
104
|
// Tool Class
|
|
90
105
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -175,7 +190,6 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
175
190
|
readonly concurrency = "exclusive";
|
|
176
191
|
readonly loadMode = "discoverable";
|
|
177
192
|
readonly summary = "Write content to a file (creates or overwrites)";
|
|
178
|
-
readonly intent = (args: Partial<WriteToolInput>) => (args.path ? `writing ${args.path}` : "writing");
|
|
179
193
|
|
|
180
194
|
readonly #writethrough: WritethroughCallback;
|
|
181
195
|
|
|
@@ -489,6 +503,26 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
489
503
|
};
|
|
490
504
|
}
|
|
491
505
|
|
|
506
|
+
/**
|
|
507
|
+
* Look up a single conflict entry by id and dispatch to {@link #resolveConflict}.
|
|
508
|
+
* Throws a clear `not found` error when the id has been invalidated.
|
|
509
|
+
*/
|
|
510
|
+
async #resolveSingleConflictById(
|
|
511
|
+
id: number,
|
|
512
|
+
replacementContent: string,
|
|
513
|
+
stripped: boolean,
|
|
514
|
+
signal: AbortSignal | undefined,
|
|
515
|
+
context: AgentToolContext | undefined,
|
|
516
|
+
): Promise<AgentToolResult<WriteToolDetails>> {
|
|
517
|
+
const entry = getConflictHistory(this.session).get(id);
|
|
518
|
+
if (!entry) {
|
|
519
|
+
throw new ToolError(
|
|
520
|
+
`Conflict #${id} not found. Conflict ids are registered when \`read\` surfaces a marker block; re-read the file to get a current id.`,
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
return this.#resolveConflict(entry, replacementContent, stripped, signal, context);
|
|
524
|
+
}
|
|
525
|
+
|
|
492
526
|
/**
|
|
493
527
|
* Bulk-resolve every registered conflict via `conflict://*`.
|
|
494
528
|
*
|
|
@@ -631,16 +665,17 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
631
665
|
`Conflict URI scope '/${conflictUri.scope}' is read-only — read \`conflict://${conflictUri.id}/${conflictUri.scope}\` to inspect that side. To write, drop the scope (\`conflict://${conflictUri.id}\`) and put the chosen content (or shorthand like \`@${conflictUri.scope}\`) in \`content\`.`,
|
|
632
666
|
);
|
|
633
667
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
if (
|
|
639
|
-
|
|
640
|
-
|
|
668
|
+
const result =
|
|
669
|
+
conflictUri.id === "*"
|
|
670
|
+
? await this.#resolveAllConflicts(cleanContent, stripped, signal, context)
|
|
671
|
+
: await this.#resolveSingleConflictById(conflictUri.id, cleanContent, stripped, signal, context);
|
|
672
|
+
if (conflictUri.recoveredPrefix !== undefined) {
|
|
673
|
+
appendNoteToResult(
|
|
674
|
+
result,
|
|
675
|
+
`Note: stripped erroneous '${conflictUri.recoveredPrefix}:' prefix from path; conflict URIs are global (use \`conflict://${conflictUri.id}\`, not \`<file>:conflict://${conflictUri.id}\`).`,
|
|
641
676
|
);
|
|
642
677
|
}
|
|
643
|
-
return
|
|
678
|
+
return result;
|
|
644
679
|
}
|
|
645
680
|
const resolvedArchivePath = await this.#resolveArchiveWritePath(path);
|
|
646
681
|
if (resolvedArchivePath) {
|
|
@@ -10,6 +10,7 @@ import path from "node:path";
|
|
|
10
10
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
11
11
|
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
12
12
|
import { glob } from "@oh-my-pi/pi-natives";
|
|
13
|
+
import { fuzzyMatch } from "@oh-my-pi/pi-tui";
|
|
13
14
|
import { formatAge, formatBytes, readImageMetadata } from "@oh-my-pi/pi-utils";
|
|
14
15
|
import { formatHashLines } from "../hashline/hash";
|
|
15
16
|
import type { FileMentionMessage } from "../session/messages";
|
|
@@ -20,7 +21,6 @@ import {
|
|
|
20
21
|
truncateHeadBytes,
|
|
21
22
|
} from "../session/streaming-output";
|
|
22
23
|
import { resolveReadPath } from "../tools/path-utils";
|
|
23
|
-
import { fuzzyMatch } from "./fuzzy";
|
|
24
24
|
import { formatDimensionNote, resizeImage } from "./image-resize";
|
|
25
25
|
|
|
26
26
|
/** Regex to match @filepath patterns in text */
|
|
@@ -2,7 +2,6 @@ import * as fs from "node:fs/promises";
|
|
|
2
2
|
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
3
3
|
import { formatBytes, readImageMetadata, SUPPORTED_IMAGE_MIME_TYPES } from "@oh-my-pi/pi-utils";
|
|
4
4
|
import { resolveReadPath } from "../tools/path-utils";
|
|
5
|
-
import { convertToPng } from "./image-convert";
|
|
6
5
|
import { formatDimensionNote, resizeImage } from "./image-resize";
|
|
7
6
|
|
|
8
7
|
export const MAX_IMAGE_INPUT_BYTES = 20 * 1024 * 1024;
|
|
@@ -42,8 +41,13 @@ export async function ensureSupportedImageInput(image: ImageContent): Promise<Im
|
|
|
42
41
|
if (SUPPORTED_INPUT_IMAGE_MIME_TYPES.has(image.mimeType)) {
|
|
43
42
|
return image;
|
|
44
43
|
}
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
try {
|
|
45
|
+
const bytes = Buffer.from(image.data, "base64");
|
|
46
|
+
const data = await new Bun.Image(bytes).png().toBase64();
|
|
47
|
+
return { type: "image", data, mimeType: "image/png" };
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
export async function loadImageInput(options: LoadImageInputOptions): Promise<LoadedImageInput | null> {
|