This commit is contained in:
Dax Raad
2025-08-07 00:41:50 -04:00
parent d45d5d664c
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()
LSP.init()
Snapshot.init()
return cb(app)
})
}

View File

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

View File

@@ -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() {

View File

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

View File

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

View File

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

View File

@@ -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(

View File

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

View File

@@ -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<string, any>
}
type RootFunction = (file: string, app: App.Info) => Promise<string | undefined>
type RootFunction = (file: string) => Promise<string | undefined>
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<Handle | undefined>
spawn(root: string): Promise<Handle | undefined>
}
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,
})

View File

@@ -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<typeof Info>
@@ -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<Info>(["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<Info>(["project", project.id], (draft) => {
draft.time.initialized = Date.now()
})
}
export async function list() {
await init()
const keys = await StorageNext.list(["project"])

View File

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

View File

@@ -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<MessageV2.Part[]> => {
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()
}
}

View File

@@ -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:`,
`<env>`,
` 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()}`,
`</env>`,
`<project>`,
` ${
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<string>()
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))
}
}

View File

@@ -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<typeof Patch>
export async function patch(hash: string): Promise<Patch> {
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)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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