From 1fb54efdd2fc669aad24023e1785d7f96ef5704a Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Thu, 7 Aug 2025 00:41:50 -0400 Subject: [PATCH] sync --- packages/opencode/src/cli/bootstrap.ts | 1 - .../opencode/src/cli/cmd/debug/ripgrep.ts | 8 ++-- packages/opencode/src/cli/cmd/github.ts | 8 ++-- packages/opencode/src/file/index.ts | 37 ++++++++++++------- packages/opencode/src/format/formatter.ts | 16 +++----- packages/opencode/src/format/index.ts | 3 +- packages/opencode/src/lsp/client.ts | 7 ++-- packages/opencode/src/lsp/index.ts | 4 +- packages/opencode/src/lsp/server.ts | 34 ++++++++--------- packages/opencode/src/project/project.ts | 19 +++++++++- packages/opencode/src/server/server.ts | 7 ++-- packages/opencode/src/session/index.ts | 22 +++++------ packages/opencode/src/session/system.ts | 18 ++++----- packages/opencode/src/snapshot/index.ts | 31 ++++++++-------- packages/opencode/src/tool/bash.ts | 9 ++--- packages/opencode/src/tool/edit.ts | 9 ++--- packages/opencode/src/tool/glob.ts | 9 ++--- packages/opencode/src/tool/grep.ts | 5 +-- packages/opencode/src/tool/ls.ts | 7 ++-- packages/opencode/src/tool/lsp-diagnostics.ts | 7 ++-- packages/opencode/src/tool/lsp-hover.ts | 7 ++-- packages/opencode/src/tool/multiedit.ts | 4 +- packages/opencode/src/tool/read.ts | 6 +-- packages/opencode/src/tool/write.ts | 9 ++--- .../tui/internal/components/chat/message.go | 4 +- 25 files changed, 149 insertions(+), 142 deletions(-) diff --git a/packages/opencode/src/cli/bootstrap.ts b/packages/opencode/src/cli/bootstrap.ts index d26694d11f..c3578ab830 100644 --- a/packages/opencode/src/cli/bootstrap.ts +++ b/packages/opencode/src/cli/bootstrap.ts @@ -12,7 +12,6 @@ export async function bootstrap(input: App.Input, cb: (app: App.Info) => Prom Plugin.init() LSP.init() Snapshot.init() - return cb(app) }) } diff --git a/packages/opencode/src/cli/cmd/debug/ripgrep.ts b/packages/opencode/src/cli/cmd/debug/ripgrep.ts index b8005c9088..40567cdbf2 100644 --- a/packages/opencode/src/cli/cmd/debug/ripgrep.ts +++ b/packages/opencode/src/cli/cmd/debug/ripgrep.ts @@ -1,5 +1,5 @@ -import { App } from "../../../app/app" import { Ripgrep } from "../../../file/ripgrep" +import { Paths } from "../../../project/path" import { bootstrap } from "../../bootstrap" import { cmd } from "../cmd" @@ -17,8 +17,7 @@ const TreeCommand = cmd({ }), async handler(args) { await bootstrap({ cwd: process.cwd() }, async () => { - const app = App.info() - console.log(await Ripgrep.tree({ cwd: app.path.cwd, limit: args.limit })) + console.log(await Ripgrep.tree({ cwd: Paths.directory, limit: args.limit })) }) }, }) @@ -41,9 +40,8 @@ const FilesCommand = cmd({ }), async handler(args) { await bootstrap({ cwd: process.cwd() }, async () => { - const app = App.info() const files = await Ripgrep.files({ - cwd: app.path.cwd, + cwd: Paths.directory, query: args.query, glob: args.glob ? [args.glob] : undefined, limit: args.limit, diff --git a/packages/opencode/src/cli/cmd/github.ts b/packages/opencode/src/cli/cmd/github.ts index f33cb3ec73..ca92830e07 100644 --- a/packages/opencode/src/cli/cmd/github.ts +++ b/packages/opencode/src/cli/cmd/github.ts @@ -19,6 +19,8 @@ import { Identifier } from "../../id/id" import { Provider } from "../../provider/provider" import { Bus } from "../../bus" import { MessageV2 } from "../../session/message-v2" +import { Project } from "../../project/project" +import { Paths } from "../../project/path" type GitHubAuthor = { login: string @@ -178,8 +180,8 @@ export const GithubInstallCommand = cmd({ } async function getAppInfo() { - const app = App.info() - if (!app.git) { + const project = Project.use() + if (project.vcs !== "git") { prompts.log.error(`Could not find git repository. Please run this command from a git repository.`) throw new UI.CancelledError() } @@ -195,7 +197,7 @@ export const GithubInstallCommand = cmd({ throw new UI.CancelledError() } const [owner, repo] = parsed[1].split("/") - return { owner, repo, root: app.path.root } + return { owner, repo, root: Paths.worktree } } async function promptProvider() { diff --git a/packages/opencode/src/file/index.ts b/packages/opencode/src/file/index.ts index b99f35e1fb..bc11c1ca02 100644 --- a/packages/opencode/src/file/index.ts +++ b/packages/opencode/src/file/index.ts @@ -4,9 +4,10 @@ import { $ } from "bun" import { createPatch } from "diff" import path from "path" import * as git from "isomorphic-git" -import { App } from "../app/app" import fs from "fs" import { Log } from "../util/log" +import { Paths } from "../project/path" +import { Project } from "../project/project" export namespace File { const log = Log.create({ service: "file" }) @@ -34,10 +35,10 @@ export namespace File { } export async function status() { - const app = App.info() - if (!app.git) return [] + const project = Project.use() + 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(Paths.directory).quiet().nothrow().text() const changedFiles: Info[] = [] @@ -54,13 +55,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(Paths.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(Paths.worktree, filepath)).text() const lines = content.split("\n").length changedFiles.push({ path: filepath, @@ -75,7 +80,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(Paths.directory) + .quiet() + .nothrow() + .text() if (deletedOutput.trim()) { const deletedFiles = deletedOutput.trim().split("\n") @@ -91,27 +100,27 @@ export namespace File { return changedFiles.map((x) => ({ ...x, - path: path.relative(app.path.cwd, path.join(app.path.root, x.path)), + path: path.relative(Paths.directory, path.join(Paths.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 = Project.use() + const full = path.join(Paths.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) + if (project.vcs === "git") { + const rel = path.relative(Paths.worktree, full) const diff = await git.status({ fs, - dir: app.path.root, + dir: Paths.worktree, filepath: rel, }) if (diff !== "unmodified") { - const original = await $`git show HEAD:${rel}`.cwd(app.path.root).quiet().nothrow().text() + const original = await $`git show HEAD:${rel}`.cwd(Paths.worktree).quiet().nothrow().text() const patch = createPatch(file, original, content, "old", "new", { context: Infinity, }) diff --git a/packages/opencode/src/format/formatter.ts b/packages/opencode/src/format/formatter.ts index 8a8bbc9aab..f7023d4bc2 100644 --- a/packages/opencode/src/format/formatter.ts +++ b/packages/opencode/src/format/formatter.ts @@ -1,5 +1,5 @@ -import { App } from "../app/app" import { BunProc } from "../bun" +import { Paths } from "../project/path" import { Filesystem } from "../util/filesystem" export interface Info { @@ -63,8 +63,7 @@ export const prettier: Info = { ".gql", ], async enabled() { - const app = App.info() - const items = await Filesystem.findUp("package.json", app.path.cwd, app.path.root) + const items = await Filesystem.findUp("package.json", Paths.directory, Paths.worktree) for (const item of items) { const json = await Bun.file(item).json() if (json.dependencies?.prettier) return true @@ -109,8 +108,7 @@ export const biome: Info = { ".gql", ], async enabled() { - const app = App.info() - const items = await Filesystem.findUp("biome.json", app.path.cwd, app.path.root) + const items = await Filesystem.findUp("biome.json", Paths.directory, Paths.worktree) return items.length > 0 }, } @@ -129,8 +127,7 @@ export const clang: Info = { command: ["clang-format", "-i", "$FILE"], extensions: [".c", ".cc", ".cpp", ".cxx", ".c++", ".h", ".hh", ".hpp", ".hxx", ".h++", ".ino", ".C", ".H"], async enabled() { - const app = App.info() - const items = await Filesystem.findUp(".clang-format", app.path.cwd, app.path.root) + const items = await Filesystem.findUp(".clang-format", Paths.directory, Paths.worktree) return items.length > 0 }, } @@ -150,10 +147,9 @@ export const ruff: Info = { extensions: [".py", ".pyi"], async enabled() { if (!Bun.which("ruff")) return false - const app = App.info() const configs = ["pyproject.toml", "ruff.toml", ".ruff.toml"] for (const config of configs) { - const found = await Filesystem.findUp(config, app.path.cwd, app.path.root) + const found = await Filesystem.findUp(config, Paths.directory, Paths.worktree) if (found.length > 0) { if (config === "pyproject.toml") { const content = await Bun.file(found[0]).text() @@ -165,7 +161,7 @@ export const ruff: Info = { } const deps = ["requirements.txt", "pyproject.toml", "Pipfile"] for (const dep of deps) { - const found = await Filesystem.findUp(dep, app.path.cwd, app.path.root) + const found = await Filesystem.findUp(dep, Paths.directory, Paths.worktree) if (found.length > 0) { const content = await Bun.file(found[0]).text() if (content.includes("ruff")) return true diff --git a/packages/opencode/src/format/index.ts b/packages/opencode/src/format/index.ts index 01be356734..1fe8a4ccd7 100644 --- a/packages/opencode/src/format/index.ts +++ b/packages/opencode/src/format/index.ts @@ -1,4 +1,3 @@ -import { App } from "../app/app" import { Bus } from "../bus" import { File } from "../file" import { Log } from "../util/log" @@ -75,7 +74,7 @@ export namespace Format { log.info("running", { command: item.command }) const proc = Bun.spawn({ cmd: item.command.map((x) => x.replace("$FILE", file)), - cwd: App.info().path.cwd, + cwd: Paths.directory, env: item.environment, stdout: "ignore", stderr: "ignore", diff --git a/packages/opencode/src/lsp/client.ts b/packages/opencode/src/lsp/client.ts index c63e02592e..e17f98d41a 100644 --- a/packages/opencode/src/lsp/client.ts +++ b/packages/opencode/src/lsp/client.ts @@ -1,7 +1,6 @@ import path from "path" import { createMessageConnection, StreamMessageReader, StreamMessageWriter } from "vscode-jsonrpc/node" import type { Diagnostic as VSCodeDiagnostic } from "vscode-languageserver-types" -import { App } from "../app/app" import { Log } from "../util/log" import { LANGUAGE_EXTENSIONS } from "./language" import { Bus } from "../bus" @@ -9,6 +8,7 @@ import z from "zod" import type { LSPServer } from "./server" import { NamedError } from "../util/error" import { withTimeout } from "../util/timeout" +import { Paths } from "../project/path" export namespace LSPClient { const log = Log.create({ service: "lsp.client" }) @@ -35,7 +35,6 @@ export namespace LSPClient { } export async function create(input: { serverID: string; server: LSPServer.Handle; root: string }) { - const app = App.info() const l = log.clone().tag("serverID", input.serverID) l.info("starting client") @@ -123,7 +122,7 @@ export namespace LSPClient { }, notify: { async open(input: { path: string }) { - input.path = path.isAbsolute(input.path) ? input.path : path.resolve(app.path.cwd, input.path) + input.path = path.isAbsolute(input.path) ? input.path : path.resolve(Paths.directory, input.path) const file = Bun.file(input.path) const text = await file.text() const version = files[input.path] @@ -155,7 +154,7 @@ export namespace LSPClient { return diagnostics }, async waitForDiagnostics(input: { path: string }) { - input.path = path.isAbsolute(input.path) ? input.path : path.resolve(app.path.cwd, input.path) + input.path = path.isAbsolute(input.path) ? input.path : path.resolve(Paths.directory, input.path) log.info("waiting for diagnostics", input) let unsub: () => void return await withTimeout( diff --git a/packages/opencode/src/lsp/index.ts b/packages/opencode/src/lsp/index.ts index 961b1e5d44..2b1a2d1b79 100644 --- a/packages/opencode/src/lsp/index.ts +++ b/packages/opencode/src/lsp/index.ts @@ -108,7 +108,7 @@ export namespace LSP { const result: LSPClient.Info[] = [] for (const server of Object.values(LSPServer)) { if (server.extensions.length && !server.extensions.includes(extension)) continue - const root = await server.root(file, App.info()) + const root = await server.root(file) if (!root) continue if (s.broken.has(root + server.id)) continue @@ -117,7 +117,7 @@ export namespace LSP { result.push(match) continue } - const handle = await server.spawn(App.info(), root) + const handle = await server.spawn(root) if (!handle) continue const client = await LSPClient.create({ serverID: server.id, diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts index f4648f0c23..f781e1fe76 100644 --- a/packages/opencode/src/lsp/server.ts +++ b/packages/opencode/src/lsp/server.ts @@ -1,5 +1,4 @@ import { spawn, type ChildProcessWithoutNullStreams } from "child_process" -import type { App } from "../app/app" import path from "path" import { Global } from "../global" import { Log } from "../util/log" @@ -7,6 +6,7 @@ import { BunProc } from "../bun" import { $ } from "bun" import fs from "fs/promises" import { Filesystem } from "../util/filesystem" +import { Paths } from "../project/path" export namespace LSPServer { const log = Log.create({ service: "lsp.server" }) @@ -16,18 +16,18 @@ export namespace LSPServer { initialization?: Record } - type RootFunction = (file: string, app: App.Info) => Promise + type RootFunction = (file: string) => Promise const NearestRoot = (patterns: string[]): RootFunction => { - return async (file, app) => { + return async (file) => { const files = Filesystem.up({ targets: patterns, start: path.dirname(file), - stop: app.path.root, + stop: Paths.worktree, }) const first = await files.next() await files.return() - if (!first.value) return app.path.root + if (!first.value) return Paths.worktree return path.dirname(first.value) } } @@ -37,15 +37,15 @@ export namespace LSPServer { extensions: string[] global?: boolean root: RootFunction - spawn(app: App.Info, root: string): Promise + spawn(root: string): Promise } export const Typescript: Info = { id: "typescript", root: NearestRoot(["tsconfig.json", "package.json", "jsconfig.json"]), extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"], - async spawn(app, root) { - const tsserver = await Bun.resolve("typescript/lib/tsserver.js", app.path.cwd).catch(() => {}) + async spawn(root) { + const tsserver = await Bun.resolve("typescript/lib/tsserver.js", Paths.directory).catch(() => {}) if (!tsserver) return const proc = spawn(BunProc.which(), ["x", "typescript-language-server", "--stdio"], { cwd: root, @@ -67,13 +67,13 @@ export namespace LSPServer { export const Gopls: Info = { id: "golang", - root: async (file, app) => { - const work = await NearestRoot(["go.work"])(file, app) + root: async (file) => { + const work = await NearestRoot(["go.work"])(file) if (work) return work - return NearestRoot(["go.mod", "go.sum"])(file, app) + return NearestRoot(["go.mod", "go.sum"])(file) }, extensions: [".go"], - async spawn(_, root) { + async spawn(root) { let bin = Bun.which("gopls", { PATH: process.env["PATH"] + ":" + Global.Path.bin, }) @@ -109,7 +109,7 @@ export namespace LSPServer { id: "ruby-lsp", root: NearestRoot(["Gemfile"]), extensions: [".rb", ".rake", ".gemspec", ".ru"], - async spawn(_, root) { + async spawn(root) { let bin = Bun.which("ruby-lsp", { PATH: process.env["PATH"] + ":" + Global.Path.bin, }) @@ -149,7 +149,7 @@ export namespace LSPServer { id: "pyright", extensions: [".py", ".pyi"], root: NearestRoot(["pyproject.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile", "pyrightconfig.json"]), - async spawn(_, root) { + async spawn(root) { const proc = spawn(BunProc.which(), ["x", "pyright-langserver", "--stdio"], { cwd: root, env: { @@ -167,7 +167,7 @@ export namespace LSPServer { id: "elixir-ls", extensions: [".ex", ".exs"], root: NearestRoot(["mix.exs", "mix.lock"]), - async spawn(_, root) { + async spawn(root) { let binary = Bun.which("elixir-ls") if (!binary) { const elixirLsPath = path.join(Global.Path.bin, "elixir-ls") @@ -222,7 +222,7 @@ export namespace LSPServer { id: "zls", extensions: [".zig", ".zon"], root: NearestRoot(["build.zig"]), - async spawn(_, root) { + async spawn(root) { let bin = Bun.which("zls", { PATH: process.env["PATH"] + ":" + Global.Path.bin, }) @@ -327,7 +327,7 @@ export namespace LSPServer { id: "csharp", root: NearestRoot([".sln", ".csproj", "global.json"]), extensions: [".cs"], - async spawn(_, root) { + async spawn(root) { let bin = Bun.which("csharp-ls", { PATH: process.env["PATH"] + ":" + Global.Path.bin, }) diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index b3cf55eca3..e3652db138 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -11,6 +11,10 @@ export namespace Project { id: z.string(), worktree: z.string(), vcs: z.literal("git").optional(), + time: z.object({ + created: z.number(), + initialized: z.number().optional(), + }), }) export type Info = z.infer @@ -25,9 +29,12 @@ export namespace Project { const git = await matches.next().then((x) => x.value) await matches.return() if (!git) { - await StorageNext.write(["project", "global"], { + await StorageNext.write(["project", "global"], { id: "global", worktree: "/", + time: { + created: Date.now(), + }, }) return } @@ -56,9 +63,19 @@ export namespace Project { id, worktree, vcs: "git", + time: { + created: Date.now(), + }, }) }) + export async function setInitialized() { + const project = use() + await StorageNext.update(["project", project.id], (draft) => { + draft.time.initialized = Date.now() + }) + } + export async function list() { await init() const keys = await StorageNext.list(["project"]) diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 7e6a4c145f..d6cd940f36 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -20,6 +20,7 @@ import { Mode } from "../session/mode" import { callTui, TuiRoute } from "./tui" import { Permission } from "../permission" import { lazy } from "../util/lazy" +import { Paths } from "../project/path" const ERRORS = { 400: { @@ -692,10 +693,9 @@ export namespace Server { }), ), async (c) => { - const app = App.info() const pattern = c.req.valid("query").pattern const result = await Ripgrep.search({ - cwd: app.path.cwd, + cwd: Paths.directory, pattern, limit: 10, }) @@ -726,9 +726,8 @@ export namespace Server { ), async (c) => { const query = c.req.valid("query").query - const app = App.info() const result = await Ripgrep.files({ - cwd: app.path.cwd, + cwd: Paths.directory, query, limit: 10, }) diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index 38d8ef6ff8..265cbaa4c1 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -161,10 +161,9 @@ export namespace Session { ) export async function create(parentID?: string) { - const app = App.info() return createNext({ parentID, - directory: app.path.cwd, + directory: Paths.directory, }) } @@ -443,7 +442,6 @@ export namespace Session { }, } - const app = App.info() const userParts = await Promise.all( input.parts.map(async (part): Promise => { if (part.type === "file") { @@ -714,8 +712,8 @@ export namespace Session { system, mode: inputMode, path: { - cwd: app.path.cwd, - root: app.path.root, + cwd: Paths.directory, + root: Paths.worktree, }, cost: 0, tokens: { @@ -869,8 +867,8 @@ export namespace Session { role: "assistant", system, path: { - cwd: app.path.cwd, - root: app.path.root, + cwd: Paths.directory, + root: Paths.worktree, }, cost: 0, tokens: { @@ -1266,7 +1264,6 @@ export namespace Session { const lastSummary = msgs.findLast((msg) => msg.info.role === "assistant" && msg.info.summary === true) const filtered = msgs.filter((msg) => !lastSummary || msg.info.id >= lastSummary.info.id) const model = await Provider.getModel(input.providerID, input.modelID) - const app = App.info() const system = [ ...SystemPrompt.summarize(input.providerID), ...(await SystemPrompt.environment()), @@ -1280,8 +1277,8 @@ export namespace Session { system, mode: "build", path: { - cwd: app.path.cwd, - root: app.path.root, + cwd: Paths.directory, + root: Paths.worktree, }, summary: true, cost: 0, @@ -1395,7 +1392,6 @@ export namespace Session { providerID: string messageID: string }) { - const app = App.info() await Session.chat({ sessionID: input.sessionID, messageID: input.messageID, @@ -1405,10 +1401,10 @@ export namespace Session { { id: Identifier.ascending("part"), type: "text", - text: PROMPT_INITIALIZE.replace("${path}", app.path.root), + text: PROMPT_INITIALIZE.replace("${path}", Paths.worktree), }, ], }) - await App.initialize() + await Project.setInitialized() } } diff --git a/packages/opencode/src/session/system.ts b/packages/opencode/src/session/system.ts index a9b167be40..9578d5aea9 100644 --- a/packages/opencode/src/session/system.ts +++ b/packages/opencode/src/session/system.ts @@ -1,4 +1,3 @@ -import { App } from "../app/app" import { Ripgrep } from "../file/ripgrep" import { Global } from "../global" import { Filesystem } from "../util/filesystem" @@ -13,6 +12,8 @@ import PROMPT_GEMINI from "./prompt/gemini.txt" import PROMPT_ANTHROPIC_SPOOF from "./prompt/anthropic_spoof.txt" import PROMPT_SUMMARIZE from "./prompt/summarize.txt" import PROMPT_TITLE from "./prompt/title.txt" +import { Project } from "../project/project" +import { Paths } from "../project/path" export namespace SystemPrompt { export function header(providerID: string) { @@ -27,21 +28,21 @@ export namespace SystemPrompt { } export async function environment() { - const app = App.info() + const project = Project.use() return [ [ `Here is some useful information about the environment you are running in:`, ``, - ` Working directory: ${app.path.cwd}`, - ` Is directory a git repo: ${app.git ? "yes" : "no"}`, + ` Working directory: ${Paths.directory}`, + ` Is directory a git repo: ${project.vcs === "git" ? "yes" : "no"}`, ` Platform: ${process.platform}`, ` Today's date: ${new Date().toDateString()}`, ``, ``, ` ${ - app.git + project.vcs === "git" ? await Ripgrep.tree({ - cwd: app.path.cwd, + cwd: Paths.directory, limit: 200, }) : "" @@ -58,12 +59,11 @@ export namespace SystemPrompt { ] export async function custom() { - const { cwd, root } = App.info().path const config = await Config.get() const paths = new Set() for (const item of CUSTOM_FILES) { - const matches = await Filesystem.findUp(item, cwd, root) + const matches = await Filesystem.findUp(item, Paths.directory, Paths.worktree) matches.forEach((path) => paths.add(path)) } @@ -72,7 +72,7 @@ export namespace SystemPrompt { if (config.instructions) { for (const instruction of config.instructions) { - const matches = await Filesystem.globUp(instruction, cwd, root).catch(() => []) + const matches = await Filesystem.globUp(instruction, Paths.directory, Paths.worktree).catch(() => []) matches.forEach((path) => paths.add(path)) } } diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index 0c8cc5e82c..738621a0ef 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -6,6 +6,8 @@ import { Log } from "../util/log" import { Global } from "../global" import { z } from "zod" import { Config } from "../config/config" +import { Project } from "../project/project" +import { Paths } from "../project/path" export namespace Snapshot { const log = Log.create({ service: "snapshot" }) @@ -25,8 +27,8 @@ export namespace Snapshot { } export async function track() { - const app = App.info() - if (!app.git) return + const project = Project.use() + if (project.vcs !== "git") return const cfg = await Config.get() if (cfg.snapshot === false) return const git = gitdir() @@ -35,14 +37,14 @@ export namespace Snapshot { .env({ ...process.env, GIT_DIR: git, - GIT_WORK_TREE: app.path.root, + GIT_WORK_TREE: Paths.worktree, }) .quiet() .nothrow() log.info("initialized") } - await $`git --git-dir ${git} add .`.quiet().cwd(app.path.cwd).nothrow() - const hash = await $`git --git-dir ${git} write-tree`.quiet().cwd(app.path.cwd).nothrow().text() + await $`git --git-dir ${git} add .`.quiet().cwd(Paths.directory).nothrow() + const hash = await $`git --git-dir ${git} write-tree`.quiet().cwd(Paths.directory).nothrow().text() return hash.trim() } @@ -53,10 +55,9 @@ export namespace Snapshot { export type Patch = z.infer export async function patch(hash: string): Promise { - const app = App.info() const git = gitdir() - await $`git --git-dir ${git} add .`.quiet().cwd(app.path.cwd).nothrow() - const files = await $`git --git-dir ${git} diff --name-only ${hash} -- .`.cwd(app.path.cwd).text() + await $`git --git-dir ${git} add .`.quiet().cwd(Paths.directory).nothrow() + const files = await $`git --git-dir ${git} diff --name-only ${hash} -- .`.cwd(Paths.directory).text() return { hash, files: files @@ -64,17 +65,16 @@ export namespace Snapshot { .split("\n") .map((x) => x.trim()) .filter(Boolean) - .map((x) => path.join(app.path.cwd, x)), + .map((x) => path.join(Paths.directory, x)), } } export async function restore(snapshot: string) { log.info("restore", { commit: snapshot }) - const app = App.info() const git = gitdir() await $`git --git-dir=${git} read-tree ${snapshot} && git --git-dir=${git} checkout-index -a -f` .quiet() - .cwd(app.path.root) + .cwd(Paths.worktree) } export async function revert(patches: Patch[]) { @@ -86,7 +86,7 @@ export namespace Snapshot { log.info("reverting", { file, hash: item.hash }) const result = await $`git --git-dir=${git} checkout ${item.hash} -- ${file}` .quiet() - .cwd(App.info().path.root) + .cwd(Paths.worktree) .nothrow() if (result.exitCode !== 0) { log.info("file not found in history, deleting", { file }) @@ -98,14 +98,13 @@ export namespace Snapshot { } export async function diff(hash: string) { - const app = App.info() const git = gitdir() - const result = await $`git --git-dir=${git} diff ${hash} -- .`.quiet().cwd(app.path.root).text() + const result = await $`git --git-dir=${git} diff ${hash} -- .`.quiet().cwd(Paths.worktree).text() return result.trim() } function gitdir() { - const app = App.info() - return path.join(app.path.data, "snapshots") + const project = Project.use() + return path.join(Global.Path.data, "snapshot", project.id) } } diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts index de1eedaa84..1db68813d1 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/bash.ts @@ -3,7 +3,6 @@ import { exec } from "child_process" import { text } from "stream/consumers" import { Tool } from "./tool" import DESCRIPTION from "./bash.txt" -import { App } from "../app/app" import { Permission } from "../permission" import { Config } from "../config/config" import { Filesystem } from "../util/filesystem" @@ -11,6 +10,7 @@ import { lazy } from "../util/lazy" import { Log } from "../util/log" import { Wildcard } from "../util/wildcard" import { $ } from "bun" +import { Paths } from "../project/path" const MAX_OUTPUT_LENGTH = 30000 const DEFAULT_TIMEOUT = 1 * 60 * 1000 @@ -39,7 +39,6 @@ export const BashTool = Tool.define("bash", { }), async execute(params, ctx) { const timeout = Math.min(params.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT) - const app = App.info() const cfg = await Config.get() const tree = await parser().then((p) => p.parse(params.command)) const permissions = (() => { @@ -83,9 +82,9 @@ export const BashTool = Tool.define("bash", { .text() .then((x) => x.trim()) log.info("resolved path", { arg, resolved }) - if (resolved && !Filesystem.contains(app.path.cwd, resolved)) { + if (resolved && !Filesystem.contains(Paths.directory, resolved)) { throw new Error( - `This command references paths outside of ${app.path.cwd} so it is not allowed to be executed.`, + `This command references paths outside of ${Paths.directory} so it is not allowed to be executed.`, ) } } @@ -124,7 +123,7 @@ export const BashTool = Tool.define("bash", { } const process = exec(params.command, { - cwd: app.path.cwd, + cwd: Paths.directory, signal: ctx.abort, maxBuffer: MAX_OUTPUT_LENGTH, timeout, diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts index fbda9e4d14..42c0fb5a28 100644 --- a/packages/opencode/src/tool/edit.ts +++ b/packages/opencode/src/tool/edit.ts @@ -10,12 +10,12 @@ import { LSP } from "../lsp" import { createTwoFilesPatch } from "diff" import { Permission } from "../permission" import DESCRIPTION from "./edit.txt" -import { App } from "../app/app" import { File } from "../file" import { Bus } from "../bus" import { FileTime } from "../file/time" import { Config } from "../config/config" import { Filesystem } from "../util/filesystem" +import { Paths } from "../project/path" export const EditTool = Tool.define("edit", { description: DESCRIPTION, @@ -34,9 +34,8 @@ export const EditTool = Tool.define("edit", { throw new Error("oldString and newString must be different") } - const app = App.info() - const filePath = path.isAbsolute(params.filePath) ? params.filePath : path.join(app.path.cwd, params.filePath) - if (!Filesystem.contains(app.path.cwd, filePath)) { + const filePath = path.isAbsolute(params.filePath) ? params.filePath : path.join(Paths.directory, params.filePath) + if (!Filesystem.contains(Paths.directory, filePath)) { throw new Error(`File ${filePath} is not in the current working directory`) } @@ -121,7 +120,7 @@ export const EditTool = Tool.define("edit", { diagnostics, diff, }, - title: `${path.relative(app.path.root, filePath)}`, + title: `${path.relative(Paths.worktree, filePath)}`, output, } }, diff --git a/packages/opencode/src/tool/glob.ts b/packages/opencode/src/tool/glob.ts index 777c069340..eec734a4e3 100644 --- a/packages/opencode/src/tool/glob.ts +++ b/packages/opencode/src/tool/glob.ts @@ -1,9 +1,9 @@ import { z } from "zod" import path from "path" import { Tool } from "./tool" -import { App } from "../app/app" import DESCRIPTION from "./glob.txt" import { Ripgrep } from "../file/ripgrep" +import { Paths } from "../project/path" export const GlobTool = Tool.define("glob", { description: DESCRIPTION, @@ -17,9 +17,8 @@ export const GlobTool = Tool.define("glob", { ), }), async execute(params) { - const app = App.info() - let search = params.path ?? app.path.cwd - search = path.isAbsolute(search) ? search : path.resolve(app.path.cwd, search) + let search = params.path ?? Paths.directory + search = path.isAbsolute(search) ? search : path.resolve(Paths.directory, search) const limit = 100 const files = [] @@ -55,7 +54,7 @@ export const GlobTool = Tool.define("glob", { } return { - title: path.relative(app.path.root, search), + title: path.relative(Paths.worktree, search), metadata: { count: files.length, truncated, diff --git a/packages/opencode/src/tool/grep.ts b/packages/opencode/src/tool/grep.ts index cc0a290da8..9d81a3c896 100644 --- a/packages/opencode/src/tool/grep.ts +++ b/packages/opencode/src/tool/grep.ts @@ -1,9 +1,9 @@ import { z } from "zod" import { Tool } from "./tool" -import { App } from "../app/app" import { Ripgrep } from "../file/ripgrep" import DESCRIPTION from "./grep.txt" +import { Paths } from "../project/path" export const GrepTool = Tool.define("grep", { description: DESCRIPTION, @@ -17,8 +17,7 @@ export const GrepTool = Tool.define("grep", { throw new Error("pattern is required") } - const app = App.info() - const searchPath = params.path || app.path.cwd + const searchPath = params.path || Paths.directory const rgPath = await Ripgrep.filepath() const args = ["-n", params.pattern] diff --git a/packages/opencode/src/tool/ls.ts b/packages/opencode/src/tool/ls.ts index e0f7fbbf91..e1f072a0a5 100644 --- a/packages/opencode/src/tool/ls.ts +++ b/packages/opencode/src/tool/ls.ts @@ -1,8 +1,8 @@ import { z } from "zod" import { Tool } from "./tool" -import { App } from "../app/app" import * as path from "path" import DESCRIPTION from "./ls.txt" +import { Paths } from "../project/path" export const IGNORE_PATTERNS = [ "node_modules/", @@ -40,8 +40,7 @@ export const ListTool = Tool.define("list", { ignore: z.array(z.string()).describe("List of glob patterns to ignore").optional(), }), async execute(params) { - const app = App.info() - const searchPath = path.resolve(app.path.cwd, params.path || ".") + const searchPath = path.resolve(Paths.directory, params.path || ".") const glob = new Bun.Glob("**/*") const files = [] @@ -102,7 +101,7 @@ export const ListTool = Tool.define("list", { const output = `${searchPath}/\n` + renderDir(".", 0) return { - title: path.relative(app.path.root, searchPath), + title: path.relative(Paths.worktree, searchPath), metadata: { count: files.length, truncated: files.length >= LIMIT, diff --git a/packages/opencode/src/tool/lsp-diagnostics.ts b/packages/opencode/src/tool/lsp-diagnostics.ts index 19415d5ad3..bc9556e45f 100644 --- a/packages/opencode/src/tool/lsp-diagnostics.ts +++ b/packages/opencode/src/tool/lsp-diagnostics.ts @@ -2,8 +2,8 @@ import { z } from "zod" import { Tool } from "./tool" import path from "path" import { LSP } from "../lsp" -import { App } from "../app/app" import DESCRIPTION from "./lsp-diagnostics.txt" +import { Paths } from "../project/path" export const LspDiagnosticTool = Tool.define("lsp_diagnostics", { description: DESCRIPTION, @@ -11,13 +11,12 @@ export const LspDiagnosticTool = Tool.define("lsp_diagnostics", { path: z.string().describe("The path to the file to get diagnostics."), }), execute: async (args) => { - const app = App.info() - const normalized = path.isAbsolute(args.path) ? args.path : path.join(app.path.cwd, args.path) + const normalized = path.isAbsolute(args.path) ? args.path : path.join(Paths.directory, args.path) await LSP.touchFile(normalized, true) const diagnostics = await LSP.diagnostics() const file = diagnostics[normalized] return { - title: path.relative(app.path.root, normalized), + title: path.relative(Paths.worktree, normalized), metadata: { diagnostics, }, diff --git a/packages/opencode/src/tool/lsp-hover.ts b/packages/opencode/src/tool/lsp-hover.ts index b642dd5884..9404b8b31b 100644 --- a/packages/opencode/src/tool/lsp-hover.ts +++ b/packages/opencode/src/tool/lsp-hover.ts @@ -2,8 +2,8 @@ import { z } from "zod" import { Tool } from "./tool" import path from "path" import { LSP } from "../lsp" -import { App } from "../app/app" import DESCRIPTION from "./lsp-hover.txt" +import { Paths } from "../project/path" export const LspHoverTool = Tool.define("lsp_hover", { description: DESCRIPTION, @@ -13,8 +13,7 @@ export const LspHoverTool = Tool.define("lsp_hover", { character: z.number().describe("The character number to get diagnostics."), }), execute: async (args) => { - const app = App.info() - const file = path.isAbsolute(args.file) ? args.file : path.join(app.path.cwd, args.file) + const file = path.isAbsolute(args.file) ? args.file : path.join(Paths.directory, args.file) await LSP.touchFile(file, true) const result = await LSP.hover({ ...args, @@ -22,7 +21,7 @@ export const LspHoverTool = Tool.define("lsp_hover", { }) return { - title: path.relative(app.path.root, file) + ":" + args.line + ":" + args.character, + title: path.relative(Paths.worktree, file) + ":" + args.line + ":" + args.character, metadata: { result, }, diff --git a/packages/opencode/src/tool/multiedit.ts b/packages/opencode/src/tool/multiedit.ts index 432039d4b8..19f777bcd1 100644 --- a/packages/opencode/src/tool/multiedit.ts +++ b/packages/opencode/src/tool/multiedit.ts @@ -4,6 +4,7 @@ import { EditTool } from "./edit" import DESCRIPTION from "./multiedit.txt" import path from "path" import { App } from "../app/app" +import { Paths } from "../project/path" export const MultiEditTool = Tool.define("multiedit", { description: DESCRIPTION, @@ -35,9 +36,8 @@ export const MultiEditTool = Tool.define("multiedit", { ) results.push(result) } - const app = App.info() return { - title: path.relative(app.path.root, params.filePath), + title: path.relative(Paths.worktree, params.filePath), metadata: { results: results.map((r) => r.metadata), }, diff --git a/packages/opencode/src/tool/read.ts b/packages/opencode/src/tool/read.ts index 793579308b..7594daa41f 100644 --- a/packages/opencode/src/tool/read.ts +++ b/packages/opencode/src/tool/read.ts @@ -7,6 +7,7 @@ import { FileTime } from "../file/time" import DESCRIPTION from "./read.txt" import { App } from "../app/app" import { Filesystem } from "../util/filesystem" +import { Paths } from "../project/path" const DEFAULT_READ_LIMIT = 2000 const MAX_LINE_LENGTH = 2000 @@ -23,8 +24,7 @@ export const ReadTool = Tool.define("read", { if (!path.isAbsolute(filepath)) { filepath = path.join(process.cwd(), filepath) } - const app = App.info() - if (!Filesystem.contains(app.path.cwd, filepath)) { + if (!Filesystem.contains(Paths.directory, filepath)) { throw new Error(`File ${filepath} is not in the current working directory`) } @@ -77,7 +77,7 @@ export const ReadTool = Tool.define("read", { FileTime.read(ctx.sessionID, filepath) return { - title: path.relative(App.info().path.root, filepath), + title: path.relative(Paths.worktree, filepath), output, metadata: { preview, diff --git a/packages/opencode/src/tool/write.ts b/packages/opencode/src/tool/write.ts index 5b0028f849..5ffaf6dcdf 100644 --- a/packages/opencode/src/tool/write.ts +++ b/packages/opencode/src/tool/write.ts @@ -4,12 +4,12 @@ import { Tool } from "./tool" import { LSP } from "../lsp" import { Permission } from "../permission" import DESCRIPTION from "./write.txt" -import { App } from "../app/app" import { Bus } from "../bus" import { File } from "../file" import { FileTime } from "../file/time" import { Config } from "../config/config" import { Filesystem } from "../util/filesystem" +import { Paths } from "../project/path" export const WriteTool = Tool.define("write", { description: DESCRIPTION, @@ -18,9 +18,8 @@ export const WriteTool = Tool.define("write", { content: z.string().describe("The content to write to the file"), }), async execute(params, ctx) { - const app = App.info() - const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(app.path.cwd, params.filePath) - if (!Filesystem.contains(app.path.cwd, filepath)) { + const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(Paths.directory, params.filePath) + if (!Filesystem.contains(Paths.directory, filepath)) { throw new Error(`File ${filepath} is not in the current working directory`) } @@ -62,7 +61,7 @@ export const WriteTool = Tool.define("write", { } return { - title: path.relative(app.path.root, filepath), + title: path.relative(Paths.worktree, filepath), metadata: { diagnostics, filepath, diff --git a/packages/tui/internal/components/chat/message.go b/packages/tui/internal/components/chat/message.go index e471e74fc3..9e965f3ad8 100644 --- a/packages/tui/internal/components/chat/message.go +++ b/packages/tui/internal/components/chat/message.go @@ -54,6 +54,8 @@ func WithBackgroundColor(color compat.AdaptiveColor) renderingOption { func WithNoBorder() renderingOption { return func(c *blockRenderer) { c.border = false + c.paddingLeft++ + c.paddingRight++ } } @@ -302,7 +304,7 @@ func renderText( return renderContentBlock( app, content, - width+2, + width, WithNoBorder(), WithBackgroundColor(t.Background()), )