refactor(tool): convert lsp tool internals to Effect (#21806)

This commit is contained in:
Kit Langton
2026-04-10 10:07:19 -04:00
committed by GitHub
parent 157c5d77f8
commit 8063e0b5c6
2 changed files with 69 additions and 75 deletions
+37 -44
View File
@@ -1,12 +1,13 @@
import z from "zod" import z from "zod"
import { Effect } from "effect"
import { Tool } from "./tool" import { Tool } from "./tool"
import path from "path" import path from "path"
import { LSP } from "../lsp" import { LSP } from "../lsp"
import DESCRIPTION from "./lsp.txt" import DESCRIPTION from "./lsp.txt"
import { Instance } from "../project/instance" import { Instance } from "../project/instance"
import { pathToFileURL } from "url" import { pathToFileURL } from "url"
import { assertExternalDirectory } from "./external-directory" import { assertExternalDirectoryEffect } from "./external-directory"
import { Filesystem } from "../util/filesystem" import { AppFileSystem } from "../filesystem"
const operations = [ const operations = [
"goToDefinition", "goToDefinition",
@@ -20,7 +21,13 @@ const operations = [
"outgoingCalls", "outgoingCalls",
] as const ] as const
export const LspTool = Tool.define("lsp", { export const LspTool = Tool.defineEffect(
"lsp",
Effect.gen(function* () {
const lsp = yield* LSP.Service
const fs = yield* AppFileSystem.Service
return {
description: DESCRIPTION, description: DESCRIPTION,
parameters: z.object({ parameters: z.object({
operation: z.enum(operations).describe("The LSP operation to perform"), operation: z.enum(operations).describe("The LSP operation to perform"),
@@ -28,70 +35,56 @@ export const LspTool = Tool.define("lsp", {
line: z.number().int().min(1).describe("The line number (1-based, as shown in editors)"), line: z.number().int().min(1).describe("The line number (1-based, as shown in editors)"),
character: z.number().int().min(1).describe("The character offset (1-based, as shown in editors)"), character: z.number().int().min(1).describe("The character offset (1-based, as shown in editors)"),
}), }),
execute: async (args, ctx) => { execute: (args: { operation: (typeof operations)[number]; filePath: string; line: number; character: number }, ctx: Tool.Context) =>
Effect.gen(function* () {
const file = path.isAbsolute(args.filePath) ? args.filePath : path.join(Instance.directory, args.filePath) const file = path.isAbsolute(args.filePath) ? args.filePath : path.join(Instance.directory, args.filePath)
await assertExternalDirectory(ctx, file) yield* assertExternalDirectoryEffect(ctx, file)
yield* Effect.promise(() =>
ctx.ask({ permission: "lsp", patterns: ["*"], always: ["*"], metadata: {} }),
)
await ctx.ask({
permission: "lsp",
patterns: ["*"],
always: ["*"],
metadata: {},
})
const uri = pathToFileURL(file).href const uri = pathToFileURL(file).href
const position = { const position = { file, line: args.line - 1, character: args.character - 1 }
file,
line: args.line - 1,
character: args.character - 1,
}
const relPath = path.relative(Instance.worktree, file) const relPath = path.relative(Instance.worktree, file)
const title = `${args.operation} ${relPath}:${args.line}:${args.character}` const title = `${args.operation} ${relPath}:${args.line}:${args.character}`
const exists = await Filesystem.exists(file) const exists = yield* fs.existsSafe(file)
if (!exists) { if (!exists) throw new Error(`File not found: ${file}`)
throw new Error(`File not found: ${file}`)
}
const available = await LSP.hasClients(file) const available = yield* lsp.hasClients(file)
if (!available) { if (!available) throw new Error("No LSP server available for this file type.")
throw new Error("No LSP server available for this file type.")
}
await LSP.touchFile(file, true) yield* lsp.touchFile(file, true)
const result: unknown[] = await (async () => { const result: unknown[] = yield* (() => {
switch (args.operation) { switch (args.operation) {
case "goToDefinition": case "goToDefinition":
return LSP.definition(position) return lsp.definition(position)
case "findReferences": case "findReferences":
return LSP.references(position) return lsp.references(position)
case "hover": case "hover":
return LSP.hover(position) return lsp.hover(position)
case "documentSymbol": case "documentSymbol":
return LSP.documentSymbol(uri) return lsp.documentSymbol(uri)
case "workspaceSymbol": case "workspaceSymbol":
return LSP.workspaceSymbol("") return lsp.workspaceSymbol("")
case "goToImplementation": case "goToImplementation":
return LSP.implementation(position) return lsp.implementation(position)
case "prepareCallHierarchy": case "prepareCallHierarchy":
return LSP.prepareCallHierarchy(position) return lsp.prepareCallHierarchy(position)
case "incomingCalls": case "incomingCalls":
return LSP.incomingCalls(position) return lsp.incomingCalls(position)
case "outgoingCalls": case "outgoingCalls":
return LSP.outgoingCalls(position) return lsp.outgoingCalls(position)
} }
})() })()
const output = (() => {
if (result.length === 0) return `No results found for ${args.operation}`
return JSON.stringify(result, null, 2)
})()
return { return {
title, title,
metadata: { result }, metadata: { result },
output, output: result.length === 0 ? `No results found for ${args.operation}` : JSON.stringify(result, null, 2),
} }
}, }).pipe(Effect.runPromise),
}) }
}),
)
+2 -1
View File
@@ -92,6 +92,7 @@ export namespace ToolRegistry {
const read = yield* ReadTool const read = yield* ReadTool
const question = yield* QuestionTool const question = yield* QuestionTool
const todo = yield* TodoWriteTool const todo = yield* TodoWriteTool
const lsptool = yield* LspTool
const state = yield* InstanceState.make<State>( const state = yield* InstanceState.make<State>(
Effect.fn("ToolRegistry.state")(function* (ctx) { Effect.fn("ToolRegistry.state")(function* (ctx) {
@@ -164,7 +165,7 @@ export namespace ToolRegistry {
skill: Tool.init(SkillTool), skill: Tool.init(SkillTool),
patch: Tool.init(ApplyPatchTool), patch: Tool.init(ApplyPatchTool),
question: Tool.init(question), question: Tool.init(question),
lsp: Tool.init(LspTool), lsp: Tool.init(lsptool),
plan: Tool.init(PlanExitTool), plan: Tool.init(PlanExitTool),
}) })