Просмотр исходного кода

Refactor LSP tools and add hover functionality

- 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 <[email protected]>
Dax Raad 9 месяцев назад
Родитель
Сommit
c040baae11

+ 3 - 0
js/bun.lock

@@ -11,6 +11,7 @@
         "ai": "^5.0.0-alpha.4",
         "cac": "^6.7.14",
         "clipanion": "^4.0.0-rc.4",
+        "diff": "^8.0.2",
         "hono": "^4.7.10",
         "hono-openapi": "^0.4.8",
         "jsdom": "^26.1.0",
@@ -159,6 +160,8 @@
 
     "decimal.js": ["[email protected]", "", {}, "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw=="],
 
+    "diff": ["[email protected]", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="],
+
     "duplexify": ["[email protected]", "", { "dependencies": { "end-of-stream": "^1.4.1", "inherits": "^2.0.3", "readable-stream": "^3.1.1", "stream-shift": "^1.0.2" } }, "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA=="],
 
     "emoji-regex": ["[email protected]", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="],

+ 1 - 0
js/package.json

@@ -27,6 +27,7 @@
     "ai": "^5.0.0-alpha.4",
     "cac": "^6.7.14",
     "clipanion": "^4.0.0-rc.4",
+    "diff": "^8.0.2",
     "hono": "^4.7.10",
     "hono-openapi": "^0.4.8",
     "jsdom": "^26.1.0",

+ 47 - 10
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
-              : "",
-          );
-        }
       }
     });
   });

+ 4 - 3
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<void>((resolve) => {
             timeout = setTimeout(() => {
+              log.info("timed out refreshing diagnostics", input);
               unsub?.();
               resolve();
             }, 5000);

+ 18 - 0
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<T>(
     input: (client: LSPClient.Info) => Promise<T>,
   ): Promise<T[]> {

+ 3 - 0
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<string, Tool.Metadata>;
@@ -305,6 +306,8 @@ export namespace Session {
       }
       await write(next);
     }
+    next.metadata!.time.completed = Date.now();
+    await write(next);
     return next;
   }
 }

+ 2 - 1
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";

+ 1 - 1
js/src/tool/diagnostics.ts → js/src/tool/lsp-diagnostics.ts

@@ -4,7 +4,7 @@ import path from "node:path";
 import { LSP } from "../lsp";
 import { App } from "../app";
 
-export const DiagnosticsTool = Tool.define({
+export const LspDiagnosticTool = Tool.define({
   name: "diagnostics",
   description: `Get diagnostics for a file and/or project.
 

+ 38 - 0
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),
+    };
+  },
+});