Skip to content

Commit 4177d69

Browse files
wherka-amaWaldek Herka
andauthored
feat: add WSL remote support for prompt syncing (#33)
Fixes #22 When VS Code is connected to a WSL remote window, the extension now properly syncs prompts to the Windows filesystem where GitHub Copilot runs, rather than the WSL filesystem. Changes: - Added WSL detection using vscode.env.remoteName === 'wsl' - Implemented getWindowsPromptDirectoryFromWSL() method with comprehensive edge case handling: - Different WSL and Windows usernames (uses cmd.exe to get Windows user) - Multiple drive letters (checks /mnt/c, /mnt/d, /mnt/e, /mnt/f) - All VS Code flavors (Code, Insiders, Windsurf, Cursor) - VS Code profiles support - Added comprehensive unit tests (6 new test cases) - Added detailed documentation in docs/WSL_SUPPORT.md Technical details: - Imports: Added os and child_process for system operations - Detection: Parses globalStoragePath for Windows mount patterns - Fallback: Gracefully handles missing or inaccessible Windows paths - Logging: Extensive debug logging for troubleshooting The implementation handles both scenarios: 1. globalStorage already on Windows mount (/mnt/c/Users/...) 2. globalStorage on WSL filesystem (/home/.../.vscode-server) All unit tests passing including new WSL tests. Co-authored-by: Waldek Herka <waldek.herka@no.reply>
1 parent 0ce8e74 commit 4177d69

File tree

3 files changed

+512
-0
lines changed

3 files changed

+512
-0
lines changed

docs/WSL_SUPPORT.md

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# WSL Support
2+
3+
The Prompt Registry extension now fully supports Windows Subsystem for Linux (WSL) environments.
4+
5+
## Background
6+
7+
When VS Code is connected to a WSL remote window:
8+
- The extension runs in the **WSL (Linux) context**
9+
- GitHub Copilot runs in the **Windows UI context**
10+
- Prompts must be synced to the **Windows filesystem** for Copilot to access them
11+
12+
## How It Works
13+
14+
The extension automatically detects WSL remote connections using `vscode.env.remoteName` and:
15+
16+
1. **Detects the Windows username** via `cmd.exe /c echo %USERNAME%`
17+
2. **Maps to Windows filesystem** via WSL mount points (`/mnt/c/`, `/mnt/d/`, etc.)
18+
3. **Syncs prompts to Windows directory**: `/mnt/c/Users/{username}/AppData/Roaming/Code/User/prompts`
19+
20+
## Supported Scenarios
21+
22+
### ✅ Scenario A: Windows Mount Storage
23+
When VS Code uses Windows-mounted storage:
24+
```
25+
globalStoragePath: /mnt/c/Users/username/AppData/Roaming/Code/User/globalStorage/...
26+
→ Prompts synced to: /mnt/c/Users/username/AppData/Roaming/Code/User/prompts
27+
```
28+
29+
### ✅ Scenario B: WSL Remote Storage
30+
When VS Code uses WSL remote storage:
31+
```
32+
globalStoragePath: /home/username/.vscode-server/data/User/globalStorage/...
33+
→ Prompts synced to: /mnt/c/Users/{windows-username}/AppData/Roaming/Code/User/prompts
34+
```
35+
36+
## Edge Cases Handled
37+
38+
### Different Usernames
39+
If your WSL username differs from Windows username:
40+
- Extension executes `cmd.exe /c echo %USERNAME%` to get actual Windows username
41+
- Fallback: Uses WSL username if command fails
42+
43+
### Multiple Drive Letters
44+
If Windows is installed on D: or other drive:
45+
- Extension checks `/mnt/c/`, `/mnt/d/`, `/mnt/e/`, `/mnt/f/` in priority order
46+
- Uses first accessible drive with `Users/{username}/AppData/Roaming`
47+
48+
### VS Code Flavors
49+
Automatically detects and supports:
50+
- VS Code Stable (`Code`)
51+
- VS Code Insiders (`Code - Insiders`)
52+
- Windsurf (`Windsurf`)
53+
- Cursor (`Cursor`)
54+
55+
### VS Code Profiles
56+
Profile-based installations are fully supported:
57+
```
58+
→ Prompts synced to: /mnt/c/Users/username/AppData/Roaming/Code/User/profiles/{profile-id}/prompts
59+
```
60+
61+
## Other Remote Types
62+
63+
- **SSH Remote** (`remoteName === 'ssh-remote'`): Uses existing logic (not WSL-specific)
64+
- **Local** (`remoteName === undefined`): Uses platform-native paths (Windows/macOS/Linux)
65+
66+
## Troubleshooting
67+
68+
### Prompts not appearing in Copilot chat
69+
70+
**Symptoms**: Downloaded collections don't show in Copilot when working in WSL project
71+
72+
**Solutions**:
73+
1. Check VS Code Output panel → "Prompt Registry" for WSL detection logs
74+
2. Verify Windows username detection: Look for `WSL: Windows username from cmd.exe:`
75+
3. Ensure Windows AppData directory is accessible from WSL: `ls /mnt/c/Users/{username}/AppData/Roaming/Code/User/prompts`
76+
4. Check permissions on Windows prompts directory
77+
5. Restart VS Code after installing collections
78+
79+
### Permission Errors
80+
81+
If you see `EACCES: permission denied` errors:
82+
- Ensure Windows user has write access to `C:\Users\{username}\AppData\Roaming\Code\User\prompts`
83+
- Check if antivirus is blocking WSL file access
84+
- Verify WSL mount is accessible: `ls /mnt/c/Users`
85+
86+
### Wrong Drive Letter
87+
88+
If extension uses wrong drive (e.g., C: instead of D:):
89+
- Extension checks drives in order: C → D → E → F
90+
- Manually create directory if needed: `mkdir -p /mnt/d/Users/{username}/AppData/Roaming/Code/User/prompts`
91+
- Check logs for drive detection: `WSL: Found Windows drive:`
92+
93+
## Technical Details
94+
95+
### Detection Method
96+
```typescript
97+
if (vscode.env.remoteName === 'wsl') {
98+
// Use WSL-specific logic
99+
}
100+
```
101+
102+
### Username Detection
103+
```bash
104+
# Primary method
105+
cmd.exe /c echo %USERNAME%
106+
107+
# Fallback methods
108+
process.env.LOGNAME
109+
process.env.USER
110+
os.userInfo().username
111+
```
112+
113+
### Drive Detection
114+
Checks in priority order:
115+
1. `/mnt/c/Users/{username}/AppData/Roaming` (most common)
116+
2. `/mnt/d/Users/{username}/AppData/Roaming`
117+
3. `/mnt/e/Users/{username}/AppData/Roaming`
118+
4. `/mnt/f/Users/{username}/AppData/Roaming`
119+
120+
## Related Issues
121+
122+
- [Issue #22: WSL Support](https://github.com/AmadeusITGroup/prompt-registry/issues/22)
123+
124+
## Testing
125+
126+
Comprehensive unit tests cover:
127+
- WSL with `/mnt/c/` mount paths
128+
- WSL with `/mnt/d/` (alternate drive)
129+
- WSL with Code Insiders flavor
130+
- WSL with profile-based paths
131+
- Local context (non-WSL)
132+
- SSH remote context
133+
134+
See `test/services/CopilotSyncService.test.ts` → "WSL Support" suite.

src/services/CopilotSyncService.ts

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
import * as vscode from 'vscode';
1717
import * as fs from 'fs';
1818
import * as path from 'path';
19+
import * as os from 'os';
1920
import { promisify } from 'util';
21+
import { execSync } from 'child_process';
2022
import * as yaml from 'js-yaml';
2123
import { Logger } from '../utils/logger';
2224
import { escapeRegex } from '../utils/regexUtils';
@@ -62,8 +64,18 @@ export class CopilotSyncService {
6264
*
6365
* WORKAROUND: If extension is installed globally but user is in a profile,
6466
* we detect the active profile using combined detection methods
67+
*
68+
* WSL Support: When running in WSL remote context, GitHub Copilot runs in the Windows UI,
69+
* so we need to sync prompts to the Windows filesystem, not the WSL filesystem.
6570
*/
6671
private getCopilotPromptsDirectory(): string {
72+
// WSL Support: Check if we're running in WSL remote context
73+
// In WSL, the extension runs in the remote (Linux) context, but Copilot runs in UI (Windows) context
74+
// We need to write prompts to the Windows filesystem where Copilot can find them
75+
if (vscode.env.remoteName === 'wsl') {
76+
return this.getWindowsPromptDirectoryFromWSL();
77+
}
78+
6779
const globalStoragePath = this.context.globalStorageUri.fsPath;
6880

6981
// Find the User directory by looking for '/User/' or '\User\' in the path
@@ -117,6 +129,158 @@ export class CopilotSyncService {
117129
return path.join(userDir, 'prompts');
118130
}
119131

132+
133+
/**
134+
* Get Windows Copilot prompts directory when running in WSL
135+
*
136+
* In WSL, the extension runs in the remote (Linux) context,
137+
* but Copilot runs in the UI (Windows) context.
138+
* We need to sync prompts to the Windows filesystem.
139+
*
140+
* Strategy:
141+
* 1. Detect if globalStorageUri already points to Windows mount (/mnt/c/)
142+
* 2. If not, get Windows username and construct Windows path
143+
* 3. Handle multiple drive letters (C:, D:, etc.)
144+
* 4. Detect VS Code flavor (Code, Insiders, Windsurf)
145+
* 5. Support profile detection
146+
*
147+
* Edge cases handled:
148+
* - Different WSL and Windows usernames (exec Windows USERNAME command)
149+
* - Multiple drive letters (/mnt/c, /mnt/d, etc.)
150+
* - VS Code profiles
151+
* - Different VS Code flavors
152+
*/
153+
private getWindowsPromptDirectoryFromWSL(): string {
154+
const globalStoragePath = this.context.globalStorageUri.fsPath;
155+
this.logger.info(`[CopilotSync] WSL detected, globalStoragePath: ${globalStoragePath}`);
156+
157+
let windowsUsername: string;
158+
let appDataPath: string;
159+
let vscodeFlavorDir: string;
160+
let driveLetter = 'c'; // Default to C:
161+
162+
// Check if globalStoragePath already points to Windows mount (/mnt/X/)
163+
const mountMatch = globalStoragePath.match(/^\/mnt\/([a-z])\/Users\/([^/]+)\/AppData\/Roaming\/([^/]+)/);
164+
165+
if (mountMatch) {
166+
// Scenario A: Already pointing to Windows filesystem via WSL mount
167+
// Path like: /mnt/c/Users/username/AppData/Roaming/Code/User/globalStorage/...
168+
driveLetter = mountMatch[1];
169+
windowsUsername = mountMatch[2];
170+
vscodeFlavorDir = mountMatch[3]; // "Code", "Code - Insiders", "Windsurf", etc.
171+
appDataPath = `/mnt/${driveLetter}/Users/${windowsUsername}/AppData/Roaming`;
172+
173+
this.logger.info(`[CopilotSync] WSL: Detected Windows mount - User: ${windowsUsername}, Flavor: ${vscodeFlavorDir}, Drive: ${driveLetter.toUpperCase()}:`);
174+
} else {
175+
// Scenario B: globalStoragePath points to WSL filesystem (e.g., /home/username/.vscode-server)
176+
// Need to map to Windows equivalent
177+
178+
this.logger.info(`[CopilotSync] WSL: Remote storage detected, mapping to Windows filesystem`);
179+
180+
// Get Windows username - try multiple methods for robustness
181+
try {
182+
// Method 1: Execute Windows command to get actual Windows username
183+
// This handles cases where WSL username differs from Windows username
184+
const result = execSync('cmd.exe /c echo %USERNAME%', {
185+
encoding: 'utf-8',
186+
timeout: 5000,
187+
stdio: ['pipe', 'pipe', 'ignore'] // Suppress stderr
188+
}).trim();
189+
190+
if (result && result !== '%USERNAME%') {
191+
windowsUsername = result;
192+
this.logger.info(`[CopilotSync] WSL: Windows username from cmd.exe: ${windowsUsername}`);
193+
} else {
194+
throw new Error('cmd.exe returned empty or unexpanded variable');
195+
}
196+
} catch (error) {
197+
// Method 2: Fallback to WSL username (assumes same as Windows username)
198+
windowsUsername = process.env.LOGNAME || process.env.USER || os.userInfo().username || 'default';
199+
this.logger.warn(`[CopilotSync] WSL: Could not get Windows username via cmd.exe, using WSL username: ${windowsUsername}`);
200+
this.logger.debug(`[CopilotSync] WSL: cmd.exe error: ${error}`);
201+
}
202+
203+
// Detect VS Code flavor from appName
204+
const appName = vscode.env.appName;
205+
if (appName.includes('Insiders')) {
206+
vscodeFlavorDir = 'Code - Insiders';
207+
} else if (appName.includes('Windsurf')) {
208+
vscodeFlavorDir = 'Windsurf';
209+
} else if (appName.includes('Cursor')) {
210+
vscodeFlavorDir = 'Cursor';
211+
} else {
212+
vscodeFlavorDir = 'Code';
213+
}
214+
215+
this.logger.info(`[CopilotSync] WSL: Detected VS Code flavor: ${vscodeFlavorDir}`);
216+
217+
// Try to find the correct Windows drive by checking common mount points
218+
// Priority: C: > D: > E: > F:
219+
const driveLetters = ['c', 'd', 'e', 'f'];
220+
let foundDrive = false;
221+
222+
for (const letter of driveLetters) {
223+
const testPath = `/mnt/${letter}/Users/${windowsUsername}/AppData/Roaming`;
224+
try {
225+
if (fs.existsSync(testPath)) {
226+
driveLetter = letter;
227+
foundDrive = true;
228+
this.logger.info(`[CopilotSync] WSL: Found Windows drive: ${letter.toUpperCase()}:`);
229+
break;
230+
}
231+
} catch (error) {
232+
// Drive not accessible, continue to next
233+
continue;
234+
}
235+
}
236+
237+
if (!foundDrive) {
238+
this.logger.warn(`[CopilotSync] WSL: Could not find Windows AppData, defaulting to C: drive`);
239+
driveLetter = 'c';
240+
}
241+
242+
appDataPath = `/mnt/${driveLetter}/Users/${windowsUsername}/AppData/Roaming`;
243+
}
244+
245+
// Check for profile in globalStoragePath
246+
// Profiles can appear in both Windows mount paths and WSL remote paths
247+
const escapedSep = escapeRegex(path.sep);
248+
const profilesMatch = globalStoragePath.match(new RegExp(`profiles${escapedSep}([^${escapedSep}]+)`));
249+
250+
if (profilesMatch) {
251+
const profileId = profilesMatch[1];
252+
const promptsDir = path.join(appDataPath, vscodeFlavorDir, 'User', 'profiles', profileId, 'prompts');
253+
this.logger.info(`[CopilotSync] WSL: Using profile prompts directory: ${promptsDir}`);
254+
255+
// Ensure directory exists
256+
try {
257+
if (!fs.existsSync(promptsDir)) {
258+
fs.mkdirSync(promptsDir, { recursive: true });
259+
this.logger.info(`[CopilotSync] WSL: Created profile prompts directory`);
260+
}
261+
} catch (error) {
262+
this.logger.error(`[CopilotSync] WSL: Failed to create profile prompts directory: ${error}`);
263+
}
264+
265+
return promptsDir;
266+
}
267+
268+
// Standard path: User/prompts
269+
const promptsDir = path.join(appDataPath, vscodeFlavorDir, 'User', 'prompts');
270+
this.logger.info(`[CopilotSync] WSL: Using default prompts directory: ${promptsDir}`);
271+
272+
// Ensure directory exists
273+
try {
274+
if (!fs.existsSync(promptsDir)) {
275+
fs.mkdirSync(promptsDir, { recursive: true });
276+
this.logger.info(`[CopilotSync] WSL: Created prompts directory`);
277+
}
278+
} catch (error) {
279+
this.logger.error(`[CopilotSync] WSL: Failed to create prompts directory: ${error}`);
280+
}
281+
282+
return promptsDir;
283+
}
120284
/**
121285
* Detect active profile using combined workarounds
122286
*

0 commit comments

Comments
 (0)