From c0065deeeffac294d4b44f1fcf280395c4c110fb Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 22 Apr 2026 19:31:10 -0700 Subject: [PATCH 1/5] Show line range in agent host file-read tool display When the Copilot CLI's `view` tool is called with a `view_range`, surface the line range in the invocation and past-tense messages so users see e.g. "Reading file.ts, lines 10 to 20" instead of just "Reading file.ts". (Written by Copilot) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../node/copilot/copilotToolDisplay.ts | 50 +++++++++++++++++-- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/src/vs/platform/agentHost/node/copilot/copilotToolDisplay.ts b/src/vs/platform/agentHost/node/copilot/copilotToolDisplay.ts index aaf96fe302948..c23468f5eb1d3 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotToolDisplay.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotToolDisplay.ts @@ -68,6 +68,32 @@ interface ICopilotFileToolArgs { path: string; } +/** + * Parameters for the `view` tool. The Copilot CLI accepts an optional + * `view_range: [startLine, endLine]` (1-based, inclusive). `endLine` may be + * `-1` to mean "to end of file". + */ +interface ICopilotViewToolArgs extends ICopilotFileToolArgs { + view_range?: number[]; +} + +/** + * Formats a `view_range` array into a human-readable line range string, + * or returns `undefined` if the range is not a usable two-element array. + */ +function formatViewRange(view_range: number[] | undefined): { startLine: number; endLine: number | undefined } | undefined { + if (!Array.isArray(view_range) || view_range.length < 1) { + return undefined; + } + const startLine = view_range[0]; + if (typeof startLine !== 'number' || !isFinite(startLine)) { + return undefined; + } + const rawEnd = view_range.length >= 2 ? view_range[1] : undefined; + const endLine = typeof rawEnd === 'number' && isFinite(rawEnd) && rawEnd >= 0 ? rawEnd : undefined; + return { startLine, endLine }; +} + /** Parameters for the `grep` tool. */ interface ICopilotGrepToolArgs { pattern: string; @@ -212,9 +238,17 @@ export function getInvocationMessage(toolName: string, displayName: string, para switch (toolName) { case CopilotToolName.View: { - const args = parameters as ICopilotFileToolArgs | undefined; + const args = parameters as ICopilotViewToolArgs | undefined; if (args?.path) { - return md(localize('toolInvoke.viewFile', "Reading {0}", formatPathAsMarkdownLink(args.path))); + const link = formatPathAsMarkdownLink(args.path); + const range = formatViewRange(args.view_range); + if (range) { + if (range.endLine !== undefined && range.endLine !== range.startLine) { + return md(localize('toolInvoke.viewFileRange', "Reading {0}, lines {1} to {2}", link, range.startLine, range.endLine)); + } + return md(localize('toolInvoke.viewFileLine', "Reading {0}, line {1}", link, range.startLine)); + } + return md(localize('toolInvoke.viewFile', "Reading {0}", link)); } return localize('toolInvoke.view', "Reading file"); } @@ -267,9 +301,17 @@ export function getPastTenseMessage(toolName: string, displayName: string, param switch (toolName) { case CopilotToolName.View: { - const args = parameters as ICopilotFileToolArgs | undefined; + const args = parameters as ICopilotViewToolArgs | undefined; if (args?.path) { - return md(localize('toolComplete.viewFile', "Read {0}", formatPathAsMarkdownLink(args.path))); + const link = formatPathAsMarkdownLink(args.path); + const range = formatViewRange(args.view_range); + if (range) { + if (range.endLine !== undefined && range.endLine !== range.startLine) { + return md(localize('toolComplete.viewFileRange', "Read {0}, lines {1} to {2}", link, range.startLine, range.endLine)); + } + return md(localize('toolComplete.viewFileLine', "Read {0}, line {1}", link, range.startLine)); + } + return md(localize('toolComplete.viewFile', "Read {0}", link)); } return localize('toolComplete.view', "Read file"); } From 5321c25c3b05e9862379ca0f0036e5de9a45623e Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 22 Apr 2026 19:41:38 -0700 Subject: [PATCH 2/5] Match Copilot Chat extension's view_range validation Drop the EOF-sentinel handling and require a strictly valid two-element range (`length === 2`, integers, `start >= 0`, `end >= start`); fall back to the path-only display otherwise. Mirrors `formatViewToolInvocation` in the Copilot Chat extension and addresses review feedback on the `-1` end-of-file sentinel and the stale doc comment. (Written by Copilot) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../node/copilot/copilotToolDisplay.ts | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/vs/platform/agentHost/node/copilot/copilotToolDisplay.ts b/src/vs/platform/agentHost/node/copilot/copilotToolDisplay.ts index c23468f5eb1d3..687ed574c106a 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotToolDisplay.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotToolDisplay.ts @@ -71,26 +71,30 @@ interface ICopilotFileToolArgs { /** * Parameters for the `view` tool. The Copilot CLI accepts an optional * `view_range: [startLine, endLine]` (1-based, inclusive). `endLine` may be - * `-1` to mean "to end of file". + * `-1` to mean "to end of file"; we treat that (and any other invalid range) + * as "no range" and fall back to the path-only display. */ interface ICopilotViewToolArgs extends ICopilotFileToolArgs { view_range?: number[]; } /** - * Formats a `view_range` array into a human-readable line range string, - * or returns `undefined` if the range is not a usable two-element array. + * Normalizes a `view_range` array into a `{ startLine, endLine }` pair. + * Returns `undefined` unless the array has exactly two integer elements with + * `startLine >= 0` and `endLine >= startLine`. Mirrors the validation in the + * Copilot Chat extension's `formatViewToolInvocation`. */ -function formatViewRange(view_range: number[] | undefined): { startLine: number; endLine: number | undefined } | undefined { - if (!Array.isArray(view_range) || view_range.length < 1) { +function formatViewRange(view_range: number[] | undefined): { startLine: number; endLine: number } | undefined { + if (!Array.isArray(view_range) || view_range.length !== 2) { return undefined; } - const startLine = view_range[0]; - if (typeof startLine !== 'number' || !isFinite(startLine)) { + const [startLine, endLine] = view_range; + if (!Number.isInteger(startLine) || !Number.isInteger(endLine)) { + return undefined; + } + if (startLine < 0 || endLine < startLine) { return undefined; } - const rawEnd = view_range.length >= 2 ? view_range[1] : undefined; - const endLine = typeof rawEnd === 'number' && isFinite(rawEnd) && rawEnd >= 0 ? rawEnd : undefined; return { startLine, endLine }; } @@ -243,7 +247,7 @@ export function getInvocationMessage(toolName: string, displayName: string, para const link = formatPathAsMarkdownLink(args.path); const range = formatViewRange(args.view_range); if (range) { - if (range.endLine !== undefined && range.endLine !== range.startLine) { + if (range.endLine !== range.startLine) { return md(localize('toolInvoke.viewFileRange', "Reading {0}, lines {1} to {2}", link, range.startLine, range.endLine)); } return md(localize('toolInvoke.viewFileLine', "Reading {0}, line {1}", link, range.startLine)); @@ -306,7 +310,7 @@ export function getPastTenseMessage(toolName: string, displayName: string, param const link = formatPathAsMarkdownLink(args.path); const range = formatViewRange(args.view_range); if (range) { - if (range.endLine !== undefined && range.endLine !== range.startLine) { + if (range.endLine !== range.startLine) { return md(localize('toolComplete.viewFileRange', "Read {0}, lines {1} to {2}", link, range.startLine, range.endLine)); } return md(localize('toolComplete.viewFileLine', "Read {0}, line {1}", link, range.startLine)); From 97b9bb4bbee31511083eb04dc9e7a49cbc49e957 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 22 Apr 2026 19:53:18 -0700 Subject: [PATCH 3/5] Handle view_range `-1` end-of-file sentinel The Copilot CLI uses `-1` as the documented "to end of file" sentinel for the second element of `view_range`. The Copilot Chat extension doesn't handle this and either drops the range or renders "-1" literally; do better here by rendering "from line {n} to the end". (Written by Copilot) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../node/copilot/copilotToolDisplay.ts | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/vs/platform/agentHost/node/copilot/copilotToolDisplay.ts b/src/vs/platform/agentHost/node/copilot/copilotToolDisplay.ts index 687ed574c106a..8657f2970c8eb 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotToolDisplay.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotToolDisplay.ts @@ -71,18 +71,17 @@ interface ICopilotFileToolArgs { /** * Parameters for the `view` tool. The Copilot CLI accepts an optional * `view_range: [startLine, endLine]` (1-based, inclusive). `endLine` may be - * `-1` to mean "to end of file"; we treat that (and any other invalid range) - * as "no range" and fall back to the path-only display. + * `-1` to mean "to end of file". */ interface ICopilotViewToolArgs extends ICopilotFileToolArgs { view_range?: number[]; } /** - * Normalizes a `view_range` array into a `{ startLine, endLine }` pair. - * Returns `undefined` unless the array has exactly two integer elements with - * `startLine >= 0` and `endLine >= startLine`. Mirrors the validation in the - * Copilot Chat extension's `formatViewToolInvocation`. + * Normalizes a `view_range` array. Returns `undefined` unless the array has + * exactly two integer elements with `startLine >= 0`. `endLine === -1` is + * preserved as the "to end of file" sentinel; otherwise `endLine` must be + * `>= startLine`. */ function formatViewRange(view_range: number[] | undefined): { startLine: number; endLine: number } | undefined { if (!Array.isArray(view_range) || view_range.length !== 2) { @@ -92,7 +91,10 @@ function formatViewRange(view_range: number[] | undefined): { startLine: number; if (!Number.isInteger(startLine) || !Number.isInteger(endLine)) { return undefined; } - if (startLine < 0 || endLine < startLine) { + if (startLine < 0) { + return undefined; + } + if (endLine !== -1 && endLine < startLine) { return undefined; } return { startLine, endLine }; @@ -247,6 +249,9 @@ export function getInvocationMessage(toolName: string, displayName: string, para const link = formatPathAsMarkdownLink(args.path); const range = formatViewRange(args.view_range); if (range) { + if (range.endLine === -1) { + return md(localize('toolInvoke.viewFileFromLine', "Reading {0}, from line {1} to the end", link, range.startLine)); + } if (range.endLine !== range.startLine) { return md(localize('toolInvoke.viewFileRange', "Reading {0}, lines {1} to {2}", link, range.startLine, range.endLine)); } @@ -310,6 +315,9 @@ export function getPastTenseMessage(toolName: string, displayName: string, param const link = formatPathAsMarkdownLink(args.path); const range = formatViewRange(args.view_range); if (range) { + if (range.endLine === -1) { + return md(localize('toolComplete.viewFileFromLine', "Read {0}, from line {1} to the end", link, range.startLine)); + } if (range.endLine !== range.startLine) { return md(localize('toolComplete.viewFileRange', "Read {0}, lines {1} to {2}", link, range.startLine, range.endLine)); } From 56c737522bde24d038ad550b18588090698bf8cf Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 22 Apr 2026 19:57:15 -0700 Subject: [PATCH 4/5] Tweak EOF view_range wording (Written by Copilot) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/vs/platform/agentHost/node/copilot/copilotToolDisplay.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/platform/agentHost/node/copilot/copilotToolDisplay.ts b/src/vs/platform/agentHost/node/copilot/copilotToolDisplay.ts index 8657f2970c8eb..b843dd74ac733 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotToolDisplay.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotToolDisplay.ts @@ -250,7 +250,7 @@ export function getInvocationMessage(toolName: string, displayName: string, para const range = formatViewRange(args.view_range); if (range) { if (range.endLine === -1) { - return md(localize('toolInvoke.viewFileFromLine', "Reading {0}, from line {1} to the end", link, range.startLine)); + return md(localize('toolInvoke.viewFileFromLine', "Reading {0}, line {1} to the end", link, range.startLine)); } if (range.endLine !== range.startLine) { return md(localize('toolInvoke.viewFileRange', "Reading {0}, lines {1} to {2}", link, range.startLine, range.endLine)); @@ -316,7 +316,7 @@ export function getPastTenseMessage(toolName: string, displayName: string, param const range = formatViewRange(args.view_range); if (range) { if (range.endLine === -1) { - return md(localize('toolComplete.viewFileFromLine', "Read {0}, from line {1} to the end", link, range.startLine)); + return md(localize('toolComplete.viewFileFromLine', "Read {0}, line {1} to the end", link, range.startLine)); } if (range.endLine !== range.startLine) { return md(localize('toolComplete.viewFileRange', "Read {0}, lines {1} to {2}", link, range.startLine, range.endLine)); From 04236e61bf9b875cedff76d9a062f96c8b8f229e Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 22 Apr 2026 20:18:53 -0700 Subject: [PATCH 5/5] Add tests for view_range display formatting Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../test/node/copilotToolDisplay.test.ts | 51 ++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/src/vs/platform/agentHost/test/node/copilotToolDisplay.test.ts b/src/vs/platform/agentHost/test/node/copilotToolDisplay.test.ts index 29981cd61fb7e..5aee3be444229 100644 --- a/src/vs/platform/agentHost/test/node/copilotToolDisplay.test.ts +++ b/src/vs/platform/agentHost/test/node/copilotToolDisplay.test.ts @@ -6,7 +6,7 @@ import assert from 'assert'; import { URI } from '../../../../base/common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; -import { getPermissionDisplay, type ITypedPermissionRequest } from '../../node/copilot/copilotToolDisplay.js'; +import { getInvocationMessage, getPastTenseMessage, getPermissionDisplay, type ITypedPermissionRequest } from '../../node/copilot/copilotToolDisplay.js'; suite('getPermissionDisplay — cd-prefix stripping', () => { @@ -75,3 +75,52 @@ suite('getPermissionDisplay — cd-prefix stripping', () => { assert.strictEqual(display.toolInput, 'dir'); }); }); + +suite('view tool — view_range display', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + function invocation(parameters: Record | undefined): string { + const result = getInvocationMessage('view', 'View File', parameters); + return typeof result === 'string' ? result : result.markdown; + } + + function pastTense(parameters: Record | undefined): string { + const result = getPastTenseMessage('view', 'View File', parameters, true); + return typeof result === 'string' ? result : result.markdown; + } + + test('renders path-only when view_range is absent', () => { + assert.ok(invocation({ path: '/repo/file.ts' }).startsWith('Reading [')); + assert.ok(pastTense({ path: '/repo/file.ts' }).startsWith('Read [')); + }); + + test('renders "lines X to Y" for a valid two-element range', () => { + assert.ok(invocation({ path: '/repo/file.ts', view_range: [10, 20] }).endsWith(', lines 10 to 20')); + assert.ok(pastTense({ path: '/repo/file.ts', view_range: [10, 20] }).endsWith(', lines 10 to 20')); + }); + + test('renders "line X" when start === end', () => { + assert.ok(invocation({ path: '/repo/file.ts', view_range: [10, 10] }).endsWith(', line 10')); + assert.ok(pastTense({ path: '/repo/file.ts', view_range: [10, 10] }).endsWith(', line 10')); + }); + + test('renders "line X to the end" for the -1 EOF sentinel', () => { + assert.ok(invocation({ path: '/repo/file.ts', view_range: [10, -1] }).endsWith(', line 10 to the end')); + assert.ok(pastTense({ path: '/repo/file.ts', view_range: [10, -1] }).endsWith(', line 10 to the end')); + }); + + test('falls back to path-only for invalid ranges', () => { + // end < start (and not -1) + assert.ok(!invocation({ path: '/repo/file.ts', view_range: [20, 10] }).includes(',')); + // negative start + assert.ok(!invocation({ path: '/repo/file.ts', view_range: [-5, 10] }).includes(',')); + // non-integer + assert.ok(!invocation({ path: '/repo/file.ts', view_range: [1.5, 10] }).includes(',')); + // wrong arity + assert.ok(!invocation({ path: '/repo/file.ts', view_range: [10] }).includes(',')); + assert.ok(!invocation({ path: '/repo/file.ts', view_range: [10, 20, 30] }).includes(',')); + // non-array + assert.ok(!invocation({ path: '/repo/file.ts', view_range: 'whatever' }).includes(',')); + }); +});