From 4a7da9c0d0d9d99d638487b9d877479a949dd242 Mon Sep 17 00:00:00 2001 From: FND Date: Mon, 6 Jul 2026 14:27:18 +0200 Subject: [PATCH 1/3] feat(lsp): add Astral ty as built-in Python primary LSP server Add ty (ty server) to defaults.json behind pyright/basedpyright/pylsp in primary selection order and ahead of ruff (linter). ty uses the generic LSP client path with no adapter code; settings.ty pass-through works via the existing settings field. - defaults.json: insert ty entry between pylsp and ruff - lsp-regressions.test.ts: 4 tests (selection order for .py/.pyi, auto-detect via $which + pyproject.toml, coexistence with ruff) - CHANGELOG.md: Added entry under [Unreleased] referencing #4617 Closes #4617 --- packages/coding-agent/CHANGELOG.md | 3 + packages/coding-agent/src/lsp/defaults.json | 6 ++ .../test/tools/lsp-regressions.test.ts | 64 +++++++++++++++++++ 3 files changed, 73 insertions(+) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index a4d66e8d2..7a3d55ac5 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1718,6 +1718,9 @@ - Fixed session titles occasionally showing raw `{"title": "..."}` JSON. Online title generation now always uses the `...` marker prompt instead of a forced `set_title` tool call — hosts that ignored or rejected forced `tool_choice` echoed the prompt's JSON example verbatim as the title — and JSON-shaped responses (bare, code-fenced, marker-wrapped, or truncated) are unwrapped to the bare title. - Fixed Linux startup prompt construction to read the CPU model from `/proc/cpuinfo` instead of `os.cpus()`, avoiding per-core sysfs frequency probes on many-core hosts ([#4712](https://github.com/can1357/oh-my-pi/issues/4712)). - Fixed llama.cpp model discovery to honor per-model `architecture.input_modalities` from `/v1/models`, so router presets that advertise image input are no longer treated as text-only ([#4719](https://github.com/can1357/oh-my-pi/issues/4719)). +### Added + +- Added Astral `ty` as a built-in Python primary LSP server (`ty server`), ordered behind `pyright`/`basedpyright`/`pylsp` so it becomes the primary Python LSP only when the existing servers are unavailable. `ruff` remains the Python linter and coexists alongside `ty` ([#4617](https://github.com/can1357/oh-my-pi/issues/4617)). ## [16.3.10] - 2026-07-06 diff --git a/packages/coding-agent/src/lsp/defaults.json b/packages/coding-agent/src/lsp/defaults.json index 21ec89a63..b537fea97 100644 --- a/packages/coding-agent/src/lsp/defaults.json +++ b/packages/coding-agent/src/lsp/defaults.json @@ -189,6 +189,12 @@ "fileTypes": [".py"], "rootMarkers": ["pyproject.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile"] }, + "ty": { + "command": "ty", + "args": ["server"], + "fileTypes": [".py", ".pyi"], + "rootMarkers": ["pyproject.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile"] + }, "ruff": { "command": "ruff", "args": ["server"], diff --git a/packages/coding-agent/test/tools/lsp-regressions.test.ts b/packages/coding-agent/test/tools/lsp-regressions.test.ts index 06468dab9..f4483d555 100644 --- a/packages/coding-agent/test/tools/lsp-regressions.test.ts +++ b/packages/coding-agent/test/tools/lsp-regressions.test.ts @@ -3449,3 +3449,67 @@ describe("expert elixir lsp", () => { expect(names.indexOf("elixirls")).toBeLessThan(names.indexOf("expert")); }); }); + +describe("ty python lsp", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("registers ty for .py behind existing Python primaries and before ruff", () => { + const config = { servers: DEFAULTS as unknown as Record }; + const names = getServersForFile(config, "app.py").map(([name]) => name); + expect(names).toContain("ty"); + expect(names).toContain("ruff"); + // ty is behind all existing Python primaries + expect(names.indexOf("pyright")).toBeLessThan(names.indexOf("ty")); + expect(names.indexOf("basedpyright")).toBeLessThan(names.indexOf("ty")); + expect(names.indexOf("pylsp")).toBeLessThan(names.indexOf("ty")); + // ruff (linter) sorts after all primaries including ty + expect(names.indexOf("ty")).toBeLessThan(names.indexOf("ruff")); + }); + + it("registers ty for .pyi stub files", () => { + const config = { servers: DEFAULTS as unknown as Record }; + const names = getServersForFile(config, "app.pyi").map(([name]) => name); + expect(names).toContain("ty"); + }); + + it("auto-detects ty when its binary and Python root markers are present", async () => { + const tempDir = TempDir.createSync("@omp-lsp-ty-detect-"); + const resolvedTy = path.join(tempDir.path(), "bin", "ty"); + const whichSpy = vi + .spyOn(piUtils, "$which") + .mockImplementation(command => (command === "ty" ? resolvedTy : null)); + try { + await Bun.write(path.join(tempDir.path(), "pyproject.toml"), '[project]\nname = "demo"\n'); + const config = loadConfig(tempDir.path()); + expect(config.servers.ty?.resolvedCommand).toBe(resolvedTy); + expect(config.servers.ty?.command).toBe("ty"); + expect(config.servers.ty?.args).toEqual(["server"]); + expect(whichSpy).toHaveBeenCalledWith("ty"); + } finally { + tempDir.removeSync(); + } + }); + + it("coexists with ruff: ty is primary, ruff is linter, both auto-detected", async () => { + const tempDir = TempDir.createSync("@omp-lsp-ty-ruff-"); + const resolvedTy = path.join(tempDir.path(), "bin", "ty"); + const resolvedRuff = path.join(tempDir.path(), "bin", "ruff"); + vi.spyOn(piUtils, "$which").mockImplementation(command => + command === "ty" ? resolvedTy : command === "ruff" ? resolvedRuff : null, + ); + try { + await Bun.write(path.join(tempDir.path(), "pyproject.toml"), '[project]\nname = "demo"\n'); + const config = loadConfig(tempDir.path()); + expect(config.servers.ty?.resolvedCommand).toBe(resolvedTy); + expect(config.servers.ruff?.resolvedCommand).toBe(resolvedRuff); + expect(config.servers.ruff?.isLinter).toBe(true); + expect(config.servers.ty?.isLinter).toBeFalsy(); + const names = getServersForFile(config, path.join(tempDir.path(), "app.py")).map(([name]) => name); + expect(names.indexOf("ty")).toBeLessThan(names.indexOf("ruff")); + } finally { + tempDir.removeSync(); + } + }); +}); From 770147c89e0f7bbf54ebd25764a659f302011d2d Mon Sep 17 00:00:00 2001 From: FND Date: Mon, 10 Aug 2026 23:23:43 +0200 Subject: [PATCH 2/3] feat(lsp): support ty.toml-only projects and fix changelog placement Address review feedback on #8186: - add ty.toml to ty rootMarkers so ty.toml-only projects pass the loadConfig marker gate (Codex P2, roboomp should-fix) - add ty.toml to PYTHON_ROOT_MARKERS so project-local .venv/bin/ty resolution runs for ty.toml-only projects (roboomp should-fix) - add a ty.toml-only regression test covering both the detection gate and the local venv bin resolution path - move the changelog entry from the released [16.3.11] section into [Unreleased] (roboomp should-fix) --- packages/coding-agent/CHANGELOG.md | 7 +++--- packages/coding-agent/src/lsp/config.ts | 1 + packages/coding-agent/src/lsp/defaults.json | 2 +- .../test/tools/lsp-regressions.test.ts | 22 +++++++++++++++++++ 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 7a3d55ac5..b0e7cdc69 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- Added Astral `ty` as a built-in Python primary LSP server (`ty server`), ordered behind `pyright`/`basedpyright`/`pylsp` so it becomes the primary Python LSP only when the existing servers are unavailable. `ruff` remains the Python linter and coexists alongside `ty` ([#4617](https://github.com/can1357/oh-my-pi/issues/4617)). + ## [17.2.12] - 2026-08-08 ### Fixed @@ -1718,9 +1722,6 @@ - Fixed session titles occasionally showing raw `{"title": "..."}` JSON. Online title generation now always uses the `...` marker prompt instead of a forced `set_title` tool call — hosts that ignored or rejected forced `tool_choice` echoed the prompt's JSON example verbatim as the title — and JSON-shaped responses (bare, code-fenced, marker-wrapped, or truncated) are unwrapped to the bare title. - Fixed Linux startup prompt construction to read the CPU model from `/proc/cpuinfo` instead of `os.cpus()`, avoiding per-core sysfs frequency probes on many-core hosts ([#4712](https://github.com/can1357/oh-my-pi/issues/4712)). - Fixed llama.cpp model discovery to honor per-model `architecture.input_modalities` from `/v1/models`, so router presets that advertise image input are no longer treated as text-only ([#4719](https://github.com/can1357/oh-my-pi/issues/4719)). -### Added - -- Added Astral `ty` as a built-in Python primary LSP server (`ty server`), ordered behind `pyright`/`basedpyright`/`pylsp` so it becomes the primary Python LSP only when the existing servers are unavailable. `ruff` remains the Python linter and coexists alongside `ty` ([#4617](https://github.com/can1357/oh-my-pi/issues/4617)). ## [16.3.10] - 2026-07-06 diff --git a/packages/coding-agent/src/lsp/config.ts b/packages/coding-agent/src/lsp/config.ts index e31092c85..ce4270644 100644 --- a/packages/coding-agent/src/lsp/config.ts +++ b/packages/coding-agent/src/lsp/config.ts @@ -229,6 +229,7 @@ export function hasRootMarkerAncestor(filePath: string, markers: string[]): bool */ const PYTHON_ROOT_MARKERS = [ "pyproject.toml", + "ty.toml", "requirements.txt", "setup.py", "setup.cfg", diff --git a/packages/coding-agent/src/lsp/defaults.json b/packages/coding-agent/src/lsp/defaults.json index b537fea97..3eaacf4f9 100644 --- a/packages/coding-agent/src/lsp/defaults.json +++ b/packages/coding-agent/src/lsp/defaults.json @@ -193,7 +193,7 @@ "command": "ty", "args": ["server"], "fileTypes": [".py", ".pyi"], - "rootMarkers": ["pyproject.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile"] + "rootMarkers": ["pyproject.toml", "ty.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile"] }, "ruff": { "command": "ruff", diff --git a/packages/coding-agent/test/tools/lsp-regressions.test.ts b/packages/coding-agent/test/tools/lsp-regressions.test.ts index f4483d555..718b738fc 100644 --- a/packages/coding-agent/test/tools/lsp-regressions.test.ts +++ b/packages/coding-agent/test/tools/lsp-regressions.test.ts @@ -3512,4 +3512,26 @@ describe("ty python lsp", () => { tempDir.removeSync(); } }); + + it("auto-detects ty in a ty.toml-only project, resolving via project-local venv bin", async () => { + // Astral documents ty.toml as a first-class project config file; a project + // that opts into ty with only that file (no pyproject/setup/requirements) + // must still pass the root-marker gate AND resolve the local venv binary. + const tempDir = TempDir.createSync("@omp-lsp-ty-toml-"); + const venvBin = process.platform === "win32" ? ".venv/Scripts" : ".venv/bin"; + const resolvedTy = path.join(tempDir.path(), venvBin, "ty"); + // $which never succeeds: only LOCAL_BIN_PATHS resolution can find ty. + vi.spyOn(piUtils, "$which").mockImplementation(() => null); + try { + await Bun.write(path.join(tempDir.path(), "ty.toml"), "[configuration]\n"); + fs.mkdirSync(path.dirname(resolvedTy), { recursive: true }); + await Bun.write(resolvedTy, '#!/bin/sh\nexec ty "$@"\n'); + const config = loadConfig(tempDir.path()); + expect(config.servers.ty?.resolvedCommand).toBe(resolvedTy); + expect(config.servers.ty?.command).toBe("ty"); + expect(config.servers.ty?.args).toEqual(["server"]); + } finally { + tempDir.removeSync(); + } + }); }); From 1f2fca50352e61f3ad77ca0a1287bf5b38be4c03 Mon Sep 17 00:00:00 2001 From: FND Date: Mon, 10 Aug 2026 23:28:29 +0200 Subject: [PATCH 3/3] test(lsp): drop redundant mkdirSync in ty.toml test Bun.write creates parent directories, so the preceding fs.mkdirSync was dead setup (Codex P2 on #8186). --- packages/coding-agent/test/tools/lsp-regressions.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/coding-agent/test/tools/lsp-regressions.test.ts b/packages/coding-agent/test/tools/lsp-regressions.test.ts index 718b738fc..694559047 100644 --- a/packages/coding-agent/test/tools/lsp-regressions.test.ts +++ b/packages/coding-agent/test/tools/lsp-regressions.test.ts @@ -3524,7 +3524,6 @@ describe("ty python lsp", () => { vi.spyOn(piUtils, "$which").mockImplementation(() => null); try { await Bun.write(path.join(tempDir.path(), "ty.toml"), "[configuration]\n"); - fs.mkdirSync(path.dirname(resolvedTy), { recursive: true }); await Bun.write(resolvedTy, '#!/bin/sh\nexec ty "$@"\n'); const config = loadConfig(tempDir.path()); expect(config.servers.ty?.resolvedCommand).toBe(resolvedTy);