From cb12b134bc76db38d4eb8d1bd5d2e8ee75432cf3 Mon Sep 17 00:00:00 2001 From: Paul Date: Tue, 17 Feb 2026 23:54:45 -0800 Subject: [PATCH] Fix instructions reading in WSL (#295898) --- .../computeAutomaticInstructions.ts | 29 ++++++++-- .../computeAutomaticInstructions.test.ts | 58 ++++++++++++++++++- .../service/promptsService.test.ts | 5 ++ 3 files changed, 85 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts index 50c007fc116..3c3e2597382 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts @@ -7,6 +7,7 @@ import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { match, splitGlobAware } from '../../../../../base/common/glob.js'; import { ResourceMap, ResourceSet } from '../../../../../base/common/map.js'; import { Schemas } from '../../../../../base/common/network.js'; +import { OperatingSystem } from '../../../../../base/common/platform.js'; import { basename, dirname } from '../../../../../base/common/resources.js'; import { URI } from '../../../../../base/common/uri.js'; import { localize } from '../../../../../nls.js'; @@ -14,6 +15,7 @@ import { IConfigurationService } from '../../../../../platform/configuration/com import { IFileService } from '../../../../../platform/files/common/files.js'; import { ILabelService } from '../../../../../platform/label/common/label.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; +import { IRemoteAgentService } from '../../../../services/remote/common/remoteAgentService.js'; import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; import { ChatRequestVariableSet, IChatRequestVariableEntry, isPromptFileVariableEntry, toPromptFileVariableEntry, toPromptTextVariableEntry, PromptFileVariableKind, IPromptTextVariableEntry, ChatRequestToolReferenceEntry, toToolVariableEntry } from '../attachments/chatVariableEntries.js'; @@ -68,6 +70,7 @@ export class ComputeAutomaticInstructions { @IConfigurationService private readonly _configurationService: IConfigurationService, @IWorkspaceContextService private readonly _workspaceService: IWorkspaceContextService, @IFileService private readonly _fileService: IFileService, + @IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService, @ITelemetryService private readonly _telemetryService: ITelemetryService, @ILanguageModelToolsService private readonly _languageModelToolsService: ILanguageModelToolsService, ) { @@ -294,6 +297,10 @@ export class ComputeAutomaticInstructions { const readTool = this._getTool('readFile'); const runSubagentTool = this._getTool(VSCodeToolReference.runSubagent); + const remoteEnv = await this._remoteAgentService.getEnvironment(); + const remoteOS = remoteEnv?.os; + const filePath = (uri: URI) => getFilePath(uri, remoteOS); + const entries: string[] = []; if (readTool) { @@ -316,13 +323,13 @@ export class ComputeAutomaticInstructions { if (description) { entries.push(`${description}`); } - entries.push(`${getFilePath(uri)}`); + entries.push(`${filePath(uri)}`); const applyToPattern = this._getApplyToPattern(applyTo, paths); if (applyToPattern) { entries.push(`${applyToPattern}`); } } else { - entries.push(`${getFilePath(uri)}`); + entries.push(`${filePath(uri)}`); } entries.push(''); hasContent = true; @@ -335,7 +342,7 @@ export class ComputeAutomaticInstructions { const description = folderName.trim().length === 0 ? localize('instruction.file.description.agentsmd.root', 'Instructions for the workspace') : localize('instruction.file.description.agentsmd.folder', 'Instructions for folder \'{0}\'', folderName); entries.push(''); entries.push(`${description}`); - entries.push(`${getFilePath(uri)}`); + entries.push(`${filePath(uri)}`); entries.push(''); hasContent = true; @@ -378,7 +385,7 @@ export class ComputeAutomaticInstructions { if (skill.description) { entries.push(`${skill.description}`); } - entries.push(`${getFilePath(skill.uri)}`); + entries.push(`${filePath(skill.uri)}`); entries.push(''); } entries.push('', '', ''); // add trailing newline @@ -492,9 +499,19 @@ export class ComputeAutomaticInstructions { } -function getFilePath(uri: URI): string { +export function getFilePath(uri: URI, remoteOS: OperatingSystem | undefined): string { if (uri.scheme === Schemas.file || uri.scheme === Schemas.vscodeRemote) { - return uri.fsPath; + const fsPath = uri.fsPath; + // uri.fsPath uses the local OS's path separators, but the path + // may belong to a remote with a different OS. Normalize separators + // to match the remote OS (idempotent when local and remote match). + if (remoteOS !== undefined) { + if (remoteOS === OperatingSystem.Windows) { + return fsPath.replace(/\//g, '\\'); + } + return fsPath.replace(/\\/g, '/'); + } + return fsPath; } return uri.toString(); } diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/computeAutomaticInstructions.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/computeAutomaticInstructions.test.ts index 3cb6fe2ad58..2d008a2567f 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/computeAutomaticInstructions.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/computeAutomaticInstructions.test.ts @@ -8,6 +8,7 @@ import * as sinon from 'sinon'; import { CancellationToken } from '../../../../../../base/common/cancellation.js'; import { Event } from '../../../../../../base/common/event.js'; import { Schemas } from '../../../../../../base/common/network.js'; +import { OperatingSystem } from '../../../../../../base/common/platform.js'; import { URI } from '../../../../../../base/common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import { ILanguageService } from '../../../../../../editor/common/languages/language.js'; @@ -27,7 +28,7 @@ import { testWorkspace } from '../../../../../../platform/workspace/test/common/ import { IUserDataProfileService } from '../../../../../services/userDataProfile/common/userDataProfile.js'; import { TestContextService, TestUserDataProfileService } from '../../../../../test/common/workbenchTestServices.js'; import { ChatRequestVariableSet, isPromptFileVariableEntry, isPromptTextVariableEntry, toFileVariableEntry } from '../../../common/attachments/chatVariableEntries.js'; -import { ComputeAutomaticInstructions, InstructionsCollectionEvent } from '../../../common/promptSyntax/computeAutomaticInstructions.js'; +import { ComputeAutomaticInstructions, getFilePath, InstructionsCollectionEvent } from '../../../common/promptSyntax/computeAutomaticInstructions.js'; import { PromptsConfig } from '../../../common/promptSyntax/config/config.js'; import { AGENTS_SOURCE_FOLDER, CLAUDE_RULES_SOURCE_FOLDER, INSTRUCTION_FILE_EXTENSION, INSTRUCTIONS_DEFAULT_SOURCE_FOLDER, LEGACY_MODE_DEFAULT_SOURCE_FOLDER, PROMPT_DEFAULT_SOURCE_FOLDER, PROMPT_FILE_EXTENSION } from '../../../common/promptSyntax/config/promptFileLocations.js'; import { INSTRUCTIONS_LANGUAGE_ID, PROMPT_LANGUAGE_ID } from '../../../common/promptSyntax/promptTypes.js'; @@ -39,6 +40,7 @@ import { IPathService } from '../../../../../services/path/common/pathService.js import { IFileQuery, ISearchService } from '../../../../../services/search/common/search.js'; import { IExtensionService } from '../../../../../services/extensions/common/extensions.js'; import { ILanguageModelToolsService } from '../../../common/tools/languageModelToolsService.js'; +import { IRemoteAgentService } from '../../../../../../workbench/services/remote/common/remoteAgentService.js'; import { basename } from '../../../../../../base/common/resources.js'; import { match } from '../../../../../../base/common/glob.js'; import { ChatModeKind } from '../../../common/constants.js'; @@ -169,6 +171,10 @@ suite('ComputeAutomaticInstructions', () => { } as unknown as ILanguageModelToolsService; instaService.stub(ILanguageModelToolsService, toolsService); + instaService.stub(IRemoteAgentService, { + getEnvironment: () => Promise.resolve(null), + }); + service = disposables.add(instaService.createInstance(PromptsService)); instaService.stub(IPromptsService, service); }); @@ -2011,3 +2017,53 @@ suite('ComputeAutomaticInstructions', () => { assert.ok(!paths.includes(claudeMdUri.path), 'Should not include CLAUDE.md (symlink to copilot)'); }); }); + +suite('getFilePath', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('should return fsPath for file:// URIs', () => { + const uri = URI.file('/workspace/src/file.ts'); + const result = getFilePath(uri, undefined); + assert.strictEqual(result, uri.fsPath); + }); + + test('should return fsPath for vscode-remote URIs', () => { + const uri = URI.from({ scheme: Schemas.vscodeRemote, path: '/workspace/src/file.ts' }); + const result = getFilePath(uri, undefined); + assert.strictEqual(result, uri.fsPath); + }); + + test('should return uri.toString() for other schemes', () => { + const uri = URI.from({ scheme: 'untitled', path: '/workspace/src/file.ts' }); + const result = getFilePath(uri, undefined); + assert.strictEqual(result, uri.toString()); + }); + + test('should use backslashes when remote is Windows', () => { + const uri = URI.from({ scheme: Schemas.vscodeRemote, path: '/C:/Users/dev/project/file.ts' }); + const result = getFilePath(uri, OperatingSystem.Windows); + assert.ok(!result.includes('/'), 'Should not contain forward slashes'); + assert.ok(result.includes('\\'), 'Should contain backslashes'); + }); + + test('should use forward slashes when remote is Linux', () => { + const uri = URI.from({ scheme: Schemas.vscodeRemote, path: '/home/user/project/file.ts' }); + const result = getFilePath(uri, OperatingSystem.Linux); + assert.ok(!result.includes('\\'), 'Should not contain backslashes'); + assert.ok(result.includes('/home/user/project/file.ts'), 'Should contain the forward-slash path'); + }); + + test('should use forward slashes when remote is macOS', () => { + const uri = URI.from({ scheme: Schemas.vscodeRemote, path: '/Users/dev/project/file.ts' }); + const result = getFilePath(uri, OperatingSystem.Macintosh); + assert.ok(!result.includes('\\'), 'Should not contain backslashes'); + assert.ok(result.includes('/Users/dev/project/file.ts'), 'Should contain the forward-slash path'); + }); + + test('should not replace slashes when remoteOS is undefined', () => { + const uri = URI.file('/workspace/src/file.ts'); + const result = getFilePath(uri, undefined); + assert.strictEqual(result, uri.fsPath); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts index 05eabacb54d..dd8f81d4dd3 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts @@ -47,6 +47,7 @@ import { InMemoryStorageService, IStorageService } from '../../../../../../../pl import { IPathService } from '../../../../../../services/path/common/pathService.js'; import { IFileMatch, IFileQuery, ISearchService } from '../../../../../../services/search/common/search.js'; import { IExtensionService } from '../../../../../../services/extensions/common/extensions.js'; +import { IRemoteAgentService } from '../../../../../../services/remote/common/remoteAgentService.js'; import { ChatModeKind } from '../../../../common/constants.js'; import { HookType } from '../../../../common/promptSyntax/hookSchema.js'; @@ -153,6 +154,10 @@ suite('PromptsService', () => { } }); + instaService.stub(IRemoteAgentService, { + getEnvironment: () => Promise.resolve(null), + }); + service = disposables.add(instaService.createInstance(PromptsService)); instaService.stub(IPromptsService, service); });