|
16 | 16 | import * as vscode from 'vscode'; |
17 | 17 | import * as fs from 'fs'; |
18 | 18 | import * as path from 'path'; |
| 19 | +import * as os from 'os'; |
19 | 20 | import { promisify } from 'util'; |
| 21 | +import { execSync } from 'child_process'; |
20 | 22 | import * as yaml from 'js-yaml'; |
21 | 23 | import { Logger } from '../utils/logger'; |
22 | 24 | import { escapeRegex } from '../utils/regexUtils'; |
@@ -62,8 +64,18 @@ export class CopilotSyncService { |
62 | 64 | * |
63 | 65 | * WORKAROUND: If extension is installed globally but user is in a profile, |
64 | 66 | * 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. |
65 | 70 | */ |
66 | 71 | 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 | + |
67 | 79 | const globalStoragePath = this.context.globalStorageUri.fsPath; |
68 | 80 |
|
69 | 81 | // Find the User directory by looking for '/User/' or '\User\' in the path |
@@ -117,6 +129,158 @@ export class CopilotSyncService { |
117 | 129 | return path.join(userDir, 'prompts'); |
118 | 130 | } |
119 | 131 |
|
| 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 | + } |
120 | 284 | /** |
121 | 285 | * Detect active profile using combined workarounds |
122 | 286 | * |
|
0 commit comments