From 29359eff779965205e489587261e5ff3c632d43b Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 23:18:42 +0100 Subject: [PATCH] Accessible View: include file paths for inline references in chat responses (#301565) * Initial plan * Add inline reference handling with file paths to Accessible View When the Accessible View renders chat responses, inline file references (kind: 'inlineReference') were previously skipped entirely. This adds handling to include the file name and full path (and line number for locations) so screen reader users get the same context as the visual UI. - Handle URI references: show "name (path)" - Handle Location references: show "name (path:line)" - Handle workspace symbol references: show "name (path:line)" - Add 3 new tests verifying inline reference rendering Co-authored-by: meganrogge <29464607+meganrogge@users.noreply.github.com> * Revert unintended monaco.d.ts changes from gulp compile Co-authored-by: meganrogge <29464607+meganrogge@users.noreply.github.com> * Rename ambiguous variable in test (location -> fileLocation) Co-authored-by: meganrogge <29464607+meganrogge@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: meganrogge <29464607+meganrogge@users.noreply.github.com> Co-authored-by: Megan Rogge --- .../chatResponseAccessibleView.ts | 26 ++- .../chatResponseAccessibleView.test.ts | 159 ++++++++++++++++++ 2 files changed, 184 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/accessibility/chatResponseAccessibleView.ts b/src/vs/workbench/contrib/chat/browser/accessibility/chatResponseAccessibleView.ts index f20aabab996..e183775db5c 100644 --- a/src/vs/workbench/contrib/chat/browser/accessibility/chatResponseAccessibleView.ts +++ b/src/vs/workbench/contrib/chat/browser/accessibility/chatResponseAccessibleView.ts @@ -8,6 +8,7 @@ import { Emitter, Event } from '../../../../../base/common/event.js'; import { IMarkdownString, isMarkdownString } from '../../../../../base/common/htmlContent.js'; import { stripIcons } from '../../../../../base/common/iconLabels.js'; import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; +import { basename } from '../../../../../base/common/resources.js'; import { URI } from '../../../../../base/common/uri.js'; import { localize } from '../../../../../nls.js'; import { AccessibleViewProviderId, AccessibleViewType, IAccessibleViewContentProvider } from '../../../../../platform/accessibility/browser/accessibleView.js'; @@ -21,7 +22,7 @@ import { IChatExtensionsContent, IChatModifiedFilesConfirmationData, IChatPullRe import { isResponseVM } from '../../common/model/chatViewModel.js'; import { IToolResultInputOutputDetails, IToolResultOutputDetails, isToolResultInputOutputDetails, isToolResultOutputDetails, toolContentToA11yString } from '../../common/tools/languageModelToolsService.js'; import { ChatTreeItem, IChatWidget, IChatWidgetService } from '../chat.js'; -import { Location } from '../../../../../editor/common/languages.js'; +import { isLocation, Location } from '../../../../../editor/common/languages.js'; export class ChatResponseAccessibleView implements IAccessibleViewImplementation { readonly priority = 100; @@ -299,6 +300,29 @@ class ChatResponseAccessibleProvider extends Disposable implements IAccessibleVi } break; } + case 'inlineReference': { + const ref = part.inlineReference; + let text: string; + if (URI.isUri(ref)) { + const name = part.name || basename(ref); + const isFileUri = ref.scheme === 'file'; + const path = isFileUri ? (ref.fsPath || ref.path) : ref.toString(true); + text = name !== path ? `${name} (${path})` : path; + } else if (isLocation(ref)) { + const name = part.name || basename(ref.uri); + const isFileUri = ref.uri.scheme === 'file'; + const basePath = isFileUri ? (ref.uri.fsPath || ref.uri.path) : ref.uri.toString(true); + const location = `${basePath}:${ref.range.startLineNumber}`; + text = `${name} (${location})`; + } else { + // IWorkspaceSymbol + const isFileUri = ref.location.uri.scheme === 'file'; + const basePath = isFileUri ? (ref.location.uri.fsPath || ref.location.uri.path) : ref.location.uri.toString(true); + text = `${ref.name} (${basePath}:${ref.location.range.startLineNumber})`; + } + contentParts.push(text); + break; + } case 'elicitation2': case 'elicitationSerialized': { const title = part.title; diff --git a/src/vs/workbench/contrib/chat/test/browser/accessibility/chatResponseAccessibleView.test.ts b/src/vs/workbench/contrib/chat/test/browser/accessibility/chatResponseAccessibleView.test.ts index ba7821dd358..b4ad1e4bf36 100644 --- a/src/vs/workbench/contrib/chat/test/browser/accessibility/chatResponseAccessibleView.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/accessibility/chatResponseAccessibleView.test.ts @@ -486,5 +486,164 @@ suite('ChatResponseAccessibleView', () => { assert.ok(content.includes('Response content')); assert.ok(content.includes('Thinking: Reasoning')); }); + + test('includes file path for URI inline references', () => { + const instantiationService = store.add(new TestInstantiationService()); + const storageService = store.add(new TestStorageService()); + + const inlineReferenceUri = URI.file('/path/to/index.ts'); + const responseItem = { + response: { + value: [ + { kind: 'markdownContent', content: new MarkdownString('See file ') }, + { kind: 'inlineReference', inlineReference: inlineReferenceUri, name: 'index.ts' }, + { kind: 'markdownContent', content: new MarkdownString(' for details') } + ] + }, + model: { onDidChange: Event.None }, + setVote: () => undefined + }; + const items = [responseItem]; + let focusedItem: unknown = responseItem; + + const widget = { + hasInputFocus: () => false, + focusResponseItem: () => { focusedItem = responseItem; }, + getFocus: () => focusedItem, + focus: (item: unknown) => { focusedItem = item; }, + viewModel: { getItems: () => items } + } as unknown as IChatWidget; + + const widgetService = { + _serviceBrand: undefined, + lastFocusedWidget: widget, + onDidAddWidget: Event.None, + onDidBackgroundSession: Event.None, + reveal: async () => true, + revealWidget: async () => widget, + getAllWidgets: () => [widget], + getWidgetByInputUri: () => widget, + openSession: async () => widget, + getWidgetBySessionResource: () => widget + } as unknown as IChatWidgetService; + + instantiationService.stub(IChatWidgetService, widgetService); + instantiationService.stub(IStorageService, storageService); + + const accessibleView = new ChatResponseAccessibleView(); + const provider = instantiationService.invokeFunction(accessor => accessibleView.getProvider(accessor)); + assert.ok(provider); + store.add(provider); + const content = provider.provideContent(); + const expectedPath = inlineReferenceUri.fsPath || inlineReferenceUri.path; + assert.ok(content.includes('index.ts')); + assert.ok(content.includes(expectedPath)); + assert.ok(content.includes('See file')); + assert.ok(content.includes('for details')); + }); + + test('includes file path and line number for Location inline references', () => { + const instantiationService = store.add(new TestInstantiationService()); + const storageService = store.add(new TestStorageService()); + + const fileLocation: Location = { + uri: URI.file('/src/app/main.ts'), + range: new Range(42, 1, 42, 20) + }; + + const responseItem = { + response: { + value: [ + { kind: 'markdownContent', content: new MarkdownString('Error at ') }, + { kind: 'inlineReference', inlineReference: fileLocation, name: 'main.ts' } + ] + }, + model: { onDidChange: Event.None }, + setVote: () => undefined + }; + const items = [responseItem]; + let focusedItem: unknown = responseItem; + + const widget = { + hasInputFocus: () => false, + focusResponseItem: () => { focusedItem = responseItem; }, + getFocus: () => focusedItem, + focus: (item: unknown) => { focusedItem = item; }, + viewModel: { getItems: () => items } + } as unknown as IChatWidget; + + const widgetService = { + _serviceBrand: undefined, + lastFocusedWidget: widget, + onDidAddWidget: Event.None, + onDidBackgroundSession: Event.None, + reveal: async () => true, + revealWidget: async () => widget, + getAllWidgets: () => [widget], + getWidgetByInputUri: () => widget, + openSession: async () => widget, + getWidgetBySessionResource: () => widget + } as unknown as IChatWidgetService; + + instantiationService.stub(IChatWidgetService, widgetService); + instantiationService.stub(IStorageService, storageService); + + const accessibleView = new ChatResponseAccessibleView(); + const provider = instantiationService.invokeFunction(accessor => accessibleView.getProvider(accessor)); + assert.ok(provider); + store.add(provider); + const content = provider.provideContent(); + assert.ok(content.includes('main.ts')); + assert.ok(content.includes('/src/app/main.ts:42')); + }); + + test('uses basename as name for URI inline references without explicit name', () => { + const instantiationService = store.add(new TestInstantiationService()); + const storageService = store.add(new TestStorageService()); + + const responseItem = { + response: { + value: [ + { kind: 'inlineReference', inlineReference: URI.file('/workspace/src/utils.ts') } + ] + }, + model: { onDidChange: Event.None }, + setVote: () => undefined + }; + const items = [responseItem]; + let focusedItem: unknown = responseItem; + + const widget = { + hasInputFocus: () => false, + focusResponseItem: () => { focusedItem = responseItem; }, + getFocus: () => focusedItem, + focus: (item: unknown) => { focusedItem = item; }, + viewModel: { getItems: () => items } + } as unknown as IChatWidget; + + const widgetService = { + _serviceBrand: undefined, + lastFocusedWidget: widget, + onDidAddWidget: Event.None, + onDidBackgroundSession: Event.None, + reveal: async () => true, + revealWidget: async () => widget, + getAllWidgets: () => [widget], + getWidgetByInputUri: () => widget, + openSession: async () => widget, + getWidgetBySessionResource: () => widget + } as unknown as IChatWidgetService; + + instantiationService.stub(IChatWidgetService, widgetService); + instantiationService.stub(IStorageService, storageService); + + const accessibleView = new ChatResponseAccessibleView(); + const provider = instantiationService.invokeFunction(accessor => accessibleView.getProvider(accessor)); + assert.ok(provider); + store.add(provider); + const content = provider.provideContent(); + assert.ok(content.includes('utils.ts')); + assert.ok(content.includes('/workspace/src/utils.ts')); + }); }); });