From c84bdba0a431663013dc9c2d1b10e50c7205dd7c Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 28 Jan 2026 16:40:56 -0800 Subject: [PATCH] Prompt file diagnostics feedback (#291450) --- .../chatCustomizationDiagnosticsAction.ts | 40 +++++++----- ...chatCustomizationDiagnosticsAction.test.ts | 62 +++++++++++++++---- 2 files changed, 76 insertions(+), 26 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCustomizationDiagnosticsAction.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCustomizationDiagnosticsAction.ts index e6c881e8a0e..a5b99a144dd 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCustomizationDiagnosticsAction.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCustomizationDiagnosticsAction.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from '../../../../../base/common/cancellation.js'; +import { Schemas } from '../../../../../base/common/network.js'; import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; import { localize2 } from '../../../../../nls.js'; import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js'; @@ -38,15 +39,20 @@ function encodePathForMarkdown(path: string): string { * The returned path is URL encoded for use in markdown link targets. */ function getRelativePath(uri: URI, workspaceFolders: readonly IWorkspaceFolder[]): string { + // On desktop, vscode-userdata scheme maps 1:1 to file scheme paths via FileUserDataProvider. + // Convert to file scheme so relativePath() can compute paths correctly. + // On web, vscode-userdata uses IndexedDB so this conversion has no effect (different schemes won't match workspace folders). + const normalizedUri = uri.scheme === Schemas.vscodeUserData ? uri.with({ scheme: Schemas.file }) : uri; + for (const folder of workspaceFolders) { - const relative = relativePath(folder.uri, uri); + const relative = relativePath(folder.uri, normalizedUri); if (relative) { return encodePathForMarkdown(relative); } } // Fall back to fsPath if not under any workspace folder // Use forward slashes for consistency in markdown links - return encodePathForMarkdown(uri.fsPath.replace(/\\/g, '/')); + return encodePathForMarkdown(normalizedUri.fsPath.replace(/\\/g, '/')); } // Tree prefixes @@ -117,8 +123,13 @@ export function registerChatCustomizationDiagnosticsAction() { }, { id: CHAT_CONFIG_MENU_ID, when: ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.equals('view', ChatViewId)), - order: 20, + order: 14, group: '3_configure' + }, { + id: MenuId.ChatWelcomeContext, + group: '2_settings', + order: 0, + when: ChatContextKeys.inChatEditor.negate() }] }); } @@ -423,12 +434,14 @@ export function formatStatusOutput( // Count loaded and skipped files (overwritten counts as skipped) let loadedCount = info.files.filter(f => f.status === 'loaded').length; const skippedCount = info.files.filter(f => f.status === 'skipped' || f.status === 'overwritten').length; - // Include special files in the loaded count - if (info.type === PromptsType.agent && specialFiles.agentsMd.enabled) { - loadedCount += specialFiles.agentsMd.files.length; - } - if (info.type === PromptsType.instructions && specialFiles.copilotInstructions.enabled) { - loadedCount += specialFiles.copilotInstructions.files.length; + // Include special files in the loaded count for instructions + if (info.type === PromptsType.instructions) { + if (specialFiles.agentsMd.enabled) { + loadedCount += specialFiles.agentsMd.files.length; + } + if (specialFiles.copilotInstructions.enabled) { + loadedCount += specialFiles.copilotInstructions.files.length; + } } lines.push(`**${typeName}**${enabledStatus}
`); @@ -558,8 +571,9 @@ export function formatStatusOutput( hasContent = true; } - // Add special files for agents (AGENTS.md) - if (info.type === PromptsType.agent) { + // Add special files for instructions (AGENTS.md and copilot-instructions.md) + if (info.type === PromptsType.instructions) { + // AGENTS.md if (specialFiles.agentsMd.enabled && specialFiles.agentsMd.files.length > 0) { lines.push(`AGENTS.md
`); for (let i = 0; i < specialFiles.agentsMd.files.length; i++) { @@ -575,10 +589,8 @@ export function formatStatusOutput( lines.push(`AGENTS.md -
`); hasContent = true; } - } - // Add special files for instructions (copilot-instructions.md) - if (info.type === PromptsType.instructions) { + // copilot-instructions.md if (specialFiles.copilotInstructions.enabled && specialFiles.copilotInstructions.files.length > 0) { lines.push(`${COPILOT_CUSTOM_INSTRUCTIONS_FILENAME}
`); for (let i = 0; i < specialFiles.copilotInstructions.files.length; i++) { diff --git a/src/vs/workbench/contrib/chat/test/browser/actions/chatCustomizationDiagnosticsAction.test.ts b/src/vs/workbench/contrib/chat/test/browser/actions/chatCustomizationDiagnosticsAction.test.ts index 1aa7974773e..105de058437 100644 --- a/src/vs/workbench/contrib/chat/test/browser/actions/chatCustomizationDiagnosticsAction.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/actions/chatCustomizationDiagnosticsAction.test.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; +import { Schemas } from '../../../../../../base/common/network.js'; import { URI } from '../../../../../../base/common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import { formatStatusOutput, IFileStatusInfo, IPathInfo, ITypeStatusInfo } from '../../../browser/actions/chatCustomizationDiagnosticsAction.js'; @@ -91,7 +92,6 @@ suite('formatStatusOutput', () => { '.github/agents
', `${TREE_BRANCH} [\`code-reviewer.agent.md\`](${filePath('.github/agents/code-reviewer.agent.md')})
`, `${TREE_END} [\`test-helper.agent.md\`](${filePath('.github/agents/test-helper.agent.md')})
`, - 'AGENTS.md -
', '' )); }); @@ -119,7 +119,6 @@ suite('formatStatusOutput', () => { '.github/agents
', `${TREE_BRANCH} [\`good-agent.agent.md\`](${filePath('.github/agents/good-agent.agent.md')})
`, `${TREE_END} ${ICON_ERROR} [\`broken-agent.agent.md\`](${filePath('.github/agents/broken-agent.agent.md')}) - *Missing name attribute*
`, - 'AGENTS.md -
', '' )); }); @@ -151,7 +150,6 @@ suite('formatStatusOutput', () => { `${TREE_END} [\`my-agent.agent.md\`](${filePath('.github/agents/my-agent.agent.md')})
`, '~/.copilot/agents
', `${TREE_END} ${ICON_WARN} [\`my-agent.agent.md\`](${filePath('home/.copilot/agents/my-agent.agent.md')}) - *Overwritten by higher priority file*
`, - 'AGENTS.md -
', '' )); }); @@ -229,16 +227,17 @@ suite('formatStatusOutput', () => { '', '.github/instructions
', `${TREE_END} [\`testing.instructions.md\`](${filePath('.github/instructions/testing.instructions.md')})
`, + 'AGENTS.md -
', 'copilot-instructions.md
', `${TREE_END} [\`copilot-instructions.md\`](${filePath('.github/copilot-instructions.md')})
`, '' )); }); - test('agents with AGENTS.md enabled', () => { + test('instructions with AGENTS.md enabled', () => { const statusInfos: ITypeStatusInfo[] = [{ - type: PromptsType.agent, - paths: [createPath('.github/agents', true)], + type: PromptsType.instructions, + paths: [createPath('.github/instructions', true)], files: [], enabled: true }]; @@ -254,13 +253,14 @@ suite('formatStatusOutput', () => { '## Chat Customization Diagnostics', '*WARNING: This file may contain sensitive information.*', '', - '**Custom Agents**
', + '**Instructions**
', '*2 files loaded*', '', - '.github/agents
', + '.github/instructions
', 'AGENTS.md
', `${TREE_BRANCH} [\`AGENTS.md\`](${filePath('AGENTS.md')})
`, `${TREE_END} [\`AGENTS.md\`](${filePath('docs/AGENTS.md')})
`, + 'copilot-instructions.md -
', '' )); }); @@ -286,7 +286,6 @@ suite('formatStatusOutput', () => { '', '.github/agents
', `${ICON_ERROR} custom/agents - *Folder does not exist*
`, - 'AGENTS.md -
', '' )); }); @@ -310,7 +309,6 @@ suite('formatStatusOutput', () => { '**Custom Agents**
', '', '.github/agents
', - 'AGENTS.md -
', '' )); }); @@ -341,7 +339,6 @@ suite('formatStatusOutput', () => { `${TREE_END} [\`local-agent.agent.md\`](${filePath('.github/agents/local-agent.agent.md')})
`, 'Extension: my-publisher.my-extension
', `${TREE_END} [\`ext-agent.agent.md\`](${filePath('extensions/my-publisher.my-extension/agents/ext-agent.agent.md')})
`, - 'AGENTS.md -
', '' )); }); @@ -406,13 +403,13 @@ suite('formatStatusOutput', () => { '', '.github/agents
', `${TREE_END} [\`helper.agent.md\`](${filePath('.github/agents/helper.agent.md')})
`, - 'AGENTS.md -
', '', '**Instructions**
', '*1 file loaded*', '', '.github/instructions
', `${TREE_END} [\`code-style.instructions.md\`](${filePath('.github/instructions/code-style.instructions.md')})
`, + 'AGENTS.md -
', 'copilot-instructions.md -
', '', '**Prompt Files**
', @@ -483,4 +480,45 @@ suite('formatStatusOutput', () => { assert.ok(output.includes('docs%20%26%20notes'), 'Ampersand should be URL-encoded'); assert.ok(output.includes('test%5B1%5D.prompt.md'), 'Brackets should be URL-encoded'); }); + + test('vscode-userdata scheme URIs are converted to file scheme for relative paths', () => { + // Create a workspace folder + const workspaceFolderUri = URI.file('/Users/test/workspace'); + const workspaceFolder = { + uri: workspaceFolderUri, + name: 'workspace', + index: 0, + toResource: (relativePath: string) => URI.joinPath(workspaceFolderUri, relativePath) + }; + + // Create a vscode-userdata URI that maps to a path under the workspace + const userDataUri = URI.file('/Users/test/workspace/.github/agents/my-agent.agent.md').with({ scheme: Schemas.vscodeUserData }); + + const statusInfos: ITypeStatusInfo[] = [{ + type: PromptsType.agent, + paths: [{ + uri: URI.file('/Users/test/workspace/.github/agents'), + exists: true, + storage: PromptsStorage.local, + scanOrder: 1, + displayPath: '.github/agents', + isDefault: true + }], + files: [{ + uri: userDataUri, + status: 'loaded', + name: 'my-agent.agent.md', + storage: PromptsStorage.local + }], + enabled: true + }]; + + const output = formatStatusOutput(statusInfos, emptySpecialFiles, [workspaceFolder]); + + // The vscode-userdata URI should be converted to file scheme internally, + // allowing relative path computation against workspace folders + assert.ok(output.includes('.github/agents/my-agent.agent.md'), 'Should use relative path from workspace folder'); + // Should not contain the full absolute path + assert.ok(!output.includes('/Users/test/workspace/.github'), 'Should not contain absolute path when relative path is available'); + }); });