This commit is contained in:
Dax Raad
2025-08-06 23:15:19 -04:00
parent 323ce486a5
commit d45d5d664c
15 changed files with 71 additions and 43 deletions
+1 -1
View File
@@ -26,7 +26,7 @@ export namespace Agent {
}) })
export type Info = z.infer<typeof Info> export type Info = z.infer<typeof Info>
const state = State.create( const state = State.create(
() => Paths.use().directory, () => Paths.directory,
async () => { async () => {
const cfg = await Config.get() const cfg = await Config.get()
const result: Record<string, Info> = { const result: Record<string, Info> = {
+4 -1
View File
@@ -8,6 +8,7 @@ import os from "os"
import { z } from "zod" import { z } from "zod"
import { Project } from "../project/project" import { Project } from "../project/project"
import { Paths } from "../project/path" import { Paths } from "../project/path"
import { State } from "../project/state"
export namespace App { export namespace App {
const log = Log.create({ service: "app" }) const log = Log.create({ service: "app" })
@@ -100,7 +101,7 @@ export namespace App {
return ctx.provide(app, async () => { return ctx.provide(app, async () => {
return Project.provide(project, async () => { return Project.provide(project, async () => {
return Paths.provide( const result = await Paths.provide(
{ {
worktree: app.info.path.root, worktree: app.info.path.root,
directory: app.info.path.cwd, directory: app.info.path.cwd,
@@ -118,6 +119,8 @@ export namespace App {
} }
}, },
) )
await State.dispose(app.info.path.cwd)
return result
}) })
}) })
} }
+14 -6
View File
@@ -1,11 +1,22 @@
import { z, type ZodType } from "zod" import { z, type ZodType } from "zod"
import { Log } from "../util/log" import { Log } from "../util/log"
import { State } from "../project/state"
import { Paths } from "../project/path"
export namespace Bus { export namespace Bus {
const log = Log.create({ service: "bus" }) const log = Log.create({ service: "bus" })
type Subscription = (event: any) => void type Subscription = (event: any) => void
const subscriptions = new Map<any, Subscription[]>() const state = State.create(
() => Paths.directory,
() => {
const subscriptions = new Map<any, Subscription[]>()
return {
subscriptions,
}
},
)
export type EventDefinition = ReturnType<typeof event> export type EventDefinition = ReturnType<typeof event>
@@ -30,10 +41,6 @@ export namespace Bus {
.object({ .object({
type: z.literal(type), type: z.literal(type),
properties: def.properties, properties: def.properties,
context: z.object({
projectID: z.string().optional(),
sessionID: z.string().optional(),
}),
}) })
.openapi({ .openapi({
ref: "Event" + "." + def.type, ref: "Event" + "." + def.type,
@@ -56,7 +63,7 @@ export namespace Bus {
}) })
const pending = [] const pending = []
for (const key of [def.type, "*"]) { for (const key of [def.type, "*"]) {
const match = subscriptions.get(key) const match = state().subscriptions.get(key)
for (const sub of match ?? []) { for (const sub of match ?? []) {
pending.push(sub(payload)) pending.push(sub(payload))
} }
@@ -89,6 +96,7 @@ export namespace Bus {
function raw(type: string, callback: (event: any) => void) { function raw(type: string, callback: (event: any) => void) {
log.info("subscribing", { type }) log.info("subscribing", { type })
const subscriptions = state().subscriptions
let match = subscriptions.get(type) ?? [] let match = subscriptions.get(type) ?? []
match.push(callback) match.push(callback)
subscriptions.set(type, match) subscriptions.set(type, match)
+5 -6
View File
@@ -20,13 +20,12 @@ export namespace Config {
const log = Log.create({ service: "config" }) const log = Log.create({ service: "config" })
export const state = State.create( export const state = State.create(
() => Paths.use().directory, () => Paths.directory,
async () => { async () => {
const paths = Paths.use()
const auth = await Auth.all() const auth = await Auth.all()
let result = await global() let result = await global()
for (const file of ["opencode.jsonc", "opencode.json"]) { 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()) { for (const resolved of found.toReversed()) {
result = mergeDeep(result, await loadFile(resolved)) result = mergeDeep(result, await loadFile(resolved))
} }
@@ -49,7 +48,7 @@ export namespace Config {
result.agent = result.agent || {} result.agent = result.agent || {}
const markdownAgents = [ const markdownAgents = [
...(await Filesystem.globUp("agent/*.md", Global.Path.config, Global.Path.config)), ...(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) { for (const item of markdownAgents) {
const content = await Bun.file(item).text() const content = await Bun.file(item).text()
@@ -75,7 +74,7 @@ export namespace Config {
result.mode = result.mode || {} result.mode = result.mode || {}
const markdownModes = [ const markdownModes = [
...(await Filesystem.globUp("mode/*.md", Global.Path.config, Global.Path.config)), ...(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) { for (const item of markdownModes) {
const content = await Bun.file(item).text() const content = await Bun.file(item).text()
@@ -101,7 +100,7 @@ export namespace Config {
result.plugin.push( result.plugin.push(
...[ ...[
...(await Filesystem.globUp("plugin/*.ts", Global.Path.config, Global.Path.config)), ...(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), ].map((x) => "file://" + x),
) )
+14 -10
View File
@@ -1,18 +1,22 @@
import { App } from "../app/app" import { Paths } from "../project/path"
import { State } from "../project/state"
import { Log } from "../util/log" import { Log } from "../util/log"
export namespace FileTime { export namespace FileTime {
const log = Log.create({ service: "file.time" }) const log = Log.create({ service: "file.time" })
export const state = App.state("tool.filetimes", () => { export const state = State.create(
const read: { () => Paths.directory,
[sessionID: string]: { () => {
[path: string]: Date | undefined const read: {
[sessionID: string]: {
[path: string]: Date | undefined
}
} = {}
return {
read,
} }
} = {} },
return { )
read,
}
})
export function read(sessionID: string, file: string) { export function read(sessionID: string, file: string) {
log.info("read", { sessionID, file }) log.info("read", { sessionID, file })
+3 -2
View File
@@ -5,6 +5,7 @@ import { App } from "../app/app"
import { Log } from "../util/log" import { Log } from "../util/log"
import { Flag } from "../flag/flag" import { Flag } from "../flag/flag"
import { Paths } from "../project/path" import { Paths } from "../project/path"
import { State } from "../project/state"
export namespace FileWatcher { export namespace FileWatcher {
const log = Log.create({ service: "file.watcher" }) const log = Log.create({ service: "file.watcher" })
@@ -18,8 +19,8 @@ export namespace FileWatcher {
}), }),
), ),
} }
const state = App.state( const state = State.create(
() => Paths.use().worktree, () => Paths.directory,
() => { () => {
const app = App.use() const app = App.use()
if (!app.info.git) return {} if (!app.info.git) return {}
+1 -1
View File
@@ -14,7 +14,7 @@ export namespace Format {
const log = Log.create({ service: "format" }) const log = Log.create({ service: "format" })
const state = State.create( const state = State.create(
() => Paths.use().directory, () => Paths.directory,
async () => { async () => {
const enabled: Record<string, boolean> = {} const enabled: Record<string, boolean> = {}
const cfg = await Config.get() const cfg = await Config.get()
+1 -1
View File
@@ -56,7 +56,7 @@ export namespace LSP {
export type DocumentSymbol = z.infer<typeof DocumentSymbol> export type DocumentSymbol = z.infer<typeof DocumentSymbol>
const state = State.create( const state = State.create(
() => Paths.use().worktree, () => Paths.directory,
async () => { async () => {
const clients: LSPClient.Info[] = [] const clients: LSPClient.Info[] = []
const servers: Record<string, LSPServer.Info> = LSPServer const servers: Record<string, LSPServer.Info> = LSPServer
+2 -2
View File
@@ -2,7 +2,6 @@ import { experimental_createMCPClient, type Tool } from "ai"
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js" import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js" import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js" import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
import { App } from "../app/app"
import { Config } from "../config/config" import { Config } from "../config/config"
import { Log } from "../util/log" import { Log } from "../util/log"
import { NamedError } from "../util/error" import { NamedError } from "../util/error"
@@ -10,6 +9,7 @@ import { z } from "zod"
import { Session } from "../session" import { Session } from "../session"
import { Bus } from "../bus" import { Bus } from "../bus"
import { State } from "../project/state" import { State } from "../project/state"
import { Paths } from "../project/path"
export namespace MCP { export namespace MCP {
const log = Log.create({ service: "mcp" }) const log = Log.create({ service: "mcp" })
@@ -22,7 +22,7 @@ export namespace MCP {
) )
const state = State.create( const state = State.create(
"mcp", () => Paths.directory,
async () => { async () => {
const cfg = await Config.get() const cfg = await Config.get()
const clients: { const clients: {
+2 -2
View File
@@ -4,7 +4,7 @@ import { Log } from "../util/log"
import { Identifier } from "../id/id" import { Identifier } from "../id/id"
import { Plugin } from "../plugin" import { Plugin } from "../plugin"
import { State } from "../project/state" import { State } from "../project/state"
import { Project } from "../project/project" import { Paths } from "../project/path"
export namespace Permission { export namespace Permission {
const log = Log.create({ service: "permission" }) const log = Log.create({ service: "permission" })
@@ -37,7 +37,7 @@ export namespace Permission {
} }
const state = State.create( const state = State.create(
() => Project.use().id, () => Paths.directory,
() => { () => {
const pending: { const pending: {
[sessionID: string]: { [sessionID: string]: {
+1 -1
View File
@@ -3,7 +3,7 @@ import { Context } from "../util/context"
const context = Context.create<{ directory: string; worktree: string }>("path") const context = Context.create<{ directory: string; worktree: string }>("path")
export const Paths = { export const Paths = {
provider: context.provide, provide: context.provide,
get directory() { get directory() {
return context.use().directory return context.use().directory
}, },
+10 -5
View File
@@ -4,15 +4,20 @@ export namespace State {
dispose?: (state: any) => Promise<void> dispose?: (state: any) => Promise<void>
} }
const entries = new Map<string, Entry>() const entries = new Map<string, Map<any, Entry>>()
export function create<S>(root: () => string, init: () => S, dispose?: (state: Awaited<S>) => Promise<void>) { export function create<S>(root: () => string, init: () => S, dispose?: (state: Awaited<S>) => Promise<void>) {
return () => { return () => {
const key = root() const key = root()
const exists = entries.get(key) let collection = entries.get(key)
if (!collection) {
collection = new Map<string, Entry>()
entries.set(key, collection)
}
const exists = collection.get(init)
if (exists) return exists.state as S if (exists) return exists.state as S
const state = init() const state = init()
entries.set(key, { collection.set(init, {
state, state,
dispose, dispose,
}) })
@@ -20,8 +25,8 @@ export namespace State {
} }
} }
export async function dispose() { export async function dispose(key: string) {
for (const [_, entry] of entries.entries()) { for (const [_, entry] of entries.get(key)?.entries() ?? []) {
if (!entry.dispose) continue if (!entry.dispose) continue
await entry.dispose(await entry.state) await entry.dispose(await entry.state)
} }
+3 -3
View File
@@ -1,7 +1,7 @@
import { Bus } from "../bus" import { Bus } from "../bus"
import { Installation } from "../installation" import { Installation } from "../installation"
import { Session } from "../session" import { Session } from "../session"
import { Storage } from "../storage/storage" import { StorageNext } from "../storage/storage-next"
import { Log } from "../util/log" import { Log } from "../util/log"
export namespace Share { export namespace Share {
@@ -46,8 +46,8 @@ export namespace Share {
} }
export function init() { export function init() {
Bus.subscribe(Storage.Event.Write, async (payload) => { Bus.subscribe(StorageNext.Event.Write, async (payload) => {
await sync(payload.properties.key, payload.properties.content) await sync(payload.properties.key.join("/"), payload.properties.content)
}) })
} }
@@ -4,8 +4,14 @@ import fs from "fs/promises"
import { Global } from "../global" import { Global } from "../global"
import { lazy } from "../util/lazy" import { lazy } from "../util/lazy"
import { Lock } from "../util/lock" import { Lock } from "../util/lock"
import { Bus } from "../bus"
import z from "zod"
export namespace StorageNext { 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" }) const log = Log.create({ service: "storage" })
type Migration = (dir: string) => Promise<void> type Migration = (dir: string) => Promise<void>
@@ -49,6 +55,7 @@ export namespace StorageNext {
const content = await Bun.file(target).json() const content = await Bun.file(target).json()
fn(content) fn(content)
await Bun.write(target, JSON.stringify(content, null, 2)) await Bun.write(target, JSON.stringify(content, null, 2))
Bus.publish(Event.Write, { key, content })
return content as T return content as T
} }
@@ -57,6 +64,7 @@ export namespace StorageNext {
const target = path.join(dir, ...key) + ".json" const target = path.join(dir, ...key) + ".json"
using _ = await Lock.write("storage") using _ = await Lock.write("storage")
await Bun.write(target, JSON.stringify(content, null, 2)) await Bun.write(target, JSON.stringify(content, null, 2))
Bus.publish(Event.Write, { key, content })
} }
const glob = new Bun.Glob("**/*") const glob = new Bun.Glob("**/*")
+2 -2
View File
@@ -2,7 +2,7 @@ import { z } from "zod"
import { Tool } from "./tool" import { Tool } from "./tool"
import DESCRIPTION_WRITE from "./todowrite.txt" import DESCRIPTION_WRITE from "./todowrite.txt"
import { State } from "../project/state" import { State } from "../project/state"
import { Project } from "../project/project" import { Paths } from "../project/path"
const TodoInfo = z.object({ const TodoInfo = z.object({
content: z.string().describe("Brief description of the task"), content: z.string().describe("Brief description of the task"),
@@ -13,7 +13,7 @@ const TodoInfo = z.object({
type TodoInfo = z.infer<typeof TodoInfo> type TodoInfo = z.infer<typeof TodoInfo>
const state = State.create( const state = State.create(
() => Project.use().id, () => Paths.directory,
() => { () => {
const todos: { const todos: {
[sessionId: string]: TodoInfo[] [sessionId: string]: TodoInfo[]