@oh-my-pi/pi-coding-agent 15.3.2 → 15.4.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 +110 -0
- package/dist/types/cli/file-processor.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +45 -3
- package/dist/types/config/settings.d.ts +1 -1
- package/dist/types/debug/raw-sse.d.ts +2 -0
- package/dist/types/edit/file-read-cache.d.ts +15 -4
- package/dist/types/edit/index.d.ts +3 -8
- package/dist/types/edit/renderer.d.ts +1 -2
- package/dist/types/eval/__tests__/shared-executors.test.d.ts +1 -0
- package/dist/types/eval/js/shared/local-module-loader.d.ts +16 -0
- package/dist/types/eval/js/shared/rewrite-imports.d.ts +4 -0
- package/dist/types/eval/js/shared/runtime.d.ts +14 -8
- package/dist/types/eval/py/executor.d.ts +1 -2
- package/dist/types/eval/py/kernel.d.ts +6 -0
- package/dist/types/eval/py/tool-bridge.d.ts +1 -5
- package/dist/types/eval/session-id.d.ts +3 -0
- package/dist/types/extensibility/extensions/types.d.ts +1 -3
- package/dist/types/hashline/anchors.d.ts +15 -9
- package/dist/types/hashline/constants.d.ts +0 -2
- package/dist/types/hashline/diff.d.ts +1 -2
- package/dist/types/hashline/executor.d.ts +52 -0
- package/dist/types/hashline/hash.d.ts +44 -93
- package/dist/types/hashline/index.d.ts +2 -1
- package/dist/types/hashline/input.d.ts +2 -9
- package/dist/types/hashline/recovery.d.ts +3 -9
- package/dist/types/hashline/tokenizer.d.ts +91 -0
- package/dist/types/hashline/types.d.ts +5 -7
- package/dist/types/modes/components/extensions/types.d.ts +0 -4
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +11 -15
- package/dist/types/session/agent-storage.d.ts +11 -10
- package/dist/types/slash-commands/acp-builtins.d.ts +3 -3
- package/dist/types/slash-commands/types.d.ts +0 -5
- package/dist/types/task/executor.d.ts +2 -0
- package/dist/types/tool-discovery/tool-index.d.ts +0 -50
- package/dist/types/tools/index.d.ts +2 -8
- package/dist/types/tools/match-line-format.d.ts +4 -4
- package/dist/types/tools/output-schema-validator.d.ts +64 -0
- package/dist/types/tools/review.d.ts +13 -0
- package/dist/types/tools/search-tool-bm25.d.ts +1 -1
- package/dist/types/tools/search.d.ts +4 -3
- package/dist/types/utils/edit-mode.d.ts +1 -1
- package/dist/types/web/kagi.d.ts +4 -2
- package/dist/types/web/parallel.d.ts +4 -3
- package/dist/types/web/scrapers/types.d.ts +2 -1
- package/dist/types/web/search/index.d.ts +12 -4
- package/dist/types/web/search/provider.d.ts +2 -1
- package/dist/types/web/search/providers/anthropic.d.ts +9 -4
- package/dist/types/web/search/providers/base.d.ts +34 -2
- package/dist/types/web/search/providers/brave.d.ts +8 -1
- package/dist/types/web/search/providers/codex.d.ts +13 -9
- package/dist/types/web/search/providers/exa.d.ts +10 -1
- package/dist/types/web/search/providers/gemini.d.ts +20 -23
- package/dist/types/web/search/providers/jina.d.ts +2 -1
- package/dist/types/web/search/providers/kagi.d.ts +4 -1
- package/dist/types/web/search/providers/kimi.d.ts +10 -1
- package/dist/types/web/search/providers/parallel.d.ts +3 -2
- package/dist/types/web/search/providers/perplexity.d.ts +5 -2
- package/dist/types/web/search/providers/searxng.d.ts +2 -1
- package/dist/types/web/search/providers/synthetic.d.ts +5 -8
- package/dist/types/web/search/providers/tavily.d.ts +11 -4
- package/dist/types/web/search/providers/utils.d.ts +8 -6
- package/dist/types/web/search/providers/zai.d.ts +12 -3
- package/package.json +7 -7
- package/src/cli/file-processor.ts +12 -2
- package/src/cli.ts +0 -8
- package/src/commands/commit.ts +8 -8
- package/src/config/prompt-templates.ts +6 -6
- package/src/config/settings-schema.ts +47 -3
- package/src/config/settings.ts +5 -5
- package/src/debug/raw-sse.ts +68 -3
- package/src/edit/file-read-cache.ts +68 -25
- package/src/edit/index.ts +6 -37
- package/src/edit/renderer.ts +9 -47
- package/src/edit/streaming.ts +43 -56
- package/src/eval/__tests__/shared-executors.test.ts +520 -0
- package/src/eval/js/context-manager.ts +64 -53
- package/src/eval/js/shared/local-module-loader.ts +265 -0
- package/src/eval/js/shared/prelude.txt +4 -0
- package/src/eval/js/shared/rewrite-imports.ts +85 -0
- package/src/eval/js/shared/runtime.ts +129 -86
- package/src/eval/js/worker-core.ts +23 -38
- package/src/eval/py/executor.ts +155 -84
- package/src/eval/py/kernel.ts +10 -1
- package/src/eval/py/prelude.py +22 -24
- package/src/eval/py/runner.py +203 -85
- package/src/eval/py/tool-bridge.ts +17 -10
- package/src/eval/session-id.ts +8 -0
- package/src/exec/bash-executor.ts +27 -16
- package/src/extensibility/extensions/runner.ts +0 -1
- package/src/extensibility/extensions/types.ts +1 -3
- package/src/hashline/anchors.ts +56 -65
- package/src/hashline/apply.ts +29 -31
- package/src/hashline/constants.ts +0 -3
- package/src/hashline/diff-preview.ts +4 -5
- package/src/hashline/diff.ts +30 -4
- package/src/hashline/execute.ts +91 -26
- package/src/hashline/executor.ts +239 -0
- package/src/hashline/grammar.lark +12 -10
- package/src/hashline/hash.ts +69 -114
- package/src/hashline/index.ts +2 -1
- package/src/hashline/input.ts +48 -41
- package/src/hashline/prefixes.ts +21 -11
- package/src/hashline/recovery.ts +63 -71
- package/src/hashline/stream.ts +2 -2
- package/src/hashline/tokenizer.ts +467 -0
- package/src/hashline/types.ts +6 -8
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/modes/components/extensions/types.ts +0 -5
- package/src/modes/components/session-observer-overlay.ts +11 -2
- package/src/modes/components/settings-selector.ts +10 -1
- package/src/modes/components/tree-selector.ts +10 -2
- package/src/modes/controllers/command-controller.ts +1 -3
- package/src/modes/controllers/extension-ui-controller.ts +10 -11
- package/src/modes/controllers/selector-controller.ts +5 -5
- package/src/modes/theme/theme.ts +4 -2
- package/src/modes/types.ts +4 -1
- package/src/modes/utils/ui-helpers.ts +4 -0
- package/src/prompts/agents/explore.md +1 -1
- package/src/prompts/tools/ast-edit.md +1 -1
- package/src/prompts/tools/ast-grep.md +1 -1
- package/src/prompts/tools/eval.md +1 -1
- package/src/prompts/tools/hashline.md +73 -94
- package/src/prompts/tools/read.md +4 -4
- package/src/prompts/tools/search.md +3 -3
- package/src/sdk.ts +33 -26
- package/src/session/agent-session.ts +59 -66
- package/src/session/agent-storage.ts +13 -14
- package/src/slash-commands/acp-builtins.ts +3 -3
- package/src/slash-commands/types.ts +0 -6
- package/src/task/executor.ts +26 -57
- package/src/task/index.ts +8 -4
- package/src/tool-discovery/tool-index.ts +0 -134
- package/src/tools/ast-edit.ts +36 -13
- package/src/tools/ast-grep.ts +45 -4
- package/src/tools/browser/tab-worker.ts +3 -2
- package/src/tools/eval.ts +2 -1
- package/src/tools/fetch.ts +23 -14
- package/src/tools/index.ts +2 -8
- package/src/tools/irc.ts +59 -5
- package/src/tools/match-line-format.ts +5 -7
- package/src/tools/output-schema-validator.ts +132 -0
- package/src/tools/read.ts +142 -31
- package/src/tools/review.ts +23 -0
- package/src/tools/search-tool-bm25.ts +3 -30
- package/src/tools/search.ts +48 -16
- package/src/tools/write.ts +3 -3
- package/src/tools/yield.ts +32 -41
- package/src/utils/edit-mode.ts +1 -2
- package/src/utils/file-mentions.ts +2 -2
- package/src/web/kagi.ts +15 -6
- package/src/web/parallel.ts +9 -6
- package/src/web/scrapers/types.ts +7 -1
- package/src/web/scrapers/youtube.ts +13 -7
- package/src/web/search/index.ts +37 -11
- package/src/web/search/provider.ts +5 -3
- package/src/web/search/providers/anthropic.ts +30 -21
- package/src/web/search/providers/base.ts +35 -2
- package/src/web/search/providers/brave.ts +4 -4
- package/src/web/search/providers/codex.ts +118 -89
- package/src/web/search/providers/exa.ts +3 -2
- package/src/web/search/providers/gemini.ts +58 -155
- package/src/web/search/providers/jina.ts +4 -4
- package/src/web/search/providers/kagi.ts +17 -11
- package/src/web/search/providers/kimi.ts +29 -13
- package/src/web/search/providers/parallel.ts +171 -23
- package/src/web/search/providers/perplexity.ts +38 -37
- package/src/web/search/providers/searxng.ts +3 -1
- package/src/web/search/providers/synthetic.ts +16 -19
- package/src/web/search/providers/tavily.ts +23 -18
- package/src/web/search/providers/utils.ts +11 -17
- package/src/web/search/providers/zai.ts +16 -8
- package/dist/types/hashline/parser.d.ts +0 -7
- package/dist/types/mcp/discoverable-tool-metadata.d.ts +0 -7
- package/dist/types/tools/vim.d.ts +0 -58
- package/dist/types/vim/buffer.d.ts +0 -41
- package/dist/types/vim/commands.d.ts +0 -6
- package/dist/types/vim/engine.d.ts +0 -47
- package/dist/types/vim/parser.d.ts +0 -3
- package/dist/types/vim/render.d.ts +0 -25
- package/dist/types/vim/types.d.ts +0 -182
- package/src/hashline/parser.ts +0 -246
- package/src/mcp/discoverable-tool-metadata.ts +0 -24
- package/src/prompts/tools/vim.md +0 -98
- package/src/tools/vim.ts +0 -949
- package/src/vim/buffer.ts +0 -309
- package/src/vim/commands.ts +0 -382
- package/src/vim/engine.ts +0 -2409
- package/src/vim/parser.ts +0 -134
- package/src/vim/render.ts +0 -252
- package/src/vim/types.ts +0 -197
package/src/tools/read.ts
CHANGED
|
@@ -9,9 +9,10 @@ import { Text } from "@oh-my-pi/pi-tui";
|
|
|
9
9
|
import { getRemoteDir, logger, prompt, readImageMetadata, untilAborted } from "@oh-my-pi/pi-utils";
|
|
10
10
|
import * as z from "zod/v4";
|
|
11
11
|
import { getFileReadCache } from "../edit/file-read-cache";
|
|
12
|
+
import { normalizeToLF } from "../edit/normalize";
|
|
12
13
|
import { isNotebookPath, readEditableNotebookText } from "../edit/notebook";
|
|
13
14
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
14
|
-
import {
|
|
15
|
+
import { computeFileHash, formatHashlineHeader, formatNumberedLine, formatNumberedLines } from "../hashline/hash";
|
|
15
16
|
import { InternalUrlRouter } from "../internal-urls";
|
|
16
17
|
import { parseInternalUrl } from "../internal-urls/parse";
|
|
17
18
|
import type { InternalUrl } from "../internal-urls/types";
|
|
@@ -113,13 +114,50 @@ function prependLineNumbers(text: string, startNum: number): string {
|
|
|
113
114
|
return textLines.map((line, i) => `${startNum + i}|${line}`).join("\n");
|
|
114
115
|
}
|
|
115
116
|
|
|
117
|
+
interface HashlineHeaderContext {
|
|
118
|
+
header: string;
|
|
119
|
+
fileHash: string;
|
|
120
|
+
fullText: string;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function buildHashlineHeaderContext(displayPath: string, fullText: string): HashlineHeaderContext {
|
|
124
|
+
const normalized = normalizeToLF(fullText);
|
|
125
|
+
const fileHash = computeFileHash(normalized);
|
|
126
|
+
return {
|
|
127
|
+
header: formatHashlineHeader(displayPath, fileHash),
|
|
128
|
+
fileHash,
|
|
129
|
+
fullText: normalized,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function readHashlineHeaderContext(absolutePath: string, cwd: string): Promise<HashlineHeaderContext> {
|
|
134
|
+
const fullText = await Bun.file(absolutePath).text();
|
|
135
|
+
return buildHashlineHeaderContext(formatPathRelativeToCwd(absolutePath, cwd), fullText);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function prependHashlineHeader(text: string, context: HashlineHeaderContext | undefined): string {
|
|
139
|
+
return context ? `${context.header}\n${text}` : text;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function recordHashlineSnapshot(
|
|
143
|
+
session: ToolSession,
|
|
144
|
+
absolutePath: string | undefined,
|
|
145
|
+
context: HashlineHeaderContext | undefined,
|
|
146
|
+
): void {
|
|
147
|
+
if (!context || !absolutePath || !path.isAbsolute(absolutePath)) return;
|
|
148
|
+
getFileReadCache(session).recordContiguous(absolutePath, 1, context.fullText.split("\n"), {
|
|
149
|
+
fullText: context.fullText,
|
|
150
|
+
fileHash: context.fileHash,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
116
154
|
function formatTextWithMode(
|
|
117
155
|
text: string,
|
|
118
156
|
startNum: number,
|
|
119
157
|
shouldAddHashLines: boolean,
|
|
120
158
|
shouldAddLineNumbers: boolean,
|
|
121
159
|
): string {
|
|
122
|
-
if (shouldAddHashLines) return
|
|
160
|
+
if (shouldAddHashLines) return formatNumberedLines(text, startNum);
|
|
123
161
|
if (shouldAddLineNumbers) return prependLineNumbers(text, startNum);
|
|
124
162
|
return text;
|
|
125
163
|
}
|
|
@@ -150,7 +188,7 @@ function formatSingleLine(
|
|
|
150
188
|
shouldAddHashLines: boolean,
|
|
151
189
|
shouldAddLineNumbers: boolean,
|
|
152
190
|
): string {
|
|
153
|
-
if (shouldAddHashLines) return
|
|
191
|
+
if (shouldAddHashLines) return formatNumberedLine(line, text);
|
|
154
192
|
if (shouldAddLineNumbers) return `${line}|${text}`;
|
|
155
193
|
return text;
|
|
156
194
|
}
|
|
@@ -165,9 +203,7 @@ function formatMergedBraceLine(
|
|
|
165
203
|
): { model: string; display: string } {
|
|
166
204
|
const merged = `${headText.trimEnd()} .. ${tailText.trim()}`;
|
|
167
205
|
if (shouldAddHashLines) {
|
|
168
|
-
|
|
169
|
-
const end = formatLineHash(endLine, tailText);
|
|
170
|
-
return { model: `${start}-${end}${HL_BODY_SEP}${merged}`, display: merged };
|
|
206
|
+
return { model: `${startLine}-${endLine}:${merged}`, display: merged };
|
|
171
207
|
}
|
|
172
208
|
if (shouldAddLineNumbers) {
|
|
173
209
|
return { model: `${startLine}-${endLine}|${merged}`, display: merged };
|
|
@@ -180,17 +216,38 @@ function countTextLines(text: string): number {
|
|
|
180
216
|
return text.split("\n").length;
|
|
181
217
|
}
|
|
182
218
|
|
|
219
|
+
/** Inclusive line range describing one elided span in a structural summary. */
|
|
220
|
+
interface ElidedRange {
|
|
221
|
+
start: number;
|
|
222
|
+
end: number;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Sample ranges shown in the footer to demonstrate the multi-range syntax. */
|
|
226
|
+
const FOOTER_RANGE_SAMPLES = 2;
|
|
227
|
+
|
|
183
228
|
/**
|
|
184
229
|
* Footer appended to summarized reads telling the model how to recover the
|
|
185
230
|
* elided body. Without this hint, agents either ignore the `...`/`{ .. }`
|
|
186
|
-
* markers or burn a turn guessing the right selector (see issue #1046).
|
|
231
|
+
* markers or burn a turn guessing the right selector (see issue #1046). The
|
|
232
|
+
* footer demonstrates the multi-range selector syntax with concrete sample
|
|
233
|
+
* ranges drawn from the actual elision so the model re-reads only what it
|
|
234
|
+
* needs instead of falling back to `:raw` or whole-file reads.
|
|
187
235
|
*/
|
|
188
|
-
function formatSummaryElisionFooter(
|
|
189
|
-
|
|
190
|
-
|
|
236
|
+
function formatSummaryElisionFooter(
|
|
237
|
+
readPath: string,
|
|
238
|
+
elidedRanges: ReadonlyArray<ElidedRange>,
|
|
239
|
+
elidedLines: number,
|
|
240
|
+
): string {
|
|
241
|
+
if (elidedRanges.length === 0) return "";
|
|
191
242
|
const lineWord = elidedLines === 1 ? "line" : "lines";
|
|
192
|
-
const
|
|
193
|
-
|
|
243
|
+
const sampleCount = Math.min(elidedRanges.length, FOOTER_RANGE_SAMPLES);
|
|
244
|
+
const selector = elidedRanges
|
|
245
|
+
.slice(0, sampleCount)
|
|
246
|
+
.map(r => `${r.start}-${r.end}`)
|
|
247
|
+
.join(",");
|
|
248
|
+
const example = `${readPath}:${selector}`;
|
|
249
|
+
const tail = elidedRanges.length > sampleCount ? `, e.g. ${example}` : ` with ${example}`;
|
|
250
|
+
return `[${elidedLines} ${lineWord} elided; re-read needed ranges${tail}]`;
|
|
194
251
|
}
|
|
195
252
|
const READ_CHUNK_SIZE = 8 * 1024;
|
|
196
253
|
|
|
@@ -844,9 +901,18 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
844
901
|
|
|
845
902
|
const shouldAddHashLines = displayMode.hashLines;
|
|
846
903
|
const shouldAddLineNumbers = shouldAddHashLines ? false : displayMode.lineNumbers;
|
|
904
|
+
const hashContext =
|
|
905
|
+
shouldAddHashLines && options.sourcePath
|
|
906
|
+
? buildHashlineHeaderContext(formatPathRelativeToCwd(options.sourcePath, this.session.cwd), text)
|
|
907
|
+
: undefined;
|
|
908
|
+
recordHashlineSnapshot(this.session, options.sourcePath, hashContext);
|
|
909
|
+
let emittedHashlineHeader = false;
|
|
847
910
|
const formatText = (content: string, startNum: number): string => {
|
|
848
911
|
details.displayContent = { text: content, startLine: startNum };
|
|
849
|
-
|
|
912
|
+
const formatted = formatTextWithMode(content, startNum, shouldAddHashLines, shouldAddLineNumbers);
|
|
913
|
+
if (!hashContext || emittedHashlineHeader) return formatted;
|
|
914
|
+
emittedHashlineHeader = true;
|
|
915
|
+
return prependHashlineHeader(formatted, hashContext);
|
|
850
916
|
};
|
|
851
917
|
|
|
852
918
|
let outputText: string;
|
|
@@ -862,7 +928,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
862
928
|
if (shouldAddHashLines) {
|
|
863
929
|
outputText = `[Line ${startLineDisplay} is ${formatBytes(
|
|
864
930
|
firstLineBytes,
|
|
865
|
-
)}, exceeds ${formatBytes(DEFAULT_MAX_BYTES)} limit. Hashline output requires full lines; cannot
|
|
931
|
+
)}, exceeds ${formatBytes(DEFAULT_MAX_BYTES)} limit. Hashline output requires full lines; cannot emit an editable numbered preview for a truncated line.]`;
|
|
866
932
|
} else {
|
|
867
933
|
outputText = formatText(snippet.text, startLineDisplay);
|
|
868
934
|
}
|
|
@@ -928,6 +994,12 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
928
994
|
const totalLines = allLines.length;
|
|
929
995
|
const shouldAddHashLines = displayMode.hashLines;
|
|
930
996
|
const shouldAddLineNumbers = shouldAddHashLines ? false : displayMode.lineNumbers;
|
|
997
|
+
const hashContext =
|
|
998
|
+
shouldAddHashLines && options.sourcePath
|
|
999
|
+
? buildHashlineHeaderContext(formatPathRelativeToCwd(options.sourcePath, this.session.cwd), text)
|
|
1000
|
+
: undefined;
|
|
1001
|
+
recordHashlineSnapshot(this.session, options.sourcePath, hashContext);
|
|
1002
|
+
let emittedHashlineHeader = false;
|
|
931
1003
|
|
|
932
1004
|
const resultBuilder = toolResult(details);
|
|
933
1005
|
if (options.sourcePath) resultBuilder.sourcePath(options.sourcePath);
|
|
@@ -943,7 +1015,9 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
943
1015
|
}
|
|
944
1016
|
const effectiveEnd = Math.min(range.endLine ?? totalLines, totalLines);
|
|
945
1017
|
const sliced = allLines.slice(range.startLine - 1, effectiveEnd).join("\n");
|
|
946
|
-
|
|
1018
|
+
const formatted = formatTextWithMode(sliced, range.startLine, shouldAddHashLines, shouldAddLineNumbers);
|
|
1019
|
+
parts.push(hashContext && !emittedHashlineHeader ? prependHashlineHeader(formatted, hashContext) : formatted);
|
|
1020
|
+
if (hashContext) emittedHashlineHeader = true;
|
|
947
1021
|
}
|
|
948
1022
|
|
|
949
1023
|
const outputText = parts.length > 0 ? parts.join("\n\n…\n\n") : "";
|
|
@@ -1002,6 +1076,11 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1002
1076
|
|
|
1003
1077
|
const shouldAddHashLines = !rawSelector && displayMode.hashLines;
|
|
1004
1078
|
const shouldAddLineNumbers = rawSelector ? false : shouldAddHashLines ? false : displayMode.lineNumbers;
|
|
1079
|
+
const hashContext = shouldAddHashLines
|
|
1080
|
+
? await readHashlineHeaderContext(absolutePath, this.session.cwd)
|
|
1081
|
+
: undefined;
|
|
1082
|
+
recordHashlineSnapshot(this.session, absolutePath, hashContext);
|
|
1083
|
+
let emittedHashlineHeader = false;
|
|
1005
1084
|
const maxColumns = resolveOutputMaxColumns(this.session.settings);
|
|
1006
1085
|
|
|
1007
1086
|
const blocks: string[] = [];
|
|
@@ -1042,11 +1121,18 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1042
1121
|
}
|
|
1043
1122
|
|
|
1044
1123
|
if (collectedLines.length > 0) {
|
|
1045
|
-
getFileReadCache(this.session).recordContiguous(
|
|
1124
|
+
getFileReadCache(this.session).recordContiguous(
|
|
1125
|
+
absolutePath,
|
|
1126
|
+
range.startLine,
|
|
1127
|
+
collectedLines,
|
|
1128
|
+
hashContext ? { fullText: hashContext.fullText, fileHash: hashContext.fileHash } : {},
|
|
1129
|
+
);
|
|
1046
1130
|
}
|
|
1047
1131
|
|
|
1048
1132
|
const blockText = collectedLines.join("\n");
|
|
1049
|
-
|
|
1133
|
+
const formatted = formatTextWithMode(blockText, range.startLine, shouldAddHashLines, shouldAddLineNumbers);
|
|
1134
|
+
blocks.push(hashContext && !emittedHashlineHeader ? prependHashlineHeader(formatted, hashContext) : formatted);
|
|
1135
|
+
if (hashContext) emittedHashlineHeader = true;
|
|
1050
1136
|
}
|
|
1051
1137
|
|
|
1052
1138
|
let outputText = blocks.join("\n\n…\n\n");
|
|
@@ -1335,7 +1421,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1335
1421
|
#renderSummary(summary: SummaryResult): {
|
|
1336
1422
|
text: string;
|
|
1337
1423
|
displayText: string;
|
|
1338
|
-
|
|
1424
|
+
elidedRanges: ElidedRange[];
|
|
1339
1425
|
elidedLines: number;
|
|
1340
1426
|
} {
|
|
1341
1427
|
const displayMode = resolveFileDisplayMode(this.session);
|
|
@@ -1396,13 +1482,13 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1396
1482
|
|
|
1397
1483
|
const modelParts: string[] = [];
|
|
1398
1484
|
const displayParts: string[] = [];
|
|
1399
|
-
|
|
1485
|
+
const elidedRanges: ElidedRange[] = [];
|
|
1400
1486
|
let elidedLines = 0;
|
|
1401
1487
|
for (const unit of units) {
|
|
1402
1488
|
if (unit.kind === "elided") {
|
|
1403
1489
|
modelParts.push("...");
|
|
1404
1490
|
displayParts.push("...");
|
|
1405
|
-
|
|
1491
|
+
elidedRanges.push({ start: unit.startLine, end: unit.endLine });
|
|
1406
1492
|
elidedLines += unit.endLine - unit.startLine + 1;
|
|
1407
1493
|
continue;
|
|
1408
1494
|
}
|
|
@@ -1417,7 +1503,9 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1417
1503
|
);
|
|
1418
1504
|
modelParts.push(formatted.model);
|
|
1419
1505
|
displayParts.push(formatted.display);
|
|
1420
|
-
|
|
1506
|
+
// Suggest the full brace range so re-reading shows both braces
|
|
1507
|
+
// plus the elided body in one shot.
|
|
1508
|
+
elidedRanges.push({ start: unit.startLine, end: unit.endLine });
|
|
1421
1509
|
// Merged brace pair encloses (start+1)..(end-1) as elided.
|
|
1422
1510
|
elidedLines += Math.max(0, unit.endLine - unit.startLine - 1);
|
|
1423
1511
|
continue;
|
|
@@ -1426,7 +1514,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1426
1514
|
displayParts.push(unit.text);
|
|
1427
1515
|
}
|
|
1428
1516
|
|
|
1429
|
-
return { text: modelParts.join("\n"), displayText: displayParts.join("\n"),
|
|
1517
|
+
return { text: modelParts.join("\n"), displayText: displayParts.join("\n"), elidedRanges, elidedLines };
|
|
1430
1518
|
}
|
|
1431
1519
|
|
|
1432
1520
|
async execute(
|
|
@@ -1674,15 +1762,20 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1674
1762
|
const renderedSummary = this.#renderSummary(summary);
|
|
1675
1763
|
const footer = formatSummaryElisionFooter(
|
|
1676
1764
|
localReadPath,
|
|
1677
|
-
renderedSummary.
|
|
1765
|
+
renderedSummary.elidedRanges,
|
|
1678
1766
|
renderedSummary.elidedLines,
|
|
1679
1767
|
);
|
|
1680
|
-
const
|
|
1768
|
+
const summaryHashContext = displayMode.hashLines
|
|
1769
|
+
? await readHashlineHeaderContext(absolutePath, this.session.cwd)
|
|
1770
|
+
: undefined;
|
|
1771
|
+
recordHashlineSnapshot(this.session, absolutePath, summaryHashContext);
|
|
1772
|
+
const bodyText = footer ? `${renderedSummary.text}\n\n${footer}` : renderedSummary.text;
|
|
1773
|
+
const modelText = prependHashlineHeader(bodyText, summaryHashContext);
|
|
1681
1774
|
details = {
|
|
1682
1775
|
displayContent: { text: renderedSummary.displayText, startLine: 1 },
|
|
1683
1776
|
summary: {
|
|
1684
1777
|
lines: countTextLines(renderedSummary.text),
|
|
1685
|
-
elidedSpans: renderedSummary.
|
|
1778
|
+
elidedSpans: renderedSummary.elidedRanges.length,
|
|
1686
1779
|
elidedLines: renderedSummary.elidedLines,
|
|
1687
1780
|
},
|
|
1688
1781
|
};
|
|
@@ -1820,16 +1913,29 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1820
1913
|
firstLineExceedsLimit,
|
|
1821
1914
|
};
|
|
1822
1915
|
|
|
1916
|
+
const shouldAddHashLines = !rawSelector && displayMode.hashLines;
|
|
1917
|
+
const shouldAddLineNumbers = rawSelector ? false : shouldAddHashLines ? false : displayMode.lineNumbers;
|
|
1918
|
+
const hashContext = shouldAddHashLines
|
|
1919
|
+
? await readHashlineHeaderContext(absolutePath, this.session.cwd)
|
|
1920
|
+
: undefined;
|
|
1921
|
+
|
|
1823
1922
|
if (collectedLines.length > 0 && !firstLineExceedsLimit) {
|
|
1824
|
-
getFileReadCache(this.session).recordContiguous(
|
|
1923
|
+
getFileReadCache(this.session).recordContiguous(
|
|
1924
|
+
absolutePath,
|
|
1925
|
+
startLineDisplay,
|
|
1926
|
+
collectedLines,
|
|
1927
|
+
hashContext ? { fullText: hashContext.fullText, fileHash: hashContext.fileHash } : {},
|
|
1928
|
+
);
|
|
1825
1929
|
}
|
|
1826
1930
|
|
|
1827
|
-
const shouldAddHashLines = !rawSelector && displayMode.hashLines;
|
|
1828
|
-
const shouldAddLineNumbers = rawSelector ? false : shouldAddHashLines ? false : displayMode.lineNumbers;
|
|
1829
1931
|
let capturedDisplayContent: { text: string; startLine: number } | undefined;
|
|
1932
|
+
let emittedHashlineHeader = false;
|
|
1830
1933
|
const formatText = (text: string, startNum: number): string => {
|
|
1831
1934
|
capturedDisplayContent = { text, startLine: startNum };
|
|
1832
|
-
|
|
1935
|
+
const formatted = formatTextWithMode(text, startNum, shouldAddHashLines, shouldAddLineNumbers);
|
|
1936
|
+
if (!hashContext || emittedHashlineHeader) return formatted;
|
|
1937
|
+
emittedHashlineHeader = true;
|
|
1938
|
+
return prependHashlineHeader(formatted, hashContext);
|
|
1833
1939
|
};
|
|
1834
1940
|
|
|
1835
1941
|
let outputText: string;
|
|
@@ -1841,7 +1947,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1841
1947
|
if (shouldAddHashLines) {
|
|
1842
1948
|
outputText = `[Line ${startLineDisplay} is ${formatBytes(
|
|
1843
1949
|
firstLineBytes,
|
|
1844
|
-
)}, exceeds ${formatBytes(maxBytesForRead)} limit. Hashline output requires full lines; cannot
|
|
1950
|
+
)}, exceeds ${formatBytes(maxBytesForRead)} limit. Hashline output requires full lines; cannot emit an editable numbered preview for a truncated line.]`;
|
|
1845
1951
|
} else {
|
|
1846
1952
|
outputText = formatText(snippet.text, startLineDisplay);
|
|
1847
1953
|
}
|
|
@@ -1964,7 +2070,12 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1964
2070
|
const shouldAddLineNumbers = shouldAddHashLines ? false : displayMode.lineNumbers;
|
|
1965
2071
|
|
|
1966
2072
|
const rawText = region.lines.join("\n");
|
|
1967
|
-
const
|
|
2073
|
+
const hashContext = shouldAddHashLines
|
|
2074
|
+
? await readHashlineHeaderContext(entry.absolutePath, this.session.cwd)
|
|
2075
|
+
: undefined;
|
|
2076
|
+
recordHashlineSnapshot(this.session, entry.absolutePath, hashContext);
|
|
2077
|
+
const formattedBody = formatTextWithMode(rawText, region.startLine, shouldAddHashLines, shouldAddLineNumbers);
|
|
2078
|
+
const formattedText = prependHashlineHeader(formattedBody, hashContext);
|
|
1968
2079
|
|
|
1969
2080
|
const details: ReadToolDetails = {
|
|
1970
2081
|
resolvedPath: entry.absolutePath,
|
package/src/tools/review.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { isRecord } from "@oh-my-pi/pi-utils";
|
|
|
15
15
|
import * as z from "zod/v4";
|
|
16
16
|
import type { Theme, ThemeColor } from "../modes/theme/theme";
|
|
17
17
|
import { subprocessToolRegistry } from "../task/subprocess-tool-registry";
|
|
18
|
+
import type { ReviewFinding } from "../task/types";
|
|
18
19
|
export type FindingPriority = "P0" | "P1" | "P2" | "P3";
|
|
19
20
|
|
|
20
21
|
export interface FindingPriorityInfo {
|
|
@@ -186,6 +187,28 @@ export interface SubmitReviewDetails {
|
|
|
186
187
|
|
|
187
188
|
// Re-export types for external use
|
|
188
189
|
export type { ReportFindingDetails };
|
|
190
|
+
/**
|
|
191
|
+
* Coerce a tool-side `ReportFindingDetails` into the cross-boundary
|
|
192
|
+
* `ReviewFinding` shape consumed by the reviewer agent's JTD output schema.
|
|
193
|
+
*
|
|
194
|
+
* The `report_finding` tool exposes `priority` as a string enum (`"P0".."P3"`)
|
|
195
|
+
* for ergonomics, but the bundled reviewer schema (and every custom review
|
|
196
|
+
* agent that mirrors it) declares `priority: number`. Without this coercion
|
|
197
|
+
* the auto-populated `findings[]` fails JTD validation and every review run
|
|
198
|
+
* that surfaces a finding is rejected with `findings.0.priority: expected
|
|
199
|
+
* number, received string`.
|
|
200
|
+
*/
|
|
201
|
+
export function toReviewFinding(details: ReportFindingDetails): ReviewFinding {
|
|
202
|
+
return {
|
|
203
|
+
title: details.title,
|
|
204
|
+
body: details.body,
|
|
205
|
+
priority: getPriorityInfo(details.priority).ord,
|
|
206
|
+
confidence: details.confidence,
|
|
207
|
+
file_path: details.file_path,
|
|
208
|
+
line_start: details.line_start,
|
|
209
|
+
line_end: details.line_end,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
189
212
|
|
|
190
213
|
// Register report_finding handler
|
|
191
214
|
subprocessToolRegistry.register<ReportFindingDetails>("report_finding", {
|
|
@@ -19,12 +19,6 @@ import type { ToolSession } from ".";
|
|
|
19
19
|
import { formatCount, replaceTabs, TRUNCATE_LENGTHS } from "./render-utils";
|
|
20
20
|
import { ToolError } from "./tool-errors";
|
|
21
21
|
|
|
22
|
-
// Re-export legacy MCP types for back-compat (tests and external callers may reference them)
|
|
23
|
-
export type {
|
|
24
|
-
DiscoverableMCPSearchIndex,
|
|
25
|
-
DiscoverableMCPTool,
|
|
26
|
-
} from "../mcp/discoverable-tool-metadata";
|
|
27
|
-
|
|
28
22
|
const DEFAULT_LIMIT = 8;
|
|
29
23
|
const TOOL_DISCOVERY_TITLE = "Tool Discovery";
|
|
30
24
|
const COLLAPSED_MATCH_LIMIT = 5;
|
|
@@ -81,21 +75,7 @@ function buildSearchToolBm25Content(details: SearchToolBm25Details): string {
|
|
|
81
75
|
/** Get discoverable tools for description rendering. Falls back to empty array on error. */
|
|
82
76
|
function getDiscoverableToolsForDescription(session: ToolSession): DiscoverableTool[] {
|
|
83
77
|
try {
|
|
84
|
-
|
|
85
|
-
if (session.getDiscoverableTools) {
|
|
86
|
-
return session.getDiscoverableTools();
|
|
87
|
-
}
|
|
88
|
-
// Legacy MCP path — adapt DiscoverableMCPTool (with `description`) → DiscoverableTool.
|
|
89
|
-
const legacy = session.getDiscoverableMCPTools?.() ?? [];
|
|
90
|
-
return legacy.map(t => ({
|
|
91
|
-
name: t.name,
|
|
92
|
-
label: t.label,
|
|
93
|
-
summary: t.description,
|
|
94
|
-
source: "mcp" as const,
|
|
95
|
-
serverName: t.serverName,
|
|
96
|
-
mcpToolName: t.mcpToolName,
|
|
97
|
-
schemaKeys: t.schemaKeys,
|
|
98
|
-
}));
|
|
78
|
+
return session.getDiscoverableTools?.() ?? [];
|
|
99
79
|
} catch {
|
|
100
80
|
return [];
|
|
101
81
|
}
|
|
@@ -103,15 +83,8 @@ function getDiscoverableToolsForDescription(session: ToolSession): DiscoverableT
|
|
|
103
83
|
|
|
104
84
|
function getDiscoverableToolSearchIndexForExecution(session: ToolSession): DiscoverableToolSearchIndex {
|
|
105
85
|
try {
|
|
106
|
-
|
|
107
|
-
if (
|
|
108
|
-
const cached = session.getDiscoverableToolSearchIndex();
|
|
109
|
-
if (cached) return cached;
|
|
110
|
-
}
|
|
111
|
-
// Legacy MCP: use cached MCP index. Its documents expose `tool.description` as well as
|
|
112
|
-
// `tool.summary`, so it is structurally compatible with DiscoverableToolSearchIndex.
|
|
113
|
-
const mcpCached = session.getDiscoverableMCPSearchIndex?.();
|
|
114
|
-
if (mcpCached) return mcpCached as unknown as DiscoverableToolSearchIndex;
|
|
86
|
+
const cached = session.getDiscoverableToolSearchIndex?.();
|
|
87
|
+
if (cached) return cached;
|
|
115
88
|
} catch {}
|
|
116
89
|
return buildDiscoverableToolSearchIndex(getDiscoverableToolsForDescription(session));
|
|
117
90
|
}
|
package/src/tools/search.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
|
9
9
|
import * as z from "zod/v4";
|
|
10
10
|
import { getFileReadCache } from "../edit/file-read-cache";
|
|
11
11
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
12
|
+
import { computeFileHash, formatHashlineHeader } from "../hashline/hash";
|
|
12
13
|
import type { Theme } from "../modes/theme/theme";
|
|
13
14
|
import searchDescription from "../prompts/tools/search.md" with { type: "text" };
|
|
14
15
|
import { DEFAULT_MAX_COLUMN, type TruncationResult, truncateHead } from "../session/streaming-output";
|
|
@@ -38,13 +39,13 @@ import {
|
|
|
38
39
|
import { ToolError } from "./tool-errors";
|
|
39
40
|
import { toolResult } from "./tool-result";
|
|
40
41
|
|
|
42
|
+
const searchPathEntrySchema = z.string().describe("file, directory, glob, or internal URL to search");
|
|
41
43
|
const searchSchema = z
|
|
42
44
|
.object({
|
|
43
45
|
pattern: z.string().describe("regex pattern"),
|
|
44
46
|
paths: z
|
|
45
|
-
.
|
|
46
|
-
.
|
|
47
|
-
.describe("files, directories, globs, or internal URLs to search"),
|
|
47
|
+
.union([searchPathEntrySchema, z.array(searchPathEntrySchema).min(1)])
|
|
48
|
+
.describe("file, directory, glob, internal URL, or array of those to search"),
|
|
48
49
|
i: z.boolean().optional().describe("case-insensitive search"),
|
|
49
50
|
gitignore: z.boolean().optional().describe("respect gitignore"),
|
|
50
51
|
skip: z
|
|
@@ -55,6 +56,9 @@ const searchSchema = z
|
|
|
55
56
|
.strict();
|
|
56
57
|
|
|
57
58
|
export type SearchToolInput = z.infer<typeof searchSchema>;
|
|
59
|
+
export function toPathList(input: string | string[] | undefined): string[] {
|
|
60
|
+
return typeof input === "string" ? [input] : (input ?? []);
|
|
61
|
+
}
|
|
58
62
|
|
|
59
63
|
/** Maximum number of distinct files surfaced in a single response. The
|
|
60
64
|
* agent paginates further pages via `skip`. */
|
|
@@ -236,7 +240,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
236
240
|
_onUpdate?: AgentToolUpdateCallback<SearchToolDetails>,
|
|
237
241
|
_toolContext?: AgentToolContext,
|
|
238
242
|
): Promise<AgentToolResult<SearchToolDetails>> {
|
|
239
|
-
const { pattern, paths, i, gitignore, skip } = params;
|
|
243
|
+
const { pattern, paths: rawPaths, i, gitignore, skip } = params;
|
|
240
244
|
|
|
241
245
|
return untilAborted(signal, async () => {
|
|
242
246
|
const normalizedPattern = pattern.trim();
|
|
@@ -248,6 +252,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
248
252
|
if (normalizedSkip < 0 || !Number.isFinite(normalizedSkip)) {
|
|
249
253
|
throw new ToolError("Skip must be a non-negative number");
|
|
250
254
|
}
|
|
255
|
+
const paths = toPathList(rawPaths);
|
|
251
256
|
for (const entry of paths) {
|
|
252
257
|
if (containsTopLevelComma(entry)) {
|
|
253
258
|
throw new ToolError('paths is an array — pass ["a", "b"] not ["a,b"]');
|
|
@@ -303,7 +308,6 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
303
308
|
}
|
|
304
309
|
const { globFilter } = scope;
|
|
305
310
|
const baseDisplayMode = resolveFileDisplayMode(this.session);
|
|
306
|
-
const immutableDisplayMode = resolveFileDisplayMode(this.session, { immutable: true });
|
|
307
311
|
|
|
308
312
|
const effectiveOutputMode = GrepOutputMode.Content;
|
|
309
313
|
// Multi-scope = more than one file may match. We fetch up to
|
|
@@ -485,14 +489,27 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
485
489
|
matchesByFile.get(relativePath)!.push(match);
|
|
486
490
|
}
|
|
487
491
|
const displayLines: string[] = [];
|
|
492
|
+
const hashContexts = new Map<string, { absolutePath: string; fileHash: string }>();
|
|
493
|
+
if (baseDisplayMode.hashLines) {
|
|
494
|
+
for (const relativePath of fileList) {
|
|
495
|
+
if (archiveDisplaySet.has(relativePath)) continue;
|
|
496
|
+
const absoluteFilePath = path.resolve(this.session.cwd, relativePath);
|
|
497
|
+
if (immutableSourcePaths.has(absoluteFilePath)) continue;
|
|
498
|
+
try {
|
|
499
|
+
const fullText = await Bun.file(absoluteFilePath).text();
|
|
500
|
+
const fileHash = computeFileHash(fullText);
|
|
501
|
+
hashContexts.set(relativePath, { absolutePath: absoluteFilePath, fileHash });
|
|
502
|
+
} catch {
|
|
503
|
+
// Best-effort: if the file disappeared between grep and render, fall back to plain line output.
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
488
507
|
const renderMatchesForFile = (relativePath: string): { model: string[]; display: string[] } => {
|
|
489
508
|
const modelOut: string[] = [];
|
|
490
509
|
const displayOut: string[] = [];
|
|
491
510
|
const fileMatches = matchesByFile.get(relativePath) ?? [];
|
|
492
|
-
const
|
|
493
|
-
const useHashLines =
|
|
494
|
-
? immutableDisplayMode.hashLines
|
|
495
|
-
: baseDisplayMode.hashLines;
|
|
511
|
+
const hashContext = hashContexts.get(relativePath);
|
|
512
|
+
const useHashLines = hashContext !== undefined;
|
|
496
513
|
const lineNumberWidth = fileMatches.reduce((width, match) => {
|
|
497
514
|
let nextWidth = Math.max(width, String(match.lineNumber).length);
|
|
498
515
|
for (const ctx of match.contextBefore ?? []) {
|
|
@@ -533,17 +550,21 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
533
550
|
}
|
|
534
551
|
fileMatchCounts.set(relativePath, (fileMatchCounts.get(relativePath) ?? 0) + 1);
|
|
535
552
|
}
|
|
536
|
-
if (cacheEntries.length > 0 &&
|
|
537
|
-
getFileReadCache(this.session).recordSparse(
|
|
553
|
+
if (cacheEntries.length > 0 && hashContext) {
|
|
554
|
+
getFileReadCache(this.session).recordSparse(hashContext.absolutePath, cacheEntries, {
|
|
555
|
+
fileHash: hashContext.fileHash,
|
|
556
|
+
});
|
|
538
557
|
}
|
|
539
558
|
return { model: modelOut, display: displayOut };
|
|
540
559
|
};
|
|
541
560
|
if (isDirectory) {
|
|
542
561
|
const grouped = formatGroupedFiles(fileList, relativePath => {
|
|
543
562
|
const rendered = renderMatchesForFile(relativePath);
|
|
563
|
+
const hashContext = hashContexts.get(relativePath);
|
|
544
564
|
return {
|
|
545
565
|
modelLines: rendered.model,
|
|
546
566
|
displayLines: rendered.display,
|
|
567
|
+
headerSuffix: hashContext ? `#${hashContext.fileHash}` : "",
|
|
547
568
|
skip: rendered.model.length === 0,
|
|
548
569
|
};
|
|
549
570
|
});
|
|
@@ -552,6 +573,15 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
552
573
|
} else {
|
|
553
574
|
for (const relativePath of fileList) {
|
|
554
575
|
const rendered = renderMatchesForFile(relativePath);
|
|
576
|
+
if (rendered.model.length === 0) continue;
|
|
577
|
+
if (outputLines.length > 0) {
|
|
578
|
+
outputLines.push("");
|
|
579
|
+
displayLines.push("");
|
|
580
|
+
}
|
|
581
|
+
const hashContext = hashContexts.get(relativePath);
|
|
582
|
+
if (hashContext) {
|
|
583
|
+
outputLines.push(formatHashlineHeader(relativePath, hashContext.fileHash));
|
|
584
|
+
}
|
|
555
585
|
outputLines.push(...rendered.model);
|
|
556
586
|
displayLines.push(...rendered.display);
|
|
557
587
|
}
|
|
@@ -607,7 +637,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
607
637
|
|
|
608
638
|
interface SearchRenderArgs {
|
|
609
639
|
pattern: string;
|
|
610
|
-
paths?: string[];
|
|
640
|
+
paths?: string | string[];
|
|
611
641
|
i?: boolean;
|
|
612
642
|
gitignore?: boolean;
|
|
613
643
|
skip?: number;
|
|
@@ -618,8 +648,9 @@ const COLLAPSED_TEXT_LIMIT = PREVIEW_LIMITS.COLLAPSED_LINES * 2;
|
|
|
618
648
|
export const searchToolRenderer = {
|
|
619
649
|
inline: true,
|
|
620
650
|
renderCall(args: SearchRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
651
|
+
const paths = toPathList(args.paths);
|
|
621
652
|
const meta: string[] = [];
|
|
622
|
-
if (
|
|
653
|
+
if (paths.length) meta.push(`in ${paths.join(", ")}`);
|
|
623
654
|
if (args.i) meta.push("case:insensitive");
|
|
624
655
|
if (args.gitignore === false) meta.push("gitignore:false");
|
|
625
656
|
if (args.skip !== undefined && args.skip > 0) meta.push(`skip:${args.skip}`);
|
|
@@ -745,11 +776,12 @@ export const searchToolRenderer = {
|
|
|
745
776
|
let contextDir = searchBase ?? "";
|
|
746
777
|
return group.map(line => {
|
|
747
778
|
if (line.startsWith("## ")) {
|
|
748
|
-
// Strip optional ` (suffix)`
|
|
779
|
+
// Strip optional ` (suffix)` and `#hash` before resolving.
|
|
749
780
|
const fileName = line
|
|
750
781
|
.slice(3)
|
|
751
782
|
.trimEnd()
|
|
752
|
-
.replace(/\s+\([^)]*\)\s*$/, "")
|
|
783
|
+
.replace(/\s+\([^)]*\)\s*$/, "")
|
|
784
|
+
.replace(/#[0-9a-f]+$/, "");
|
|
753
785
|
const absPath = contextDir && fileName ? path.join(contextDir, fileName) : undefined;
|
|
754
786
|
const styled = uiTheme.fg("dim", line);
|
|
755
787
|
return absPath ? fileHyperlink(absPath, styled) : styled;
|
|
@@ -760,7 +792,7 @@ export const searchToolRenderer = {
|
|
|
760
792
|
.trimEnd()
|
|
761
793
|
.replace(/\s+\([^)]*\)\s*$/, "");
|
|
762
794
|
const isDirectory = raw.endsWith("/");
|
|
763
|
-
const name = raw.replace(/\/$/, "");
|
|
795
|
+
const name = isDirectory ? raw.replace(/\/$/, "") : raw.replace(/#[0-9a-f]+$/, "");
|
|
764
796
|
if (isDirectory) {
|
|
765
797
|
if (searchBase) {
|
|
766
798
|
contextDir = name === "." ? searchBase : path.join(searchBase, name);
|
package/src/tools/write.ts
CHANGED
|
@@ -74,8 +74,8 @@ export interface WriteToolDetails {
|
|
|
74
74
|
/**
|
|
75
75
|
* Strip hashline display prefixes from write content.
|
|
76
76
|
*
|
|
77
|
-
* Only active when hashline edit mode is enabled — the model sees `
|
|
78
|
-
* prefixes in read output and sometimes copies them into write content.
|
|
77
|
+
* Only active when hashline edit mode is enabled — the model sees `¶PATH#HASH`
|
|
78
|
+
* headers plus `LINE:` prefixes in read output and sometimes copies them into write content.
|
|
79
79
|
*/
|
|
80
80
|
function stripWriteContent(session: ToolSession, content: string): { text: string; stripped: boolean } {
|
|
81
81
|
if (!resolveFileDisplayMode(session).hashLines) {
|
|
@@ -658,7 +658,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
658
658
|
context?: AgentToolContext,
|
|
659
659
|
): Promise<AgentToolResult<WriteToolDetails>> {
|
|
660
660
|
return untilAborted(signal, async () => {
|
|
661
|
-
// Strip hashline display prefixes (LINE
|
|
661
|
+
// Strip hashline display prefixes (¶PATH#HASH + LINE:) if the model copied them from read output
|
|
662
662
|
const { text: cleanContent, stripped } = stripWriteContent(this.session, content);
|
|
663
663
|
const internalRouter = InternalUrlRouter.instance();
|
|
664
664
|
if (internalRouter.canHandle(path)) {
|