From d45d5d664c833c621321a18e2ef0d46843514700 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Wed, 6 Aug 2025 23:15:06 -0400 Subject: [PATCH] progress --- packages/opencode/src/agent/agent.ts | 2 +- packages/opencode/src/app/app.ts | 5 +++- packages/opencode/src/bus/index.ts | 20 +++++++++++----- packages/opencode/src/config/config.ts | 11 ++++----- packages/opencode/src/file/time.ts | 24 +++++++++++-------- packages/opencode/src/file/watch.ts | 5 ++-- packages/opencode/src/format/index.ts | 2 +- packages/opencode/src/lsp/index.ts | 2 +- packages/opencode/src/mcp/index.ts | 4 ++-- packages/opencode/src/permission/index.ts | 4 ++-- packages/opencode/src/project/path.ts | 2 +- packages/opencode/src/project/state.ts | 15 ++++++++---- packages/opencode/src/share/share.ts | 6 ++--- packages/opencode/src/storage/storage-next.ts | 8 +++++++ packages/opencode/src/tool/todo.ts | 4 ++-- 15 files changed, 71 insertions(+), 43 deletions(-) diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 96fdb6220..b292cc7f8 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -26,7 +26,7 @@ export namespace Agent { }) export type Info = z.infer const state = State.create( - () => Paths.use().directory, + () => Paths.directory, async () => { const cfg = await Config.get() const result: Record = { diff --git a/packages/opencode/src/app/app.ts b/packages/opencode/src/app/app.ts index acc438a9b..e279cd70c 100644 --- a/packages/opencode/src/app/app.ts +++ b/packages/opencode/src/app/app.ts @@ -8,6 +8,7 @@ import os from "os" import { z } from "zod" import { Project } from "../project/project" import { Paths } from "../project/path" +import { State } from "../project/state" export namespace App { const log = Log.create({ service: "app" }) @@ -100,7 +101,7 @@ export namespace App { return ctx.provide(app, async () => { return Project.provide(project, async () => { - return Paths.provide( + const result = await Paths.provide( { worktree: app.info.path.root, directory: app.info.path.cwd, @@ -118,6 +119,8 @@ export namespace App { } }, ) + await State.dispose(app.info.path.cwd) + return result }) }) } diff --git a/packages/opencode/src/bus/index.ts b/packages/opencode/src/bus/index.ts index 0d810edee..bd97bd6ff 100644 --- a/packages/opencode/src/bus/index.ts +++ b/packages/opencode/src/bus/index.ts @@ -1,11 +1,22 @@ import { z, type ZodType } from "zod" import { Log } from "../util/log" +import { State } from "../project/state" +import { Paths } from "../project/path" export namespace Bus { const log = Log.create({ service: "bus" }) type Subscription = (event: any) => void - const subscriptions = new Map() + const state = State.create( + () => Paths.directory, + () => { + const subscriptions = new Map() + + return { + subscriptions, + } + }, + ) export type EventDefinition = ReturnType @@ -30,10 +41,6 @@ export namespace Bus { .object({ type: z.literal(type), properties: def.properties, - context: z.object({ - projectID: z.string().optional(), - sessionID: z.string().optional(), - }), }) .openapi({ ref: "Event" + "." + def.type, @@ -56,7 +63,7 @@ export namespace Bus { }) const pending = [] for (const key of [def.type, "*"]) { - const match = subscriptions.get(key) + const match = state().subscriptions.get(key) for (const sub of match ?? []) { pending.push(sub(payload)) } @@ -89,6 +96,7 @@ export namespace Bus { function raw(type: string, callback: (event: any) => void) { log.info("subscribing", { type }) + const subscriptions = state().subscriptions let match = subscriptions.get(type) ?? [] match.push(callback) subscriptions.set(type, match) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 62a65ec5d..3422eb31a 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -20,13 +20,12 @@ export namespace Config { const log = Log.create({ service: "config" }) export const state = State.create( - () => Paths.use().directory, + () => Paths.directory, async () => { - const paths = Paths.use() const auth = await Auth.all() let result = await global() for (const file of ["opencode.jsonc", "opencode.json"]) { - const found = await Filesystem.findUp(file, paths.directory, paths.worktree) + const found = await Filesystem.findUp(file, Paths.directory, Paths.worktree) for (const resolved of found.toReversed()) { result = mergeDeep(result, await loadFile(resolved)) } @@ -49,7 +48,7 @@ export namespace Config { result.agent = result.agent || {} const markdownAgents = [ ...(await Filesystem.globUp("agent/*.md", Global.Path.config, Global.Path.config)), - ...(await Filesystem.globUp(".opencode/agent/*.md", paths.directory, paths.worktree)), + ...(await Filesystem.globUp(".opencode/agent/*.md", Paths.directory, Paths.worktree)), ] for (const item of markdownAgents) { const content = await Bun.file(item).text() @@ -75,7 +74,7 @@ export namespace Config { result.mode = result.mode || {} const markdownModes = [ ...(await Filesystem.globUp("mode/*.md", Global.Path.config, Global.Path.config)), - ...(await Filesystem.globUp(".opencode/mode/*.md", paths.directory, paths.worktree)), + ...(await Filesystem.globUp(".opencode/mode/*.md", Paths.directory, Paths.worktree)), ] for (const item of markdownModes) { const content = await Bun.file(item).text() @@ -101,7 +100,7 @@ export namespace Config { result.plugin.push( ...[ ...(await Filesystem.globUp("plugin/*.ts", Global.Path.config, Global.Path.config)), - ...(await Filesystem.globUp(".opencode/plugin/*.ts", paths.directory, paths.worktree)), + ...(await Filesystem.globUp(".opencode/plugin/*.ts", Paths.directory, Paths.worktree)), ].map((x) => "file://" + x), ) diff --git a/packages/opencode/src/file/time.ts b/packages/opencode/src/file/time.ts index 453259e87..cfc224452 100644 --- a/packages/opencode/src/file/time.ts +++ b/packages/opencode/src/file/time.ts @@ -1,18 +1,22 @@ -import { App } from "../app/app" +import { Paths } from "../project/path" +import { State } from "../project/state" import { Log } from "../util/log" export namespace FileTime { const log = Log.create({ service: "file.time" }) - export const state = App.state("tool.filetimes", () => { - const read: { - [sessionID: string]: { - [path: string]: Date | undefined + export const state = State.create( + () => Paths.directory, + () => { + const read: { + [sessionID: string]: { + [path: string]: Date | undefined + } + } = {} + return { + read, } - } = {} - return { - read, - } - }) + }, + ) export function read(sessionID: string, file: string) { log.info("read", { sessionID, file }) diff --git a/packages/opencode/src/file/watch.ts b/packages/opencode/src/file/watch.ts index 5a84aa354..b86e44b48 100644 --- a/packages/opencode/src/file/watch.ts +++ b/packages/opencode/src/file/watch.ts @@ -5,6 +5,7 @@ import { App } from "../app/app" import { Log } from "../util/log" import { Flag } from "../flag/flag" import { Paths } from "../project/path" +import { State } from "../project/state" export namespace FileWatcher { const log = Log.create({ service: "file.watcher" }) @@ -18,8 +19,8 @@ export namespace FileWatcher { }), ), } - const state = App.state( - () => Paths.use().worktree, + const state = State.create( + () => Paths.directory, () => { const app = App.use() if (!app.info.git) return {} diff --git a/packages/opencode/src/format/index.ts b/packages/opencode/src/format/index.ts index 3863ae88e..01be35673 100644 --- a/packages/opencode/src/format/index.ts +++ b/packages/opencode/src/format/index.ts @@ -14,7 +14,7 @@ export namespace Format { const log = Log.create({ service: "format" }) const state = State.create( - () => Paths.use().directory, + () => Paths.directory, async () => { const enabled: Record = {} const cfg = await Config.get() diff --git a/packages/opencode/src/lsp/index.ts b/packages/opencode/src/lsp/index.ts index 5260a5a26..961b1e5d4 100644 --- a/packages/opencode/src/lsp/index.ts +++ b/packages/opencode/src/lsp/index.ts @@ -56,7 +56,7 @@ export namespace LSP { export type DocumentSymbol = z.infer const state = State.create( - () => Paths.use().worktree, + () => Paths.directory, async () => { const clients: LSPClient.Info[] = [] const servers: Record = LSPServer diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index 763b7b336..15815f966 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -2,7 +2,6 @@ import { experimental_createMCPClient, type Tool } from "ai" import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js" import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js" import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js" -import { App } from "../app/app" import { Config } from "../config/config" import { Log } from "../util/log" import { NamedError } from "../util/error" @@ -10,6 +9,7 @@ import { z } from "zod" import { Session } from "../session" import { Bus } from "../bus" import { State } from "../project/state" +import { Paths } from "../project/path" export namespace MCP { const log = Log.create({ service: "mcp" }) @@ -22,7 +22,7 @@ export namespace MCP { ) const state = State.create( - "mcp", + () => Paths.directory, async () => { const cfg = await Config.get() const clients: { diff --git a/packages/opencode/src/permission/index.ts b/packages/opencode/src/permission/index.ts index 37101a74a..e9525160a 100644 --- a/packages/opencode/src/permission/index.ts +++ b/packages/opencode/src/permission/index.ts @@ -4,7 +4,7 @@ import { Log } from "../util/log" import { Identifier } from "../id/id" import { Plugin } from "../plugin" import { State } from "../project/state" -import { Project } from "../project/project" +import { Paths } from "../project/path" export namespace Permission { const log = Log.create({ service: "permission" }) @@ -37,7 +37,7 @@ export namespace Permission { } const state = State.create( - () => Project.use().id, + () => Paths.directory, () => { const pending: { [sessionID: string]: { diff --git a/packages/opencode/src/project/path.ts b/packages/opencode/src/project/path.ts index 47575935e..acbed3158 100644 --- a/packages/opencode/src/project/path.ts +++ b/packages/opencode/src/project/path.ts @@ -3,7 +3,7 @@ import { Context } from "../util/context" const context = Context.create<{ directory: string; worktree: string }>("path") export const Paths = { - provider: context.provide, + provide: context.provide, get directory() { return context.use().directory }, diff --git a/packages/opencode/src/project/state.ts b/packages/opencode/src/project/state.ts index 542c6b776..2ffef3b39 100644 --- a/packages/opencode/src/project/state.ts +++ b/packages/opencode/src/project/state.ts @@ -4,15 +4,20 @@ export namespace State { dispose?: (state: any) => Promise } - const entries = new Map() + const entries = new Map>() export function create(root: () => string, init: () => S, dispose?: (state: Awaited) => Promise) { return () => { const key = root() - const exists = entries.get(key) + let collection = entries.get(key) + if (!collection) { + collection = new Map() + entries.set(key, collection) + } + const exists = collection.get(init) if (exists) return exists.state as S const state = init() - entries.set(key, { + collection.set(init, { state, dispose, }) @@ -20,8 +25,8 @@ export namespace State { } } - export async function dispose() { - for (const [_, entry] of entries.entries()) { + export async function dispose(key: string) { + for (const [_, entry] of entries.get(key)?.entries() ?? []) { if (!entry.dispose) continue await entry.dispose(await entry.state) } diff --git a/packages/opencode/src/share/share.ts b/packages/opencode/src/share/share.ts index 2996e4d9b..b4f2efa8e 100644 --- a/packages/opencode/src/share/share.ts +++ b/packages/opencode/src/share/share.ts @@ -1,7 +1,7 @@ import { Bus } from "../bus" import { Installation } from "../installation" import { Session } from "../session" -import { Storage } from "../storage/storage" +import { StorageNext } from "../storage/storage-next" import { Log } from "../util/log" export namespace Share { @@ -46,8 +46,8 @@ export namespace Share { } export function init() { - Bus.subscribe(Storage.Event.Write, async (payload) => { - await sync(payload.properties.key, payload.properties.content) + Bus.subscribe(StorageNext.Event.Write, async (payload) => { + await sync(payload.properties.key.join("/"), payload.properties.content) }) } diff --git a/packages/opencode/src/storage/storage-next.ts b/packages/opencode/src/storage/storage-next.ts index 7992191a8..1955fe843 100644 --- a/packages/opencode/src/storage/storage-next.ts +++ b/packages/opencode/src/storage/storage-next.ts @@ -4,8 +4,14 @@ import fs from "fs/promises" import { Global } from "../global" import { lazy } from "../util/lazy" import { Lock } from "../util/lock" +import { Bus } from "../bus" +import z from "zod" export namespace StorageNext { + export const Event = { + Write: Bus.event("storage.write", z.object({ key: z.string().array(), content: z.any() })), + } + const log = Log.create({ service: "storage" }) type Migration = (dir: string) => Promise @@ -49,6 +55,7 @@ export namespace StorageNext { const content = await Bun.file(target).json() fn(content) await Bun.write(target, JSON.stringify(content, null, 2)) + Bus.publish(Event.Write, { key, content }) return content as T } @@ -57,6 +64,7 @@ export namespace StorageNext { const target = path.join(dir, ...key) + ".json" using _ = await Lock.write("storage") await Bun.write(target, JSON.stringify(content, null, 2)) + Bus.publish(Event.Write, { key, content }) } const glob = new Bun.Glob("**/*") diff --git a/packages/opencode/src/tool/todo.ts b/packages/opencode/src/tool/todo.ts index fa8351ced..686293ed6 100644 --- a/packages/opencode/src/tool/todo.ts +++ b/packages/opencode/src/tool/todo.ts @@ -2,7 +2,7 @@ import { z } from "zod" import { Tool } from "./tool" import DESCRIPTION_WRITE from "./todowrite.txt" import { State } from "../project/state" -import { Project } from "../project/project" +import { Paths } from "../project/path" const TodoInfo = z.object({ content: z.string().describe("Brief description of the task"), @@ -13,7 +13,7 @@ const TodoInfo = z.object({ type TodoInfo = z.infer const state = State.create( - () => Project.use().id, + () => Paths.directory, () => { const todos: { [sessionId: string]: TodoInfo[]