Merge pull request #9039 from Kilo-Org/fix/restore-tsgo-lightweight-diagnostics

fix(cli): restore tsgo lightweight diagnostics
This commit is contained in:
Marian Alexandru Alecu
2026-04-17 09:32:56 +03:00
committed by GitHub
4 changed files with 147 additions and 75 deletions
+15
View File
@@ -278,6 +278,21 @@ export namespace LSP {
if (!root) continue
if (s.broken.has(root + server.id)) continue
// kilocode_change start - use lightweight tsgo-based client when persistent LSP is not enabled
if (server.id === "typescript" && !Flag.KILO_EXPERIMENTAL_LSP_TOOL) {
const existing = s.clients.find((x) => x.root === root && x.serverID === server.id)
if (existing) {
result.push(existing)
continue
}
const client = TsClient.create({ root })
s.clients.push(client)
result.push(client)
Bus.publish(Event.Updated, {})
continue
}
// kilocode_change end
const match = s.clients.find((x) => x.root === root && x.serverID === server.id)
if (match) {
result.push(match)
+6 -20
View File
@@ -106,27 +106,13 @@ export namespace LSPServer {
),
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
async spawn(root) {
const tsserver = Module.resolve("typescript/lib/tsserver.js", Instance.directory)
log.info("typescript server", { tsserver })
if (!tsserver) return
const bin = await Npm.which("typescript-language-server")
if (!bin) return
const args = ["--stdio", "--tsserver-log-verbosity", "off", "--tsserver-path", tsserver]
if (
!(await pathExists(path.join(root, "tsconfig.json"))) &&
!(await pathExists(path.join(root, "jsconfig.json")))
) {
args.push("--ignore-node-modules")
if (!Flag.KILO_EXPERIMENTAL_LSP_TOOL) return undefined
const bin = await TsCheck.native_tsgo(root)
if (!bin) {
log.info("tsgo native binary not found, falling back to lightweight client")
return undefined
}
const proc = spawn(bin, args, {
cwd: root,
env: {
...process.env,
},
})
log.info("spawning tsgo --lsp", { bin, root })
return {
process: spawn(bin, ["--lsp", "--stdio"], { cwd: root }),
}
@@ -0,0 +1,89 @@
// Tests for the lightweight TypeScript diagnostic mode.
// These are regression guards — if an upstream OpenCode merge overwrites the
// kilocode integration in shared LSP files, these tests catch it.
import { describe, test, expect, spyOn, afterEach } from "bun:test"
import path from "path"
import { LSPServer } from "../../src/lsp/server"
import { TsClient } from "../../src/kilocode/ts-client"
import { TsCheck } from "../../src/kilocode/ts-check"
import { Flag } from "../../src/flag/flag"
import { Instance } from "../../src/project/instance"
afterEach(async () => {
await Instance.disposeAll()
})
describe("typescript lightweight mode", () => {
describe("spawn gate", () => {
test("Typescript.spawn returns undefined when flag is off", async () => {
const saved = Flag.KILO_EXPERIMENTAL_LSP_TOOL
// @ts-expect-error - override static flag
Flag.KILO_EXPERIMENTAL_LSP_TOOL = false
try {
const result = await LSPServer.Typescript.spawn("/tmp/any")
expect(result).toBeUndefined()
} finally {
// @ts-expect-error
Flag.KILO_EXPERIMENTAL_LSP_TOOL = saved
}
})
test("Typescript.spawn calls native_tsgo when flag is on", async () => {
const saved = Flag.KILO_EXPERIMENTAL_LSP_TOOL
// @ts-expect-error
Flag.KILO_EXPERIMENTAL_LSP_TOOL = true
const spy = spyOn(TsCheck, "native_tsgo").mockResolvedValue(undefined)
try {
const result = await LSPServer.Typescript.spawn("/tmp/any")
expect(spy).toHaveBeenCalled()
expect(result).toBeUndefined() // undefined because mock returns no binary
} finally {
// @ts-expect-error
Flag.KILO_EXPERIMENTAL_LSP_TOOL = saved
spy.mockRestore()
}
})
})
describe("TsClient", () => {
test("create returns a valid LSPClient.Info", () => {
const client = TsClient.create({ root: "/tmp/test" })
expect(client.serverID).toBe("typescript")
expect(client.root).toBe("/tmp/test")
expect(client.diagnostics).toBeInstanceOf(Map)
expect(typeof client.shutdown).toBe("function")
expect(typeof client.waitForDiagnostics).toBe("function")
expect(typeof client.notify.open).toBe("function")
})
test("connection.sendRequest rejects with descriptive error", async () => {
const client = TsClient.create({ root: "/tmp/test" })
await expect(client.connection.sendRequest("anything")).rejects.toThrow("lightweight diagnostic mode")
})
test("shutdown clears diagnostics", async () => {
const client = TsClient.create({ root: "/tmp/test" })
await client.shutdown()
expect(client.diagnostics.size).toBe(0)
})
})
describe("source integration guards", () => {
// These tests verify that kilocode integration code exists in shared
// files. If an upstream merge strips the integration blocks, these fail.
test("lsp/server.ts gates Typescript.spawn behind flag", async () => {
const src = await Bun.file(path.resolve(import.meta.dir, "../../src/lsp/server.ts")).text()
expect(src).toContain("KILO_EXPERIMENTAL_LSP_TOOL")
expect(src).toContain("native_tsgo")
})
test("lsp/index.ts uses TsClient for lightweight diagnostics", async () => {
const src = await Bun.file(path.resolve(import.meta.dir, "../../src/lsp/index.ts")).text()
expect(src).toContain("TsClient.create")
expect(src).toContain("KILO_EXPERIMENTAL_LSP_TOOL")
})
})
})
+37 -55
View File
@@ -6,6 +6,8 @@ import * as launch from "../../src/lsp/launch"
import { LSPServer } from "../../src/lsp/server"
import { Instance } from "../../src/project/instance"
import { tmpdir } from "../fixture/fixture"
import { Flag } from "../../src/flag/flag" // kilocode_change
import { TsCheck } from "../../src/kilocode/ts-check" // kilocode_change
describe("lsp.spawn", () => {
test("does not spawn builtin LSP for files outside instance", async () => {
@@ -32,7 +34,11 @@ describe("lsp.spawn", () => {
}
})
// kilocode_change start - enable flag so spawn() is actually reached (lightweight mode skips it)
test("would spawn builtin LSP for files inside instance", async () => {
const saved = Flag.KILO_EXPERIMENTAL_LSP_TOOL
// @ts-expect-error - override static flag for test
Flag.KILO_EXPERIMENTAL_LSP_TOOL = true
await using tmp = await tmpdir()
const spy = spyOn(LSPServer.Typescript, "spawn").mockResolvedValue(undefined)
@@ -50,84 +56,60 @@ describe("lsp.spawn", () => {
expect(spy).toHaveBeenCalledTimes(1)
} finally {
// @ts-expect-error
Flag.KILO_EXPERIMENTAL_LSP_TOOL = saved
spy.mockRestore()
await Instance.disposeAll()
}
})
// kilocode_change end
test("spawns builtin Typescript LSP with correct arguments", async () => {
// kilocode_change start - Typescript spawn is gated behind KILO_EXPERIMENTAL_LSP_TOOL.
// When the flag is off (default), spawn() returns undefined immediately (lightweight
// TsClient mode). These tests verify the experimental tsgo LSP spawn path.
test("spawns tsgo LSP when KILO_EXPERIMENTAL_LSP_TOOL is enabled", async () => {
const saved = Flag.KILO_EXPERIMENTAL_LSP_TOOL
// @ts-expect-error - override static flag for test
Flag.KILO_EXPERIMENTAL_LSP_TOOL = true
await using tmp = await tmpdir()
// Create dummy tsserver to satisfy Module.resolve
const tsdk = path.join(tmp.path, "node_modules", "typescript", "lib")
await fs.mkdir(tsdk, { recursive: true })
await fs.writeFile(path.join(tsdk, "tsserver.js"), "")
const spawnSpy = spyOn(launch, "spawn").mockImplementation(
() =>
({
stdin: {},
stdout: {},
stderr: {},
on: () => {},
kill: () => {},
}) as any,
() => ({ stdin: {}, stdout: {}, stderr: {}, on: () => {}, kill: () => {} }) as any,
)
const tsgoSpy = spyOn(TsCheck, "native_tsgo").mockResolvedValue("/fake/tsgo")
try {
await Instance.provide({
directory: tmp.path,
fn: async () => {
await LSPServer.Typescript.spawn(tmp.path)
const result = await LSPServer.Typescript.spawn(tmp.path)
expect(result).toBeDefined()
expect(tsgoSpy).toHaveBeenCalledWith(tmp.path)
expect(spawnSpy).toHaveBeenCalled()
const args = spawnSpy.mock.calls[0][1] as string[]
expect(args).toContain("--lsp")
expect(args).toContain("--stdio")
},
})
expect(spawnSpy).toHaveBeenCalled()
const args = spawnSpy.mock.calls[0][1] as string[]
expect(args).toContain("--tsserver-path")
expect(args).toContain("--tsserver-log-verbosity")
expect(args).toContain("off")
} finally {
// @ts-expect-error
Flag.KILO_EXPERIMENTAL_LSP_TOOL = saved
spawnSpy.mockRestore()
tsgoSpy.mockRestore()
}
})
test("spawns builtin Typescript LSP with --ignore-node-modules if no config is found", async () => {
await using tmp = await tmpdir()
// Create dummy tsserver to satisfy Module.resolve
const tsdk = path.join(tmp.path, "node_modules", "typescript", "lib")
await fs.mkdir(tsdk, { recursive: true })
await fs.writeFile(path.join(tsdk, "tsserver.js"), "")
// NO tsconfig.json or jsconfig.json created here
const spawnSpy = spyOn(launch, "spawn").mockImplementation(
() =>
({
stdin: {},
stdout: {},
stderr: {},
on: () => {},
kill: () => {},
}) as any,
)
test("Typescript.spawn returns undefined when KILO_EXPERIMENTAL_LSP_TOOL is off", async () => {
const saved = Flag.KILO_EXPERIMENTAL_LSP_TOOL
// @ts-expect-error
Flag.KILO_EXPERIMENTAL_LSP_TOOL = false
try {
await Instance.provide({
directory: tmp.path,
fn: async () => {
await LSPServer.Typescript.spawn(tmp.path)
},
})
expect(spawnSpy).toHaveBeenCalled()
const args = spawnSpy.mock.calls[0][1] as string[]
expect(args).toContain("--ignore-node-modules")
const result = await LSPServer.Typescript.spawn("/tmp/any")
expect(result).toBeUndefined()
} finally {
spawnSpy.mockRestore()
// @ts-expect-error
Flag.KILO_EXPERIMENTAL_LSP_TOOL = saved
}
})
// kilocode_change end
})