@oh-my-pi/pi-coding-agent 15.0.0 → 15.0.2
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 +79 -0
- package/examples/extensions/plan-mode.ts +0 -1
- package/package.json +10 -10
- 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/commands/commit.ts +10 -0
- 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 +44 -3
- package/src/config/model-resolver.ts +1 -4
- package/src/config/settings-schema.ts +82 -1
- package/src/config/settings.ts +1 -1
- package/src/config.ts +3 -219
- package/src/discovery/claude-plugins.ts +19 -7
- 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/eval/py/runner.py +42 -11
- package/src/eval/py/runtime.ts +1 -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/get-commands-handler.ts +77 -0
- 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/plugins/legacy-pi-compat.ts +48 -31
- 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/hashline/input.ts +2 -1
- package/src/hashline/parser.ts +27 -3
- 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 +11 -12
- package/src/internal-urls/registry-helpers.ts +25 -0
- package/src/internal-urls/router.ts +8 -0
- package/src/internal-urls/types.ts +21 -0
- package/src/lsp/config.ts +15 -6
- package/src/lsp/defaults.json +6 -2
- package/src/main.ts +11 -2
- package/src/mcp/oauth-flow.ts +20 -0
- package/src/modes/acp/acp-agent.ts +327 -95
- 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 +93 -8
- 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/extension-ui-controller.ts +3 -2
- 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/host-uris.ts +235 -0
- package/src/modes/rpc/rpc-mode.ts +41 -88
- package/src/modes/rpc/rpc-types.ts +57 -0
- package/src/modes/runtime-init.ts +116 -0
- package/src/modes/theme/defaults/dark-poimandres.json +3 -0
- package/src/modes/theme/defaults/light-poimandres.json +3 -0
- package/src/modes/theme/theme.ts +24 -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/github.md +4 -4
- package/src/prompts/tools/goal.md +13 -0
- package/src/prompts/tools/hashline.md +101 -117
- package/src/prompts/tools/read.md +55 -36
- 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/discovery.ts +5 -2
- package/src/task/executor.ts +19 -8
- 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-command-fixup.ts +47 -0
- package/src/tools/bash-interactive.ts +9 -1
- package/src/tools/bash.ts +66 -19
- 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/render.ts +2 -2
- package/src/tools/browser/tab-supervisor.ts +51 -14
- package/src/tools/conflict-detect.ts +15 -4
- package/src/tools/eval.ts +12 -2
- package/src/tools/find.ts +20 -38
- package/src/tools/gh.ts +44 -10
- package/src/tools/index.ts +22 -11
- package/src/tools/inspect-image.ts +3 -10
- package/src/tools/job.ts +16 -7
- package/src/tools/output-meta.ts +202 -37
- package/src/tools/path-utils.ts +125 -2
- package/src/tools/read.ts +548 -237
- 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/ssh.ts +3 -2
- package/src/tools/write.ts +64 -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/anthropic.ts +5 -0
- package/src/web/search/providers/exa.ts +3 -0
- package/src/web/search/providers/gemini.ts +40 -95
- package/src/web/search/providers/jina.ts +5 -2
- package/src/web/search/providers/zai.ts +5 -2
- 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/ssh.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { executeSSH } from "../ssh/ssh-executor";
|
|
|
16
16
|
import { renderStatusLine } from "../tui";
|
|
17
17
|
import { CachedOutputBlock } from "../tui/output-block";
|
|
18
18
|
import type { ToolSession } from ".";
|
|
19
|
-
import { formatStyledTruncationWarning, type OutputMeta } from "./output-meta";
|
|
19
|
+
import { formatStyledTruncationWarning, type OutputMeta, stripOutputNotice } from "./output-meta";
|
|
20
20
|
import { ToolError } from "./tool-errors";
|
|
21
21
|
import { toolResult } from "./tool-result";
|
|
22
22
|
import { clampTimeout } from "./tool-timeouts";
|
|
@@ -253,7 +253,8 @@ export const sshToolRenderer = {
|
|
|
253
253
|
render: (width: number): string[] => {
|
|
254
254
|
// REACTIVE: read mutable options at render time
|
|
255
255
|
const { expanded, renderContext } = options;
|
|
256
|
-
|
|
256
|
+
// Strip LLM-facing notice so we don't echo it next to the styled warning.
|
|
257
|
+
const output = stripOutputNotice(textContent, details?.meta).trimEnd();
|
|
257
258
|
const outputLines: string[] = [];
|
|
258
259
|
|
|
259
260
|
if (output) {
|
package/src/tools/write.ts
CHANGED
|
@@ -8,6 +8,8 @@ import { isEnoent, isRecord, prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
|
8
8
|
import { type Static, Type } from "@sinclair/typebox";
|
|
9
9
|
import { stripHashlinePrefixes } from "../edit";
|
|
10
10
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
11
|
+
import { InternalUrlRouter } from "../internal-urls";
|
|
12
|
+
import { parseInternalUrl } from "../internal-urls/parse";
|
|
11
13
|
import { createLspWritethrough, type FileDiagnosticsResult, type WritethroughCallback, writethroughNoop } from "../lsp";
|
|
12
14
|
import { getLanguageFromPath, highlightCode, type Theme } from "../modes/theme/theme";
|
|
13
15
|
import writeDescription from "../prompts/tools/write.md" with { type: "text" };
|
|
@@ -85,6 +87,21 @@ function stripWriteContent(session: ToolSession, content: string): { text: strin
|
|
|
85
87
|
return { text: cleaned.join("\n"), stripped: true };
|
|
86
88
|
}
|
|
87
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Append a trailing note line to the first text block of a tool result.
|
|
92
|
+
* Mutates `result` in place (the result object is owned by this call).
|
|
93
|
+
*/
|
|
94
|
+
function appendNoteToResult(result: AgentToolResult<WriteToolDetails>, note: string): void {
|
|
95
|
+
const firstText = result.content.find(
|
|
96
|
+
(block): block is { type: "text"; text: string } => block.type === "text" && typeof block.text === "string",
|
|
97
|
+
);
|
|
98
|
+
if (firstText) {
|
|
99
|
+
firstText.text = firstText.text.length > 0 ? `${firstText.text}\n${note}` : note;
|
|
100
|
+
} else {
|
|
101
|
+
result.content.push({ type: "text", text: note });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
88
105
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
89
106
|
// Tool Class
|
|
90
107
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -175,7 +192,6 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
175
192
|
readonly concurrency = "exclusive";
|
|
176
193
|
readonly loadMode = "discoverable";
|
|
177
194
|
readonly summary = "Write content to a file (creates or overwrites)";
|
|
178
|
-
readonly intent = (args: Partial<WriteToolInput>) => (args.path ? `writing ${args.path}` : "writing");
|
|
179
195
|
|
|
180
196
|
readonly #writethrough: WritethroughCallback;
|
|
181
197
|
|
|
@@ -489,6 +505,26 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
489
505
|
};
|
|
490
506
|
}
|
|
491
507
|
|
|
508
|
+
/**
|
|
509
|
+
* Look up a single conflict entry by id and dispatch to {@link #resolveConflict}.
|
|
510
|
+
* Throws a clear `not found` error when the id has been invalidated.
|
|
511
|
+
*/
|
|
512
|
+
async #resolveSingleConflictById(
|
|
513
|
+
id: number,
|
|
514
|
+
replacementContent: string,
|
|
515
|
+
stripped: boolean,
|
|
516
|
+
signal: AbortSignal | undefined,
|
|
517
|
+
context: AgentToolContext | undefined,
|
|
518
|
+
): Promise<AgentToolResult<WriteToolDetails>> {
|
|
519
|
+
const entry = getConflictHistory(this.session).get(id);
|
|
520
|
+
if (!entry) {
|
|
521
|
+
throw new ToolError(
|
|
522
|
+
`Conflict #${id} not found. Conflict ids are registered when \`read\` surfaces a marker block; re-read the file to get a current id.`,
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
return this.#resolveConflict(entry, replacementContent, stripped, signal, context);
|
|
526
|
+
}
|
|
527
|
+
|
|
492
528
|
/**
|
|
493
529
|
* Bulk-resolve every registered conflict via `conflict://*`.
|
|
494
530
|
*
|
|
@@ -624,6 +660,24 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
624
660
|
return untilAborted(signal, async () => {
|
|
625
661
|
// Strip hashline display prefixes (LINE+ID|) if the model copied them from read output
|
|
626
662
|
const { text: cleanContent, stripped } = stripWriteContent(this.session, content);
|
|
663
|
+
const internalRouter = InternalUrlRouter.instance();
|
|
664
|
+
if (internalRouter.canHandle(path)) {
|
|
665
|
+
const parsed = parseInternalUrl(path);
|
|
666
|
+
const scheme = parsed.protocol.replace(/:$/, "").toLowerCase();
|
|
667
|
+
const handler = internalRouter.getHandler(scheme);
|
|
668
|
+
if (handler?.write) {
|
|
669
|
+
await handler.write(parsed, cleanContent, { cwd: this.session.cwd, signal });
|
|
670
|
+
let resultText = `Successfully wrote ${cleanContent.length} bytes to ${path}`;
|
|
671
|
+
if (stripped) {
|
|
672
|
+
resultText += `\nNote: auto-stripped hashline display prefixes from content before writing.`;
|
|
673
|
+
}
|
|
674
|
+
return { content: [{ type: "text", text: resultText }], details: {} };
|
|
675
|
+
}
|
|
676
|
+
// Schemes without a `write` hook fall through to existing logic
|
|
677
|
+
// (local:// resolves to a backing file via plan-mode-guard) or are
|
|
678
|
+
// rejected downstream when no backing file exists.
|
|
679
|
+
}
|
|
680
|
+
|
|
627
681
|
const conflictUri = parseConflictUri(path);
|
|
628
682
|
if (conflictUri) {
|
|
629
683
|
if (conflictUri.scope) {
|
|
@@ -631,16 +685,17 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
631
685
|
`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
686
|
);
|
|
633
687
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
if (
|
|
639
|
-
|
|
640
|
-
|
|
688
|
+
const result =
|
|
689
|
+
conflictUri.id === "*"
|
|
690
|
+
? await this.#resolveAllConflicts(cleanContent, stripped, signal, context)
|
|
691
|
+
: await this.#resolveSingleConflictById(conflictUri.id, cleanContent, stripped, signal, context);
|
|
692
|
+
if (conflictUri.recoveredPrefix !== undefined) {
|
|
693
|
+
appendNoteToResult(
|
|
694
|
+
result,
|
|
695
|
+
`Note: stripped erroneous '${conflictUri.recoveredPrefix}:' prefix from path; conflict URIs are global (use \`conflict://${conflictUri.id}\`, not \`<file>:conflict://${conflictUri.id}\`).`,
|
|
641
696
|
);
|
|
642
697
|
}
|
|
643
|
-
return
|
|
698
|
+
return result;
|
|
644
699
|
}
|
|
645
700
|
const resolvedArchivePath = await this.#resolveArchiveWritePath(path);
|
|
646
701
|
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> {
|