From 1a3a0872c2a40629074864eea91161710a92c1f6 Mon Sep 17 00:00:00 2001 From: gchurch Date: Tue, 19 May 2026 14:01:58 -0700 Subject: [PATCH 1/5] fix broken shell support for Intellij on Windows Agents using Windows Subsystem For Linux (WSL) and add user facing test(s) supporting multiple Intellij version ranges --- .../implementations/runTerminalCommand.ts | 59 ++++++++++++++-- .../.run/Run Continue WSL (CE).run.xml | 9 +++ .../intellij/.run/Run Continue WSL.run.xml | 10 +++ .../.run/Run Extension WSL (use TCP).run.xml | 31 ++++++++ extensions/intellij/build.gradle.kts | 70 ++++++++++++++++++- 5 files changed, 171 insertions(+), 8 deletions(-) create mode 100644 extensions/intellij/.run/Run Continue WSL (CE).run.xml create mode 100644 extensions/intellij/.run/Run Continue WSL.run.xml create mode 100644 extensions/intellij/.run/Run Extension WSL (use TCP).run.xml diff --git a/core/tools/implementations/runTerminalCommand.ts b/core/tools/implementations/runTerminalCommand.ts index 8f6201bd12c..1323ef8df1b 100644 --- a/core/tools/implementations/runTerminalCommand.ts +++ b/core/tools/implementations/runTerminalCommand.ts @@ -21,8 +21,42 @@ function getDecodedOutput(data: Buffer): string { } else { return data.toString(); } -} // Simple helper function to use login shell on Unix/macOS and PowerShell on Windows -function getShellCommand(command: string): { shell: string; args: string[] } { +} + +// Simple helper function to use login shell on Unix/macOS and PowerShell on Windows +// Detects WSL paths and uses the appropriate WSL shell +function getShellCommand( + command: string, + cwd?: string, +): { shell: string; args: string[] } { + // Check if the working directory is in WSL on Intellij + const isWslPath = + cwd && (cwd.includes("\\wsl.localhost\\") || cwd.includes("\\wsl$\\")); + + if (isWslPath) { + // Extract the WSL distribution name from the path + // Path format: \\wsl.localhost\UbuntuAny\home\user\project + // or: \\wsl$\UbuntuAny\home\user\project + let distroName = "Ubuntu"; // Default fallback + try { + const wslPathSegments = cwd.split(/\\+/); + // Find the segment after wsl.localhost or wsl$ + const wslIndex = wslPathSegments.findIndex( + (seg) => seg.includes("wsl.localhost") || seg.includes("wsl$"), + ); + if (wslIndex !== -1 && wslIndex + 1 < wslPathSegments.length) { + distroName = wslPathSegments[wslIndex + 1]; + } + } catch { + // If parsing fails, use default Ubuntu + } + // Use wsl.exe to run commands in the specific distribution + return { + shell: "wsl.exe", + args: ["-d", distroName, "-e", "bash", "-c", command], + }; + } + if (process.platform === "win32") { // Windows: Use PowerShell return { @@ -52,6 +86,17 @@ import { getBooleanArg, getStringArg } from "../parseArgs"; * Falls back to home directory or temp directory if no workspace is available. */ function resolveWorkingDirectory(workspaceDirs: string[]): string { + // Check for valid existing local paths first (IntelliJ support) so \\wsl$\ or \\wsl.local\ can pass + for (const dir of workspaceDirs) { + try { + if (dir.startsWith("file:////wsl")) { + return path.normalize(dir.replace("file://", "")); + } + } catch { + // Ignore errors (e.g. invalid path formats) + } + } + // Handle file:// URIs (local workspaces) const fileWorkspaceDir = workspaceDirs.find((dir) => dir.startsWith("file:/"), @@ -145,7 +190,7 @@ export const runTerminalCommandImpl: ToolImpl = async (args, extras) => { } // Use spawn with color environment - const { shell, args } = getShellCommand(command); + const { shell, args } = getShellCommand(command, cwd); const childProc = childProcess.spawn(shell, args, { cwd, env: getColorEnv(), // Add enhanced environment for colors @@ -376,7 +421,7 @@ export const runTerminalCommandImpl: ToolImpl = async (args, extras) => { try { // Use spawn approach for consistency with streaming version const { shell: nonStreamingShell, args: nonStreamingArgs } = - getShellCommand(command); + getShellCommand(command, cwd); const output = await new Promise<{ stdout: string; stderr: string }>( (resolve, reject) => { let timeoutId: ReturnType | undefined; @@ -503,8 +548,10 @@ export const runTerminalCommandImpl: ToolImpl = async (args, extras) => { // but don't attach any listeners other than error try { // Use spawn with color environment - const { shell: detachedShell, args: detachedArgs } = - getShellCommand(command); + const { shell: detachedShell, args: detachedArgs } = getShellCommand( + command, + cwd, + ); const childProc = childProcess.spawn(detachedShell, detachedArgs, { cwd, env: getColorEnv(), // Add color environment diff --git a/extensions/intellij/.run/Run Continue WSL (CE).run.xml b/extensions/intellij/.run/Run Continue WSL (CE).run.xml new file mode 100644 index 00000000000..38bfd7038d7 --- /dev/null +++ b/extensions/intellij/.run/Run Continue WSL (CE).run.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/extensions/intellij/.run/Run Continue WSL.run.xml b/extensions/intellij/.run/Run Continue WSL.run.xml new file mode 100644 index 00000000000..9158883d891 --- /dev/null +++ b/extensions/intellij/.run/Run Continue WSL.run.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/extensions/intellij/.run/Run Extension WSL (use TCP).run.xml b/extensions/intellij/.run/Run Extension WSL (use TCP).run.xml new file mode 100644 index 00000000000..3617b65aceb --- /dev/null +++ b/extensions/intellij/.run/Run Extension WSL (use TCP).run.xml @@ -0,0 +1,31 @@ + + + + + + + + true + true + false + false + + + \ No newline at end of file diff --git a/extensions/intellij/build.gradle.kts b/extensions/intellij/build.gradle.kts index 86231350ff7..7e454d854a9 100644 --- a/extensions/intellij/build.gradle.kts +++ b/extensions/intellij/build.gradle.kts @@ -119,10 +119,43 @@ tasks { } runIde { - val openProject = "$projectDir/../../manual-testing-sandbox" + val wslKernel = environment("WSL_KERNEL").getOrElse("") + val openProject = if (wslKernel.isNotEmpty()) { + //If gradle.properties >= 2025.X -- can direct run mount from WSL windows path in Intellij + // instead of copy to /tmp -- IE \\wsl?\Ubuntu\mnt\c\Users + val pluginVersion = providers.gradleProperty("platformVersion").getOrElse("2024.1") + val majorVersion = pluginVersion.split(".").firstOrNull()?.toIntOrNull() ?: 2024 + + if (majorVersion >= 2025) { + "\\\\wsl$\\$wslKernel\\mnt\\c" + projectDir.absolutePath.replace("C:", "").replace("/extensions/intellij/", "") + .replace("/", "\\") + "\\..\\..\\manual-testing-sandbox" + } + //For now we must copy the test file to WSL filesystem to be cleaned up at exit + else { + val wslSourcePath = "/mnt/c" + projectDir.absolutePath.replace("C:", "") + .replace("\\", "/") + "/../../manual-testing-sandbox" + val wslDestPath = "/tmp/manual-testing-sandbox" + val command = listOf("wsl", "-d", wslKernel, "-e", "cp", "-r", wslSourcePath, wslDestPath) + val process = ProcessBuilder(command).start() + val exitCode = process.waitFor() + if (exitCode != 0) { + throw GradleException( + "Failed to copy $wslSourcePath to $wslKernel:$wslDestPath\" via WSL: ${ + process.errorStream.bufferedReader().readText() + }" + ) + } else { + logger.lifecycle("Successfully copied $wslSourcePath to $wslKernel:$wslDestPath") + } + "\\\\wsl$\\$wslKernel" + wslDestPath.replace("/", "\\") + } + } else { + "$projectDir/../../manual-testing-sandbox" + } argumentProviders += CommandLineArgumentProvider { - listOf(openProject, "$openProject/test.kt") + listOf(openProject) } + finalizedBy("cleanupAfterRunIde") } test { @@ -132,6 +165,39 @@ tasks { } } +tasks.register("cleanupAfterRunIde") { + doLast { + val pluginVersion = providers.gradleProperty("platformVersion").getOrElse("2024.1") + val majorVersion = pluginVersion.split(".").firstOrNull()?.toIntOrNull() ?: 2024 + //cleanup the temp file created in runIde if WSL test was used + if (majorVersion < 2025) { + val wslKernel = environment("WSL_KERNEL").getOrElse("") + if (wslKernel.isNotEmpty()) { + val wslDestPath = "/tmp/manual-testing-sandbox" + logger.lifecycle("Cleaning up WSL temp file: $wslDestPath in $wslKernel") + val command = listOf("wsl", "-d", wslKernel, "-e", "rm", "-rf", wslDestPath) + try { + val process = ProcessBuilder(command).start() + val exitCode = process.waitFor() + if (exitCode != 0) { + logger.lifecycle( + "Warning: Failed to cleanup WSL temp file: ${ + process.errorStream.bufferedReader().readText() + }" + ) + } else { + logger.lifecycle("Successfully cleaned up $wslDestPath") + } + } catch (e: Exception) { + logger.lifecycle("Error during cleanup: ${e.message}") + } + + } + logger.lifecycle("IDE stopped. Performing cleanup...") + } + } +} + val testIntegration = task("testIntegration") { val integrationTestSourceSet = sourceSets.getByName("testIntegration") testClassesDirs = integrationTestSourceSet.output.classesDirs From bc74d21b86379f9ecf91f40fc41df6f5c478f04f Mon Sep 17 00:00:00 2001 From: gchurch Date: Tue, 19 May 2026 14:26:04 -0700 Subject: [PATCH 2/5] default wsl kernel should be most common -- Ubuntu --- extensions/intellij/.run/Run Extension WSL (use TCP).run.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/intellij/.run/Run Extension WSL (use TCP).run.xml b/extensions/intellij/.run/Run Extension WSL (use TCP).run.xml index 3617b65aceb..fb946b626c1 100644 --- a/extensions/intellij/.run/Run Extension WSL (use TCP).run.xml +++ b/extensions/intellij/.run/Run Extension WSL (use TCP).run.xml @@ -4,7 +4,7 @@ From 90dd8f362047b3d358b2a6dc79c3b58469c7ac77 Mon Sep 17 00:00:00 2001 From: gchurch Date: Tue, 19 May 2026 18:02:20 -0700 Subject: [PATCH 3/5] fix missing import from accidental commit branch --- core/tools/implementations/runTerminalCommand.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/core/tools/implementations/runTerminalCommand.ts b/core/tools/implementations/runTerminalCommand.ts index 1323ef8df1b..366527cec5f 100644 --- a/core/tools/implementations/runTerminalCommand.ts +++ b/core/tools/implementations/runTerminalCommand.ts @@ -2,6 +2,7 @@ import iconv from "iconv-lite"; import childProcess from "node:child_process"; import os from "node:os"; import { ContinueError, ContinueErrorReason } from "../../util/errors"; +import * as path from "node:path"; // Default timeout for terminal commands (2 minutes) const DEFAULT_TOOL_TIMEOUT_MS = 120_000; From 20922be187cfe1f6369b614ef5fc6f019ad0e113 Mon Sep 17 00:00:00 2001 From: gchurch Date: Tue, 19 May 2026 18:41:45 -0700 Subject: [PATCH 4/5] remove the dependence on C drive and allow any named drive for use in the test --- extensions/intellij/build.gradle.kts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/extensions/intellij/build.gradle.kts b/extensions/intellij/build.gradle.kts index 7e454d854a9..45313d5c688 100644 --- a/extensions/intellij/build.gradle.kts +++ b/extensions/intellij/build.gradle.kts @@ -121,18 +121,19 @@ tasks { runIde { val wslKernel = environment("WSL_KERNEL").getOrElse("") val openProject = if (wslKernel.isNotEmpty()) { + val currentDrive = projectDir.absolutePath.first().lowercase() //If gradle.properties >= 2025.X -- can direct run mount from WSL windows path in Intellij // instead of copy to /tmp -- IE \\wsl?\Ubuntu\mnt\c\Users val pluginVersion = providers.gradleProperty("platformVersion").getOrElse("2024.1") val majorVersion = pluginVersion.split(".").firstOrNull()?.toIntOrNull() ?: 2024 if (majorVersion >= 2025) { - "\\\\wsl$\\$wslKernel\\mnt\\c" + projectDir.absolutePath.replace("C:", "").replace("/extensions/intellij/", "") + "\\\\wsl$\\$wslKernel\\mnt\\$currentDrive" + projectDir.absolutePath.substring(2).replace("/extensions/intellij/", "") .replace("/", "\\") + "\\..\\..\\manual-testing-sandbox" } //For now we must copy the test file to WSL filesystem to be cleaned up at exit else { - val wslSourcePath = "/mnt/c" + projectDir.absolutePath.replace("C:", "") + val wslSourcePath = "/mnt/$currentDrive" + projectDir.absolutePath.substring(2) .replace("\\", "/") + "/../../manual-testing-sandbox" val wslDestPath = "/tmp/manual-testing-sandbox" val command = listOf("wsl", "-d", wslKernel, "-e", "cp", "-r", wslSourcePath, wslDestPath) From 9631923abf45d43100ea5f6155e045737ee692b0 Mon Sep 17 00:00:00 2001 From: gchurch Date: Wed, 20 May 2026 08:39:19 -0700 Subject: [PATCH 5/5] move wsl copy in test to new task execution that occurs before initialization, re-include sample test.kt at test launch --- extensions/intellij/build.gradle.kts | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/extensions/intellij/build.gradle.kts b/extensions/intellij/build.gradle.kts index 45313d5c688..529deb84cc9 100644 --- a/extensions/intellij/build.gradle.kts +++ b/extensions/intellij/build.gradle.kts @@ -120,19 +120,27 @@ tasks { runIde { val wslKernel = environment("WSL_KERNEL").getOrElse("") + val pluginVersion = providers.gradleProperty("platformVersion").getOrElse("2024.1") + val majorVersion = pluginVersion.split(".").firstOrNull()?.toIntOrNull() ?: 2024 + val currentDrive = projectDir.absolutePath.first().lowercase() + val openProject = if (wslKernel.isNotEmpty()) { - val currentDrive = projectDir.absolutePath.first().lowercase() //If gradle.properties >= 2025.X -- can direct run mount from WSL windows path in Intellij // instead of copy to /tmp -- IE \\wsl?\Ubuntu\mnt\c\Users - val pluginVersion = providers.gradleProperty("platformVersion").getOrElse("2024.1") - val majorVersion = pluginVersion.split(".").firstOrNull()?.toIntOrNull() ?: 2024 - if (majorVersion >= 2025) { "\\\\wsl$\\$wslKernel\\mnt\\$currentDrive" + projectDir.absolutePath.substring(2).replace("/extensions/intellij/", "") .replace("/", "\\") + "\\..\\..\\manual-testing-sandbox" } //For now we must copy the test file to WSL filesystem to be cleaned up at exit else { + "\\\\wsl$\\$wslKernel\\tmp\\manual-testing-sandbox" + } + } else { + "$projectDir/../../manual-testing-sandbox" + } + + doFirst { + if (wslKernel.isNotEmpty() && majorVersion < 2025) { val wslSourcePath = "/mnt/$currentDrive" + projectDir.absolutePath.substring(2) .replace("\\", "/") + "/../../manual-testing-sandbox" val wslDestPath = "/tmp/manual-testing-sandbox" @@ -148,13 +156,10 @@ tasks { } else { logger.lifecycle("Successfully copied $wslSourcePath to $wslKernel:$wslDestPath") } - "\\\\wsl$\\$wslKernel" + wslDestPath.replace("/", "\\") } - } else { - "$projectDir/../../manual-testing-sandbox" } argumentProviders += CommandLineArgumentProvider { - listOf(openProject) + listOf(openProject, "$openProject/test.kt") } finalizedBy("cleanupAfterRunIde") }