Skip to content

Commit a599728

Browse files
author
qiuhuayang
committed
fix(lsp): surface gopls spawn failure reasons to LLM via tool output
When gopls fails to start (e.g. Go runtime not installed), the error was silently swallowed, causing the LLM to only see a generic 'No LSP server available' message with no actionable guidance. Changes: - server.ts: Gopls.spawn() now throws descriptive errors instead of silently returning undefined when Go is missing or installation fails - lsp.ts: broken set -> map to store failure reasons; add failureReason() method to query stored reasons by file - tool/lsp.ts: query failureReason() when LSP unavailable, pass specific error message to LLM (e.g. 'Go runtime not found. Please install Go first')
1 parent fd2278e commit a599728

6 files changed

Lines changed: 34 additions & 9 deletions

File tree

packages/opencode/src/lsp/lsp.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -116,14 +116,15 @@ type LocInput = { file: string; line: number; character: number }
116116
interface State {
117117
clients: LSPClient.Info[]
118118
servers: Record<string, LSPServer.Info>
119-
broken: Set<string>
119+
broken: Map<string, string>
120120
spawning: Map<string, Promise<LSPClient.Info | undefined>>
121121
}
122122

123123
export interface Interface {
124124
readonly init: () => Effect.Effect<void>
125125
readonly status: () => Effect.Effect<Status[]>
126126
readonly hasClients: (file: string) => Effect.Effect<boolean>
127+
readonly failureReason: (file: string) => Effect.Effect<string | undefined>
127128
readonly touchFile: (input: string, diagnostics?: "document" | "full") => Effect.Effect<void>
128129
readonly diagnostics: () => Effect.Effect<Record<string, LSPClient.Diagnostic[]>>
129130
readonly hover: (input: LocInput) => Effect.Effect<any>
@@ -195,7 +196,7 @@ export const layer = Layer.effect(
195196
const s: State = {
196197
clients: [],
197198
servers,
198-
broken: new Set(),
199+
broken: new Map(),
199200
spawning: new Map(),
200201
}
201202

@@ -222,11 +223,11 @@ export const layer = Layer.effect(
222223
const handle = await server
223224
.spawn(root, ctx, flags)
224225
.then((value) => {
225-
if (!value) s.broken.add(key)
226+
if (!value) s.broken.set(key, `LSP server '${server.id}' is not available`)
226227
return value
227228
})
228229
.catch((err) => {
229-
s.broken.add(key)
230+
s.broken.set(key, err instanceof Error ? err.message : `Failed to spawn LSP server ${server.id}`)
230231
log.error(`Failed to spawn LSP server ${server.id}`, { error: err })
231232
return undefined
232233
})
@@ -241,7 +242,7 @@ export const layer = Layer.effect(
241242
directory: ctx.directory,
242243
instance: ctx,
243244
}).catch(async (err) => {
244-
s.broken.add(key)
245+
s.broken.set(key, `Failed to initialize LSP client ${server.id}`)
245246
await Process.stop(handle.process)
246247
log.error(`Failed to initialize LSP client ${server.id}`, { error: err })
247248
return undefined
@@ -349,6 +350,22 @@ export const layer = Layer.effect(
349350
})
350351
})
351352

353+
const failureReason = Effect.fn("LSP.failureReason")(function* (file: string) {
354+
const ctx = yield* InstanceState.context
355+
const s = yield* InstanceState.get(state)
356+
return yield* Effect.promise(async () => {
357+
const extension = path.parse(file).ext || file
358+
for (const server of Object.values(s.servers)) {
359+
if (server.extensions.length && !server.extensions.includes(extension)) continue
360+
const root = await server.root(file, ctx)
361+
if (!root) continue
362+
const reason = s.broken.get(root + server.id)
363+
if (reason) return reason
364+
}
365+
return undefined
366+
})
367+
})
368+
352369
const touchFile = Effect.fn("LSP.touchFile")(function* (input: string, diagnostics?: "document" | "full") {
353370
log.info("touching file", { file: input })
354371
const clients = yield* getClients(input)
@@ -491,6 +508,7 @@ export const layer = Layer.effect(
491508
init,
492509
status,
493510
hasClients,
511+
failureReason,
494512
touchFile,
495513
diagnostics,
496514
hover,

packages/opencode/src/lsp/server.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,9 @@ export const Gopls: Info = {
353353
async spawn(root, _ctx, flags) {
354354
let bin = which("gopls")
355355
if (!bin) {
356-
if (!which("go")) return
356+
if (!which("go")) {
357+
throw new Error("Go runtime not found. Please install Go first; gopls will then be installed automatically.")
358+
}
357359
if (flags.disableLspDownload) return
358360

359361
log.info("installing gopls")
@@ -365,8 +367,7 @@ export const Gopls: Info = {
365367
})
366368
const exit = await proc.exited
367369
if (exit !== 0) {
368-
log.error("Failed to install gopls")
369-
return
370+
throw new Error("Failed to install gopls via 'go install'. Please install gopls manually.")
370371
}
371372
bin = path.join(Global.Path.bin, "gopls" + (process.platform === "win32" ? ".exe" : ""))
372373
log.info(`installed gopls`, {

packages/opencode/src/tool/lsp.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,10 @@ export const LspTool = Tool.define(
7575
if (!exists) throw new Error(`File not found: ${file}`)
7676

7777
const available = yield* lsp.hasClients(file)
78-
if (!available) throw new Error("No LSP server available for this file type.")
78+
if (!available) {
79+
const reason = yield* lsp.failureReason(file)
80+
throw new Error(reason ?? "No LSP server available for this file type.")
81+
}
7982

8083
yield* lsp.touchFile(file, "document")
8184

packages/opencode/test/session/prompt.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ const lsp = Layer.succeed(
139139
init: () => Effect.void,
140140
status: () => Effect.succeed([]),
141141
hasClients: () => Effect.succeed(false),
142+
failureReason: () => Effect.succeed(undefined),
142143
touchFile: () => Effect.void,
143144
diagnostics: () => Effect.succeed({}),
144145
hover: () => Effect.succeed(undefined),

packages/opencode/test/session/snapshot-tool-race.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ const lsp = Layer.succeed(
9595
init: () => Effect.void,
9696
status: () => Effect.succeed([]),
9797
hasClients: () => Effect.succeed(false),
98+
failureReason: () => Effect.succeed(undefined),
9899
touchFile: () => Effect.void,
99100
diagnostics: () => Effect.succeed({}),
100101
hover: () => Effect.succeed(undefined),

packages/opencode/test/tool/lsp.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ const lsp = Layer.succeed(
3636
init: () => Effect.void,
3737
status: () => Effect.succeed([]),
3838
hasClients: () => Effect.succeed(true),
39+
failureReason: () => Effect.succeed(undefined),
3940
touchFile: () => Effect.void,
4041
diagnostics: () => Effect.succeed({}),
4142
hover: () => Effect.succeed([]),

0 commit comments

Comments
 (0)