mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-04-05 00:23:10 +00:00
Refactor to support multiple instances inside single opencode process (#2360)
This release has a bunch of minor breaking changes if you are using opencode plugins or sdk 1. storage events have been removed (we might bring this back but had some issues) 2. concept of `app` is gone - there is a new concept called `project` and endpoints to list projects and get the current project 3. plugin receives `directory` which is cwd and `worktree` which is where the root of the project is if it's a git repo 4. the session.chat function has been renamed to session.prompt in sdk. it no longer requires model to be passed in (model is now an object) 5. every endpoint takes an optional `directory` parameter to operate as though opencode is running in that directory
This commit is contained in:
@@ -3,10 +3,10 @@ import { Bus } from "../bus"
|
||||
import { $ } from "bun"
|
||||
import { createPatch } from "diff"
|
||||
import path from "path"
|
||||
import { App } from "../app/app"
|
||||
import fs from "fs"
|
||||
import ignore from "ignore"
|
||||
import { Log } from "../util/log"
|
||||
import { Instance } from "../project/instance"
|
||||
|
||||
export namespace File {
|
||||
const log = Log.create({ service: "file" })
|
||||
@@ -46,10 +46,10 @@ export namespace File {
|
||||
}
|
||||
|
||||
export async function status() {
|
||||
const app = App.info()
|
||||
if (!app.git) return []
|
||||
const project = Instance.project
|
||||
if (project.vcs !== "git") return []
|
||||
|
||||
const diffOutput = await $`git diff --numstat HEAD`.cwd(app.path.cwd).quiet().nothrow().text()
|
||||
const diffOutput = await $`git diff --numstat HEAD`.cwd(Instance.directory).quiet().nothrow().text()
|
||||
|
||||
const changedFiles: Info[] = []
|
||||
|
||||
@@ -66,13 +66,17 @@ export namespace File {
|
||||
}
|
||||
}
|
||||
|
||||
const untrackedOutput = await $`git ls-files --others --exclude-standard`.cwd(app.path.cwd).quiet().nothrow().text()
|
||||
const untrackedOutput = await $`git ls-files --others --exclude-standard`
|
||||
.cwd(Instance.directory)
|
||||
.quiet()
|
||||
.nothrow()
|
||||
.text()
|
||||
|
||||
if (untrackedOutput.trim()) {
|
||||
const untrackedFiles = untrackedOutput.trim().split("\n")
|
||||
for (const filepath of untrackedFiles) {
|
||||
try {
|
||||
const content = await Bun.file(path.join(app.path.root, filepath)).text()
|
||||
const content = await Bun.file(path.join(Instance.worktree, filepath)).text()
|
||||
const lines = content.split("\n").length
|
||||
changedFiles.push({
|
||||
path: filepath,
|
||||
@@ -87,7 +91,11 @@ export namespace File {
|
||||
}
|
||||
|
||||
// Get deleted files
|
||||
const deletedOutput = await $`git diff --name-only --diff-filter=D HEAD`.cwd(app.path.cwd).quiet().nothrow().text()
|
||||
const deletedOutput = await $`git diff --name-only --diff-filter=D HEAD`
|
||||
.cwd(Instance.directory)
|
||||
.quiet()
|
||||
.nothrow()
|
||||
.text()
|
||||
|
||||
if (deletedOutput.trim()) {
|
||||
const deletedFiles = deletedOutput.trim().split("\n")
|
||||
@@ -103,23 +111,23 @@ export namespace File {
|
||||
|
||||
return changedFiles.map((x) => ({
|
||||
...x,
|
||||
path: path.relative(app.path.cwd, path.join(app.path.root, x.path)),
|
||||
path: path.relative(Instance.directory, path.join(Instance.worktree, x.path)),
|
||||
}))
|
||||
}
|
||||
|
||||
export async function read(file: string) {
|
||||
using _ = log.time("read", { file })
|
||||
const app = App.info()
|
||||
const full = path.join(app.path.cwd, file)
|
||||
const project = Instance.project
|
||||
const full = path.join(Instance.directory, file)
|
||||
const content = await Bun.file(full)
|
||||
.text()
|
||||
.catch(() => "")
|
||||
.then((x) => x.trim())
|
||||
if (app.git) {
|
||||
const rel = path.relative(app.path.root, full)
|
||||
const diff = await $`git diff ${rel}`.cwd(app.path.root).quiet().nothrow().text()
|
||||
if (project.vcs === "git") {
|
||||
const rel = path.relative(Instance.worktree, full)
|
||||
const diff = await $`git diff ${rel}`.cwd(Instance.worktree).quiet().nothrow().text()
|
||||
if (diff.trim()) {
|
||||
const original = await $`git show HEAD:${rel}`.cwd(app.path.root).quiet().nothrow().text()
|
||||
const original = await $`git show HEAD:${rel}`.cwd(Instance.worktree).quiet().nothrow().text()
|
||||
const patch = createPatch(file, original, content, "old", "new", {
|
||||
context: Infinity,
|
||||
})
|
||||
@@ -131,22 +139,22 @@ export namespace File {
|
||||
|
||||
export async function list(dir?: string) {
|
||||
const exclude = [".git", ".DS_Store"]
|
||||
const app = App.info()
|
||||
const project = Instance.project
|
||||
let ignored = (_: string) => false
|
||||
if (app.git) {
|
||||
const gitignore = Bun.file(path.join(app.path.root, ".gitignore"))
|
||||
if (project.vcs === "git") {
|
||||
const gitignore = Bun.file(path.join(Instance.worktree, ".gitignore"))
|
||||
if (await gitignore.exists()) {
|
||||
const ig = ignore().add(await gitignore.text())
|
||||
ignored = ig.ignores.bind(ig)
|
||||
}
|
||||
}
|
||||
const resolved = dir ? path.join(app.path.cwd, dir) : app.path.cwd
|
||||
const resolved = dir ? path.join(Instance.directory, dir) : Instance.directory
|
||||
const nodes: Node[] = []
|
||||
for (const entry of await fs.promises.readdir(resolved, { withFileTypes: true })) {
|
||||
if (exclude.includes(entry.name)) continue
|
||||
const fullPath = path.join(resolved, entry.name)
|
||||
const relativePath = path.relative(app.path.cwd, fullPath)
|
||||
const relativeToRoot = path.relative(app.path.root, fullPath)
|
||||
const relativePath = path.relative(Instance.directory, fullPath)
|
||||
const relativeToRoot = path.relative(Instance.worktree, fullPath)
|
||||
const type = entry.isDirectory() ? "directory" : "file"
|
||||
nodes.push({
|
||||
name: entry.name,
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import { App } from "../app/app"
|
||||
import { Instance } from "../project/instance"
|
||||
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 = Instance.state(
|
||||
() => {
|
||||
const read: {
|
||||
[sessionID: string]: {
|
||||
[path: string]: Date | undefined
|
||||
}
|
||||
} = {}
|
||||
return {
|
||||
read,
|
||||
}
|
||||
} = {}
|
||||
return {
|
||||
read,
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
export function read(sessionID: string, file: string) {
|
||||
log.info("read", { sessionID, file })
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { z } from "zod"
|
||||
import { Bus } from "../bus"
|
||||
import fs from "fs"
|
||||
import { App } from "../app/app"
|
||||
import { Log } from "../util/log"
|
||||
import { Flag } from "../flag/flag"
|
||||
import { Instance } from "../project/instance"
|
||||
|
||||
export namespace FileWatcher {
|
||||
const log = Log.create({ service: "file.watcher" })
|
||||
@@ -17,22 +17,16 @@ export namespace FileWatcher {
|
||||
}),
|
||||
),
|
||||
}
|
||||
const state = App.state(
|
||||
"file.watcher",
|
||||
const state = Instance.state(
|
||||
() => {
|
||||
const app = App.use()
|
||||
if (!app.info.git) return {}
|
||||
if (Instance.project.vcs !== "git") return {}
|
||||
try {
|
||||
const watcher = fs.watch(app.info.path.cwd, { recursive: true }, (event, file) => {
|
||||
const watcher = fs.watch(Instance.directory, { recursive: true }, (event, file) => {
|
||||
log.info("change", { file, event })
|
||||
if (!file) return
|
||||
// for some reason async local storage is lost here
|
||||
// https://github.com/oven-sh/bun/issues/20754
|
||||
App.provideExisting(app, async () => {
|
||||
Bus.publish(Event.Updated, {
|
||||
file,
|
||||
event,
|
||||
})
|
||||
Bus.publish(Event.Updated, {
|
||||
file,
|
||||
event,
|
||||
})
|
||||
})
|
||||
return { watcher }
|
||||
|
||||
Reference in New Issue
Block a user