Skip to content

Commit 4e216ce

Browse files
committed
refactor(tool): convert webfetch tool to defineEffect with HttpClient
1 parent 91786d2 commit 4e216ce

7 files changed

Lines changed: 239 additions & 220 deletions

File tree

packages/opencode/src/tool/lsp.ts

Lines changed: 80 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import z from "zod"
2+
import { Effect } from "effect"
23
import { Tool } from "./tool"
34
import path from "path"
45
import { LSP } from "../lsp"
@@ -20,78 +21,93 @@ const operations = [
2021
"outgoingCalls",
2122
] as const
2223

23-
export const LspTool = Tool.define("lsp", {
24-
description: DESCRIPTION,
25-
parameters: z.object({
26-
operation: z.enum(operations).describe("The LSP operation to perform"),
27-
filePath: z.string().describe("The absolute or relative path to the file"),
28-
line: z.number().int().min(1).describe("The line number (1-based, as shown in editors)"),
29-
character: z.number().int().min(1).describe("The character offset (1-based, as shown in editors)"),
30-
}),
31-
execute: async (args, ctx) => {
32-
const file = path.isAbsolute(args.filePath) ? args.filePath : path.join(Instance.directory, args.filePath)
33-
await assertExternalDirectory(ctx, file)
24+
const parameters = z.object({
25+
operation: z.enum(operations).describe("The LSP operation to perform"),
26+
filePath: z.string().describe("The absolute or relative path to the file"),
27+
line: z.number().int().min(1).describe("The line number (1-based, as shown in editors)"),
28+
character: z.number().int().min(1).describe("The character offset (1-based, as shown in editors)"),
29+
})
3430

35-
await ctx.ask({
36-
permission: "lsp",
37-
patterns: ["*"],
38-
always: ["*"],
39-
metadata: {},
40-
})
41-
const uri = pathToFileURL(file).href
42-
const position = {
43-
file,
44-
line: args.line - 1,
45-
character: args.character - 1,
46-
}
31+
export const LspTool = Tool.defineEffect(
32+
"lsp",
33+
Effect.gen(function* () {
34+
const lsp = yield* LSP.Service
4735

48-
const relPath = path.relative(Instance.worktree, file)
49-
const title = `${args.operation} ${relPath}:${args.line}:${args.character}`
36+
const run = Effect.fn("LspTool.execute")(function* (args: z.infer<typeof parameters>, ctx: Tool.Context) {
37+
const file = path.isAbsolute(args.filePath) ? args.filePath : path.join(Instance.directory, args.filePath)
38+
yield* Effect.promise(() => assertExternalDirectory(ctx, file))
5039

51-
const exists = await Filesystem.exists(file)
52-
if (!exists) {
53-
throw new Error(`File not found: ${file}`)
54-
}
40+
yield* Effect.promise(() =>
41+
ctx.ask({
42+
permission: "lsp",
43+
patterns: ["*"],
44+
always: ["*"],
45+
metadata: {},
46+
}),
47+
)
48+
const uri = pathToFileURL(file).href
49+
const position = {
50+
file,
51+
line: args.line - 1,
52+
character: args.character - 1,
53+
}
5554

56-
const available = await LSP.hasClients(file)
57-
if (!available) {
58-
throw new Error("No LSP server available for this file type.")
59-
}
55+
const rel = path.relative(Instance.worktree, file)
56+
const title = `${args.operation} ${rel}:${args.line}:${args.character}`
6057

61-
await LSP.touchFile(file, true)
58+
const exists = yield* Effect.promise(() => Filesystem.exists(file))
59+
if (!exists) {
60+
return yield* Effect.fail(new Error(`File not found: ${file}`))
61+
}
6262

63-
const result: unknown[] = await (async () => {
64-
switch (args.operation) {
65-
case "goToDefinition":
66-
return LSP.definition(position)
67-
case "findReferences":
68-
return LSP.references(position)
69-
case "hover":
70-
return LSP.hover(position)
71-
case "documentSymbol":
72-
return LSP.documentSymbol(uri)
73-
case "workspaceSymbol":
74-
return LSP.workspaceSymbol("")
75-
case "goToImplementation":
76-
return LSP.implementation(position)
77-
case "prepareCallHierarchy":
78-
return LSP.prepareCallHierarchy(position)
79-
case "incomingCalls":
80-
return LSP.incomingCalls(position)
81-
case "outgoingCalls":
82-
return LSP.outgoingCalls(position)
63+
const available = yield* lsp.hasClients(file)
64+
if (!available) {
65+
return yield* Effect.fail(new Error("No LSP server available for this file type."))
8366
}
84-
})()
8567

86-
const output = (() => {
87-
if (result.length === 0) return `No results found for ${args.operation}`
88-
return JSON.stringify(result, null, 2)
89-
})()
68+
yield* lsp.touchFile(file, true)
69+
70+
const result: unknown[] = yield* (() => {
71+
switch (args.operation) {
72+
case "goToDefinition":
73+
return lsp.definition(position)
74+
case "findReferences":
75+
return lsp.references(position)
76+
case "hover":
77+
return lsp.hover(position)
78+
case "documentSymbol":
79+
return lsp.documentSymbol(uri)
80+
case "workspaceSymbol":
81+
return lsp.workspaceSymbol("")
82+
case "goToImplementation":
83+
return lsp.implementation(position)
84+
case "prepareCallHierarchy":
85+
return lsp.prepareCallHierarchy(position)
86+
case "incomingCalls":
87+
return lsp.incomingCalls(position)
88+
case "outgoingCalls":
89+
return lsp.outgoingCalls(position)
90+
}
91+
})()
92+
93+
const output = (() => {
94+
if (result.length === 0) return `No results found for ${args.operation}`
95+
return JSON.stringify(result, null, 2)
96+
})()
97+
98+
return {
99+
title,
100+
metadata: { result },
101+
output,
102+
}
103+
})
90104

91105
return {
92-
title,
93-
metadata: { result },
94-
output,
106+
description: DESCRIPTION,
107+
parameters,
108+
async execute(args: z.infer<typeof parameters>, ctx) {
109+
return Effect.runPromise(run(args, ctx).pipe(Effect.orDie))
110+
},
95111
}
96-
},
97-
})
112+
}),
113+
)

packages/opencode/src/tool/registry.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { Glob } from "../util/glob"
2828
import path from "path"
2929
import { pathToFileURL } from "url"
3030
import { Effect, Layer, ServiceMap } from "effect"
31+
import { FetchHttpClient, HttpClient } from "effect/unstable/http"
3132
import { InstanceState } from "@/effect/instance-state"
3233
import { makeRuntime } from "@/effect/run-service"
3334
import { Env } from "../env"
@@ -80,6 +81,7 @@ export namespace ToolRegistry {
8081
| FileTime.Service
8182
| Instruction.Service
8283
| AppFileSystem.Service
84+
| HttpClient.HttpClient
8385
> = Layer.effect(
8486
Service,
8587
Effect.gen(function* () {
@@ -92,6 +94,8 @@ export namespace ToolRegistry {
9294
const read = yield* ReadTool
9395
const question = yield* QuestionTool
9496
const todo = yield* TodoWriteTool
97+
const lsptool = yield* LspTool
98+
const webfetch = yield* WebFetchTool
9599

96100
const state = yield* InstanceState.make<State>(
97101
Effect.fn("ToolRegistry.state")(function* (ctx) {
@@ -157,14 +161,14 @@ export namespace ToolRegistry {
157161
edit: Tool.init(EditTool),
158162
write: Tool.init(WriteTool),
159163
task: Tool.init(task),
160-
fetch: Tool.init(WebFetchTool),
164+
fetch: Tool.init(webfetch),
161165
todo: Tool.init(todo),
162166
search: Tool.init(WebSearchTool),
163167
code: Tool.init(CodeSearchTool),
164168
skill: Tool.init(SkillTool),
165169
patch: Tool.init(ApplyPatchTool),
166170
question: Tool.init(question),
167-
lsp: Tool.init(LspTool),
171+
lsp: Tool.init(lsptool),
168172
plan: Tool.init(PlanExitTool),
169173
})
170174

@@ -301,6 +305,7 @@ export namespace ToolRegistry {
301305
Layer.provide(FileTime.defaultLayer),
302306
Layer.provide(Instruction.defaultLayer),
303307
Layer.provide(AppFileSystem.defaultLayer),
308+
Layer.provide(FetchHttpClient.layer),
304309
),
305310
)
306311

0 commit comments

Comments
 (0)