Dax Raad 8 месяцев назад
Родитель
Сommit
468cec545a

+ 33 - 23
packages/opencode/src/cli/cmd/run.ts

@@ -8,6 +8,25 @@ import { Message } from "../../session/message"
 import { UI } from "../ui"
 import { VERSION } from "../version"
 
+const COLOR = [
+  UI.Style.TEXT_SUCCESS_BOLD,
+  UI.Style.TEXT_INFO_BOLD,
+  UI.Style.TEXT_HIGHLIGHT_BOLD,
+  UI.Style.TEXT_WARNING_BOLD,
+]
+
+const TOOL: Record<string, [string, string]> = {
+  opencode_todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD],
+  opencode_todoread: ["Todo", UI.Style.TEXT_WARNING_BOLD],
+  opencode_bash: ["Bash", UI.Style.TEXT_DANGER_BOLD],
+  opencode_edit: ["Edit", UI.Style.TEXT_SUCCESS_BOLD],
+  opencode_glob: ["Glob", UI.Style.TEXT_INFO_BOLD],
+  opencode_grep: ["Grep", UI.Style.TEXT_INFO_BOLD],
+  opencode_list: ["List", UI.Style.TEXT_INFO_BOLD],
+  opencode_read: ["Read", UI.Style.TEXT_HIGHLIGHT_BOLD],
+  opencode_write: ["Write", UI.Style.TEXT_SUCCESS_BOLD],
+}
+
 export const RunCommand = {
   command: "run [message..]",
   describe: "Run OpenCode with a message",
@@ -63,33 +82,24 @@ export const RunCommand = {
           )
         }
 
-        Bus.subscribe(Message.Event.PartUpdated, async (message) => {
-          const part = message.properties.part
+        Bus.subscribe(Message.Event.PartUpdated, async (evt) => {
+          const part = evt.properties.part
+          const message = await Session.getMessage(
+            evt.properties.sessionID,
+            evt.properties.messageID,
+          )
+
           if (
             part.type === "tool-invocation" &&
             part.toolInvocation.state === "result"
           ) {
-            if (part.toolInvocation.toolName === "opencode_todowrite") return
-
-            const args = part.toolInvocation.args as any
-            const tool = part.toolInvocation.toolName
-
-            if (tool === "opencode_edit")
-              printEvent(UI.Style.TEXT_SUCCESS_BOLD, "Edit", args.filePath)
-            if (tool === "opencode_bash")
-              printEvent(UI.Style.TEXT_WARNING_BOLD, "Execute", args.command)
-            if (tool === "opencode_read")
-              printEvent(UI.Style.TEXT_INFO_BOLD, "Read", args.filePath)
-            if (tool === "opencode_write")
-              printEvent(UI.Style.TEXT_SUCCESS_BOLD, "Create", args.filePath)
-            if (tool === "opencode_list")
-              printEvent(UI.Style.TEXT_INFO_BOLD, "List", args.path)
-            if (tool === "opencode_glob")
-              printEvent(
-                UI.Style.TEXT_INFO_BOLD,
-                "Glob",
-                args.pattern + (args.path ? " in " + args.path : ""),
-              )
+            const metadata =
+              message.metadata.tool[part.toolInvocation.toolCallId]
+            const [tool, color] = TOOL[part.toolInvocation.toolName] ?? [
+              part.toolInvocation.toolName,
+              UI.Style.TEXT_INFO_BOLD,
+            ]
+            printEvent(color, tool, metadata.title)
           }
 
           if (part.type === "text") {

+ 21 - 2
packages/opencode/src/session/index.ts

@@ -143,14 +143,19 @@ export namespace Session {
     const result = [] as Message.Info[]
     const list = Storage.list("session/message/" + sessionID)
     for await (const p of list) {
-      const read = await Storage.readJSON<Message.Info>(p).catch(() => {})
-      if (!read) continue
+      const read = await Storage.readJSON<Message.Info>(p)
       result.push(read)
     }
     result.sort((a, b) => (a.id > b.id ? 1 : -1))
     return result
   }
 
+  export async function getMessage(sessionID: string, messageID: string) {
+    return Storage.readJSON<Message.Info>(
+      "session/message/" + sessionID + "/" + messageID,
+    )
+  }
+
   export async function* list() {
     for await (const item of Storage.list("session/info")) {
       const sessionID = path.basename(item, ".json")
@@ -371,16 +376,19 @@ export namespace Session {
                 end: Date.now(),
               },
             }
+            await updateMessage(next)
             return result.output
           } catch (e: any) {
             next.metadata!.tool![opts.toolCallId] = {
               error: true,
               message: e.toString(),
+              title: e.toString(),
               time: {
                 start,
                 end: Date.now(),
               },
             }
+            await updateMessage(next)
             return e.toString()
           }
         },
@@ -400,6 +408,7 @@ export namespace Session {
               end: Date.now(),
             },
           }
+          await updateMessage(next)
           return result.content
             .filter((x: any) => x.type === "text")
             .map((x: any) => x.text)
@@ -408,11 +417,13 @@ export namespace Session {
           next.metadata!.tool![opts.toolCallId] = {
             error: true,
             message: e.toString(),
+            title: "mcp",
             time: {
               start,
               end: Date.now(),
             },
           }
+          await updateMessage(next)
           return e.toString()
         }
       }
@@ -433,6 +444,8 @@ export namespace Session {
         if (text) {
           Bus.publish(Message.Event.PartUpdated, {
             part: text,
+            messageID: next.id,
+            sessionID: next.metadata.sessionID,
           })
         }
         text = undefined
@@ -463,6 +476,8 @@ export namespace Session {
             })
             Bus.publish(Message.Event.PartUpdated, {
               part: next.parts[next.parts.length - 1],
+              messageID: next.id,
+              sessionID: next.metadata.sessionID,
             })
             break
 
@@ -478,6 +493,8 @@ export namespace Session {
             })
             Bus.publish(Message.Event.PartUpdated, {
               part: next.parts[next.parts.length - 1],
+              messageID: next.id,
+              sessionID: next.metadata.sessionID,
             })
             break
 
@@ -500,6 +517,8 @@ export namespace Session {
               }
               Bus.publish(Message.Event.PartUpdated, {
                 part: match,
+                messageID: next.id,
+                sessionID: next.metadata.sessionID,
               })
             }
             break

+ 5 - 2
packages/opencode/src/session/message.ts

@@ -151,7 +151,7 @@ export namespace Message {
           z.string(),
           z
             .object({
-              title: z.string().optional(),
+              title: z.string(),
               time: z.object({
                 start: z.number(),
                 end: z.number(),
@@ -186,6 +186,9 @@ export namespace Message {
         info: Info,
       }),
     ),
-    PartUpdated: Bus.event("message.part.updated", z.object({ part: Part })),
+    PartUpdated: Bus.event(
+      "message.part.updated",
+      z.object({ part: Part, sessionID: z.string(), messageID: z.string() }),
+    ),
   }
 }

+ 20 - 19
packages/opencode/src/storage/storage.ts

@@ -1,11 +1,9 @@
-import { FileStorage } from "@flystorage/file-storage"
-import { LocalStorageAdapter } from "@flystorage/local-fs"
-import fs from "fs/promises"
 import { Log } from "../util/log"
 import { App } from "../app/app"
 import { Bus } from "../bus"
 import path from "path"
 import z from "zod"
+import fs from "fs/promises"
 
 export namespace Storage {
   const log = Log.create({ service: "storage" })
@@ -17,36 +15,39 @@ export namespace Storage {
     ),
   }
 
-  const state = App.state("storage", async () => {
+  const state = App.state("storage", () => {
     const app = App.info()
-    const storageDir = path.join(app.path.data, "storage")
-    await fs.mkdir(storageDir, { recursive: true })
-    const storage = new FileStorage(new LocalStorageAdapter(storageDir))
-    log.info("created", { path: storageDir })
+    const dir = path.join(app.path.data, "storage")
+    log.info("init", { path: dir })
     return {
-      storage,
+      dir,
     }
   })
 
+  const locks = new Map<string, Promise<void>>()
+
   export async function readJSON<T>(key: string) {
-    const storage = await state().then((x) => x.storage)
-    const data = await storage.readToString(key + ".json")
-    return JSON.parse(data) as T
+    return Bun.file(path.join(state().dir, key + ".json")).json() as Promise<T>
   }
 
   export async function writeJSON<T>(key: string, content: T) {
-    const storage = await state().then((x) => x.storage)
-    const json = JSON.stringify(content)
-    await storage.write(key + ".json", json)
+    const target = path.join(state().dir, key + ".json")
+    const tmp = target + Date.now() + ".tmp"
+    await Bun.write(tmp, JSON.stringify(content))
+    await fs.rename(tmp, target).catch(() => {})
+    await fs.unlink(tmp).catch(() => {})
     Bus.publish(Event.Write, { key, content })
   }
 
+  const glob = new Bun.Glob("**/*")
   export async function* list(prefix: string) {
     try {
-      const storage = await state().then((x) => x.storage)
-      const list = storage.list(prefix)
-      for await (const item of list) {
-        yield item.path.slice(0, -5)
+      for await (const item of glob.scan({
+        cwd: path.join(state().dir, prefix),
+        onlyFiles: true,
+      })) {
+        const result = path.join(prefix, item.slice(0, -5))
+        yield result
       }
     } catch {
       return

+ 2 - 3
packages/opencode/src/tool/grep.ts

@@ -11,7 +11,6 @@ export const GrepTool = Tool.define({
   parameters: z.object({
     pattern: z
       .string()
-      .nullable()
       .describe("The regex pattern to search for in file contents"),
     path: z
       .string()
@@ -52,7 +51,7 @@ export const GrepTool = Tool.define({
 
     if (exitCode === 1) {
       return {
-        metadata: { matches: 0, truncated: false },
+        metadata: { matches: 0, truncated: false, title: params.pattern },
         output: "No files found",
       }
     }
@@ -94,7 +93,7 @@ export const GrepTool = Tool.define({
 
     if (finalMatches.length === 0) {
       return {
-        metadata: { matches: 0, truncated: false },
+        metadata: { matches: 0, truncated: false, title: params.pattern },
         output: "No files found",
       }
     }

+ 2 - 2
packages/opencode/src/tool/todo.ts

@@ -34,7 +34,7 @@ export const TodoWriteTool = Tool.define({
     return {
       output: JSON.stringify(params.todos, null, 2),
       metadata: {
-        title: `${params.todos.length} todos`,
+        title: `${params.todos.filter((x) => x.status !== "completed").length} todos`,
         todos: params.todos,
       },
     }
@@ -50,7 +50,7 @@ export const TodoReadTool = Tool.define({
     return {
       metadata: {
         todos,
-        title: ``,
+        title: `${todos.filter((x) => x.status !== "completed").length} todos`,
       },
       output: JSON.stringify(todos, null, 2),
     }

+ 1 - 1
packages/opencode/src/util/log.ts

@@ -36,7 +36,7 @@ export namespace Log {
       .filter((entry) => entry.isFile() && entry.name.endsWith(".log"))
       .map((entry) => path.join(dir, entry.name))
 
-    if (files.length <= 10) return
+    if (files.length <= 5) return
 
     const filesToDelete = files.slice(0, -10)