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 <merogge@microsoft.com>
This commit is contained in:
Copilot
2026-03-23 23:18:42 +01:00
committed by GitHub
parent 6c52b71c9a
commit 29359eff77
2 changed files with 184 additions and 1 deletions

View File

@@ -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;

View File

@@ -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'));
});
});
});