From c040baae118787cd0573e5b674a2a225f36d898c Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Tue, 27 May 2025 02:17:35 -0400 Subject: Refactor LSP tools and add hover functionality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Split diagnostics tool into separate lsp-diagnostics.ts file - Add new lsp-hover.ts tool for LSP hover information - Update tool exports and session integration - Remove old diagnostics.ts file 🤖 Generated with opencode Co-Authored-By: opencode --- js/src/index.ts | 57 ++++++++++++++++++++++++++++++++++-------- js/src/lsp/client.ts | 7 +++--- js/src/lsp/index.ts | 18 +++++++++++++ js/src/session/session.ts | 3 +++ js/src/tool/diagnostics.ts | 53 --------------------------------------- js/src/tool/index.ts | 3 ++- js/src/tool/lsp-diagnostics.ts | 53 +++++++++++++++++++++++++++++++++++++++ js/src/tool/lsp-hover.ts | 38 ++++++++++++++++++++++++++++ 8 files changed, 165 insertions(+), 67 deletions(-) delete mode 100644 js/src/tool/diagnostics.ts create mode 100644 js/src/tool/lsp-diagnostics.ts create mode 100644 js/src/tool/lsp-hover.ts (limited to 'js/src') diff --git a/js/src/index.ts b/js/src/index.ts index b0e3ed270..7d6feed45 100644 --- a/js/src/index.ts +++ b/js/src/index.ts @@ -6,6 +6,7 @@ import { Bus } from "./bus"; import { Session } from "./session/session"; import cac from "cac"; import { Share } from "./share/share"; +import { Storage } from "./storage/storage"; const cli = cac("opencode"); @@ -41,6 +42,52 @@ cli const shareID = await Session.share(session.id); if (shareID) console.log("Share ID: https://dev.opencode.ai/share?id=" + session.id); + + let index = 0; + Bus.subscribe(Storage.Event.Write, async (payload) => { + const [root, , type, messageID] = payload.properties.key.split("/"); + if (root !== "session" && type !== "message") return; + const message = await Session.messages(session.id).then((x) => + x.find((x) => x.id === messageID), + ); + if (!message) return; + + for (; index < message.parts.length; index++) { + const part = message.parts[index]; + if (part.type === "text") continue; + if (part.type === "step-start") continue; + if ( + part.type === "tool-invocation" && + part.toolInvocation.state !== "result" + ) + break; + + if (part.type === "tool-invocation") { + console.log(`🔧 ${part.toolInvocation.toolName}`); + if ( + part.toolInvocation.state === "result" && + "result" in part.toolInvocation + ) { + const result = part.toolInvocation.result; + if (typeof result === "string") { + const lines = result.split("\n"); + const truncated = lines.slice(0, 4); + if (lines.length > 4) truncated.push("..."); + console.log(truncated.join("\n")); + } else if (result && typeof result === "object") { + const jsonStr = JSON.stringify(result, null, 2); + const lines = jsonStr.split("\n"); + const truncated = lines.slice(0, 4); + if (lines.length > 4) truncated.push("..."); + console.log(truncated.join("\n")); + } + } + continue; + } + console.log(part); + } + }); + const result = await Session.chat(session.id, { type: "text", text: message.join(" "), @@ -50,16 +97,6 @@ cli if (part.type === "text") { console.log("opencode:", part.text); } - if (part.type === "tool-invocation") { - console.log( - "tool:", - part.toolInvocation.toolName, - part.toolInvocation.args, - part.toolInvocation.state === "result" - ? part.toolInvocation.result - : "", - ); - } } }); }); diff --git a/js/src/lsp/client.ts b/js/src/lsp/client.ts index e8e7d694e..1de187a94 100644 --- a/js/src/lsp/client.ts +++ b/js/src/lsp/client.ts @@ -144,7 +144,7 @@ export namespace LSPClient { textDocument: { uri: `file://` + input.path, languageId, - version: ++version, + version: Date.now(), text, }, }); @@ -157,7 +157,7 @@ export namespace LSPClient { await connection.sendNotification("textDocument/didChange", { textDocument: { uri: `file://` + input.path, - version: ++version, + version: Date.now(), }, contentChanges: [ { @@ -181,7 +181,7 @@ export namespace LSPClient { event.properties.path === input.path && event.properties.serverID === result.clientID ) { - log.info("refreshed diagnostics", input); + log.info("got diagnostics", input); clearTimeout(timeout); unsub?.(); resolve(); @@ -190,6 +190,7 @@ export namespace LSPClient { }), new Promise((resolve) => { timeout = setTimeout(() => { + log.info("timed out refreshing diagnostics", input); unsub?.(); resolve(); }, 5000); diff --git a/js/src/lsp/index.ts b/js/src/lsp/index.ts index f9d48e7bf..2294d439f 100644 --- a/js/src/lsp/index.ts +++ b/js/src/lsp/index.ts @@ -54,6 +54,24 @@ export namespace LSP { return results; } + export async function hover(input: { + file: string; + line: number; + character: number; + }) { + return run((client) => { + return client.connection.sendRequest("textDocument/hover", { + textDocument: { + uri: `file://${input.file}`, + }, + position: { + line: input.line, + character: input.character, + }, + }); + }); + } + async function run( input: (client: LSPClient.Info) => Promise, ): Promise { diff --git a/js/src/session/session.ts b/js/src/session/session.ts index cbe6cd89c..8b3f7fba1 100644 --- a/js/src/session/session.ts +++ b/js/src/session/session.ts @@ -42,6 +42,7 @@ export namespace Session { export type Message = UIMessage<{ time: { created: number; + completed?: number; }; sessionID: string; tool: Record; @@ -305,6 +306,8 @@ export namespace Session { } await write(next); } + next.metadata!.time.completed = Date.now(); + await write(next); return next; } } diff --git a/js/src/tool/diagnostics.ts b/js/src/tool/diagnostics.ts deleted file mode 100644 index 3610c7781..000000000 --- a/js/src/tool/diagnostics.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { z } from "zod"; -import { Tool } from "./tool"; -import path from "node:path"; -import { LSP } from "../lsp"; -import { App } from "../app"; - -export const DiagnosticsTool = Tool.define({ - name: "diagnostics", - description: `Get diagnostics for a file and/or project. - -WHEN TO USE THIS TOOL: -- Use when you need to check for errors or warnings in your code -- Helpful for debugging and ensuring code quality -- Good for getting a quick overview of issues in a file or project - -HOW TO USE: -- Provide a path to a file to get diagnostics for that file -- Results are displayed in a structured format with severity levels - -FEATURES: -- Displays errors, warnings, and hints -- Groups diagnostics by severity -- Provides detailed information about each diagnostic - -LIMITATIONS: -- Results are limited to the diagnostics provided by the LSP clients -- May not cover all possible issues in the code -- Does not provide suggestions for fixing issues - -TIPS: -- Use in conjunction with other tools for a comprehensive code review -- Combine with the LSP client for real-time diagnostics`, - parameters: z.object({ - path: z.string().describe("The path to the file to get diagnostics."), - }), - execute: async (args) => { - const app = await App.use(); - const normalized = path.isAbsolute(args.path) - ? args.path - : path.join(app.root, args.path); - await LSP.file(normalized); - const diagnostics = await LSP.diagnostics(); - const file = diagnostics[normalized]; - return { - metadata: { - diagnostics, - }, - output: file?.length - ? file.map(LSP.Diagnostic.pretty).join("\n") - : "No errors found", - }; - }, -}); diff --git a/js/src/tool/index.ts b/js/src/tool/index.ts index b18f85012..3930c87c4 100644 --- a/js/src/tool/index.ts +++ b/js/src/tool/index.ts @@ -5,4 +5,5 @@ export * from "./glob"; export * from "./grep"; export * from "./view"; export * from "./ls"; -export * from "./diagnostics"; +export * from "./lsp-diagnostics"; +export * from "./lsp-hover"; diff --git a/js/src/tool/lsp-diagnostics.ts b/js/src/tool/lsp-diagnostics.ts new file mode 100644 index 000000000..41c33f822 --- /dev/null +++ b/js/src/tool/lsp-diagnostics.ts @@ -0,0 +1,53 @@ +import { z } from "zod"; +import { Tool } from "./tool"; +import path from "node:path"; +import { LSP } from "../lsp"; +import { App } from "../app"; + +export const LspDiagnosticTool = Tool.define({ + name: "diagnostics", + description: `Get diagnostics for a file and/or project. + +WHEN TO USE THIS TOOL: +- Use when you need to check for errors or warnings in your code +- Helpful for debugging and ensuring code quality +- Good for getting a quick overview of issues in a file or project + +HOW TO USE: +- Provide a path to a file to get diagnostics for that file +- Results are displayed in a structured format with severity levels + +FEATURES: +- Displays errors, warnings, and hints +- Groups diagnostics by severity +- Provides detailed information about each diagnostic + +LIMITATIONS: +- Results are limited to the diagnostics provided by the LSP clients +- May not cover all possible issues in the code +- Does not provide suggestions for fixing issues + +TIPS: +- Use in conjunction with other tools for a comprehensive code review +- Combine with the LSP client for real-time diagnostics`, + parameters: z.object({ + path: z.string().describe("The path to the file to get diagnostics."), + }), + execute: async (args) => { + const app = await App.use(); + const normalized = path.isAbsolute(args.path) + ? args.path + : path.join(app.root, args.path); + await LSP.file(normalized); + const diagnostics = await LSP.diagnostics(); + const file = diagnostics[normalized]; + return { + metadata: { + diagnostics, + }, + output: file?.length + ? file.map(LSP.Diagnostic.pretty).join("\n") + : "No errors found", + }; + }, +}); diff --git a/js/src/tool/lsp-hover.ts b/js/src/tool/lsp-hover.ts new file mode 100644 index 000000000..9957920a2 --- /dev/null +++ b/js/src/tool/lsp-hover.ts @@ -0,0 +1,38 @@ +import { z } from "zod"; +import { Tool } from "./tool"; +import path from "node:path"; +import { LSP } from "../lsp"; +import { App } from "../app"; + +export const LspHoverTool = Tool.define({ + name: "lsp.hover", + description: ` + Looks up hover information for a given position in a source file using the Language Server Protocol (LSP). + This includes type information, documentation, or symbol details at the specified line and character. + Useful for providing code insights, explanations, or context-aware assistance based on the user's current cursor location. + `, + parameters: z.object({ + file: z.string().describe("The path to the file to get diagnostics."), + line: z.number().describe("The line number to get diagnostics."), + character: z.number().describe("The character number to get diagnostics."), + }), + execute: async (args) => { + console.log(args); + const app = await App.use(); + const file = path.isAbsolute(args.file) + ? args.file + : path.join(app.root, args.file); + await LSP.file(file); + const result = await LSP.hover({ + ...args, + file, + }); + console.log(result); + return { + metadata: { + result, + }, + output: JSON.stringify(result, null, 2), + }; + }, +}); -- cgit v1.2.3