mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-02 08:15:56 +01:00
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:
@@ -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;
|
||||
|
||||
@@ -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'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user