From 8f2cd23e39da12990a84ea3ace028f738e5ae9a3 Mon Sep 17 00:00:00 2001 From: can1357 Date: Sat, 18 Jul 2026 22:04:43 +0200 Subject: [PATCH] Revert "Merge PR #5812: fix(read): honor exact line selector bounds (@roboomp)" This reverts commit 58c71d5b504ab93cc5466e21a0d7db98da66e004, reversing changes made to 7c7227bc8e60e0f366c9eb667e3ae260eefa34f4. --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/src/tools/read.ts | 172 +++++++++++++++--- .../coding-agent/src/utils/block-context.ts | 62 ++----- .../test/read-multi-range.test.ts | 27 +-- .../test/tools/read-artifact-large.test.ts | 1 - .../test/tools/read-pdf-line-range.test.ts | 1 - .../test/tools/read-raw-range.test.ts | 8 +- 7 files changed, 185 insertions(+), 87 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index e134b4cf4..3c7a5c6bb 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -131,6 +131,7 @@ ### Added - Added `PI_CONFIG_FILES`, a platform-delimited (`:` on Unix, `;` on Windows) environment path-list of settings overlays loaded before `--config` overlays, so wrapper scripts can inject settings without argv surgery ([#5685](https://github.com/can1357/oh-my-pi/issues/5685)). +- Fixed the `/extensions` dashboard tab labeled "Agents (standard)" being confused with the `/agents` subagents feature — the `.agent`/`.agents` config-standard provider now presents as "Agent Dirs (.agent/.agents)" since it lists skills, rules, prompts, commands, and context/system files, never subagents ([#5821](https://github.com/can1357/oh-my-pi/issues/5821)). ## [17.0.2] - 2026-07-17 diff --git a/packages/coding-agent/src/tools/read.ts b/packages/coding-agent/src/tools/read.ts index d99e42d60..21cf26894 100644 --- a/packages/coding-agent/src/tools/read.ts +++ b/packages/coding-agent/src/tools/read.ts @@ -54,7 +54,7 @@ import { } from "../session/streaming-output"; import { fileHyperlink, renderCodeCell, renderMarkdownCell, renderStatusLine, tryResolveInternalUrlSync } from "../tui"; import { CachedOutputBlock, markFramedBlockComponent } from "../tui/output-block"; -import { buildLineEntries, type LineEntry, lineEntriesToPlainText } from "../utils/block-context"; +import { buildLineEntriesWithBlockContext, type LineEntry, lineEntriesToPlainText } from "../utils/block-context"; import { resolveFileDisplayMode } from "../utils/file-display-mode"; import { ImageInputTooLargeError, @@ -164,7 +164,7 @@ const PROSE_SUMMARY_EXTENSIONS = new Set([".md", ".txt"]); // Remote mount path prefix (sshfs mounts) - skip fuzzy matching to avoid hangs const REMOTE_MOUNT_PREFIX = getRemoteDir() + path.sep; -async function readSmallFileLines(absolutePath: string, fileSize: number): Promise { +async function readBracketContextFullLines(absolutePath: string, fileSize: number): Promise { if (fileSize > SNAPSHOT_MAX_BYTES) return undefined; try { return normalizeToLF(await Bun.file(absolutePath).text()).split("\n"); @@ -395,6 +395,40 @@ function formatSummaryElisionFooter( } const READ_CHUNK_SIZE = 8 * 1024; +/** + * Context lines added around an explicit range read. Anchor-stale failures + * cluster on edits whose anchors land just outside the most recent read + * window, but the data (`scripts/session-stats/analyze_selector_reads.py`) + * shows most follow-up reads are disjoint hops, not adjacent extensions — + * so symmetric padding rarely pays for itself. + * + * Leading=1 catches accidental single-line reads where the anchor is the + * line immediately above the requested start. Trailing=3 buffers the + * common case where the agent asks for a narrow range and then needs the + * next few lines to disambiguate an anchor. + */ +const RANGE_LEADING_CONTEXT_LINES = 1; +const RANGE_TRAILING_CONTEXT_LINES = 3; + +/** + * Expand a [start, end) range with leading/trailing context lines on the + * sides where the user actually constrained the range. A start of 0 (no + * explicit offset) does not get leading context — that's already an + * open-ended read from the top. + */ +function expandRangeWithContext( + requestedStart: number, + requestedEnd: number, + totalLines: number, + expandStart: boolean, + expandEnd: boolean, +): { startLine: number; endLine: number } { + return { + startLine: expandStart ? Math.max(0, requestedStart - RANGE_LEADING_CONTEXT_LINES) : requestedStart, + endLine: expandEnd ? Math.min(totalLines, requestedEnd + RANGE_TRAILING_CONTEXT_LINES) : requestedEnd, + }; +} + async function streamLinesFromFile( filePath: string, startLine: number, @@ -1271,10 +1305,26 @@ export class ReadTool implements AgentTool { const details = options.details ?? {}; const allLines = text.split("\n"); const totalLines = allLines.length; + // User-requested 0-indexed range start. Lines BEFORE this are leading + // context (added below if offset is explicit). const requestedStart = offset ? Math.max(0, offset - 1) : 0; - const startLine = requestedStart; const ignoreResultLimits = options.ignoreResultLimits ?? false; - const endLine = limit !== undefined ? Math.min(startLine + limit, allLines.length) : allLines.length; + const requestedEnd = limit !== undefined ? Math.min(requestedStart + limit, allLines.length) : allLines.length; + // Expand only on sides the user actually constrained: leading context + // when offset>1, trailing context when a finite limit was set. Raw mode + // never expands — without line numbers the padding is indistinguishable + // from requested content, so `raw:31-31` must return line 31 and nothing + // else (verbatim-extraction contract). + const rawDisplay = options.raw === true; + const expanded = expandRangeWithContext( + requestedStart, + requestedEnd, + allLines.length, + !rawDisplay && offset !== undefined && offset > 1, + !rawDisplay && limit !== undefined, + ); + const startLine = expanded.startLine; + const endLineExpanded = expanded.endLine; const startLineDisplay = startLine + 1; const resultBuilder = toolResult(details); @@ -1300,6 +1350,7 @@ export class ReadTool implements AgentTool { .done(); } + const endLine = endLineExpanded; const selectedContent = allLines.slice(startLine, endLine).join("\n"); const userLimitedLines = limit !== undefined ? endLine - startLine : undefined; const truncation = ignoreResultLimits ? noTruncResult(selectedContent) : truncateHead(selectedContent); @@ -1344,8 +1395,10 @@ export class ReadTool implements AgentTool { emittedHashlineHeader = true; return prependHashlineHeader(formatted, hashContext); }; - const buildSelectedLineEntries = (endLineDisplay: number): LineEntry[] => - buildLineEntries(allLines, [{ startLine: startLineDisplay, endLine: endLineDisplay }]); + const buildLineEntries = (endLineDisplay: number): LineEntry[] => + buildLineEntriesWithBlockContext(allLines, [{ startLine: startLineDisplay, endLine: endLineDisplay }], { + path: options.sourcePath, + }); let outputText: string; let truncationInfo: @@ -1383,7 +1436,7 @@ export class ReadTool implements AgentTool { rawSeenLines = contiguousLineNumbers(startLineDisplay, outputLines); outputText = formatText(truncation.content, startLineDisplay); } else { - outputText = formatLineEntries(buildSelectedLineEntries(endLineDisplay), startLineDisplay); + outputText = formatLineEntries(buildLineEntries(endLineDisplay), startLineDisplay); } details.truncation = truncation; truncationInfo = { @@ -1398,7 +1451,7 @@ export class ReadTool implements AgentTool { rawSeenLines = contiguousLineNumbers(startLineDisplay, userLimitedLines); outputText = formatText(selectedContent, startLineDisplay); } else { - outputText = formatLineEntries(buildSelectedLineEntries(endLine), startLineDisplay); + outputText = formatLineEntries(buildLineEntries(endLine), startLineDisplay); } outputText += `\n\n[${remaining} more lines in ${options.entityLabel}. Use :${nextOffset} to continue]`; } else { @@ -1406,7 +1459,7 @@ export class ReadTool implements AgentTool { rawSeenLines = contiguousLineNumbers(startLineDisplay, endLine - startLine); outputText = formatText(truncation.content, startLineDisplay); } else { - outputText = formatLineEntries(buildSelectedLineEntries(endLine), startLineDisplay); + outputText = formatLineEntries(buildLineEntries(endLine), startLineDisplay); } } @@ -1427,8 +1480,8 @@ export class ReadTool implements AgentTool { * Render a multi-range read against in-memory text. Each range emits a * formatted block with its own anchors / line numbers, blocks are joined * with an elision separator, and ranges past EOF surface as `[…]` notices - * so the model can correct the next call. Context is never added because - * multi-range callers always specify exact bounds. + * so the model can correct the next call. No leading/trailing context is + * added — multi-range callers always specify exact bounds. */ #buildInMemoryMultiRangeResult( text: string, @@ -1485,7 +1538,7 @@ export class ReadTool implements AgentTool { if (options.raw === true) { outputText = rawParts.length > 0 ? rawParts.join("\n\n…\n\n") : ""; } else if (visibleSpans.length > 0) { - const entries = buildLineEntries(allLines, visibleSpans); + const entries = buildLineEntriesWithBlockContext(allLines, visibleSpans, { path: options.sourcePath }); if (shouldAddHashLines) seenLines = lineNumbersFromEntries(entries); const firstLine = entries.find(entry => entry.kind === "line"); if (firstLine?.kind === "line") { @@ -1569,7 +1622,7 @@ export class ReadTool implements AgentTool { const notices: string[] = []; const visibleSpans: Array<{ startLine: number; endLine: number }> = []; const displayLineByNumber = new Map(); - const fullLines = rawSelector ? undefined : await readSmallFileLines(absolutePath, fileSize); + const fullLines = rawSelector ? undefined : await readBracketContextFullLines(absolutePath, fileSize); let columnTruncated = 0; let displayContent: { text: string; startLine: number; lineNumbers?: Array } | undefined; @@ -1637,9 +1690,23 @@ export class ReadTool implements AgentTool { let outputText: string; if (!rawSelector && fullLines && visibleSpans.length > 0) { - const entries = buildLineEntries(fullLines, visibleSpans, { - lineText: (lineNumber, sourceText) => displayLineByNumber.get(lineNumber) ?? sourceText, - }); + const entries = buildLineEntriesWithBlockContext( + fullLines, + visibleSpans, + { path: absolutePath }, + { + lineText: (lineNumber, sourceText) => { + const visibleText = displayLineByNumber.get(lineNumber); + if (visibleText !== undefined) return visibleText; + if (maxColumns <= 0) return sourceText; + const truncated = truncateLine(sourceText, maxColumns); + if (truncated.wasTruncated) { + columnTruncated = maxColumns; + } + return truncated.text; + }, + }, + ); const firstLine = entries.find(entry => entry.kind === "line"); displayContent = { text: lineEntriesToPlainText(entries, BRACKET_CONTEXT_ELLIPSIS), @@ -2446,7 +2513,7 @@ export class ReadTool implements AgentTool { // Raw text or line-range mode const { offset, limit } = selToOffsetLimit(parsed); // Try ACP bridge first — editor's in-memory buffer is source of truth. - // Request full text so local range rendering preserves exact bounds and line numbers. + // Request full text so local range rendering keeps normal context and line numbers. const bridgePromise = this.#routeReadThroughBridge(absolutePath); if (bridgePromise !== undefined) { try { @@ -2468,15 +2535,24 @@ export class ReadTool implements AgentTool { } } + // User-requested 0-indexed range start. Lines BEFORE this become + // leading context (added below if offset is explicit). Raw mode + // never adds context: without line numbers the padding is + // indistinguishable from requested content, so `raw:31-31` must + // return line 31 and nothing else. const rawSelector = isRawSelector(parsed); const requestedStart = offset ? Math.max(0, offset - 1) : 0; - const startLine = requestedStart; + const expandStart = !rawSelector && offset !== undefined && offset > 1; + const expandEnd = !rawSelector && limit !== undefined; + const leadingContext = expandStart ? Math.min(requestedStart, RANGE_LEADING_CONTEXT_LINES) : 0; + const trailingContext = expandEnd ? RANGE_TRAILING_CONTEXT_LINES : 0; + const startLine = requestedStart - leadingContext; const startLineDisplay = startLine + 1; const DEFAULT_LIMIT = this.#defaultLimit; const effectiveLimit = limit ?? DEFAULT_LIMIT; - const maxLinesToCollect = Math.min(effectiveLimit, DEFAULT_MAX_LINES); - const selectedLineLimit = effectiveLimit; + const maxLinesToCollect = Math.min(effectiveLimit + leadingContext + trailingContext, DEFAULT_MAX_LINES); + const selectedLineLimit = effectiveLimit + leadingContext + trailingContext; // Scale byte budget with line limit so the configured line count actually fits. // Assume ~512 bytes/line average; never go below the shared default. const maxBytesForRead = Math.max(DEFAULT_MAX_BYTES, maxLinesToCollect * 512); @@ -2538,6 +2614,15 @@ export class ReadTool implements AgentTool { if (cloned) displayLines = cloned; } + const displayLineByNumber = new Map(); + for (let i = 0; i < displayLines.length; i++) { + displayLineByNumber.set(startLineDisplay + i, displayLines[i] ?? ""); + } + const bracketContextFullLines = rawSelector + ? undefined + : await readBracketContextFullLines(absolutePath, fileSize); + const displayedEndLine = startLineDisplay + Math.max(0, displayLines.length - 1); + const selectedContent = displayLines.join("\n"); const userLimitedLines = collectedLines.length; @@ -2594,6 +2679,36 @@ export class ReadTool implements AgentTool { emittedHashlineHeader = true; return prependHashlineHeader(formatted, hashContext); }; + const formatBracketAwareText = (): string | undefined => { + if (!bracketContextFullLines) return undefined; + const entries = buildLineEntriesWithBlockContext( + bracketContextFullLines, + [{ startLine: startLineDisplay, endLine: displayedEndLine }], + { path: absolutePath }, + { + lineText: (lineNumber, sourceText) => { + const visibleText = displayLineByNumber.get(lineNumber); + if (visibleText !== undefined) return visibleText; + if (maxColumns <= 0) return sourceText; + const truncated = truncateLine(sourceText, maxColumns); + if (truncated.wasTruncated) { + columnTruncated = maxColumns; + } + return truncated.text; + }, + }, + ); + const firstLine = entries.find(entry => entry.kind === "line"); + capturedDisplayContent = { + text: lineEntriesToPlainText(entries, BRACKET_CONTEXT_ELLIPSIS), + startLine: firstLine?.kind === "line" ? firstLine.lineNumber : startLineDisplay, + lineNumbers: entries.map(entry => (entry.kind === "line" ? entry.lineNumber : null)), + }; + const formatted = formatLineEntriesWithMode(entries, shouldAddHashLines, shouldAddLineNumbers); + if (!hashContext || emittedHashlineHeader) return formatted; + emittedHashlineHeader = true; + return prependHashlineHeader(formatted, hashContext); + }; let outputText: string; @@ -2624,7 +2739,7 @@ export class ReadTool implements AgentTool { }, }; } else if (truncation.truncated) { - outputText = formatText(truncation.content, startLineDisplay); + outputText = formatBracketAwareText() ?? formatText(truncation.content, startLineDisplay); details = { truncation }; sourcePath = absolutePath; truncationInfo = { @@ -2638,7 +2753,7 @@ export class ReadTool implements AgentTool { } else if (startLine + userLimitedLines < totalFileLines || !reachedEof) { const nextOffset = startLine + userLimitedLines + 1; - outputText = formatText(truncation.content, startLineDisplay); + outputText = formatBracketAwareText() ?? formatText(truncation.content, startLineDisplay); outputText += reachedEof ? `\n\n[${totalFileLines - (startLine + userLimitedLines)} more lines in file. Use :${nextOffset} to continue]` : `\n\n[More lines in file (${formatBytes(fileSize)} total; not scanned to EOF). Use :${nextOffset} to continue]`; @@ -2646,7 +2761,7 @@ export class ReadTool implements AgentTool { sourcePath = absolutePath; } else { // No truncation, no user limit exceeded - outputText = formatText(truncation.content, startLineDisplay); + outputText = formatBracketAwareText() ?? formatText(truncation.content, startLineDisplay); details = {}; sourcePath = absolutePath; } @@ -2866,11 +2981,16 @@ export class ReadTool implements AgentTool { const { offset, limit } = selToOffsetLimit(parsedSel); const requestedStart = offset ? Math.max(0, offset - 1) : 0; - const startLine = requestedStart; + // Raw mode never adds context lines — see the plain-file range path. + const expandStart = !rawSelector && offset !== undefined && offset > 1; + const expandEnd = !rawSelector && limit !== undefined; + const leadingContext = expandStart ? Math.min(requestedStart, RANGE_LEADING_CONTEXT_LINES) : 0; + const trailingContext = expandEnd ? RANGE_TRAILING_CONTEXT_LINES : 0; + const startLine = requestedStart - leadingContext; const startLineDisplay = startLine + 1; const effectiveLimit = limit ?? this.#defaultLimit; - const maxLinesToCollect = Math.min(effectiveLimit, DEFAULT_MAX_LINES); - const selectedLineLimit = effectiveLimit; + const maxLinesToCollect = Math.min(effectiveLimit + leadingContext + trailingContext, DEFAULT_MAX_LINES); + const selectedLineLimit = effectiveLimit + leadingContext + trailingContext; const maxBytesForRead = Math.max(DEFAULT_MAX_BYTES, maxLinesToCollect * 512); const streamResult = await streamLinesFromFile( artifact.path, diff --git a/packages/coding-agent/src/utils/block-context.ts b/packages/coding-agent/src/utils/block-context.ts index 045d5103c..5b450cf97 100644 --- a/packages/coding-agent/src/utils/block-context.ts +++ b/packages/coding-agent/src/utils/block-context.ts @@ -266,24 +266,27 @@ export function findBlockContextLines( return nativeBlockContext(fullLines, visible, source) ?? lexicalBracketContext(fullLines, visible); } -interface LineEntryOptions { - lineText?: (lineNumber: number, sourceText: string, context: boolean) => string; -} - -function buildEntries( +/** + * Build display entries for `visibleSpans` plus any off-window block-boundary + * lines, in source order, with `{ kind: "ellipsis" }` markers inserted across + * non-contiguous gaps. `options.lineText` lets callers substitute display text + * (e.g. column-truncated lines) for a given line number. + */ +export function buildLineEntriesWithBlockContext( fullLines: readonly string[], - visible: ReadonlySet, - context: ReadonlyMap | undefined, - options: LineEntryOptions, + visibleSpans: readonly LineSpan[], + source: BlockContextSource = {}, + options: { + lineText?: (lineNumber: number, sourceText: string, context: boolean) => string; + } = {}, ): LineEntry[] { - const sorted = [...visible]; - if (context) { - for (const lineNumber of context.keys()) { - if (!visible.has(lineNumber)) sorted.push(lineNumber); - } - } - sorted.sort((left, right) => left - right); + const spans = normalizeLineSpans(visibleSpans, fullLines.length); + const visible = visibleLineNumbers(spans); + const context = findBlockContextLines(fullLines, visible, source); + const allLines = new Set(visible); + for (const lineNumber of context.keys()) allLines.add(lineNumber); + const sorted = [...allLines].sort((left, right) => left - right); const entries: LineEntry[] = []; let previousLine: number | undefined; for (const lineNumber of sorted) { @@ -291,7 +294,7 @@ function buildEntries( entries.push({ kind: "ellipsis" }); } const sourceText = fullLines[lineNumber - 1] ?? ""; - const isContext = context?.has(lineNumber) === true; + const isContext = context.has(lineNumber); entries.push({ kind: "line", lineNumber, @@ -304,33 +307,6 @@ function buildEntries( return entries; } -/** Build display entries for exactly the requested spans, separated by ellipses. */ -export function buildLineEntries( - fullLines: readonly string[], - visibleSpans: readonly LineSpan[], - options: LineEntryOptions = {}, -): LineEntry[] { - const spans = normalizeLineSpans(visibleSpans, fullLines.length); - return buildEntries(fullLines, visibleLineNumbers(spans), undefined, options); -} - -/** - * Build display entries for `visibleSpans` plus any off-window block-boundary - * lines, in source order, with `{ kind: "ellipsis" }` markers inserted across - * non-contiguous gaps. `options.lineText` lets callers substitute display text - * (e.g. column-truncated lines) for a given line number. - */ -export function buildLineEntriesWithBlockContext( - fullLines: readonly string[], - visibleSpans: readonly LineSpan[], - source: BlockContextSource = {}, - options: LineEntryOptions = {}, -): LineEntry[] { - const spans = normalizeLineSpans(visibleSpans, fullLines.length); - const visible = visibleLineNumbers(spans); - return buildEntries(fullLines, visible, findBlockContextLines(fullLines, visible, source), options); -} - export function lineEntriesToPlainText(entries: readonly LineEntry[], ellipsis = "…"): string { return entries.map(entry => (entry.kind === "ellipsis" ? ellipsis : entry.text)).join("\n"); } diff --git a/packages/coding-agent/test/read-multi-range.test.ts b/packages/coding-agent/test/read-multi-range.test.ts index 8b2abee49..f85a1da33 100644 --- a/packages/coding-agent/test/read-multi-range.test.ts +++ b/packages/coding-agent/test/read-multi-range.test.ts @@ -79,8 +79,6 @@ describe("read tool multi-range selector", () => { expect(text).toContain("line 20"); expect(text).toContain("line 21"); expect(text).toContain("line 22"); - expect(text).not.toMatch(/^2:line 2$/m); - expect(text).not.toMatch(/^6:line 6$/m); // Lines between the ranges must be elided expect(text).not.toContain("line 10"); expect(text).not.toContain("line 19"); @@ -88,7 +86,7 @@ describe("read tool multi-range selector", () => { expect(text).toContain("…"); }); - it("does not add a closing bracket outside a forward range", async () => { + it("includes the matching closing bracket line outside a forward range", async () => { const filePath = path.join(tmpDir, "brackets.ts"); await fs.writeFile( filePath, @@ -108,13 +106,13 @@ describe("read tool multi-range selector", () => { const text = textOutput(await tool.execute("call-bracket-close", { path: `${filePath}:1-1` })); expect(text).toContain("function outer() {"); - expect(text).not.toContain("…"); - expect(text).not.toMatch(/^7:}$/m); + expect(text).toContain("…"); + expect(text).toContain("}"); expect(text).not.toContain("const four"); expect(text).not.toContain("return one + two"); }); - it("does not add an opening bracket outside a reverse range", async () => { + it("includes the matching opening bracket line outside a reverse range", async () => { const filePath = path.join(tmpDir, "brackets.ts"); await fs.writeFile( filePath, @@ -133,14 +131,13 @@ describe("read tool multi-range selector", () => { const tool = new ReadTool(createSession(tmpDir)); const text = textOutput(await tool.execute("call-bracket-open", { path: `${filePath}:7-7` })); - expect(text).toMatch(/^7:}$/m); - expect(text).not.toContain("function outer() {"); - expect(text).not.toContain("…"); + expect(text.indexOf("function outer() {")).toBeLessThan(text.indexOf("}")); + expect(text).toContain("…"); expect(text).not.toContain("const one = 1"); expect(text).not.toContain("const four = 4"); }); - it("does not add Python syntactic boundaries outside a range", async () => { + it("uses tree-sitter syntactic spans for indentation languages (Python)", async () => { const filePath = path.join(tmpDir, "module.py"); await fs.writeFile( filePath, @@ -159,11 +156,15 @@ describe("read tool multi-range selector", () => { ); const tool = new ReadTool(createSession(tmpDir)); + // Read only the `def` header (expands by a few trailing context lines). + // Python has no closing delimiter, so a bracket scan would surface + // nothing; tree-sitter surfaces the def's last body line (9) as the + // block boundary, behind an ellipsis for the skipped middle. const text = textOutput(await tool.execute("call-py-def", { path: `${filePath}:1-1` })); expect(text).toContain("def greet(name):"); - expect(text).not.toContain("…"); - expect(text).not.toContain("return a + b + c + d + e + f + g + len(name)"); + expect(text).toContain("…"); + expect(text).toContain("return a + b + c + d + e + f + g + len(name)"); expect(text).not.toContain("trailing = 1"); }); @@ -178,7 +179,7 @@ describe("read tool multi-range selector", () => { // All lines from the merged range present for (const i of [3, 4, 5, 6, 7, 8, 9]) { - expect(text).toMatch(new RegExp(`^${i}:line ${i}$`, "m")); + expect(text).toContain(`line ${i}\n`); } // No separator because ranges merged into one contiguous block expect(text).not.toContain("…"); diff --git a/packages/coding-agent/test/tools/read-artifact-large.test.ts b/packages/coding-agent/test/tools/read-artifact-large.test.ts index 8c65d5b9b..46831a30f 100644 --- a/packages/coding-agent/test/tools/read-artifact-large.test.ts +++ b/packages/coding-agent/test/tools/read-artifact-large.test.ts @@ -77,7 +77,6 @@ describe("read tool large artifact handling", () => { expect(output).toContain("line-001"); expect(output).toContain("line-003"); - expect(output).not.toContain("line-004"); expect(output).toContain("Artifact storage:"); expect(output).toContain("artifact://0:raw:N-M"); expect(output).not.toContain("line-400"); diff --git a/packages/coding-agent/test/tools/read-pdf-line-range.test.ts b/packages/coding-agent/test/tools/read-pdf-line-range.test.ts index d3c0d3611..a1cb17321 100644 --- a/packages/coding-agent/test/tools/read-pdf-line-range.test.ts +++ b/packages/coding-agent/test/tools/read-pdf-line-range.test.ts @@ -137,7 +137,6 @@ describe("read PDF with a line-range selector", () => { .join("\n"); expect(selectorText).toContain("pdf line 2"); expect(selectorText).toContain("pdf line 3"); - expect(selectorText).not.toContain("pdf line 1"); expect(convert).toHaveBeenCalledTimes(1); } finally { diff --git a/packages/coding-agent/test/tools/read-raw-range.test.ts b/packages/coding-agent/test/tools/read-raw-range.test.ts index d1552a91b..75d5c3cc9 100644 --- a/packages/coding-agent/test/tools/read-raw-range.test.ts +++ b/packages/coding-agent/test/tools/read-raw-range.test.ts @@ -59,12 +59,14 @@ describe("read tool raw range exactness", () => { expect(output.trimEnd()).toBe("L01\nL02"); }); - it("returns exactly the requested numbered range without context padding", async () => { + it("keeps context padding for numbered range reads", async () => { + // Numbered mode intentionally pads (leading anchor buffer + trailing + // disambiguation lines) — line numbers make the padding self-describing. const result = await tool.execute("call-numbered", { path: `${filePath}:31-31` }); const output = getTextOutput(result); expect(output).toContain("L31"); - expect(output).not.toContain("L30"); - expect(output).not.toContain("L32"); + expect(output).toContain("L30"); + expect(output).toContain("L32"); }); });