This commit is contained in:
Dax Raad 2025-08-06 23:15:06 -04:00
commit d45d5d664c
15 changed files with 71 additions and 43 deletions

View file

@ -26,7 +26,7 @@ export namespace Agent {
})
export type Info = z.infer<typeof Info>
const state = State.create(
() => Paths.use().directory,
() => Paths.directory,
async () => {
const cfg = await Config.get()
const result: Record<string, Info> = {

View file

@ -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
})
})
}

View file

@ -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<any, Subscription[]>()
const state = State.create(
() => Paths.directory,
() => {
const subscriptions = new Map<any, Subscription[]>()
return {
subscriptions,
}
},
)
export type EventDefinition = ReturnType<typeof event>
@ -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)

View file

@ -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),
)

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"
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 })

View file

@ -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 {}

View file

@ -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<string, boolean> = {}
const cfg = await Config.get()

View file

@ -56,7 +56,7 @@ export namespace LSP {
export type DocumentSymbol = z.infer<typeof DocumentSymbol>
const state = State.create(
() => Paths.use().worktree,
() => Paths.directory,
async () => {
const clients: LSPClient.Info[] = []
const servers: Record<string, LSPServer.Info> = LSPServer

View file

@ -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: {

View file

@ -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]: {

View file

@ -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
},

View file

@ -4,15 +4,20 @@ export namespace State {
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>) {
return () => {
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
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)
}

View file

@ -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)
})
}

View file

@ -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<void>
@ -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("**/*")

View file

@ -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<typeof TodoInfo>
const state = State.create(
() => Project.use().id,
() => Paths.directory,
() => {
const todos: {
[sessionId: string]: TodoInfo[]