From 15c889941ccbe463ec91d10441aeb8323c93e415 Mon Sep 17 00:00:00 2001 From: roboomp Date: Tue, 30 Jun 2026 20:14:46 +0000 Subject: [PATCH 1/3] fix(lsp): detected windows python venv scripts Added Windows virtualenv Scripts directories to local LSP command resolution so project-local Ruff launchers are discovered before PATH fallback. Fixes #3916 --- packages/coding-agent/CHANGELOG.md | 4 ++++ packages/coding-agent/src/lsp/config.ts | 11 ++++++--- .../test/tools/lsp-regressions.test.ts | 24 +++++++++++++++++++ 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 8690c9aeb..315deb7ea 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed Ruff LSP auto-detection for Windows Python virtualenvs by checking `.venv/Scripts`, `venv/Scripts`, and `.env/Scripts` before falling back to PATH. ([#3916](https://github.com/can1357/oh-my-pi/issues/3916)) + ## [16.2.9] - 2026-06-30 ### Breaking Changes diff --git a/packages/coding-agent/src/lsp/config.ts b/packages/coding-agent/src/lsp/config.ts index 8a0a07a19..7e4b99658 100644 --- a/packages/coding-agent/src/lsp/config.ts +++ b/packages/coding-agent/src/lsp/config.ts @@ -209,13 +209,18 @@ export function hasRootMarkers(cwd: string, markers: string[]): boolean { * Local bin directories to check before $PATH, ordered by priority. * Each entry maps a root marker to the bin directory to check. */ +const PYTHON_ROOT_MARKERS = ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"]; + const LOCAL_BIN_PATHS: Array<{ markers: string[]; binDir: string }> = [ // Node.js - check node_modules/.bin/ { markers: ["package.json", "package-lock.json", "yarn.lock", "pnpm-lock.yaml"], binDir: "node_modules/.bin" }, // Python - check virtual environment bin directories - { markers: ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"], binDir: ".venv/bin" }, - { markers: ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"], binDir: "venv/bin" }, - { markers: ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"], binDir: ".env/bin" }, + { markers: PYTHON_ROOT_MARKERS, binDir: ".venv/bin" }, + { markers: PYTHON_ROOT_MARKERS, binDir: ".venv/Scripts" }, + { markers: PYTHON_ROOT_MARKERS, binDir: "venv/bin" }, + { markers: PYTHON_ROOT_MARKERS, binDir: "venv/Scripts" }, + { markers: PYTHON_ROOT_MARKERS, binDir: ".env/bin" }, + { markers: PYTHON_ROOT_MARKERS, binDir: ".env/Scripts" }, // Ruby - check vendor bundle and binstubs { markers: ["Gemfile", "Gemfile.lock"], binDir: "vendor/bundle/bin" }, { markers: ["Gemfile", "Gemfile.lock"], binDir: "bin" }, diff --git a/packages/coding-agent/test/tools/lsp-regressions.test.ts b/packages/coding-agent/test/tools/lsp-regressions.test.ts index 284e9e376..f9bc59cc8 100644 --- a/packages/coding-agent/test/tools/lsp-regressions.test.ts +++ b/packages/coding-agent/test/tools/lsp-regressions.test.ts @@ -1077,6 +1077,30 @@ describe("lsp regressions", () => { } }); + it("detects Ruff in Windows virtualenv Scripts directories", async () => { + const originalPlatform = process.platform; + Object.defineProperty(process, "platform", { value: "win32", configurable: true, writable: true }); + + const tempDir = TempDir.createSync("@omp-lsp-win32-ruff-"); + const whichSpy = vi.spyOn(Bun, "which").mockReturnValue(null); + + try { + await Bun.write(path.join(tempDir.path(), "pyproject.toml"), '[project]\nname = "demo"\n'); + const scriptsDir = path.join(tempDir.path(), ".venv", "Scripts"); + await fs.promises.mkdir(scriptsDir, { recursive: true }); + const localRuff = path.join(scriptsDir, "ruff.exe"); + await Bun.write(localRuff, ""); + + const config = loadConfig(tempDir.path()); + expect(config.servers.ruff?.resolvedCommand).toBe(localRuff); + expect(whichSpy).not.toHaveBeenCalledWith("ruff"); + } finally { + Object.defineProperty(process, "platform", { value: originalPlatform, configurable: true, writable: true }); + vi.restoreAllMocks(); + tempDir.removeSync(); + } + }); + it("detects tlaplus files for LSP startup and language ids", async () => { const tempDir = TempDir.createSync("@omp-lsp-tlaplus-"); const specPath = path.join(tempDir.path(), "Spec.tla"); From ae0ae68e14e8bac44bef8f7ca83c8bfc01fc1d56 Mon Sep 17 00:00:00 2001 From: roboomp Date: Tue, 30 Jun 2026 20:21:10 +0000 Subject: [PATCH 2/3] fix(lsp): included ruff-only roots for venv lookup Extended Python local-bin gating to Ruff-only root marker files so Windows virtualenv Scripts launchers are found before PATH fallback. Fixes #3916 --- packages/coding-agent/src/lsp/config.ts | 2 +- .../test/tools/lsp-regressions.test.ts | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/packages/coding-agent/src/lsp/config.ts b/packages/coding-agent/src/lsp/config.ts index 7e4b99658..be6d8949c 100644 --- a/packages/coding-agent/src/lsp/config.ts +++ b/packages/coding-agent/src/lsp/config.ts @@ -209,7 +209,7 @@ export function hasRootMarkers(cwd: string, markers: string[]): boolean { * Local bin directories to check before $PATH, ordered by priority. * Each entry maps a root marker to the bin directory to check. */ -const PYTHON_ROOT_MARKERS = ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"]; +const PYTHON_ROOT_MARKERS = ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile", "ruff.toml", ".ruff.toml"]; const LOCAL_BIN_PATHS: Array<{ markers: string[]; binDir: string }> = [ // Node.js - check node_modules/.bin/ diff --git a/packages/coding-agent/test/tools/lsp-regressions.test.ts b/packages/coding-agent/test/tools/lsp-regressions.test.ts index f9bc59cc8..aa324b3d7 100644 --- a/packages/coding-agent/test/tools/lsp-regressions.test.ts +++ b/packages/coding-agent/test/tools/lsp-regressions.test.ts @@ -1101,6 +1101,34 @@ describe("lsp regressions", () => { } }); + it("detects Ruff in Windows virtualenv Scripts directories for Ruff-only roots", async () => { + const originalPlatform = process.platform; + Object.defineProperty(process, "platform", { value: "win32", configurable: true, writable: true }); + const whichSpy = vi.spyOn(Bun, "which").mockReturnValue(null); + + try { + for (const marker of ["ruff.toml", ".ruff.toml"] as const) { + const tempDir = TempDir.createSync("@omp-lsp-win32-ruff-marker-"); + try { + await Bun.write(path.join(tempDir.path(), marker), ""); + const scriptsDir = path.join(tempDir.path(), ".venv", "Scripts"); + await fs.promises.mkdir(scriptsDir, { recursive: true }); + const localRuff = path.join(scriptsDir, "ruff.exe"); + await Bun.write(localRuff, ""); + + const config = loadConfig(tempDir.path()); + expect(config.servers.ruff?.resolvedCommand).toBe(localRuff); + } finally { + tempDir.removeSync(); + } + } + expect(whichSpy).not.toHaveBeenCalledWith("ruff"); + } finally { + Object.defineProperty(process, "platform", { value: originalPlatform, configurable: true, writable: true }); + vi.restoreAllMocks(); + } + }); + it("detects tlaplus files for LSP startup and language ids", async () => { const tempDir = TempDir.createSync("@omp-lsp-tlaplus-"); const specPath = path.join(tempDir.path(), "Spec.tla"); From ad6748ebe77a06eafdedf5adac72d6402c711a27 Mon Sep 17 00:00:00 2001 From: roboomp Date: Tue, 30 Jun 2026 20:26:39 +0000 Subject: [PATCH 3/3] fix(lsp): covered setup.cfg and pyrightconfig venv lookup Added setup.cfg and pyrightconfig.json to PYTHON_ROOT_MARKERS so pyright, basedpyright, and pylsp project shapes also probe Windows .venv/Scripts before PATH fallback. Fixes #3916 --- packages/coding-agent/src/lsp/config.ts | 11 ++++++- .../test/tools/lsp-regressions.test.ts | 33 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/packages/coding-agent/src/lsp/config.ts b/packages/coding-agent/src/lsp/config.ts index be6d8949c..553510a60 100644 --- a/packages/coding-agent/src/lsp/config.ts +++ b/packages/coding-agent/src/lsp/config.ts @@ -209,7 +209,16 @@ export function hasRootMarkers(cwd: string, markers: string[]): boolean { * Local bin directories to check before $PATH, ordered by priority. * Each entry maps a root marker to the bin directory to check. */ -const PYTHON_ROOT_MARKERS = ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile", "ruff.toml", ".ruff.toml"]; +const PYTHON_ROOT_MARKERS = [ + "pyproject.toml", + "requirements.txt", + "setup.py", + "setup.cfg", + "Pipfile", + "pyrightconfig.json", + "ruff.toml", + ".ruff.toml", +]; const LOCAL_BIN_PATHS: Array<{ markers: string[]; binDir: string }> = [ // Node.js - check node_modules/.bin/ diff --git a/packages/coding-agent/test/tools/lsp-regressions.test.ts b/packages/coding-agent/test/tools/lsp-regressions.test.ts index aa324b3d7..ca5851fba 100644 --- a/packages/coding-agent/test/tools/lsp-regressions.test.ts +++ b/packages/coding-agent/test/tools/lsp-regressions.test.ts @@ -1129,6 +1129,39 @@ describe("lsp regressions", () => { } }); + it("detects pyright and pylsp in Windows virtualenv Scripts for Python-only roots", async () => { + const originalPlatform = process.platform; + Object.defineProperty(process, "platform", { value: "win32", configurable: true, writable: true }); + const whichSpy = vi.spyOn(Bun, "which").mockReturnValue(null); + + try { + const cases: Array<{ marker: string; server: string; binary: string }> = [ + { marker: "pyrightconfig.json", server: "pyright", binary: "pyright-langserver.exe" }, + { marker: "setup.cfg", server: "pylsp", binary: "pylsp.exe" }, + ]; + for (const { marker, server, binary } of cases) { + const tempDir = TempDir.createSync("@omp-lsp-win32-py-marker-"); + try { + await Bun.write(path.join(tempDir.path(), marker), ""); + const scriptsDir = path.join(tempDir.path(), ".venv", "Scripts"); + await fs.promises.mkdir(scriptsDir, { recursive: true }); + const localBin = path.join(scriptsDir, binary); + await Bun.write(localBin, ""); + + const config = loadConfig(tempDir.path()); + expect(config.servers[server]?.resolvedCommand).toBe(localBin); + } finally { + tempDir.removeSync(); + } + } + expect(whichSpy).not.toHaveBeenCalledWith("pyright-langserver"); + expect(whichSpy).not.toHaveBeenCalledWith("pylsp"); + } finally { + Object.defineProperty(process, "platform", { value: originalPlatform, configurable: true, writable: true }); + vi.restoreAllMocks(); + } + }); + it("detects tlaplus files for LSP startup and language ids", async () => { const tempDir = TempDir.createSync("@omp-lsp-tlaplus-"); const specPath = path.join(tempDir.path(), "Spec.tla");