This commit is contained in:
Dax Raad 2025-08-07 00:41:50 -04:00
commit 1fb54efdd2
25 changed files with 149 additions and 142 deletions

View file

@ -12,7 +12,6 @@ export async function bootstrap<T>(input: App.Input, cb: (app: App.Info) => Prom
Plugin.init() Plugin.init()
LSP.init() LSP.init()
Snapshot.init() Snapshot.init()
return cb(app) return cb(app)
}) })
} }

View file

@ -1,5 +1,5 @@
import { App } from "../../../app/app"
import { Ripgrep } from "../../../file/ripgrep" import { Ripgrep } from "../../../file/ripgrep"
import { Paths } from "../../../project/path"
import { bootstrap } from "../../bootstrap" import { bootstrap } from "../../bootstrap"
import { cmd } from "../cmd" import { cmd } from "../cmd"
@ -17,8 +17,7 @@ const TreeCommand = cmd({
}), }),
async handler(args) { async handler(args) {
await bootstrap({ cwd: process.cwd() }, async () => { await bootstrap({ cwd: process.cwd() }, async () => {
const app = App.info() console.log(await Ripgrep.tree({ cwd: Paths.directory, limit: args.limit }))
console.log(await Ripgrep.tree({ cwd: app.path.cwd, limit: args.limit }))
}) })
}, },
}) })
@ -41,9 +40,8 @@ const FilesCommand = cmd({
}), }),
async handler(args) { async handler(args) {
await bootstrap({ cwd: process.cwd() }, async () => { await bootstrap({ cwd: process.cwd() }, async () => {
const app = App.info()
const files = await Ripgrep.files({ const files = await Ripgrep.files({
cwd: app.path.cwd, cwd: Paths.directory,
query: args.query, query: args.query,
glob: args.glob ? [args.glob] : undefined, glob: args.glob ? [args.glob] : undefined,
limit: args.limit, limit: args.limit,

View file

@ -19,6 +19,8 @@ import { Identifier } from "../../id/id"
import { Provider } from "../../provider/provider" import { Provider } from "../../provider/provider"
import { Bus } from "../../bus" import { Bus } from "../../bus"
import { MessageV2 } from "../../session/message-v2" import { MessageV2 } from "../../session/message-v2"
import { Project } from "../../project/project"
import { Paths } from "../../project/path"
type GitHubAuthor = { type GitHubAuthor = {
login: string login: string
@ -178,8 +180,8 @@ export const GithubInstallCommand = cmd({
} }
async function getAppInfo() { async function getAppInfo() {
const app = App.info() const project = Project.use()
if (!app.git) { if (project.vcs !== "git") {
prompts.log.error(`Could not find git repository. Please run this command from a git repository.`) prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
throw new UI.CancelledError() throw new UI.CancelledError()
} }
@ -195,7 +197,7 @@ export const GithubInstallCommand = cmd({
throw new UI.CancelledError() throw new UI.CancelledError()
} }
const [owner, repo] = parsed[1].split("/") const [owner, repo] = parsed[1].split("/")
return { owner, repo, root: app.path.root } return { owner, repo, root: Paths.worktree }
} }
async function promptProvider() { async function promptProvider() {

View file

@ -4,9 +4,10 @@ import { $ } from "bun"
import { createPatch } from "diff" import { createPatch } from "diff"
import path from "path" import path from "path"
import * as git from "isomorphic-git" import * as git from "isomorphic-git"
import { App } from "../app/app"
import fs from "fs" import fs from "fs"
import { Log } from "../util/log" import { Log } from "../util/log"
import { Paths } from "../project/path"
import { Project } from "../project/project"
export namespace File { export namespace File {
const log = Log.create({ service: "file" }) const log = Log.create({ service: "file" })
@ -34,10 +35,10 @@ export namespace File {
} }
export async function status() { export async function status() {
const app = App.info() const project = Project.use()
if (!app.git) return [] 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[] = [] 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()) { if (untrackedOutput.trim()) {
const untrackedFiles = untrackedOutput.trim().split("\n") const untrackedFiles = untrackedOutput.trim().split("\n")
for (const filepath of untrackedFiles) { for (const filepath of untrackedFiles) {
try { 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 const lines = content.split("\n").length
changedFiles.push({ changedFiles.push({
path: filepath, path: filepath,
@ -75,7 +80,11 @@ export namespace File {
} }
// Get deleted files // 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()) { if (deletedOutput.trim()) {
const deletedFiles = deletedOutput.trim().split("\n") const deletedFiles = deletedOutput.trim().split("\n")
@ -91,27 +100,27 @@ export namespace File {
return changedFiles.map((x) => ({ return changedFiles.map((x) => ({
...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) { export async function read(file: string) {
using _ = log.time("read", { file }) using _ = log.time("read", { file })
const app = App.info() const project = Project.use()
const full = path.join(app.path.cwd, file) const full = path.join(Paths.directory, file)
const content = await Bun.file(full) const content = await Bun.file(full)
.text() .text()
.catch(() => "") .catch(() => "")
.then((x) => x.trim()) .then((x) => x.trim())
if (app.git) { if (project.vcs === "git") {
const rel = path.relative(app.path.root, full) const rel = path.relative(Paths.worktree, full)
const diff = await git.status({ const diff = await git.status({
fs, fs,
dir: app.path.root, dir: Paths.worktree,
filepath: rel, filepath: rel,
}) })
if (diff !== "unmodified") { 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", { const patch = createPatch(file, original, content, "old", "new", {
context: Infinity, context: Infinity,
}) })

View file

@ -1,5 +1,5 @@
import { App } from "../app/app"
import { BunProc } from "../bun" import { BunProc } from "../bun"
import { Paths } from "../project/path"
import { Filesystem } from "../util/filesystem" import { Filesystem } from "../util/filesystem"
export interface Info { export interface Info {
@ -63,8 +63,7 @@ export const prettier: Info = {
".gql", ".gql",
], ],
async enabled() { async enabled() {
const app = App.info() const items = await Filesystem.findUp("package.json", Paths.directory, Paths.worktree)
const items = await Filesystem.findUp("package.json", app.path.cwd, app.path.root)
for (const item of items) { for (const item of items) {
const json = await Bun.file(item).json() const json = await Bun.file(item).json()
if (json.dependencies?.prettier) return true if (json.dependencies?.prettier) return true
@ -109,8 +108,7 @@ export const biome: Info = {
".gql", ".gql",
], ],
async enabled() { async enabled() {
const app = App.info() const items = await Filesystem.findUp("biome.json", Paths.directory, Paths.worktree)
const items = await Filesystem.findUp("biome.json", app.path.cwd, app.path.root)
return items.length > 0 return items.length > 0
}, },
} }
@ -129,8 +127,7 @@ export const clang: Info = {
command: ["clang-format", "-i", "$FILE"], command: ["clang-format", "-i", "$FILE"],
extensions: [".c", ".cc", ".cpp", ".cxx", ".c++", ".h", ".hh", ".hpp", ".hxx", ".h++", ".ino", ".C", ".H"], extensions: [".c", ".cc", ".cpp", ".cxx", ".c++", ".h", ".hh", ".hpp", ".hxx", ".h++", ".ino", ".C", ".H"],
async enabled() { async enabled() {
const app = App.info() const items = await Filesystem.findUp(".clang-format", Paths.directory, Paths.worktree)
const items = await Filesystem.findUp(".clang-format", app.path.cwd, app.path.root)
return items.length > 0 return items.length > 0
}, },
} }
@ -150,10 +147,9 @@ export const ruff: Info = {
extensions: [".py", ".pyi"], extensions: [".py", ".pyi"],
async enabled() { async enabled() {
if (!Bun.which("ruff")) return false if (!Bun.which("ruff")) return false
const app = App.info()
const configs = ["pyproject.toml", "ruff.toml", ".ruff.toml"] const configs = ["pyproject.toml", "ruff.toml", ".ruff.toml"]
for (const config of configs) { 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 (found.length > 0) {
if (config === "pyproject.toml") { if (config === "pyproject.toml") {
const content = await Bun.file(found[0]).text() const content = await Bun.file(found[0]).text()
@ -165,7 +161,7 @@ export const ruff: Info = {
} }
const deps = ["requirements.txt", "pyproject.toml", "Pipfile"] const deps = ["requirements.txt", "pyproject.toml", "Pipfile"]
for (const dep of deps) { 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) { if (found.length > 0) {
const content = await Bun.file(found[0]).text() const content = await Bun.file(found[0]).text()
if (content.includes("ruff")) return true if (content.includes("ruff")) return true

View file

@ -1,4 +1,3 @@
import { App } from "../app/app"
import { Bus } from "../bus" import { Bus } from "../bus"
import { File } from "../file" import { File } from "../file"
import { Log } from "../util/log" import { Log } from "../util/log"
@ -75,7 +74,7 @@ export namespace Format {
log.info("running", { command: item.command }) log.info("running", { command: item.command })
const proc = Bun.spawn({ const proc = Bun.spawn({
cmd: item.command.map((x) => x.replace("$FILE", file)), cmd: item.command.map((x) => x.replace("$FILE", file)),
cwd: App.info().path.cwd, cwd: Paths.directory,
env: item.environment, env: item.environment,
stdout: "ignore", stdout: "ignore",
stderr: "ignore", stderr: "ignore",

View file

@ -1,7 +1,6 @@
import path from "path" import path from "path"
import { createMessageConnection, StreamMessageReader, StreamMessageWriter } from "vscode-jsonrpc/node" import { createMessageConnection, StreamMessageReader, StreamMessageWriter } from "vscode-jsonrpc/node"
import type { Diagnostic as VSCodeDiagnostic } from "vscode-languageserver-types" import type { Diagnostic as VSCodeDiagnostic } from "vscode-languageserver-types"
import { App } from "../app/app"
import { Log } from "../util/log" import { Log } from "../util/log"
import { LANGUAGE_EXTENSIONS } from "./language" import { LANGUAGE_EXTENSIONS } from "./language"
import { Bus } from "../bus" import { Bus } from "../bus"
@ -9,6 +8,7 @@ import z from "zod"
import type { LSPServer } from "./server" import type { LSPServer } from "./server"
import { NamedError } from "../util/error" import { NamedError } from "../util/error"
import { withTimeout } from "../util/timeout" import { withTimeout } from "../util/timeout"
import { Paths } from "../project/path"
export namespace LSPClient { export namespace LSPClient {
const log = Log.create({ service: "lsp.client" }) 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 }) { export async function create(input: { serverID: string; server: LSPServer.Handle; root: string }) {
const app = App.info()
const l = log.clone().tag("serverID", input.serverID) const l = log.clone().tag("serverID", input.serverID)
l.info("starting client") l.info("starting client")
@ -123,7 +122,7 @@ export namespace LSPClient {
}, },
notify: { notify: {
async open(input: { path: string }) { 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 file = Bun.file(input.path)
const text = await file.text() const text = await file.text()
const version = files[input.path] const version = files[input.path]
@ -155,7 +154,7 @@ export namespace LSPClient {
return diagnostics return diagnostics
}, },
async waitForDiagnostics(input: { path: string }) { 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) log.info("waiting for diagnostics", input)
let unsub: () => void let unsub: () => void
return await withTimeout( return await withTimeout(

View file

@ -108,7 +108,7 @@ export namespace LSP {
const result: LSPClient.Info[] = [] const result: LSPClient.Info[] = []
for (const server of Object.values(LSPServer)) { for (const server of Object.values(LSPServer)) {
if (server.extensions.length && !server.extensions.includes(extension)) continue 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 (!root) continue
if (s.broken.has(root + server.id)) continue if (s.broken.has(root + server.id)) continue
@ -117,7 +117,7 @@ export namespace LSP {
result.push(match) result.push(match)
continue continue
} }
const handle = await server.spawn(App.info(), root) const handle = await server.spawn(root)
if (!handle) continue if (!handle) continue
const client = await LSPClient.create({ const client = await LSPClient.create({
serverID: server.id, serverID: server.id,

View file

@ -1,5 +1,4 @@
import { spawn, type ChildProcessWithoutNullStreams } from "child_process" import { spawn, type ChildProcessWithoutNullStreams } from "child_process"
import type { App } from "../app/app"
import path from "path" import path from "path"
import { Global } from "../global" import { Global } from "../global"
import { Log } from "../util/log" import { Log } from "../util/log"
@ -7,6 +6,7 @@ import { BunProc } from "../bun"
import { $ } from "bun" import { $ } from "bun"
import fs from "fs/promises" import fs from "fs/promises"
import { Filesystem } from "../util/filesystem" import { Filesystem } from "../util/filesystem"
import { Paths } from "../project/path"
export namespace LSPServer { export namespace LSPServer {
const log = Log.create({ service: "lsp.server" }) const log = Log.create({ service: "lsp.server" })
@ -16,18 +16,18 @@ export namespace LSPServer {
initialization?: Record<string, any> initialization?: Record<string, any>
} }
type RootFunction = (file: string, app: App.Info) => Promise<string | undefined> type RootFunction = (file: string) => Promise<string | undefined>
const NearestRoot = (patterns: string[]): RootFunction => { const NearestRoot = (patterns: string[]): RootFunction => {
return async (file, app) => { return async (file) => {
const files = Filesystem.up({ const files = Filesystem.up({
targets: patterns, targets: patterns,
start: path.dirname(file), start: path.dirname(file),
stop: app.path.root, stop: Paths.worktree,
}) })
const first = await files.next() const first = await files.next()
await files.return() await files.return()
if (!first.value) return app.path.root if (!first.value) return Paths.worktree
return path.dirname(first.value) return path.dirname(first.value)
} }
} }
@ -37,15 +37,15 @@ export namespace LSPServer {
extensions: string[] extensions: string[]
global?: boolean global?: boolean
root: RootFunction root: RootFunction
spawn(app: App.Info, root: string): Promise<Handle | undefined> spawn(root: string): Promise<Handle | undefined>
} }
export const Typescript: Info = { export const Typescript: Info = {
id: "typescript", id: "typescript",
root: NearestRoot(["tsconfig.json", "package.json", "jsconfig.json"]), root: NearestRoot(["tsconfig.json", "package.json", "jsconfig.json"]),
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"], extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
async spawn(app, root) { async spawn(root) {
const tsserver = await Bun.resolve("typescript/lib/tsserver.js", app.path.cwd).catch(() => {}) const tsserver = await Bun.resolve("typescript/lib/tsserver.js", Paths.directory).catch(() => {})
if (!tsserver) return if (!tsserver) return
const proc = spawn(BunProc.which(), ["x", "typescript-language-server", "--stdio"], { const proc = spawn(BunProc.which(), ["x", "typescript-language-server", "--stdio"], {
cwd: root, cwd: root,
@ -67,13 +67,13 @@ export namespace LSPServer {
export const Gopls: Info = { export const Gopls: Info = {
id: "golang", id: "golang",
root: async (file, app) => { root: async (file) => {
const work = await NearestRoot(["go.work"])(file, app) const work = await NearestRoot(["go.work"])(file)
if (work) return work if (work) return work
return NearestRoot(["go.mod", "go.sum"])(file, app) return NearestRoot(["go.mod", "go.sum"])(file)
}, },
extensions: [".go"], extensions: [".go"],
async spawn(_, root) { async spawn(root) {
let bin = Bun.which("gopls", { let bin = Bun.which("gopls", {
PATH: process.env["PATH"] + ":" + Global.Path.bin, PATH: process.env["PATH"] + ":" + Global.Path.bin,
}) })
@ -109,7 +109,7 @@ export namespace LSPServer {
id: "ruby-lsp", id: "ruby-lsp",
root: NearestRoot(["Gemfile"]), root: NearestRoot(["Gemfile"]),
extensions: [".rb", ".rake", ".gemspec", ".ru"], extensions: [".rb", ".rake", ".gemspec", ".ru"],
async spawn(_, root) { async spawn(root) {
let bin = Bun.which("ruby-lsp", { let bin = Bun.which("ruby-lsp", {
PATH: process.env["PATH"] + ":" + Global.Path.bin, PATH: process.env["PATH"] + ":" + Global.Path.bin,
}) })
@ -149,7 +149,7 @@ export namespace LSPServer {
id: "pyright", id: "pyright",
extensions: [".py", ".pyi"], extensions: [".py", ".pyi"],
root: NearestRoot(["pyproject.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile", "pyrightconfig.json"]), 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"], { const proc = spawn(BunProc.which(), ["x", "pyright-langserver", "--stdio"], {
cwd: root, cwd: root,
env: { env: {
@ -167,7 +167,7 @@ export namespace LSPServer {
id: "elixir-ls", id: "elixir-ls",
extensions: [".ex", ".exs"], extensions: [".ex", ".exs"],
root: NearestRoot(["mix.exs", "mix.lock"]), root: NearestRoot(["mix.exs", "mix.lock"]),
async spawn(_, root) { async spawn(root) {
let binary = Bun.which("elixir-ls") let binary = Bun.which("elixir-ls")
if (!binary) { if (!binary) {
const elixirLsPath = path.join(Global.Path.bin, "elixir-ls") const elixirLsPath = path.join(Global.Path.bin, "elixir-ls")
@ -222,7 +222,7 @@ export namespace LSPServer {
id: "zls", id: "zls",
extensions: [".zig", ".zon"], extensions: [".zig", ".zon"],
root: NearestRoot(["build.zig"]), root: NearestRoot(["build.zig"]),
async spawn(_, root) { async spawn(root) {
let bin = Bun.which("zls", { let bin = Bun.which("zls", {
PATH: process.env["PATH"] + ":" + Global.Path.bin, PATH: process.env["PATH"] + ":" + Global.Path.bin,
}) })
@ -327,7 +327,7 @@ export namespace LSPServer {
id: "csharp", id: "csharp",
root: NearestRoot([".sln", ".csproj", "global.json"]), root: NearestRoot([".sln", ".csproj", "global.json"]),
extensions: [".cs"], extensions: [".cs"],
async spawn(_, root) { async spawn(root) {
let bin = Bun.which("csharp-ls", { let bin = Bun.which("csharp-ls", {
PATH: process.env["PATH"] + ":" + Global.Path.bin, PATH: process.env["PATH"] + ":" + Global.Path.bin,
}) })

View file

@ -11,6 +11,10 @@ export namespace Project {
id: z.string(), id: z.string(),
worktree: z.string(), worktree: z.string(),
vcs: z.literal("git").optional(), vcs: z.literal("git").optional(),
time: z.object({
created: z.number(),
initialized: z.number().optional(),
}),
}) })
export type Info = z.infer<typeof Info> export type Info = z.infer<typeof Info>
@ -25,9 +29,12 @@ export namespace Project {
const git = await matches.next().then((x) => x.value) const git = await matches.next().then((x) => x.value)
await matches.return() await matches.return()
if (!git) { if (!git) {
await StorageNext.write(["project", "global"], { await StorageNext.write<Info>(["project", "global"], {
id: "global", id: "global",
worktree: "/", worktree: "/",
time: {
created: Date.now(),
},
}) })
return return
} }
@ -56,9 +63,19 @@ export namespace Project {
id, id,
worktree, worktree,
vcs: "git", vcs: "git",
time: {
created: Date.now(),
},
}) })
}) })
export async function setInitialized() {
const project = use()
await StorageNext.update<Info>(["project", project.id], (draft) => {
draft.time.initialized = Date.now()
})
}
export async function list() { export async function list() {
await init() await init()
const keys = await StorageNext.list(["project"]) const keys = await StorageNext.list(["project"])

View file

@ -20,6 +20,7 @@ import { Mode } from "../session/mode"
import { callTui, TuiRoute } from "./tui" import { callTui, TuiRoute } from "./tui"
import { Permission } from "../permission" import { Permission } from "../permission"
import { lazy } from "../util/lazy" import { lazy } from "../util/lazy"
import { Paths } from "../project/path"
const ERRORS = { const ERRORS = {
400: { 400: {
@ -692,10 +693,9 @@ export namespace Server {
}), }),
), ),
async (c) => { async (c) => {
const app = App.info()
const pattern = c.req.valid("query").pattern const pattern = c.req.valid("query").pattern
const result = await Ripgrep.search({ const result = await Ripgrep.search({
cwd: app.path.cwd, cwd: Paths.directory,
pattern, pattern,
limit: 10, limit: 10,
}) })
@ -726,9 +726,8 @@ export namespace Server {
), ),
async (c) => { async (c) => {
const query = c.req.valid("query").query const query = c.req.valid("query").query
const app = App.info()
const result = await Ripgrep.files({ const result = await Ripgrep.files({
cwd: app.path.cwd, cwd: Paths.directory,
query, query,
limit: 10, limit: 10,
}) })

View file

@ -161,10 +161,9 @@ export namespace Session {
) )
export async function create(parentID?: string) { export async function create(parentID?: string) {
const app = App.info()
return createNext({ return createNext({
parentID, parentID,
directory: app.path.cwd, directory: Paths.directory,
}) })
} }
@ -443,7 +442,6 @@ export namespace Session {
}, },
} }
const app = App.info()
const userParts = await Promise.all( const userParts = await Promise.all(
input.parts.map(async (part): Promise<MessageV2.Part[]> => { input.parts.map(async (part): Promise<MessageV2.Part[]> => {
if (part.type === "file") { if (part.type === "file") {
@ -714,8 +712,8 @@ export namespace Session {
system, system,
mode: inputMode, mode: inputMode,
path: { path: {
cwd: app.path.cwd, cwd: Paths.directory,
root: app.path.root, root: Paths.worktree,
}, },
cost: 0, cost: 0,
tokens: { tokens: {
@ -869,8 +867,8 @@ export namespace Session {
role: "assistant", role: "assistant",
system, system,
path: { path: {
cwd: app.path.cwd, cwd: Paths.directory,
root: app.path.root, root: Paths.worktree,
}, },
cost: 0, cost: 0,
tokens: { tokens: {
@ -1266,7 +1264,6 @@ export namespace Session {
const lastSummary = msgs.findLast((msg) => msg.info.role === "assistant" && msg.info.summary === true) 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 filtered = msgs.filter((msg) => !lastSummary || msg.info.id >= lastSummary.info.id)
const model = await Provider.getModel(input.providerID, input.modelID) const model = await Provider.getModel(input.providerID, input.modelID)
const app = App.info()
const system = [ const system = [
...SystemPrompt.summarize(input.providerID), ...SystemPrompt.summarize(input.providerID),
...(await SystemPrompt.environment()), ...(await SystemPrompt.environment()),
@ -1280,8 +1277,8 @@ export namespace Session {
system, system,
mode: "build", mode: "build",
path: { path: {
cwd: app.path.cwd, cwd: Paths.directory,
root: app.path.root, root: Paths.worktree,
}, },
summary: true, summary: true,
cost: 0, cost: 0,
@ -1395,7 +1392,6 @@ export namespace Session {
providerID: string providerID: string
messageID: string messageID: string
}) { }) {
const app = App.info()
await Session.chat({ await Session.chat({
sessionID: input.sessionID, sessionID: input.sessionID,
messageID: input.messageID, messageID: input.messageID,
@ -1405,10 +1401,10 @@ export namespace Session {
{ {
id: Identifier.ascending("part"), id: Identifier.ascending("part"),
type: "text", type: "text",
text: PROMPT_INITIALIZE.replace("${path}", app.path.root), text: PROMPT_INITIALIZE.replace("${path}", Paths.worktree),
}, },
], ],
}) })
await App.initialize() await Project.setInitialized()
} }
} }

View file

@ -1,4 +1,3 @@
import { App } from "../app/app"
import { Ripgrep } from "../file/ripgrep" import { Ripgrep } from "../file/ripgrep"
import { Global } from "../global" import { Global } from "../global"
import { Filesystem } from "../util/filesystem" 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_ANTHROPIC_SPOOF from "./prompt/anthropic_spoof.txt"
import PROMPT_SUMMARIZE from "./prompt/summarize.txt" import PROMPT_SUMMARIZE from "./prompt/summarize.txt"
import PROMPT_TITLE from "./prompt/title.txt" import PROMPT_TITLE from "./prompt/title.txt"
import { Project } from "../project/project"
import { Paths } from "../project/path"
export namespace SystemPrompt { export namespace SystemPrompt {
export function header(providerID: string) { export function header(providerID: string) {
@ -27,21 +28,21 @@ export namespace SystemPrompt {
} }
export async function environment() { export async function environment() {
const app = App.info() const project = Project.use()
return [ return [
[ [
`Here is some useful information about the environment you are running in:`, `Here is some useful information about the environment you are running in:`,
`<env>`, `<env>`,
` Working directory: ${app.path.cwd}`, ` Working directory: ${Paths.directory}`,
` Is directory a git repo: ${app.git ? "yes" : "no"}`, ` Is directory a git repo: ${project.vcs === "git" ? "yes" : "no"}`,
` Platform: ${process.platform}`, ` Platform: ${process.platform}`,
` Today's date: ${new Date().toDateString()}`, ` Today's date: ${new Date().toDateString()}`,
`</env>`, `</env>`,
`<project>`, `<project>`,
` ${ ` ${
app.git project.vcs === "git"
? await Ripgrep.tree({ ? await Ripgrep.tree({
cwd: app.path.cwd, cwd: Paths.directory,
limit: 200, limit: 200,
}) })
: "" : ""
@ -58,12 +59,11 @@ export namespace SystemPrompt {
] ]
export async function custom() { export async function custom() {
const { cwd, root } = App.info().path
const config = await Config.get() const config = await Config.get()
const paths = new Set<string>() const paths = new Set<string>()
for (const item of CUSTOM_FILES) { 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)) matches.forEach((path) => paths.add(path))
} }
@ -72,7 +72,7 @@ export namespace SystemPrompt {
if (config.instructions) { if (config.instructions) {
for (const instruction of 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)) matches.forEach((path) => paths.add(path))
} }
} }

View file

@ -6,6 +6,8 @@ import { Log } from "../util/log"
import { Global } from "../global" import { Global } from "../global"
import { z } from "zod" import { z } from "zod"
import { Config } from "../config/config" import { Config } from "../config/config"
import { Project } from "../project/project"
import { Paths } from "../project/path"
export namespace Snapshot { export namespace Snapshot {
const log = Log.create({ service: "snapshot" }) const log = Log.create({ service: "snapshot" })
@ -25,8 +27,8 @@ export namespace Snapshot {
} }
export async function track() { export async function track() {
const app = App.info() const project = Project.use()
if (!app.git) return if (project.vcs !== "git") return
const cfg = await Config.get() const cfg = await Config.get()
if (cfg.snapshot === false) return if (cfg.snapshot === false) return
const git = gitdir() const git = gitdir()
@ -35,14 +37,14 @@ export namespace Snapshot {
.env({ .env({
...process.env, ...process.env,
GIT_DIR: git, GIT_DIR: git,
GIT_WORK_TREE: app.path.root, GIT_WORK_TREE: Paths.worktree,
}) })
.quiet() .quiet()
.nothrow() .nothrow()
log.info("initialized") log.info("initialized")
} }
await $`git --git-dir ${git} add .`.quiet().cwd(app.path.cwd).nothrow() await $`git --git-dir ${git} add .`.quiet().cwd(Paths.directory).nothrow()
const hash = await $`git --git-dir ${git} write-tree`.quiet().cwd(app.path.cwd).nothrow().text() const hash = await $`git --git-dir ${git} write-tree`.quiet().cwd(Paths.directory).nothrow().text()
return hash.trim() return hash.trim()
} }
@ -53,10 +55,9 @@ export namespace Snapshot {
export type Patch = z.infer<typeof Patch> export type Patch = z.infer<typeof Patch>
export async function patch(hash: string): Promise<Patch> { export async function patch(hash: string): Promise<Patch> {
const app = App.info()
const git = gitdir() const git = gitdir()
await $`git --git-dir ${git} add .`.quiet().cwd(app.path.cwd).nothrow() await $`git --git-dir ${git} add .`.quiet().cwd(Paths.directory).nothrow()
const files = await $`git --git-dir ${git} diff --name-only ${hash} -- .`.cwd(app.path.cwd).text() const files = await $`git --git-dir ${git} diff --name-only ${hash} -- .`.cwd(Paths.directory).text()
return { return {
hash, hash,
files: files files: files
@ -64,17 +65,16 @@ export namespace Snapshot {
.split("\n") .split("\n")
.map((x) => x.trim()) .map((x) => x.trim())
.filter(Boolean) .filter(Boolean)
.map((x) => path.join(app.path.cwd, x)), .map((x) => path.join(Paths.directory, x)),
} }
} }
export async function restore(snapshot: string) { export async function restore(snapshot: string) {
log.info("restore", { commit: snapshot }) log.info("restore", { commit: snapshot })
const app = App.info()
const git = gitdir() const git = gitdir()
await $`git --git-dir=${git} read-tree ${snapshot} && git --git-dir=${git} checkout-index -a -f` await $`git --git-dir=${git} read-tree ${snapshot} && git --git-dir=${git} checkout-index -a -f`
.quiet() .quiet()
.cwd(app.path.root) .cwd(Paths.worktree)
} }
export async function revert(patches: Patch[]) { export async function revert(patches: Patch[]) {
@ -86,7 +86,7 @@ export namespace Snapshot {
log.info("reverting", { file, hash: item.hash }) log.info("reverting", { file, hash: item.hash })
const result = await $`git --git-dir=${git} checkout ${item.hash} -- ${file}` const result = await $`git --git-dir=${git} checkout ${item.hash} -- ${file}`
.quiet() .quiet()
.cwd(App.info().path.root) .cwd(Paths.worktree)
.nothrow() .nothrow()
if (result.exitCode !== 0) { if (result.exitCode !== 0) {
log.info("file not found in history, deleting", { file }) log.info("file not found in history, deleting", { file })
@ -98,14 +98,13 @@ export namespace Snapshot {
} }
export async function diff(hash: string) { export async function diff(hash: string) {
const app = App.info()
const git = gitdir() 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() return result.trim()
} }
function gitdir() { function gitdir() {
const app = App.info() const project = Project.use()
return path.join(app.path.data, "snapshots") return path.join(Global.Path.data, "snapshot", project.id)
} }
} }

View file

@ -3,7 +3,6 @@ import { exec } from "child_process"
import { text } from "stream/consumers" import { text } from "stream/consumers"
import { Tool } from "./tool" import { Tool } from "./tool"
import DESCRIPTION from "./bash.txt" import DESCRIPTION from "./bash.txt"
import { App } from "../app/app"
import { Permission } from "../permission" import { Permission } from "../permission"
import { Config } from "../config/config" import { Config } from "../config/config"
import { Filesystem } from "../util/filesystem" import { Filesystem } from "../util/filesystem"
@ -11,6 +10,7 @@ import { lazy } from "../util/lazy"
import { Log } from "../util/log" import { Log } from "../util/log"
import { Wildcard } from "../util/wildcard" import { Wildcard } from "../util/wildcard"
import { $ } from "bun" import { $ } from "bun"
import { Paths } from "../project/path"
const MAX_OUTPUT_LENGTH = 30000 const MAX_OUTPUT_LENGTH = 30000
const DEFAULT_TIMEOUT = 1 * 60 * 1000 const DEFAULT_TIMEOUT = 1 * 60 * 1000
@ -39,7 +39,6 @@ export const BashTool = Tool.define("bash", {
}), }),
async execute(params, ctx) { async execute(params, ctx) {
const timeout = Math.min(params.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT) const timeout = Math.min(params.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT)
const app = App.info()
const cfg = await Config.get() const cfg = await Config.get()
const tree = await parser().then((p) => p.parse(params.command)) const tree = await parser().then((p) => p.parse(params.command))
const permissions = (() => { const permissions = (() => {
@ -83,9 +82,9 @@ export const BashTool = Tool.define("bash", {
.text() .text()
.then((x) => x.trim()) .then((x) => x.trim())
log.info("resolved path", { arg, resolved }) log.info("resolved path", { arg, resolved })
if (resolved && !Filesystem.contains(app.path.cwd, resolved)) { if (resolved && !Filesystem.contains(Paths.directory, resolved)) {
throw new Error( 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, { const process = exec(params.command, {
cwd: app.path.cwd, cwd: Paths.directory,
signal: ctx.abort, signal: ctx.abort,
maxBuffer: MAX_OUTPUT_LENGTH, maxBuffer: MAX_OUTPUT_LENGTH,
timeout, timeout,

View file

@ -10,12 +10,12 @@ import { LSP } from "../lsp"
import { createTwoFilesPatch } from "diff" import { createTwoFilesPatch } from "diff"
import { Permission } from "../permission" import { Permission } from "../permission"
import DESCRIPTION from "./edit.txt" import DESCRIPTION from "./edit.txt"
import { App } from "../app/app"
import { File } from "../file" import { File } from "../file"
import { Bus } from "../bus" import { Bus } from "../bus"
import { FileTime } from "../file/time" import { FileTime } from "../file/time"
import { Config } from "../config/config" import { Config } from "../config/config"
import { Filesystem } from "../util/filesystem" import { Filesystem } from "../util/filesystem"
import { Paths } from "../project/path"
export const EditTool = Tool.define("edit", { export const EditTool = Tool.define("edit", {
description: DESCRIPTION, description: DESCRIPTION,
@ -34,9 +34,8 @@ export const EditTool = Tool.define("edit", {
throw new Error("oldString and newString must be different") throw new Error("oldString and newString must be different")
} }
const app = App.info() const filePath = path.isAbsolute(params.filePath) ? params.filePath : path.join(Paths.directory, params.filePath)
const filePath = path.isAbsolute(params.filePath) ? params.filePath : path.join(app.path.cwd, params.filePath) if (!Filesystem.contains(Paths.directory, filePath)) {
if (!Filesystem.contains(app.path.cwd, filePath)) {
throw new Error(`File ${filePath} is not in the current working directory`) throw new Error(`File ${filePath} is not in the current working directory`)
} }
@ -121,7 +120,7 @@ export const EditTool = Tool.define("edit", {
diagnostics, diagnostics,
diff, diff,
}, },
title: `${path.relative(app.path.root, filePath)}`, title: `${path.relative(Paths.worktree, filePath)}`,
output, output,
} }
}, },

View file

@ -1,9 +1,9 @@
import { z } from "zod" import { z } from "zod"
import path from "path" import path from "path"
import { Tool } from "./tool" import { Tool } from "./tool"
import { App } from "../app/app"
import DESCRIPTION from "./glob.txt" import DESCRIPTION from "./glob.txt"
import { Ripgrep } from "../file/ripgrep" import { Ripgrep } from "../file/ripgrep"
import { Paths } from "../project/path"
export const GlobTool = Tool.define("glob", { export const GlobTool = Tool.define("glob", {
description: DESCRIPTION, description: DESCRIPTION,
@ -17,9 +17,8 @@ export const GlobTool = Tool.define("glob", {
), ),
}), }),
async execute(params) { async execute(params) {
const app = App.info() let search = params.path ?? Paths.directory
let search = params.path ?? app.path.cwd search = path.isAbsolute(search) ? search : path.resolve(Paths.directory, search)
search = path.isAbsolute(search) ? search : path.resolve(app.path.cwd, search)
const limit = 100 const limit = 100
const files = [] const files = []
@ -55,7 +54,7 @@ export const GlobTool = Tool.define("glob", {
} }
return { return {
title: path.relative(app.path.root, search), title: path.relative(Paths.worktree, search),
metadata: { metadata: {
count: files.length, count: files.length,
truncated, truncated,

View file

@ -1,9 +1,9 @@
import { z } from "zod" import { z } from "zod"
import { Tool } from "./tool" import { Tool } from "./tool"
import { App } from "../app/app"
import { Ripgrep } from "../file/ripgrep" import { Ripgrep } from "../file/ripgrep"
import DESCRIPTION from "./grep.txt" import DESCRIPTION from "./grep.txt"
import { Paths } from "../project/path"
export const GrepTool = Tool.define("grep", { export const GrepTool = Tool.define("grep", {
description: DESCRIPTION, description: DESCRIPTION,
@ -17,8 +17,7 @@ export const GrepTool = Tool.define("grep", {
throw new Error("pattern is required") throw new Error("pattern is required")
} }
const app = App.info() const searchPath = params.path || Paths.directory
const searchPath = params.path || app.path.cwd
const rgPath = await Ripgrep.filepath() const rgPath = await Ripgrep.filepath()
const args = ["-n", params.pattern] const args = ["-n", params.pattern]

View file

@ -1,8 +1,8 @@
import { z } from "zod" import { z } from "zod"
import { Tool } from "./tool" import { Tool } from "./tool"
import { App } from "../app/app"
import * as path from "path" import * as path from "path"
import DESCRIPTION from "./ls.txt" import DESCRIPTION from "./ls.txt"
import { Paths } from "../project/path"
export const IGNORE_PATTERNS = [ export const IGNORE_PATTERNS = [
"node_modules/", "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(), ignore: z.array(z.string()).describe("List of glob patterns to ignore").optional(),
}), }),
async execute(params) { async execute(params) {
const app = App.info() const searchPath = path.resolve(Paths.directory, params.path || ".")
const searchPath = path.resolve(app.path.cwd, params.path || ".")
const glob = new Bun.Glob("**/*") const glob = new Bun.Glob("**/*")
const files = [] const files = []
@ -102,7 +101,7 @@ export const ListTool = Tool.define("list", {
const output = `${searchPath}/\n` + renderDir(".", 0) const output = `${searchPath}/\n` + renderDir(".", 0)
return { return {
title: path.relative(app.path.root, searchPath), title: path.relative(Paths.worktree, searchPath),
metadata: { metadata: {
count: files.length, count: files.length,
truncated: files.length >= LIMIT, truncated: files.length >= LIMIT,

View file

@ -2,8 +2,8 @@ import { z } from "zod"
import { Tool } from "./tool" import { Tool } from "./tool"
import path from "path" import path from "path"
import { LSP } from "../lsp" import { LSP } from "../lsp"
import { App } from "../app/app"
import DESCRIPTION from "./lsp-diagnostics.txt" import DESCRIPTION from "./lsp-diagnostics.txt"
import { Paths } from "../project/path"
export const LspDiagnosticTool = Tool.define("lsp_diagnostics", { export const LspDiagnosticTool = Tool.define("lsp_diagnostics", {
description: DESCRIPTION, 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."), path: z.string().describe("The path to the file to get diagnostics."),
}), }),
execute: async (args) => { execute: async (args) => {
const app = App.info() const normalized = path.isAbsolute(args.path) ? args.path : path.join(Paths.directory, args.path)
const normalized = path.isAbsolute(args.path) ? args.path : path.join(app.path.cwd, args.path)
await LSP.touchFile(normalized, true) await LSP.touchFile(normalized, true)
const diagnostics = await LSP.diagnostics() const diagnostics = await LSP.diagnostics()
const file = diagnostics[normalized] const file = diagnostics[normalized]
return { return {
title: path.relative(app.path.root, normalized), title: path.relative(Paths.worktree, normalized),
metadata: { metadata: {
diagnostics, diagnostics,
}, },

View file

@ -2,8 +2,8 @@ import { z } from "zod"
import { Tool } from "./tool" import { Tool } from "./tool"
import path from "path" import path from "path"
import { LSP } from "../lsp" import { LSP } from "../lsp"
import { App } from "../app/app"
import DESCRIPTION from "./lsp-hover.txt" import DESCRIPTION from "./lsp-hover.txt"
import { Paths } from "../project/path"
export const LspHoverTool = Tool.define("lsp_hover", { export const LspHoverTool = Tool.define("lsp_hover", {
description: DESCRIPTION, description: DESCRIPTION,
@ -13,8 +13,7 @@ export const LspHoverTool = Tool.define("lsp_hover", {
character: z.number().describe("The character number to get diagnostics."), character: z.number().describe("The character number to get diagnostics."),
}), }),
execute: async (args) => { execute: async (args) => {
const app = App.info() const file = path.isAbsolute(args.file) ? args.file : path.join(Paths.directory, args.file)
const file = path.isAbsolute(args.file) ? args.file : path.join(app.path.cwd, args.file)
await LSP.touchFile(file, true) await LSP.touchFile(file, true)
const result = await LSP.hover({ const result = await LSP.hover({
...args, ...args,
@ -22,7 +21,7 @@ export const LspHoverTool = Tool.define("lsp_hover", {
}) })
return { return {
title: path.relative(app.path.root, file) + ":" + args.line + ":" + args.character, title: path.relative(Paths.worktree, file) + ":" + args.line + ":" + args.character,
metadata: { metadata: {
result, result,
}, },

View file

@ -4,6 +4,7 @@ import { EditTool } from "./edit"
import DESCRIPTION from "./multiedit.txt" import DESCRIPTION from "./multiedit.txt"
import path from "path" import path from "path"
import { App } from "../app/app" import { App } from "../app/app"
import { Paths } from "../project/path"
export const MultiEditTool = Tool.define("multiedit", { export const MultiEditTool = Tool.define("multiedit", {
description: DESCRIPTION, description: DESCRIPTION,
@ -35,9 +36,8 @@ export const MultiEditTool = Tool.define("multiedit", {
) )
results.push(result) results.push(result)
} }
const app = App.info()
return { return {
title: path.relative(app.path.root, params.filePath), title: path.relative(Paths.worktree, params.filePath),
metadata: { metadata: {
results: results.map((r) => r.metadata), results: results.map((r) => r.metadata),
}, },

View file

@ -7,6 +7,7 @@ import { FileTime } from "../file/time"
import DESCRIPTION from "./read.txt" import DESCRIPTION from "./read.txt"
import { App } from "../app/app" import { App } from "../app/app"
import { Filesystem } from "../util/filesystem" import { Filesystem } from "../util/filesystem"
import { Paths } from "../project/path"
const DEFAULT_READ_LIMIT = 2000 const DEFAULT_READ_LIMIT = 2000
const MAX_LINE_LENGTH = 2000 const MAX_LINE_LENGTH = 2000
@ -23,8 +24,7 @@ export const ReadTool = Tool.define("read", {
if (!path.isAbsolute(filepath)) { if (!path.isAbsolute(filepath)) {
filepath = path.join(process.cwd(), filepath) filepath = path.join(process.cwd(), filepath)
} }
const app = App.info() if (!Filesystem.contains(Paths.directory, filepath)) {
if (!Filesystem.contains(app.path.cwd, filepath)) {
throw new Error(`File ${filepath} is not in the current working directory`) 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) FileTime.read(ctx.sessionID, filepath)
return { return {
title: path.relative(App.info().path.root, filepath), title: path.relative(Paths.worktree, filepath),
output, output,
metadata: { metadata: {
preview, preview,

View file

@ -4,12 +4,12 @@ import { Tool } from "./tool"
import { LSP } from "../lsp" import { LSP } from "../lsp"
import { Permission } from "../permission" import { Permission } from "../permission"
import DESCRIPTION from "./write.txt" import DESCRIPTION from "./write.txt"
import { App } from "../app/app"
import { Bus } from "../bus" import { Bus } from "../bus"
import { File } from "../file" import { File } from "../file"
import { FileTime } from "../file/time" import { FileTime } from "../file/time"
import { Config } from "../config/config" import { Config } from "../config/config"
import { Filesystem } from "../util/filesystem" import { Filesystem } from "../util/filesystem"
import { Paths } from "../project/path"
export const WriteTool = Tool.define("write", { export const WriteTool = Tool.define("write", {
description: DESCRIPTION, description: DESCRIPTION,
@ -18,9 +18,8 @@ export const WriteTool = Tool.define("write", {
content: z.string().describe("The content to write to the file"), content: z.string().describe("The content to write to the file"),
}), }),
async execute(params, ctx) { async execute(params, ctx) {
const app = App.info() const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(Paths.directory, params.filePath)
const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(app.path.cwd, params.filePath) if (!Filesystem.contains(Paths.directory, filepath)) {
if (!Filesystem.contains(app.path.cwd, filepath)) {
throw new Error(`File ${filepath} is not in the current working directory`) throw new Error(`File ${filepath} is not in the current working directory`)
} }
@ -62,7 +61,7 @@ export const WriteTool = Tool.define("write", {
} }
return { return {
title: path.relative(app.path.root, filepath), title: path.relative(Paths.worktree, filepath),
metadata: { metadata: {
diagnostics, diagnostics,
filepath, filepath,

View file

@ -54,6 +54,8 @@ func WithBackgroundColor(color compat.AdaptiveColor) renderingOption {
func WithNoBorder() renderingOption { func WithNoBorder() renderingOption {
return func(c *blockRenderer) { return func(c *blockRenderer) {
c.border = false c.border = false
c.paddingLeft++
c.paddingRight++
} }
} }
@ -302,7 +304,7 @@ func renderText(
return renderContentBlock( return renderContentBlock(
app, app,
content, content,
width+2, width,
WithNoBorder(), WithNoBorder(),
WithBackgroundColor(t.Background()), WithBackgroundColor(t.Background()),
) )