mirror of
https://github.com/aljazceru/opencode.git
synced 2025-12-24 19:24:22 +01:00
chore: format code
This commit is contained in:
@@ -41,9 +41,7 @@ for (const [os, arch] of targets) {
|
||||
|
||||
const opentui = `@opentui/core-${os === "windows" ? "win32" : os}-${arch.replace("-baseline", "")}`
|
||||
await $`mkdir -p ../../node_modules/${opentui}`
|
||||
await $`npm pack ${opentui}@${pkg.dependencies["@opentui/core"]}`.cwd(
|
||||
path.join(dir, "../../node_modules"),
|
||||
)
|
||||
await $`npm pack ${opentui}@${pkg.dependencies["@opentui/core"]}`.cwd(path.join(dir, "../../node_modules"))
|
||||
await $`tar -xf ../../node_modules/${opentui.replace("@opentui/", "opentui-")}-*.tgz -C ../../node_modules/${opentui} --strip-components=1`
|
||||
|
||||
const watcher = `@parcel/watcher-${os === "windows" ? "win32" : os}-${arch.replace("-baseline", "")}${os === "linux" ? "-glibc" : ""}`
|
||||
@@ -51,9 +49,7 @@ for (const [os, arch] of targets) {
|
||||
await $`npm pack ${watcher}`.cwd(path.join(dir, "../../node_modules")).quiet()
|
||||
await $`tar -xf ../../node_modules/${watcher.replace("@parcel/", "parcel-")}-*.tgz -C ../../node_modules/${watcher} --strip-components=1`
|
||||
|
||||
const parserWorker = fs.realpathSync(
|
||||
path.resolve(dir, "./node_modules/@opentui/core/parser.worker.js"),
|
||||
)
|
||||
const parserWorker = fs.realpathSync(path.resolve(dir, "./node_modules/@opentui/core/parser.worker.js"))
|
||||
const workerPath = "./src/cli/cmd/tui/worker.ts"
|
||||
|
||||
await Bun.build({
|
||||
|
||||
@@ -77,8 +77,7 @@ async function regenerateWindowsCmdWrappers() {
|
||||
|
||||
// npm_config_global is string | undefined
|
||||
// if it exists, the value is true
|
||||
const isGlobal =
|
||||
process.env.npm_config_global === "true" || pkgPath.includes(path.join("npm", "node_modules"))
|
||||
const isGlobal = process.env.npm_config_global === "true" || pkgPath.includes(path.join("npm", "node_modules"))
|
||||
|
||||
// The npm rebuild command does 2 things - Execute lifecycle scripts and rebuild bin links
|
||||
// We want to skip lifecycle scripts to avoid infinite loops, so we use --ignore-scripts
|
||||
@@ -94,9 +93,7 @@ async function regenerateWindowsCmdWrappers() {
|
||||
console.log("Successfully rebuilt npm bin links")
|
||||
} catch (error) {
|
||||
console.error("Error rebuilding npm links:", error.message)
|
||||
console.error(
|
||||
"npm rebuild failed. You may need to manually run: npm rebuild opencode-ai --ignore-scripts",
|
||||
)
|
||||
console.error("npm rebuild failed. You may need to manually run: npm rebuild opencode-ai --ignore-scripts")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,18 +55,10 @@ if (!Script.preview) {
|
||||
}
|
||||
|
||||
// Calculate SHA values
|
||||
const arm64Sha = await $`sha256sum ./dist/opencode-linux-arm64.zip | cut -d' ' -f1`
|
||||
.text()
|
||||
.then((x) => x.trim())
|
||||
const x64Sha = await $`sha256sum ./dist/opencode-linux-x64.zip | cut -d' ' -f1`
|
||||
.text()
|
||||
.then((x) => x.trim())
|
||||
const macX64Sha = await $`sha256sum ./dist/opencode-darwin-x64.zip | cut -d' ' -f1`
|
||||
.text()
|
||||
.then((x) => x.trim())
|
||||
const macArm64Sha = await $`sha256sum ./dist/opencode-darwin-arm64.zip | cut -d' ' -f1`
|
||||
.text()
|
||||
.then((x) => x.trim())
|
||||
const arm64Sha = await $`sha256sum ./dist/opencode-linux-arm64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
|
||||
const x64Sha = await $`sha256sum ./dist/opencode-linux-x64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
|
||||
const macX64Sha = await $`sha256sum ./dist/opencode-darwin-x64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
|
||||
const macArm64Sha = await $`sha256sum ./dist/opencode-darwin-arm64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
|
||||
|
||||
const [pkgver, _subver = ""] = Script.version.split(/(-.*)/, 2)
|
||||
|
||||
|
||||
@@ -19,23 +19,12 @@ const result = z.toJSONSchema(Config.Info, {
|
||||
const schema = ctx.jsonSchema
|
||||
|
||||
// Preserve strictness: set additionalProperties: false for objects
|
||||
if (
|
||||
schema &&
|
||||
typeof schema === "object" &&
|
||||
schema.type === "object" &&
|
||||
schema.additionalProperties === undefined
|
||||
) {
|
||||
if (schema && typeof schema === "object" && schema.type === "object" && schema.additionalProperties === undefined) {
|
||||
schema.additionalProperties = false
|
||||
}
|
||||
|
||||
// Add examples and default descriptions for string fields with defaults
|
||||
if (
|
||||
schema &&
|
||||
typeof schema === "object" &&
|
||||
"type" in schema &&
|
||||
schema.type === "string" &&
|
||||
schema?.default
|
||||
) {
|
||||
if (schema && typeof schema === "object" && "type" in schema && schema.type === "string" && schema?.default) {
|
||||
if (!schema.examples) {
|
||||
schema.examples = [schema.default]
|
||||
}
|
||||
|
||||
@@ -199,10 +199,8 @@ export namespace ACP {
|
||||
|
||||
if (kind === "edit") {
|
||||
const input = part.state.input
|
||||
const filePath =
|
||||
typeof input["filePath"] === "string" ? input["filePath"] : ""
|
||||
const oldText =
|
||||
typeof input["oldString"] === "string" ? input["oldString"] : ""
|
||||
const filePath = typeof input["filePath"] === "string" ? input["filePath"] : ""
|
||||
const oldText = typeof input["oldString"] === "string" ? input["oldString"] : ""
|
||||
const newText =
|
||||
typeof input["newString"] === "string"
|
||||
? input["newString"]
|
||||
@@ -218,9 +216,7 @@ export namespace ACP {
|
||||
}
|
||||
|
||||
if (part.tool === "todowrite") {
|
||||
const parsedTodos = z
|
||||
.array(Todo.Info)
|
||||
.safeParse(JSON.parse(part.state.output))
|
||||
const parsedTodos = z.array(Todo.Info).safeParse(JSON.parse(part.state.output))
|
||||
if (parsedTodos.success) {
|
||||
await this.connection
|
||||
.sessionUpdate({
|
||||
@@ -229,9 +225,7 @@ export namespace ACP {
|
||||
sessionUpdate: "plan",
|
||||
entries: parsedTodos.data.map((todo) => {
|
||||
const status: PlanEntry["status"] =
|
||||
todo.status === "cancelled"
|
||||
? "completed"
|
||||
: (todo.status as PlanEntry["status"])
|
||||
todo.status === "cancelled" ? "completed" : (todo.status as PlanEntry["status"])
|
||||
return {
|
||||
priority: "medium",
|
||||
status,
|
||||
@@ -481,8 +475,7 @@ export namespace ACP {
|
||||
description: agent.description,
|
||||
}))
|
||||
|
||||
const currentModeId =
|
||||
availableModes.find((m) => m.name === "build")?.id ?? availableModes[0].id
|
||||
const currentModeId = availableModes.find((m) => m.name === "build")?.id ?? availableModes[0].id
|
||||
|
||||
const mcpServers: Record<string, Config.Mcp> = {}
|
||||
for (const server of params.mcpServers) {
|
||||
@@ -587,8 +580,7 @@ export namespace ACP {
|
||||
const agent = session.modeId ?? "build"
|
||||
|
||||
const parts: Array<
|
||||
| { type: "text"; text: string }
|
||||
| { type: "file"; url: string; filename: string; mime: string }
|
||||
{ type: "text"; text: string } | { type: "file"; url: string; filename: string; mime: string }
|
||||
> = []
|
||||
for (const part of params.prompt) {
|
||||
switch (part.type) {
|
||||
@@ -794,9 +786,7 @@ export namespace ACP {
|
||||
|
||||
function parseUri(
|
||||
uri: string,
|
||||
):
|
||||
| { type: "file"; url: string; filename: string; mime: string }
|
||||
| { type: "text"; text: string } {
|
||||
): { type: "file"; url: string; filename: string; mime: string } | { type: "text"; text: string } {
|
||||
try {
|
||||
if (uri.startsWith("file://")) {
|
||||
const path = uri.slice(7)
|
||||
|
||||
@@ -13,11 +13,7 @@ export class ACPSessionManager {
|
||||
this.sdk = sdk
|
||||
}
|
||||
|
||||
async create(
|
||||
cwd: string,
|
||||
mcpServers: McpServer[],
|
||||
model?: ACPSessionState["model"],
|
||||
): Promise<ACPSessionState> {
|
||||
async create(cwd: string, mcpServers: McpServer[], model?: ACPSessionState["model"]): Promise<ACPSessionState> {
|
||||
const session = await this.sdk.session
|
||||
.create({
|
||||
body: {
|
||||
|
||||
@@ -143,18 +143,7 @@ export namespace Agent {
|
||||
tools: {},
|
||||
builtIn: false,
|
||||
}
|
||||
const {
|
||||
name,
|
||||
model,
|
||||
prompt,
|
||||
tools,
|
||||
description,
|
||||
temperature,
|
||||
top_p,
|
||||
mode,
|
||||
permission,
|
||||
...extra
|
||||
} = value
|
||||
const { name, model, prompt, tools, description, temperature, top_p, mode, permission, ...extra } = value
|
||||
item.options = {
|
||||
...item.options,
|
||||
...extra,
|
||||
@@ -223,10 +212,7 @@ export namespace Agent {
|
||||
}
|
||||
}
|
||||
|
||||
function mergeAgentPermissions(
|
||||
basePermission: any,
|
||||
overridePermission: any,
|
||||
): Agent.Info["permission"] {
|
||||
function mergeAgentPermissions(basePermission: any, overridePermission: any): Agent.Info["permission"] {
|
||||
if (typeof basePermission.bash === "string") {
|
||||
basePermission.bash = {
|
||||
"*": basePermission.bash,
|
||||
|
||||
@@ -8,10 +8,7 @@ import { readableStreamToText } from "bun"
|
||||
export namespace BunProc {
|
||||
const log = Log.create({ service: "bun" })
|
||||
|
||||
export async function run(
|
||||
cmd: string[],
|
||||
options?: Bun.SpawnOptions.OptionsObject<any, any, any>,
|
||||
) {
|
||||
export async function run(cmd: string[], options?: Bun.SpawnOptions.OptionsObject<any, any, any>) {
|
||||
log.info("running", {
|
||||
cmd: [which(), ...cmd],
|
||||
...options,
|
||||
|
||||
@@ -19,10 +19,7 @@ export namespace Bus {
|
||||
|
||||
const registry = new Map<string, EventDefinition>()
|
||||
|
||||
export function event<Type extends string, Properties extends ZodType>(
|
||||
type: Type,
|
||||
properties: Properties,
|
||||
) {
|
||||
export function event<Type extends string, Properties extends ZodType>(type: Type, properties: Properties) {
|
||||
const result = {
|
||||
type,
|
||||
properties,
|
||||
@@ -73,10 +70,7 @@ export namespace Bus {
|
||||
|
||||
export function subscribe<Definition extends EventDefinition>(
|
||||
def: Definition,
|
||||
callback: (event: {
|
||||
type: Definition["type"]
|
||||
properties: z.infer<Definition["properties"]>
|
||||
}) => void,
|
||||
callback: (event: { type: Definition["type"]; properties: z.infer<Definition["properties"]> }) => void,
|
||||
) {
|
||||
return raw(def.type, callback)
|
||||
}
|
||||
|
||||
@@ -14,11 +14,7 @@ export const AuthCommand = cmd({
|
||||
command: "auth",
|
||||
describe: "manage credentials",
|
||||
builder: (yargs) =>
|
||||
yargs
|
||||
.command(AuthLoginCommand)
|
||||
.command(AuthLogoutCommand)
|
||||
.command(AuthListCommand)
|
||||
.demandCommand(),
|
||||
yargs.command(AuthLoginCommand).command(AuthLogoutCommand).command(AuthListCommand).demandCommand(),
|
||||
async handler() {},
|
||||
})
|
||||
|
||||
@@ -64,9 +60,7 @@ export const AuthListCommand = cmd({
|
||||
prompts.log.info(`${provider} ${UI.Style.TEXT_DIM}${envVar}`)
|
||||
}
|
||||
|
||||
prompts.outro(
|
||||
`${activeEnvVars.length} environment variable` + (activeEnvVars.length === 1 ? "" : "s"),
|
||||
)
|
||||
prompts.outro(`${activeEnvVars.length} environment variable` + (activeEnvVars.length === 1 ? "" : "s"))
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -86,9 +80,7 @@ export const AuthLoginCommand = cmd({
|
||||
UI.empty()
|
||||
prompts.intro("Add credential")
|
||||
if (args.url) {
|
||||
const wellknown = await fetch(`${args.url}/.well-known/opencode`).then(
|
||||
(x) => x.json() as any,
|
||||
)
|
||||
const wellknown = await fetch(`${args.url}/.well-known/opencode`).then((x) => x.json() as any)
|
||||
prompts.log.info(`Running \`${wellknown.auth.command.join(" ")}\``)
|
||||
const proc = Bun.spawn({
|
||||
cmd: wellknown.auth.command,
|
||||
@@ -290,8 +282,7 @@ export const AuthLoginCommand = cmd({
|
||||
if (provider === "other") {
|
||||
provider = await prompts.text({
|
||||
message: "Enter provider id",
|
||||
validate: (x) =>
|
||||
x && x.match(/^[0-9a-z-]+$/) ? undefined : "a-z, 0-9 and hyphens only",
|
||||
validate: (x) => (x && x.match(/^[0-9a-z-]+$/) ? undefined : "a-z, 0-9 and hyphens only"),
|
||||
})
|
||||
if (prompts.isCancel(provider)) throw new UI.CancelledError()
|
||||
provider = provider.replace(/^@ai-sdk\//, "")
|
||||
|
||||
@@ -7,11 +7,7 @@ import { EOL } from "os"
|
||||
export const LSPCommand = cmd({
|
||||
command: "lsp",
|
||||
builder: (yargs) =>
|
||||
yargs
|
||||
.command(DiagnosticsCommand)
|
||||
.command(SymbolsCommand)
|
||||
.command(DocumentSymbolsCommand)
|
||||
.demandCommand(),
|
||||
yargs.command(DiagnosticsCommand).command(SymbolsCommand).command(DocumentSymbolsCommand).demandCommand(),
|
||||
async handler() {},
|
||||
})
|
||||
|
||||
|
||||
@@ -6,8 +6,7 @@ import { cmd } from "../cmd"
|
||||
|
||||
export const RipgrepCommand = cmd({
|
||||
command: "rg",
|
||||
builder: (yargs) =>
|
||||
yargs.command(TreeCommand).command(FilesCommand).command(SearchCommand).demandCommand(),
|
||||
builder: (yargs) => yargs.command(TreeCommand).command(FilesCommand).command(SearchCommand).demandCommand(),
|
||||
async handler() {},
|
||||
})
|
||||
|
||||
@@ -19,9 +18,7 @@ const TreeCommand = cmd({
|
||||
}),
|
||||
async handler(args) {
|
||||
await bootstrap(process.cwd(), async () => {
|
||||
process.stdout.write(
|
||||
(await Ripgrep.tree({ cwd: Instance.directory, limit: args.limit })) + EOL,
|
||||
)
|
||||
process.stdout.write((await Ripgrep.tree({ cwd: Instance.directory, limit: args.limit })) + EOL)
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -4,8 +4,7 @@ import { cmd } from "../cmd"
|
||||
|
||||
export const SnapshotCommand = cmd({
|
||||
command: "snapshot",
|
||||
builder: (yargs) =>
|
||||
yargs.command(TrackCommand).command(PatchCommand).command(DiffCommand).demandCommand(),
|
||||
builder: (yargs) => yargs.command(TrackCommand).command(PatchCommand).command(DiffCommand).demandCommand(),
|
||||
async handler() {},
|
||||
})
|
||||
|
||||
|
||||
@@ -189,9 +189,7 @@ export const GithubInstallCommand = cmd({
|
||||
async function getAppInfo() {
|
||||
const project = Instance.project
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -204,13 +202,9 @@ export const GithubInstallCommand = cmd({
|
||||
// ie. git@github.com:sst/opencode
|
||||
// ie. ssh://git@github.com/sst/opencode.git
|
||||
// ie. ssh://git@github.com/sst/opencode
|
||||
const parsed = info.match(
|
||||
/^(?:(?:https?|ssh):\/\/)?(?:git@)?github\.com[:/]([^/]+)\/([^/.]+?)(?:\.git)?$/,
|
||||
)
|
||||
const parsed = info.match(/^(?:(?:https?|ssh):\/\/)?(?:git@)?github\.com[:/]([^/]+)\/([^/.]+?)(?:\.git)?$/)
|
||||
if (!parsed) {
|
||||
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()
|
||||
}
|
||||
const [, owner, repo] = parsed
|
||||
@@ -451,9 +445,7 @@ export const GithubRunCommand = cmd({
|
||||
const summary = await summarize(response)
|
||||
await pushToLocalBranch(summary)
|
||||
}
|
||||
const hasShared = prData.comments.nodes.some((c) =>
|
||||
c.body.includes(`${shareBaseUrl}/s/${shareId}`),
|
||||
)
|
||||
const hasShared = prData.comments.nodes.some((c) => c.body.includes(`${shareBaseUrl}/s/${shareId}`))
|
||||
await updateComment(`${response}${footer({ image: !hasShared })}`)
|
||||
}
|
||||
// Fork PR
|
||||
@@ -465,9 +457,7 @@ export const GithubRunCommand = cmd({
|
||||
const summary = await summarize(response)
|
||||
await pushToForkBranch(summary, prData)
|
||||
}
|
||||
const hasShared = prData.comments.nodes.some((c) =>
|
||||
c.body.includes(`${shareBaseUrl}/s/${shareId}`),
|
||||
)
|
||||
const hasShared = prData.comments.nodes.some((c) => c.body.includes(`${shareBaseUrl}/s/${shareId}`))
|
||||
await updateComment(`${response}${footer({ image: !hasShared })}`)
|
||||
}
|
||||
}
|
||||
@@ -557,12 +547,8 @@ export const GithubRunCommand = cmd({
|
||||
// ie. <img alt="Image" src="https://github.com/user-attachments/assets/xxxx" />
|
||||
// ie. [api.json](https://github.com/user-attachments/files/21433810/api.json)
|
||||
// ie. 
|
||||
const mdMatches = prompt.matchAll(
|
||||
/!?\[.*?\]\((https:\/\/github\.com\/user-attachments\/[^)]+)\)/gi,
|
||||
)
|
||||
const tagMatches = prompt.matchAll(
|
||||
/<img .*?src="(https:\/\/github\.com\/user-attachments\/[^"]+)" \/>/gi,
|
||||
)
|
||||
const mdMatches = prompt.matchAll(/!?\[.*?\]\((https:\/\/github\.com\/user-attachments\/[^)]+)\)/gi)
|
||||
const tagMatches = prompt.matchAll(/<img .*?src="(https:\/\/github\.com\/user-attachments\/[^"]+)" \/>/gi)
|
||||
const matches = [...mdMatches, ...tagMatches].sort((a, b) => a.index - b.index)
|
||||
console.log("Images", JSON.stringify(matches, null, 2))
|
||||
|
||||
@@ -587,10 +573,7 @@ export const GithubRunCommand = cmd({
|
||||
|
||||
// Replace img tag with file path, ie. @image.png
|
||||
const replacement = `@${filename}`
|
||||
prompt =
|
||||
prompt.slice(0, start + offset) +
|
||||
replacement +
|
||||
prompt.slice(start + offset + tag.length)
|
||||
prompt = prompt.slice(0, start + offset) + replacement + prompt.slice(start + offset + tag.length)
|
||||
offset += replacement.length - tag.length
|
||||
|
||||
const contentType = res.headers.get("content-type")
|
||||
@@ -873,8 +856,7 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
|
||||
throw new Error(`Failed to check permissions for user ${actor}: ${error}`)
|
||||
}
|
||||
|
||||
if (!["admin", "write"].includes(permission))
|
||||
throw new Error(`User ${actor} does not have write permissions`)
|
||||
if (!["admin", "write"].includes(permission)) throw new Error(`User ${actor} does not have write permissions`)
|
||||
}
|
||||
|
||||
async function createComment() {
|
||||
@@ -922,9 +904,7 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
|
||||
|
||||
return `<a href="${shareBaseUrl}/s/${shareId}"><img width="200" alt="${titleAlt}" src="https://social-cards.sst.dev/opencode-share/${title64}.png?model=${providerID}/${modelID}&version=${session.version}&id=${shareId}" /></a>\n`
|
||||
})()
|
||||
const shareUrl = shareId
|
||||
? `[opencode session](${shareBaseUrl}/s/${shareId}) | `
|
||||
: ""
|
||||
const shareUrl = shareId ? `[opencode session](${shareBaseUrl}/s/${shareId}) | ` : ""
|
||||
return `\n\n${image}${shareUrl}[github run](${runUrl})`
|
||||
}
|
||||
|
||||
@@ -1100,13 +1080,9 @@ query($owner: String!, $repo: String!, $number: Int!) {
|
||||
})
|
||||
.map((c) => `- ${c.author.login} at ${c.createdAt}: ${c.body}`)
|
||||
|
||||
const files = (pr.files.nodes || []).map(
|
||||
(f) => `- ${f.path} (${f.changeType}) +${f.additions}/-${f.deletions}`,
|
||||
)
|
||||
const files = (pr.files.nodes || []).map((f) => `- ${f.path} (${f.changeType}) +${f.additions}/-${f.deletions}`)
|
||||
const reviewData = (pr.reviews.nodes || []).map((r) => {
|
||||
const comments = (r.comments.nodes || []).map(
|
||||
(c) => ` - ${c.path}:${c.line ?? "?"}: ${c.body}`,
|
||||
)
|
||||
const comments = (r.comments.nodes || []).map((c) => ` - ${c.path}:${c.line ?? "?"}: ${c.body}`)
|
||||
return [
|
||||
`- ${r.author.login} at ${r.submittedAt}:`,
|
||||
` - Review body: ${r.body}`,
|
||||
@@ -1128,15 +1104,9 @@ query($owner: String!, $repo: String!, $number: Int!) {
|
||||
`Deletions: ${pr.deletions}`,
|
||||
`Total Commits: ${pr.commits.totalCount}`,
|
||||
`Changed Files: ${pr.files.nodes.length} files`,
|
||||
...(comments.length > 0
|
||||
? ["<pull_request_comments>", ...comments, "</pull_request_comments>"]
|
||||
: []),
|
||||
...(files.length > 0
|
||||
? ["<pull_request_changed_files>", ...files, "</pull_request_changed_files>"]
|
||||
: []),
|
||||
...(reviewData.length > 0
|
||||
? ["<pull_request_reviews>", ...reviewData, "</pull_request_reviews>"]
|
||||
: []),
|
||||
...(comments.length > 0 ? ["<pull_request_comments>", ...comments, "</pull_request_comments>"] : []),
|
||||
...(files.length > 0 ? ["<pull_request_changed_files>", ...files, "</pull_request_changed_files>"] : []),
|
||||
...(reviewData.length > 0 ? ["<pull_request_reviews>", ...reviewData, "</pull_request_reviews>"] : []),
|
||||
"</pull_request>",
|
||||
].join("\n")
|
||||
}
|
||||
|
||||
@@ -138,9 +138,7 @@ export const RunCommand = cmd({
|
||||
|
||||
const outputJsonEvent = (type: string, data: any) => {
|
||||
if (args.format === "json") {
|
||||
process.stdout.write(
|
||||
JSON.stringify({ type, timestamp: Date.now(), sessionID, ...data }) + EOL,
|
||||
)
|
||||
process.stdout.write(JSON.stringify({ type, timestamp: Date.now(), sessionID, ...data }) + EOL)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -160,9 +158,7 @@ export const RunCommand = cmd({
|
||||
const [tool, color] = TOOL[part.tool] ?? [part.tool, UI.Style.TEXT_INFO_BOLD]
|
||||
const title =
|
||||
part.state.title ||
|
||||
(Object.keys(part.state.input).length > 0
|
||||
? JSON.stringify(part.state.input)
|
||||
: "Unknown")
|
||||
(Object.keys(part.state.input).length > 0 ? JSON.stringify(part.state.input) : "Unknown")
|
||||
printEvent(color, tool, title)
|
||||
if (part.tool === "bash" && part.state.output?.trim()) {
|
||||
UI.println()
|
||||
@@ -215,10 +211,7 @@ export const RunCommand = cmd({
|
||||
],
|
||||
initialValue: "once",
|
||||
}).catch(() => "reject")
|
||||
const response = (result.toString().includes("cancel") ? "reject" : result) as
|
||||
| "once"
|
||||
| "always"
|
||||
| "reject"
|
||||
const response = (result.toString().includes("cancel") ? "reject" : result) as "once" | "always" | "reject"
|
||||
await sdk.postSessionIdPermissionsPermissionId({
|
||||
path: { id: sessionID, permissionID: permission.id },
|
||||
body: { response },
|
||||
@@ -280,10 +273,7 @@ export const RunCommand = cmd({
|
||||
}
|
||||
|
||||
const cfgResult = await sdk.config.get()
|
||||
if (
|
||||
cfgResult.data &&
|
||||
(cfgResult.data.share === "auto" || Flag.OPENCODE_AUTO_SHARE || args.share)
|
||||
) {
|
||||
if (cfgResult.data && (cfgResult.data.share === "auto" || Flag.OPENCODE_AUTO_SHARE || args.share)) {
|
||||
const shareResult = await sdk.session.share({ path: { id: sessionID } }).catch((error) => {
|
||||
if (error instanceof Error && error.message.includes("disabled")) {
|
||||
UI.println(UI.Style.TEXT_DANGER_BOLD + "! " + error.message)
|
||||
@@ -336,10 +326,7 @@ export const RunCommand = cmd({
|
||||
}
|
||||
|
||||
const cfgResult = await sdk.config.get()
|
||||
if (
|
||||
cfgResult.data &&
|
||||
(cfgResult.data.share === "auto" || Flag.OPENCODE_AUTO_SHARE || args.share)
|
||||
) {
|
||||
if (cfgResult.data && (cfgResult.data.share === "auto" || Flag.OPENCODE_AUTO_SHARE || args.share)) {
|
||||
const shareResult = await sdk.session.share({ path: { id: sessionID } }).catch((error) => {
|
||||
if (error instanceof Error && error.message.includes("disabled")) {
|
||||
UI.println(UI.Style.TEXT_DANGER_BOLD + "! " + error.message)
|
||||
|
||||
@@ -68,9 +68,7 @@ async function getAllSessions(): Promise<Session.Info[]> {
|
||||
if (!project) continue
|
||||
|
||||
const sessionKeys = await Storage.list(["session", project.id])
|
||||
const projectSessions = await Promise.all(
|
||||
sessionKeys.map((key) => Storage.read<Session.Info>(key)),
|
||||
)
|
||||
const projectSessions = await Promise.all(sessionKeys.map((key) => Storage.read<Session.Info>(key)))
|
||||
|
||||
for (const session of projectSessions) {
|
||||
if (session) {
|
||||
@@ -87,16 +85,12 @@ async function aggregateSessionStats(days?: number, projectFilter?: string): Pro
|
||||
const DAYS_IN_SECOND = 24 * 60 * 60 * 1000
|
||||
const cutoffTime = days ? Date.now() - days * DAYS_IN_SECOND : 0
|
||||
|
||||
let filteredSessions = days
|
||||
? sessions.filter((session) => session.time.updated >= cutoffTime)
|
||||
: sessions
|
||||
let filteredSessions = days ? sessions.filter((session) => session.time.updated >= cutoffTime) : sessions
|
||||
|
||||
if (projectFilter !== undefined) {
|
||||
if (projectFilter === "") {
|
||||
const currentProject = await getCurrentProject()
|
||||
filteredSessions = filteredSessions.filter(
|
||||
(session) => session.projectID === currentProject.id,
|
||||
)
|
||||
filteredSessions = filteredSessions.filter((session) => session.projectID === currentProject.id)
|
||||
} else {
|
||||
filteredSessions = filteredSessions.filter((session) => session.projectID === projectFilter)
|
||||
}
|
||||
@@ -125,9 +119,7 @@ async function aggregateSessionStats(days?: number, projectFilter?: string): Pro
|
||||
}
|
||||
|
||||
if (filteredSessions.length > 1000) {
|
||||
console.log(
|
||||
`Large dataset detected (${filteredSessions.length} sessions). This may take a while...`,
|
||||
)
|
||||
console.log(`Large dataset detected (${filteredSessions.length} sessions). This may take a while...`)
|
||||
}
|
||||
|
||||
if (filteredSessions.length === 0) {
|
||||
@@ -262,8 +254,7 @@ export function displayStats(stats: SessionStats, toolLimit?: number) {
|
||||
const percentage = ((count / totalToolUsage) * 100).toFixed(1)
|
||||
|
||||
const maxToolLength = 18
|
||||
const truncatedTool =
|
||||
tool.length > maxToolLength ? tool.substring(0, maxToolLength - 2) + ".." : tool
|
||||
const truncatedTool = tool.length > maxToolLength ? tool.substring(0, maxToolLength - 2) + ".." : tool
|
||||
const toolName = truncatedTool.padEnd(maxToolLength)
|
||||
|
||||
const content = ` ${toolName} ${bar.padEnd(20)} ${count.toString().padStart(3)} (${percentage.padStart(4)}%)`
|
||||
|
||||
@@ -115,11 +115,7 @@ export function tui(input: {
|
||||
render(
|
||||
() => {
|
||||
return (
|
||||
<ErrorBoundary
|
||||
fallback={(error, reset) => (
|
||||
<ErrorComponent error={error} reset={reset} onExit={onExit} />
|
||||
)}
|
||||
>
|
||||
<ErrorBoundary fallback={(error, reset) => <ErrorComponent error={error} reset={reset} onExit={onExit} />}>
|
||||
<ExitProvider onExit={onExit}>
|
||||
<KVProvider>
|
||||
<ToastProvider>
|
||||
@@ -413,12 +409,7 @@ function App() {
|
||||
flexShrink={0}
|
||||
>
|
||||
<box flexDirection="row">
|
||||
<box
|
||||
flexDirection="row"
|
||||
backgroundColor={theme.backgroundElement}
|
||||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
>
|
||||
<box flexDirection="row" backgroundColor={theme.backgroundElement} paddingLeft={1} paddingRight={1}>
|
||||
<text fg={theme.textMuted}>open</text>
|
||||
<text fg={theme.text} attributes={TextAttributes.BOLD}>
|
||||
code{" "}
|
||||
@@ -434,11 +425,7 @@ function App() {
|
||||
tab
|
||||
</text>
|
||||
<text fg={local.agent.color(local.agent.current().name)}>{""}</text>
|
||||
<text
|
||||
bg={local.agent.color(local.agent.current().name)}
|
||||
fg={theme.background}
|
||||
wrapMode={undefined}
|
||||
>
|
||||
<text bg={local.agent.color(local.agent.current().name)} fg={theme.background} wrapMode={undefined}>
|
||||
<span style={{ bold: true }}> {local.agent.current().name.toUpperCase()}</span>
|
||||
<span> AGENT </span>
|
||||
</text>
|
||||
|
||||
@@ -52,11 +52,7 @@ export function DialogModel() {
|
||||
description: provider.name,
|
||||
category: provider.name,
|
||||
})),
|
||||
filter(
|
||||
(x) =>
|
||||
Boolean(ref()?.filter) ||
|
||||
!local.model.recent().find((y) => isDeepEqual(y, x.value)),
|
||||
),
|
||||
filter((x) => Boolean(ref()?.filter) || !local.model.recent().find((y) => isDeepEqual(y, x.value))),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -20,9 +20,7 @@ export function DialogSessionList() {
|
||||
|
||||
const deleteKeybind = "ctrl+d"
|
||||
|
||||
const currentSessionID = createMemo(() =>
|
||||
route.data.type === "session" ? route.data.sessionID : undefined,
|
||||
)
|
||||
const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined))
|
||||
|
||||
const options = createMemo(() => {
|
||||
const today = new Date().toDateString()
|
||||
|
||||
@@ -77,10 +77,7 @@ export function DialogStatus() {
|
||||
</For>
|
||||
</box>
|
||||
)}
|
||||
<Show
|
||||
when={enabledFormatters().length > 0}
|
||||
fallback={<text fg={theme.text}>No Formatters</text>}
|
||||
>
|
||||
<Show when={enabledFormatters().length > 0} fallback={<text fg={theme.text}>No Formatters</text>}>
|
||||
<box>
|
||||
<text fg={theme.text}>{enabledFormatters().length} Formatters</text>
|
||||
<For each={enabledFormatters()}>
|
||||
|
||||
@@ -3,19 +3,9 @@ import { TextAttributes } from "@opentui/core"
|
||||
import { For } from "solid-js"
|
||||
import { useTheme } from "@tui/context/theme"
|
||||
|
||||
const LOGO_LEFT = [
|
||||
` `,
|
||||
`█▀▀█ █▀▀█ █▀▀█ █▀▀▄`,
|
||||
`█░░█ █░░█ █▀▀▀ █░░█`,
|
||||
`▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀`,
|
||||
]
|
||||
const LOGO_LEFT = [` `, `█▀▀█ █▀▀█ █▀▀█ █▀▀▄`, `█░░█ █░░█ █▀▀▀ █░░█`, `▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀`]
|
||||
|
||||
const LOGO_RIGHT = [
|
||||
` ▄ `,
|
||||
`█▀▀▀ █▀▀█ █▀▀█ █▀▀█`,
|
||||
`█░░░ █░░█ █░░█ █▀▀▀`,
|
||||
`▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀`,
|
||||
]
|
||||
const LOGO_RIGHT = [` ▄ `, `█▀▀▀ █▀▀█ █▀▀█ █▀▀█`, `█░░░ █░░█ █░░█ █▀▀▀`, `▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀`]
|
||||
|
||||
export function Logo() {
|
||||
const { theme } = useTheme()
|
||||
|
||||
@@ -83,12 +83,7 @@ export function Autocomplete(props: {
|
||||
const extmarkStart = store.index
|
||||
const extmarkEnd = extmarkStart + Bun.stringWidth(virtualText)
|
||||
|
||||
const styleId =
|
||||
part.type === "file"
|
||||
? props.fileStyleId
|
||||
: part.type === "agent"
|
||||
? props.agentStyleId
|
||||
: undefined
|
||||
const styleId = part.type === "file" ? props.fileStyleId : part.type === "agent" ? props.agentStyleId : undefined
|
||||
|
||||
const extmarkId = input.extmarks.create({
|
||||
start: extmarkStart,
|
||||
@@ -185,9 +180,7 @@ export function Autocomplete(props: {
|
||||
)
|
||||
})
|
||||
|
||||
const session = createMemo(() =>
|
||||
props.sessionID ? sync.session.get(props.sessionID) : undefined,
|
||||
)
|
||||
const session = createMemo(() => (props.sessionID ? sync.session.get(props.sessionID) : undefined))
|
||||
const commands = createMemo((): AutocompleteOption[] => {
|
||||
const results: AutocompleteOption[] = []
|
||||
const s = session()
|
||||
@@ -324,9 +317,7 @@ export function Autocomplete(props: {
|
||||
|
||||
const options = createMemo(() => {
|
||||
const mixed: AutocompleteOption[] = (
|
||||
store.visible === "@"
|
||||
? [...agents(), ...(files.loading ? files.latest || [] : files())]
|
||||
: [...commands()]
|
||||
store.visible === "@" ? [...agents(), ...(files.loading ? files.latest || [] : files())] : [...commands()]
|
||||
).filter((x) => x.disabled !== true)
|
||||
const currentFilter = filter()
|
||||
if (!currentFilter) return mixed.slice(0, 10)
|
||||
@@ -393,9 +384,7 @@ export function Autocomplete(props: {
|
||||
return
|
||||
}
|
||||
// Check if a space was typed after the trigger character
|
||||
const currentText = props
|
||||
.input()
|
||||
.getTextRange(store.index + 1, props.input().cursorOffset + 1)
|
||||
const currentText = props.input().getTextRange(store.index + 1, props.input().cursorOffset + 1)
|
||||
if (currentText.includes(" ")) {
|
||||
hide()
|
||||
}
|
||||
@@ -433,13 +422,8 @@ export function Autocomplete(props: {
|
||||
if (e.name === "@") {
|
||||
const cursorOffset = props.input().cursorOffset
|
||||
const charBeforeCursor =
|
||||
cursorOffset === 0
|
||||
? undefined
|
||||
: props.input().getTextRange(cursorOffset - 1, cursorOffset)
|
||||
const canTrigger =
|
||||
charBeforeCursor === undefined ||
|
||||
charBeforeCursor === "" ||
|
||||
/\s/.test(charBeforeCursor)
|
||||
cursorOffset === 0 ? undefined : props.input().getTextRange(cursorOffset - 1, cursorOffset)
|
||||
const canTrigger = charBeforeCursor === undefined || charBeforeCursor === "" || /\s/.test(charBeforeCursor)
|
||||
if (canTrigger) show("@")
|
||||
}
|
||||
|
||||
@@ -487,10 +471,7 @@ export function Autocomplete(props: {
|
||||
{option.display}
|
||||
</text>
|
||||
<Show when={option.description}>
|
||||
<text
|
||||
fg={index() === store.selected ? theme.background : theme.textMuted}
|
||||
wrapMode="none"
|
||||
>
|
||||
<text fg={index() === store.selected ? theme.background : theme.textMuted} wrapMode="none">
|
||||
{option.description}
|
||||
</text>
|
||||
</Show>
|
||||
|
||||
@@ -334,9 +334,7 @@ export function Prompt(props: PromptProps) {
|
||||
|
||||
// Expand pasted text inline before submitting
|
||||
const allExtmarks = input.extmarks.getAllForTypeId(promptPartTypeId)
|
||||
const sortedExtmarks = allExtmarks.sort(
|
||||
(a: { start: number }, b: { start: number }) => b.start - a.start,
|
||||
)
|
||||
const sortedExtmarks = allExtmarks.sort((a: { start: number }, b: { start: number }) => b.start - a.start)
|
||||
|
||||
for (const extmark of sortedExtmarks) {
|
||||
const partIndex = store.extmarkToPartIndex.get(extmark.id)
|
||||
@@ -499,28 +497,15 @@ export function Prompt(props: PromptProps) {
|
||||
<box
|
||||
flexDirection="row"
|
||||
{...SplitBorder}
|
||||
borderColor={
|
||||
keybind.leader ? theme.accent : store.mode === "shell" ? theme.secondary : theme.border
|
||||
}
|
||||
borderColor={keybind.leader ? theme.accent : store.mode === "shell" ? theme.secondary : theme.border}
|
||||
justifyContent="space-evenly"
|
||||
>
|
||||
<box
|
||||
backgroundColor={theme.backgroundElement}
|
||||
width={3}
|
||||
height="100%"
|
||||
alignItems="center"
|
||||
paddingTop={1}
|
||||
>
|
||||
<box backgroundColor={theme.backgroundElement} width={3} height="100%" alignItems="center" paddingTop={1}>
|
||||
<text attributes={TextAttributes.BOLD} fg={theme.primary}>
|
||||
{store.mode === "normal" ? ">" : "!"}
|
||||
</text>
|
||||
</box>
|
||||
<box
|
||||
paddingTop={1}
|
||||
paddingBottom={1}
|
||||
backgroundColor={theme.backgroundElement}
|
||||
flexGrow={1}
|
||||
>
|
||||
<box paddingTop={1} paddingBottom={1} backgroundColor={theme.backgroundElement} flexGrow={1}>
|
||||
<textarea
|
||||
placeholder={
|
||||
props.showPlaceholder
|
||||
@@ -575,10 +560,7 @@ export function Prompt(props: PromptProps) {
|
||||
return
|
||||
}
|
||||
if (store.mode === "shell") {
|
||||
if (
|
||||
(e.name === "backspace" && input.visualCursor.offset === 0) ||
|
||||
e.name === "escape"
|
||||
) {
|
||||
if ((e.name === "backspace" && input.visualCursor.offset === 0) || e.name === "escape") {
|
||||
setStore("mode", "normal")
|
||||
e.preventDefault()
|
||||
return
|
||||
@@ -588,8 +570,7 @@ export function Prompt(props: PromptProps) {
|
||||
if (!autocomplete.visible) {
|
||||
if (
|
||||
(keybind.match("history_previous", e) && input.cursorOffset === 0) ||
|
||||
(keybind.match("history_next", e) &&
|
||||
input.cursorOffset === input.plainText.length)
|
||||
(keybind.match("history_next", e) && input.cursorOffset === input.plainText.length)
|
||||
) {
|
||||
const direction = keybind.match("history_previous", e) ? -1 : 1
|
||||
const item = history.move(direction, input.plainText)
|
||||
@@ -605,12 +586,8 @@ export function Prompt(props: PromptProps) {
|
||||
return
|
||||
}
|
||||
|
||||
if (keybind.match("history_previous", e) && input.visualCursor.visualRow === 0)
|
||||
input.cursorOffset = 0
|
||||
if (
|
||||
keybind.match("history_next", e) &&
|
||||
input.visualCursor.visualRow === input.height - 1
|
||||
)
|
||||
if (keybind.match("history_previous", e) && input.visualCursor.visualRow === 0) input.cursorOffset = 0
|
||||
if (keybind.match("history_next", e) && input.visualCursor.visualRow === input.height - 1)
|
||||
input.cursorOffset = input.plainText.length
|
||||
}
|
||||
}}
|
||||
@@ -701,12 +678,7 @@ export function Prompt(props: PromptProps) {
|
||||
syntaxStyle={syntax()}
|
||||
/>
|
||||
</box>
|
||||
<box
|
||||
backgroundColor={theme.backgroundElement}
|
||||
width={1}
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
></box>
|
||||
<box backgroundColor={theme.backgroundElement} width={1} justifyContent="center" alignItems="center"></box>
|
||||
</box>
|
||||
<box flexDirection="row" justifyContent="space-between">
|
||||
<text flexShrink={0} wrapMode="none" fg={theme.text}>
|
||||
@@ -727,8 +699,7 @@ export function Prompt(props: PromptProps) {
|
||||
<Match when={props.hint}>{props.hint!}</Match>
|
||||
<Match when={true}>
|
||||
<text fg={theme.text}>
|
||||
{keybind.print("command_list")}{" "}
|
||||
<span style={{ fg: theme.textMuted }}>commands</span>
|
||||
{keybind.print("command_list")} <span style={{ fg: theme.textMuted }}>commands</span>
|
||||
</text>
|
||||
</Match>
|
||||
</Switch>
|
||||
|
||||
@@ -22,9 +22,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
return !!provider?.models[model.modelID]
|
||||
}
|
||||
|
||||
function getFirstValidModel(
|
||||
...modelFns: (() => { providerID: string; modelID: string } | undefined)[]
|
||||
) {
|
||||
function getFirstValidModel(...modelFns: (() => { providerID: string; modelID: string } | undefined)[]) {
|
||||
for (const modelFn of modelFns) {
|
||||
const model = modelFn()
|
||||
if (!model) continue
|
||||
@@ -213,9 +211,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
const current = currentModel()
|
||||
if (!current) return
|
||||
const recent = modelStore.recent
|
||||
const index = recent.findIndex(
|
||||
(x) => x.providerID === current.providerID && x.modelID === current.modelID,
|
||||
)
|
||||
const index = recent.findIndex((x) => x.providerID === current.providerID && x.modelID === current.modelID)
|
||||
if (index === -1) return
|
||||
let next = index + direction
|
||||
if (next < 0) next = recent.length - 1
|
||||
|
||||
@@ -146,12 +146,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
}
|
||||
const result = Binary.search(messages, event.properties.info.id, (m) => m.id)
|
||||
if (result.found) {
|
||||
setStore(
|
||||
"message",
|
||||
event.properties.info.sessionID,
|
||||
result.index,
|
||||
reconcile(event.properties.info),
|
||||
)
|
||||
setStore("message", event.properties.info.sessionID, result.index, reconcile(event.properties.info))
|
||||
break
|
||||
}
|
||||
setStore(
|
||||
@@ -186,12 +181,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
}
|
||||
const result = Binary.search(parts, event.properties.part.id, (p) => p.id)
|
||||
if (result.found) {
|
||||
setStore(
|
||||
"part",
|
||||
event.properties.part.messageID,
|
||||
result.index,
|
||||
reconcile(event.properties.part),
|
||||
)
|
||||
setStore("part", event.properties.part.messageID, result.index, reconcile(event.properties.part))
|
||||
break
|
||||
}
|
||||
setStore(
|
||||
|
||||
@@ -528,13 +528,7 @@ function generateSyntax(theme: Theme) {
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: [
|
||||
"variable.builtin",
|
||||
"type.builtin",
|
||||
"function.builtin",
|
||||
"module.builtin",
|
||||
"constant.builtin",
|
||||
],
|
||||
scope: ["variable.builtin", "type.builtin", "function.builtin", "module.builtin", "constant.builtin"],
|
||||
style: {
|
||||
foreground: theme.error,
|
||||
},
|
||||
|
||||
@@ -26,11 +26,7 @@ export function Home() {
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<span style={{ fg: theme.success }}>•</span>{" "}
|
||||
{Locale.pluralize(
|
||||
Object.values(sync.data.mcp).length,
|
||||
"{} mcp server",
|
||||
"{} mcp servers",
|
||||
)}
|
||||
{Locale.pluralize(Object.values(sync.data.mcp).length, "{} mcp server", "{} mcp servers")}
|
||||
</Match>
|
||||
</Switch>
|
||||
</text>
|
||||
@@ -39,14 +35,7 @@ export function Home() {
|
||||
)
|
||||
|
||||
return (
|
||||
<box
|
||||
flexGrow={1}
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
paddingLeft={2}
|
||||
paddingRight={2}
|
||||
gap={1}
|
||||
>
|
||||
<box flexGrow={1} justifyContent="center" alignItems="center" paddingLeft={2} paddingRight={2} gap={1}>
|
||||
<Logo />
|
||||
<box width={39}>
|
||||
<HelpRow keybind="command_list">Commands</HelpRow>
|
||||
|
||||
@@ -7,9 +7,7 @@ import { useRoute } from "@tui/context/route"
|
||||
export function DialogMessage(props: { messageID: string; sessionID: string }) {
|
||||
const sync = useSync()
|
||||
const sdk = useSDK()
|
||||
const message = createMemo(() =>
|
||||
sync.data.message[props.sessionID]?.find((x) => x.id === props.messageID),
|
||||
)
|
||||
const message = createMemo(() => sync.data.message[props.sessionID]?.find((x) => x.id === props.messageID))
|
||||
const route = useRoute()
|
||||
|
||||
return (
|
||||
|
||||
@@ -19,9 +19,7 @@ export function DialogTimeline(props: { sessionID: string; onMove: (messageID: s
|
||||
const result = [] as DialogSelectOption<string>[]
|
||||
for (const message of messages) {
|
||||
if (message.role !== "user") continue
|
||||
const part = (sync.data.part[message.id] ?? []).find(
|
||||
(x) => x.type === "text" && !x.synthetic,
|
||||
) as TextPart
|
||||
const part = (sync.data.part[message.id] ?? []).find((x) => x.type === "text" && !x.synthetic) as TextPart
|
||||
if (!part) continue
|
||||
result.push({
|
||||
title: part.text.replace(/\n/g, " "),
|
||||
@@ -35,11 +33,5 @@ export function DialogTimeline(props: { sessionID: string; onMove: (messageID: s
|
||||
return result
|
||||
})
|
||||
|
||||
return (
|
||||
<DialogSelect
|
||||
onMove={(option) => props.onMove(option.value)}
|
||||
title="Timeline"
|
||||
options={options()}
|
||||
/>
|
||||
)
|
||||
return <DialogSelect onMove={(option) => props.onMove(option.value)} title="Timeline" options={options()} />
|
||||
}
|
||||
|
||||
@@ -46,16 +46,10 @@ export function Header() {
|
||||
})
|
||||
|
||||
const context = createMemo(() => {
|
||||
const last = messages().findLast(
|
||||
(x) => x.role === "assistant" && x.tokens.output > 0,
|
||||
) as AssistantMessage
|
||||
const last = messages().findLast((x) => x.role === "assistant" && x.tokens.output > 0) as AssistantMessage
|
||||
if (!last) return
|
||||
const total =
|
||||
last.tokens.input +
|
||||
last.tokens.output +
|
||||
last.tokens.reasoning +
|
||||
last.tokens.cache.read +
|
||||
last.tokens.cache.write
|
||||
last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write
|
||||
const model = sync.data.provider.find((x) => x.id === last.providerID)?.models[last.modelID]
|
||||
let result = total.toLocaleString()
|
||||
if (model?.limit.context) {
|
||||
@@ -67,13 +61,7 @@ export function Header() {
|
||||
const { theme } = useTheme()
|
||||
|
||||
return (
|
||||
<box
|
||||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
{...SplitBorder}
|
||||
borderColor={theme.backgroundElement}
|
||||
flexShrink={0}
|
||||
>
|
||||
<box paddingLeft={1} paddingRight={1} {...SplitBorder} borderColor={theme.backgroundElement} flexShrink={0}>
|
||||
<Show
|
||||
when={shareEnabled()}
|
||||
fallback={
|
||||
|
||||
@@ -19,14 +19,7 @@ import { SplitBorder } from "@tui/component/border"
|
||||
import { useTheme } from "@tui/context/theme"
|
||||
import { BoxRenderable, ScrollBoxRenderable, addDefaultParsers } from "@opentui/core"
|
||||
import { Prompt, type PromptRef } from "@tui/component/prompt"
|
||||
import type {
|
||||
AssistantMessage,
|
||||
Part,
|
||||
ToolPart,
|
||||
UserMessage,
|
||||
TextPart,
|
||||
ReasoningPart,
|
||||
} from "@opencode-ai/sdk"
|
||||
import type { AssistantMessage, Part, ToolPart, UserMessage, TextPart, ReasoningPart } from "@opencode-ai/sdk"
|
||||
import { useLocal } from "@tui/context/local"
|
||||
import { Locale } from "@/util/locale"
|
||||
import type { Tool } from "@/tool/tool"
|
||||
@@ -41,13 +34,7 @@ import type { EditTool } from "@/tool/edit"
|
||||
import type { PatchTool } from "@/tool/patch"
|
||||
import type { WebFetchTool } from "@/tool/webfetch"
|
||||
import type { TaskTool } from "@/tool/task"
|
||||
import {
|
||||
useKeyboard,
|
||||
useRenderer,
|
||||
useTerminalDimensions,
|
||||
type BoxProps,
|
||||
type JSX,
|
||||
} from "@opentui/solid"
|
||||
import { useKeyboard, useRenderer, useTerminalDimensions, type BoxProps, type JSX } from "@opentui/solid"
|
||||
import { useSDK } from "@tui/context/sdk"
|
||||
import { useCommandDialog } from "@tui/component/dialog-command"
|
||||
import { Shimmer } from "@tui/ui/shimmer"
|
||||
@@ -653,14 +640,7 @@ export function Session() {
|
||||
conceal,
|
||||
}}
|
||||
>
|
||||
<box
|
||||
flexDirection="row"
|
||||
paddingBottom={1}
|
||||
paddingTop={1}
|
||||
paddingLeft={2}
|
||||
paddingRight={2}
|
||||
gap={2}
|
||||
>
|
||||
<box flexDirection="row" paddingBottom={1} paddingTop={1} paddingLeft={2} paddingRight={2} gap={2}>
|
||||
<box flexGrow={1} gap={1}>
|
||||
<Show when={session()}>
|
||||
<Show when={session().parentID}>
|
||||
@@ -675,19 +655,13 @@ export function Session() {
|
||||
paddingRight={2}
|
||||
>
|
||||
<text fg={theme.text}>
|
||||
Previous{" "}
|
||||
<span style={{ fg: theme.textMuted }}>
|
||||
{keybind.print("session_child_cycle_reverse")}
|
||||
</span>
|
||||
Previous <span style={{ fg: theme.textMuted }}>{keybind.print("session_child_cycle_reverse")}</span>
|
||||
</text>
|
||||
<text fg={theme.text}>
|
||||
<b>Viewing subagent session</b>
|
||||
</text>
|
||||
<text fg={theme.text}>
|
||||
<span style={{ fg: theme.textMuted }}>
|
||||
{keybind.print("session_child_cycle")}
|
||||
</span>{" "}
|
||||
Next
|
||||
<span style={{ fg: theme.textMuted }}>{keybind.print("session_child_cycle")}</span> Next
|
||||
</text>
|
||||
</box>
|
||||
</Show>
|
||||
@@ -743,18 +717,12 @@ export function Session() {
|
||||
paddingTop={1}
|
||||
paddingBottom={1}
|
||||
paddingLeft={2}
|
||||
backgroundColor={
|
||||
hover() ? theme.backgroundElement : theme.backgroundPanel
|
||||
}
|
||||
backgroundColor={hover() ? theme.backgroundElement : theme.backgroundPanel}
|
||||
>
|
||||
<text fg={theme.textMuted}>{revert()!.reverted.length} message reverted</text>
|
||||
<text fg={theme.textMuted}>
|
||||
{revert()!.reverted.length} message reverted
|
||||
</text>
|
||||
<text fg={theme.textMuted}>
|
||||
<span style={{ fg: theme.text }}>
|
||||
{keybind.print("messages_redo")}
|
||||
</span>{" "}
|
||||
or /redo to restore
|
||||
<span style={{ fg: theme.text }}>{keybind.print("messages_redo")}</span> or /redo to
|
||||
restore
|
||||
</text>
|
||||
<Show when={revert()!.diffFiles?.length}>
|
||||
<box marginTop={1}>
|
||||
@@ -763,16 +731,10 @@ export function Session() {
|
||||
<text>
|
||||
{file.filename}
|
||||
<Show when={file.additions > 0}>
|
||||
<span style={{ fg: theme.diffAdded }}>
|
||||
{" "}
|
||||
+{file.additions}
|
||||
</span>
|
||||
<span style={{ fg: theme.diffAdded }}> +{file.additions}</span>
|
||||
</Show>
|
||||
<Show when={file.deletions > 0}>
|
||||
<span style={{ fg: theme.diffRemoved }}>
|
||||
{" "}
|
||||
-{file.deletions}
|
||||
</span>
|
||||
<span style={{ fg: theme.diffRemoved }}> -{file.deletions}</span>
|
||||
</Show>
|
||||
</text>
|
||||
)}
|
||||
@@ -792,9 +754,7 @@ export function Session() {
|
||||
index={index()}
|
||||
onMouseUp={() => {
|
||||
if (renderer.getSelection()?.getSelectedText()) return
|
||||
dialog.replace(() => (
|
||||
<DialogMessage messageID={message.id} sessionID={route.sessionID} />
|
||||
))
|
||||
dialog.replace(() => <DialogMessage messageID={message.id} sessionID={route.sessionID} />)
|
||||
}}
|
||||
message={message as UserMessage}
|
||||
parts={sync.data.part[message.id] ?? []}
|
||||
@@ -850,9 +810,7 @@ function UserMessage(props: {
|
||||
index: number
|
||||
pending?: string
|
||||
}) {
|
||||
const text = createMemo(
|
||||
() => props.parts.flatMap((x) => (x.type === "text" && !x.synthetic ? [x] : []))[0],
|
||||
)
|
||||
const text = createMemo(() => props.parts.flatMap((x) => (x.type === "text" && !x.synthetic ? [x] : []))[0])
|
||||
const files = createMemo(() => props.parts.flatMap((x) => (x.type === "file" ? [x] : [])))
|
||||
const sync = useSync()
|
||||
const { theme } = useTheme()
|
||||
@@ -893,14 +851,8 @@ function UserMessage(props: {
|
||||
})
|
||||
return (
|
||||
<text fg={theme.text}>
|
||||
<span style={{ bg: bg(), fg: theme.background }}>
|
||||
{" "}
|
||||
{MIME_BADGE[file.mime] ?? file.mime}{" "}
|
||||
</span>
|
||||
<span style={{ bg: theme.backgroundElement, fg: theme.textMuted }}>
|
||||
{" "}
|
||||
{file.filename}{" "}
|
||||
</span>
|
||||
<span style={{ bg: bg(), fg: theme.background }}> {MIME_BADGE[file.mime] ?? file.mime} </span>
|
||||
<span style={{ bg: theme.backgroundElement, fg: theme.textMuted }}> {file.filename} </span>
|
||||
</text>
|
||||
)
|
||||
}}
|
||||
@@ -911,16 +863,9 @@ function UserMessage(props: {
|
||||
{sync.data.config.username ?? "You"}{" "}
|
||||
<Show
|
||||
when={queued()}
|
||||
fallback={
|
||||
<span style={{ fg: theme.textMuted }}>
|
||||
({Locale.time(props.message.time.created)})
|
||||
</span>
|
||||
}
|
||||
fallback={<span style={{ fg: theme.textMuted }}>({Locale.time(props.message.time.created)})</span>}
|
||||
>
|
||||
<span style={{ bg: theme.accent, fg: theme.backgroundPanel, bold: true }}>
|
||||
{" "}
|
||||
QUEUED{" "}
|
||||
</span>
|
||||
<span style={{ bg: theme.accent, fg: theme.backgroundPanel, bold: true }}> QUEUED </span>
|
||||
</Show>
|
||||
</text>
|
||||
</box>
|
||||
@@ -960,8 +905,7 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
|
||||
<Show
|
||||
when={
|
||||
!props.message.time.completed ||
|
||||
(props.last &&
|
||||
props.parts.some((item) => item.type === "step-finish" && item.reason === "tool-calls"))
|
||||
(props.last && props.parts.some((item) => item.type === "step-finish" && item.reason === "tool-calls"))
|
||||
}
|
||||
>
|
||||
<box
|
||||
@@ -973,9 +917,7 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
|
||||
customBorderChars={SplitBorder.customBorderChars}
|
||||
borderColor={theme.backgroundElement}
|
||||
>
|
||||
<text fg={local.agent.color(props.message.mode)}>
|
||||
{Locale.titlecase(props.message.mode)}
|
||||
</text>
|
||||
<text fg={local.agent.color(props.message.mode)}>{Locale.titlecase(props.message.mode)}</text>
|
||||
<Shimmer text={`${props.message.modelID}`} color={theme.text} />
|
||||
</box>
|
||||
</Show>
|
||||
@@ -987,9 +929,7 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
|
||||
>
|
||||
<box paddingLeft={3}>
|
||||
<text marginTop={1}>
|
||||
<span style={{ fg: local.agent.color(props.message.mode) }}>
|
||||
{Locale.titlecase(props.message.mode)}
|
||||
</span>{" "}
|
||||
<span style={{ fg: local.agent.color(props.message.mode) }}>{Locale.titlecase(props.message.mode)}</span>{" "}
|
||||
<span style={{ fg: theme.textMuted }}>{props.message.modelID}</span>
|
||||
</text>
|
||||
</box>
|
||||
@@ -1016,12 +956,7 @@ function ReasoningPart(props: { part: ReasoningPart; message: AssistantMessage }
|
||||
customBorderChars={SplitBorder.customBorderChars}
|
||||
borderColor={theme.backgroundPanel}
|
||||
>
|
||||
<box
|
||||
paddingTop={1}
|
||||
paddingBottom={1}
|
||||
paddingLeft={2}
|
||||
backgroundColor={theme.backgroundPanel}
|
||||
>
|
||||
<box paddingTop={1} paddingBottom={1} paddingLeft={2} backgroundColor={theme.backgroundPanel}>
|
||||
<text fg={theme.text}>{props.part.text.trim()}</text>
|
||||
</box>
|
||||
</box>
|
||||
@@ -1261,16 +1196,10 @@ ToolRegistry.register<typeof WriteTool>({
|
||||
</ToolTitle>
|
||||
<box flexDirection="row">
|
||||
<box flexShrink={0}>
|
||||
<For each={numbers()}>
|
||||
{(value) => <text style={{ fg: theme.textMuted }}>{value}</text>}
|
||||
</For>
|
||||
<For each={numbers()}>{(value) => <text style={{ fg: theme.textMuted }}>{value}</text>}</For>
|
||||
</box>
|
||||
<box paddingLeft={1} flexGrow={1}>
|
||||
<code
|
||||
filetype={filetype(props.input.filePath!)}
|
||||
syntaxStyle={syntax()}
|
||||
content={code()}
|
||||
/>
|
||||
<code filetype={filetype(props.input.filePath!)} syntaxStyle={syntax()} content={code()} />
|
||||
</box>
|
||||
</box>
|
||||
</>
|
||||
@@ -1285,8 +1214,7 @@ ToolRegistry.register<typeof GlobTool>({
|
||||
return (
|
||||
<>
|
||||
<ToolTitle icon="✱" fallback="Finding files..." when={props.input.pattern}>
|
||||
Glob "{props.input.pattern}"{" "}
|
||||
<Show when={props.input.path}>in {normalizePath(props.input.path)} </Show>
|
||||
Glob "{props.input.pattern}" <Show when={props.input.path}>in {normalizePath(props.input.path)} </Show>
|
||||
<Show when={props.metadata.count}>({props.metadata.count} matches)</Show>
|
||||
</ToolTitle>
|
||||
</>
|
||||
@@ -1300,8 +1228,7 @@ ToolRegistry.register<typeof GrepTool>({
|
||||
render(props) {
|
||||
return (
|
||||
<ToolTitle icon="✱" fallback="Searching content..." when={props.input.pattern}>
|
||||
Grep "{props.input.pattern}"{" "}
|
||||
<Show when={props.input.path}>in {normalizePath(props.input.path)} </Show>
|
||||
Grep "{props.input.pattern}" <Show when={props.input.path}>in {normalizePath(props.input.path)} </Show>
|
||||
<Show when={props.metadata.matches}>({props.metadata.matches} matches)</Show>
|
||||
</ToolTitle>
|
||||
)
|
||||
@@ -1337,11 +1264,7 @@ ToolRegistry.register<typeof TaskTool>({
|
||||
|
||||
return (
|
||||
<>
|
||||
<ToolTitle
|
||||
icon="%"
|
||||
fallback="Delegating..."
|
||||
when={props.input.subagent_type ?? props.input.description}
|
||||
>
|
||||
<ToolTitle icon="%" fallback="Delegating..." when={props.input.subagent_type ?? props.input.description}>
|
||||
Task [{props.input.subagent_type ?? "unknown"}] {props.input.description}
|
||||
</ToolTitle>
|
||||
<Show when={props.metadata.summary?.length}>
|
||||
|
||||
@@ -22,16 +22,10 @@ export function Sidebar(props: { sessionID: string }) {
|
||||
})
|
||||
|
||||
const context = createMemo(() => {
|
||||
const last = messages().findLast(
|
||||
(x) => x.role === "assistant" && x.tokens.output > 0,
|
||||
) as AssistantMessage
|
||||
const last = messages().findLast((x) => x.role === "assistant" && x.tokens.output > 0) as AssistantMessage
|
||||
if (!last) return
|
||||
const total =
|
||||
last.tokens.input +
|
||||
last.tokens.output +
|
||||
last.tokens.reasoning +
|
||||
last.tokens.cache.read +
|
||||
last.tokens.cache.write
|
||||
last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write
|
||||
const model = sync.data.provider.find((x) => x.id === last.providerID)?.models[last.modelID]
|
||||
return {
|
||||
tokens: total.toLocaleString(),
|
||||
@@ -84,9 +78,7 @@ export function Sidebar(props: { sessionID: string }) {
|
||||
<span style={{ fg: theme.textMuted }}>
|
||||
<Switch>
|
||||
<Match when={item.status === "connected"}>Connected</Match>
|
||||
<Match when={item.status === "failed" && item}>
|
||||
{(val) => <i>{val().error}</i>}
|
||||
</Match>
|
||||
<Match when={item.status === "failed" && item}>{(val) => <i>{val().error}</i>}</Match>
|
||||
<Match when={item.status === "disabled"}>Disabled in configuration</Match>
|
||||
</Switch>
|
||||
</span>
|
||||
@@ -162,9 +154,7 @@ export function Sidebar(props: { sessionID: string }) {
|
||||
</text>
|
||||
<For each={todo()}>
|
||||
{(todo) => (
|
||||
<text
|
||||
style={{ fg: todo.status === "in_progress" ? theme.success : theme.textMuted }}
|
||||
>
|
||||
<text style={{ fg: todo.status === "in_progress" ? theme.success : theme.textMuted }}>
|
||||
[{todo.status === "completed" ? "✓" : " "}] {todo.content}
|
||||
</text>
|
||||
)}
|
||||
|
||||
@@ -41,12 +41,7 @@ export const TuiSpawnCommand = cmd({
|
||||
)
|
||||
cwd = new URL("../../../../", import.meta.url).pathname
|
||||
} else cmd.push(process.execPath)
|
||||
cmd.push(
|
||||
"attach",
|
||||
server.url.toString(),
|
||||
"--dir",
|
||||
args.project ? path.resolve(args.project) : process.cwd(),
|
||||
)
|
||||
cmd.push("attach", server.url.toString(), "--dir", args.project ? path.resolve(args.project) : process.cwd())
|
||||
const proc = Bun.spawn({
|
||||
cmd,
|
||||
cwd,
|
||||
|
||||
@@ -99,9 +99,7 @@ export const TuiThreadCommand = cmd({
|
||||
|
||||
const worker = new Worker(workerPath, {
|
||||
env: Object.fromEntries(
|
||||
Object.entries(process.env).filter(
|
||||
(entry): entry is [string, string] => entry[1] !== undefined,
|
||||
),
|
||||
Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined),
|
||||
),
|
||||
})
|
||||
worker.onerror = console.error
|
||||
|
||||
@@ -53,9 +53,7 @@ export function DialogConfirm(props: DialogConfirmProps) {
|
||||
dialog.clear()
|
||||
}}
|
||||
>
|
||||
<text fg={key === store.active ? theme.background : theme.textMuted}>
|
||||
{Locale.titlecase(key)}
|
||||
</text>
|
||||
<text fg={key === store.active ? theme.background : theme.textMuted}>{Locale.titlecase(key)}</text>
|
||||
</box>
|
||||
)}
|
||||
</For>
|
||||
|
||||
@@ -20,17 +20,10 @@ export function DialogHelp() {
|
||||
<text fg={theme.textMuted}>esc/enter</text>
|
||||
</box>
|
||||
<box paddingBottom={1}>
|
||||
<text fg={theme.textMuted}>
|
||||
Press Ctrl+P to see all available actions and commands in any context.
|
||||
</text>
|
||||
<text fg={theme.textMuted}>Press Ctrl+P to see all available actions and commands in any context.</text>
|
||||
</box>
|
||||
<box flexDirection="row" justifyContent="flex-end" paddingBottom={1}>
|
||||
<box
|
||||
paddingLeft={3}
|
||||
paddingRight={3}
|
||||
backgroundColor={theme.primary}
|
||||
onMouseUp={() => dialog.clear()}
|
||||
>
|
||||
<box paddingLeft={3} paddingRight={3} backgroundColor={theme.primary} onMouseUp={() => dialog.clear()}>
|
||||
<text fg={theme.background}>ok</text>
|
||||
</box>
|
||||
</box>
|
||||
|
||||
@@ -57,8 +57,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||
const result = pipe(
|
||||
props.options,
|
||||
filter((x) => x.disabled !== true),
|
||||
(x) =>
|
||||
!needle ? x : fuzzysort.go(needle, x, { keys: ["title", "category"] }).map((x) => x.obj),
|
||||
(x) => (!needle ? x : fuzzysort.go(needle, x, { keys: ["title", "category"] }).map((x) => x.obj)),
|
||||
)
|
||||
return result
|
||||
})
|
||||
@@ -214,15 +213,11 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||
props.onSelect?.(option)
|
||||
}}
|
||||
onMouseOver={() => {
|
||||
const index = filtered().findIndex((x) =>
|
||||
isDeepEqual(x.value, option.value),
|
||||
)
|
||||
const index = filtered().findIndex((x) => isDeepEqual(x.value, option.value))
|
||||
if (index === -1) return
|
||||
moveTo(index)
|
||||
}}
|
||||
backgroundColor={
|
||||
active() ? (option.bg ?? theme.primary) : RGBA.fromInts(0, 0, 0, 0)
|
||||
}
|
||||
backgroundColor={active() ? (option.bg ?? theme.primary) : RGBA.fromInts(0, 0, 0, 0)}
|
||||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
gap={1}
|
||||
@@ -230,9 +225,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||
<Option
|
||||
title={option.title}
|
||||
footer={option.footer}
|
||||
description={
|
||||
option.description !== category ? option.description : undefined
|
||||
}
|
||||
description={option.description !== category ? option.description : undefined}
|
||||
active={active()}
|
||||
current={isDeepEqual(option.value, props.current)}
|
||||
/>
|
||||
@@ -248,9 +241,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||
<For each={props.keybind ?? []}>
|
||||
{(item) => (
|
||||
<text>
|
||||
<span style={{ fg: theme.text, attributes: TextAttributes.BOLD }}>
|
||||
{Keybind.toString(item.keybind)}
|
||||
</span>
|
||||
<span style={{ fg: theme.text, attributes: TextAttributes.BOLD }}>{Keybind.toString(item.keybind)}</span>
|
||||
<span style={{ fg: theme.textMuted }}> {item.title}</span>
|
||||
</text>
|
||||
)}
|
||||
@@ -284,10 +275,7 @@ function Option(props: {
|
||||
wrapMode="none"
|
||||
>
|
||||
{Locale.truncate(props.title, 62)}
|
||||
<span style={{ fg: props.active ? theme.background : theme.textMuted }}>
|
||||
{" "}
|
||||
{props.description}
|
||||
</span>
|
||||
<span style={{ fg: props.active ? theme.background : theme.textMuted }}> {props.description}</span>
|
||||
</text>
|
||||
<Show when={props.footer}>
|
||||
<box flexShrink={0}>
|
||||
|
||||
@@ -5,10 +5,7 @@ import { join } from "node:path"
|
||||
import { CliRenderer } from "@opentui/core"
|
||||
|
||||
export namespace Editor {
|
||||
export async function open(opts: {
|
||||
value: string
|
||||
renderer: CliRenderer
|
||||
}): Promise<string | undefined> {
|
||||
export async function open(opts: { value: string; renderer: CliRenderer }): Promise<string | undefined> {
|
||||
const editor = process.env["EDITOR"]
|
||||
if (!editor) return
|
||||
|
||||
|
||||
@@ -27,9 +27,7 @@ export const UpgradeCommand = {
|
||||
const detectedMethod = await Installation.method()
|
||||
const method = (args.method as Installation.Method) ?? detectedMethod
|
||||
if (method === "unknown") {
|
||||
prompts.log.error(
|
||||
`opencode is installed to ${process.execPath} and may be managed by a package manager`,
|
||||
)
|
||||
prompts.log.error(`opencode is installed to ${process.execPath} and may be managed by a package manager`)
|
||||
const install = await prompts.select({
|
||||
message: "Install anyways?",
|
||||
options: [
|
||||
|
||||
@@ -56,11 +56,7 @@ export const WebCommand = cmd({
|
||||
if (hostname === "0.0.0.0") {
|
||||
// Show localhost for local access
|
||||
const localhostUrl = `http://localhost:${server.port}`
|
||||
UI.println(
|
||||
UI.Style.TEXT_INFO_BOLD + " Local access: ",
|
||||
UI.Style.TEXT_NORMAL,
|
||||
localhostUrl,
|
||||
)
|
||||
UI.println(UI.Style.TEXT_INFO_BOLD + " Local access: ", UI.Style.TEXT_NORMAL, localhostUrl)
|
||||
|
||||
// Show network IPs for remote access
|
||||
const networkIPs = getNetworkIPs()
|
||||
|
||||
@@ -8,8 +8,7 @@ export function FormatError(input: unknown) {
|
||||
return `MCP server "${input.data.name}" failed. Note, opencode does not support MCP authentication yet.`
|
||||
if (Config.JsonError.isInstance(input)) {
|
||||
return (
|
||||
`Config file at ${input.data.path} is not valid JSON(C)` +
|
||||
(input.data.message ? `: ${input.data.message}` : "")
|
||||
`Config file at ${input.data.path} is not valid JSON(C)` + (input.data.message ? `: ${input.data.message}` : "")
|
||||
)
|
||||
}
|
||||
if (Config.ConfigDirectoryTypoError.isInstance(input)) {
|
||||
@@ -20,10 +19,8 @@ export function FormatError(input: unknown) {
|
||||
}
|
||||
if (Config.InvalidError.isInstance(input))
|
||||
return [
|
||||
`Config file at ${input.data.path} is invalid` +
|
||||
(input.data.message ? `: ${input.data.message}` : ""),
|
||||
...(input.data.issues?.map((issue) => "↳ " + issue.message + " " + issue.path.join(".")) ??
|
||||
[]),
|
||||
`Config file at ${input.data.path} is invalid` + (input.data.message ? `: ${input.data.message}` : ""),
|
||||
...(input.data.issues?.map((issue) => "↳ " + issue.message + " " + issue.path.join(".")) ?? []),
|
||||
].join("\n")
|
||||
|
||||
if (UI.CancelledError.isInstance(input)) return ""
|
||||
|
||||
@@ -11,11 +11,7 @@ import { lazy } from "../util/lazy"
|
||||
import { NamedError } from "../util/error"
|
||||
import { Flag } from "../flag/flag"
|
||||
import { Auth } from "../auth"
|
||||
import {
|
||||
type ParseError as JsoncParseError,
|
||||
parse as parseJsonc,
|
||||
printParseErrorCode,
|
||||
} from "jsonc-parser"
|
||||
import { type ParseError as JsoncParseError, parse as parseJsonc, printParseErrorCode } from "jsonc-parser"
|
||||
import { Instance } from "../project/instance"
|
||||
import { LSPServer } from "../lsp/server"
|
||||
import { BunProc } from "@/bun"
|
||||
@@ -50,10 +46,7 @@ export namespace Config {
|
||||
if (value.type === "wellknown") {
|
||||
process.env[value.key] = value.token
|
||||
const wellknown = (await fetch(`${key}/.well-known/opencode`).then((x) => x.json())) as any
|
||||
result = mergeDeep(
|
||||
result,
|
||||
await load(JSON.stringify(wellknown.config ?? {}), process.cwd()),
|
||||
)
|
||||
result = mergeDeep(result, await load(JSON.stringify(wellknown.config ?? {}), process.cwd()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,18 +152,10 @@ export namespace Config {
|
||||
|
||||
const gitignore = path.join(dir, ".gitignore")
|
||||
const hasGitIgnore = await Bun.file(gitignore).exists()
|
||||
if (!hasGitIgnore)
|
||||
await Bun.write(
|
||||
gitignore,
|
||||
["node_modules", "package.json", "bun.lock", ".gitignore"].join("\n"),
|
||||
)
|
||||
if (!hasGitIgnore) await Bun.write(gitignore, ["node_modules", "package.json", "bun.lock", ".gitignore"].join("\n"))
|
||||
|
||||
await BunProc.run(
|
||||
[
|
||||
"add",
|
||||
"@opencode-ai/plugin@" + (Installation.isLocal() ? "latest" : Installation.VERSION),
|
||||
"--exact",
|
||||
],
|
||||
["add", "@opencode-ai/plugin@" + (Installation.isLocal() ? "latest" : Installation.VERSION), "--exact"],
|
||||
{
|
||||
cwd: dir,
|
||||
},
|
||||
@@ -330,10 +315,7 @@ export namespace Config {
|
||||
type: z.literal("remote").describe("Type of MCP server connection"),
|
||||
url: z.string().describe("URL of the remote MCP server"),
|
||||
enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"),
|
||||
headers: z
|
||||
.record(z.string(), z.string())
|
||||
.optional()
|
||||
.describe("Headers to send with the request"),
|
||||
headers: z.record(z.string(), z.string()).optional().describe("Headers to send with the request"),
|
||||
timeout: z
|
||||
.number()
|
||||
.int()
|
||||
@@ -389,70 +371,30 @@ export namespace Config {
|
||||
|
||||
export const Keybinds = z
|
||||
.object({
|
||||
leader: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("ctrl+x")
|
||||
.describe("Leader key for keybind combinations"),
|
||||
app_exit: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("ctrl+c,ctrl+d,<leader>q")
|
||||
.describe("Exit the application"),
|
||||
leader: z.string().optional().default("ctrl+x").describe("Leader key for keybind combinations"),
|
||||
app_exit: z.string().optional().default("ctrl+c,ctrl+d,<leader>q").describe("Exit the application"),
|
||||
editor_open: z.string().optional().default("<leader>e").describe("Open external editor"),
|
||||
theme_list: z.string().optional().default("<leader>t").describe("List available themes"),
|
||||
sidebar_toggle: z.string().optional().default("<leader>b").describe("Toggle sidebar"),
|
||||
status_view: z.string().optional().default("<leader>s").describe("View status"),
|
||||
session_export: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("<leader>x")
|
||||
.describe("Export session to editor"),
|
||||
session_export: z.string().optional().default("<leader>x").describe("Export session to editor"),
|
||||
session_new: z.string().optional().default("<leader>n").describe("Create a new session"),
|
||||
session_list: z.string().optional().default("<leader>l").describe("List all sessions"),
|
||||
session_timeline: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("<leader>g")
|
||||
.describe("Show session timeline"),
|
||||
session_timeline: z.string().optional().default("<leader>g").describe("Show session timeline"),
|
||||
session_share: z.string().optional().default("none").describe("Share current session"),
|
||||
session_unshare: z.string().optional().default("none").describe("Unshare current session"),
|
||||
session_interrupt: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("escape")
|
||||
.describe("Interrupt current session"),
|
||||
session_interrupt: z.string().optional().default("escape").describe("Interrupt current session"),
|
||||
session_compact: z.string().optional().default("<leader>c").describe("Compact the session"),
|
||||
messages_page_up: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("pageup")
|
||||
.describe("Scroll messages up by one page"),
|
||||
messages_page_down: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("pagedown")
|
||||
.describe("Scroll messages down by one page"),
|
||||
messages_half_page_up: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("ctrl+alt+u")
|
||||
.describe("Scroll messages up by half page"),
|
||||
messages_page_up: z.string().optional().default("pageup").describe("Scroll messages up by one page"),
|
||||
messages_page_down: z.string().optional().default("pagedown").describe("Scroll messages down by one page"),
|
||||
messages_half_page_up: z.string().optional().default("ctrl+alt+u").describe("Scroll messages up by half page"),
|
||||
messages_half_page_down: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("ctrl+alt+d")
|
||||
.describe("Scroll messages down by half page"),
|
||||
messages_first: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("ctrl+g,home")
|
||||
.describe("Navigate to first message"),
|
||||
messages_last: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("ctrl+alt+g,end")
|
||||
.describe("Navigate to last message"),
|
||||
messages_first: z.string().optional().default("ctrl+g,home").describe("Navigate to first message"),
|
||||
messages_last: z.string().optional().default("ctrl+alt+g,end").describe("Navigate to last message"),
|
||||
messages_copy: z.string().optional().default("<leader>y").describe("Copy message"),
|
||||
messages_undo: z.string().optional().default("<leader>u").describe("Undo message"),
|
||||
messages_redo: z.string().optional().default("<leader>r").describe("Redo message"),
|
||||
@@ -463,11 +405,7 @@ export namespace Config {
|
||||
.describe("Toggle code block concealment in messages"),
|
||||
model_list: z.string().optional().default("<leader>m").describe("List available models"),
|
||||
model_cycle_recent: z.string().optional().default("f2").describe("Next recently used model"),
|
||||
model_cycle_recent_reverse: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("shift+f2")
|
||||
.describe("Previous recently used model"),
|
||||
model_cycle_recent_reverse: z.string().optional().default("shift+f2").describe("Previous recently used model"),
|
||||
command_list: z.string().optional().default("ctrl+p").describe("List available commands"),
|
||||
agent_list: z.string().optional().default("<leader>a").describe("List agents"),
|
||||
agent_cycle: z.string().optional().default("tab").describe("Next agent"),
|
||||
@@ -476,23 +414,11 @@ export namespace Config {
|
||||
input_forward_delete: z.string().optional().default("ctrl+d").describe("Forward delete"),
|
||||
input_paste: z.string().optional().default("ctrl+v").describe("Paste from clipboard"),
|
||||
input_submit: z.string().optional().default("return").describe("Submit input"),
|
||||
input_newline: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("shift+return,ctrl+j")
|
||||
.describe("Insert newline in input"),
|
||||
input_newline: z.string().optional().default("shift+return,ctrl+j").describe("Insert newline in input"),
|
||||
history_previous: z.string().optional().default("up").describe("Previous history item"),
|
||||
history_next: z.string().optional().default("down").describe("Next history item"),
|
||||
session_child_cycle: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("ctrl+right")
|
||||
.describe("Next child session"),
|
||||
session_child_cycle_reverse: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("ctrl+left")
|
||||
.describe("Previous child session"),
|
||||
session_child_cycle: z.string().optional().default("ctrl+right").describe("Next child session"),
|
||||
session_child_cycle_reverse: z.string().optional().default("ctrl+left").describe("Previous child session"),
|
||||
})
|
||||
.strict()
|
||||
.meta({
|
||||
@@ -534,23 +460,13 @@ export namespace Config {
|
||||
autoshare: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe(
|
||||
"@deprecated Use 'share' field instead. Share newly created sessions automatically",
|
||||
),
|
||||
.describe("@deprecated Use 'share' field instead. Share newly created sessions automatically"),
|
||||
autoupdate: z.boolean().optional().describe("Automatically update to the latest version"),
|
||||
disabled_providers: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe("Disable providers that are loaded automatically"),
|
||||
model: z
|
||||
.string()
|
||||
.describe("Model to use in the format of provider/model, eg anthropic/claude-2")
|
||||
.optional(),
|
||||
disabled_providers: z.array(z.string()).optional().describe("Disable providers that are loaded automatically"),
|
||||
model: z.string().describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(),
|
||||
small_model: z
|
||||
.string()
|
||||
.describe(
|
||||
"Small model to use for tasks like title generation in the format of provider/model",
|
||||
)
|
||||
.describe("Small model to use for tasks like title generation in the format of provider/model")
|
||||
.optional(),
|
||||
username: z
|
||||
.string()
|
||||
@@ -583,10 +499,7 @@ export namespace Config {
|
||||
.object({
|
||||
apiKey: z.string().optional(),
|
||||
baseURL: z.string().optional(),
|
||||
enterpriseUrl: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("GitHub Enterprise URL for copilot authentication"),
|
||||
enterpriseUrl: z.string().optional().describe("GitHub Enterprise URL for copilot authentication"),
|
||||
timeout: z
|
||||
.union([
|
||||
z
|
||||
@@ -610,10 +523,7 @@ export namespace Config {
|
||||
)
|
||||
.optional()
|
||||
.describe("Custom provider configurations and model overrides"),
|
||||
mcp: z
|
||||
.record(z.string(), Mcp)
|
||||
.optional()
|
||||
.describe("MCP (Model Context Protocol) server configurations"),
|
||||
mcp: z.record(z.string(), Mcp).optional().describe("MCP (Model Context Protocol) server configurations"),
|
||||
formatter: z
|
||||
.record(
|
||||
z.string(),
|
||||
@@ -657,10 +567,7 @@ export namespace Config {
|
||||
error: "For custom LSP servers, 'extensions' array is required.",
|
||||
},
|
||||
),
|
||||
instructions: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe("Additional instruction files or patterns to include"),
|
||||
instructions: z.array(z.string()).optional().describe("Additional instruction files or patterns to include"),
|
||||
layout: Layout.optional().describe("@deprecated Always uses stretch layout."),
|
||||
permission: z
|
||||
.object({
|
||||
@@ -694,10 +601,7 @@ export namespace Config {
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
chatMaxRetries: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe("Number of retries for chat completions on failure"),
|
||||
chatMaxRetries: z.number().optional().describe("Number of retries for chat completions on failure"),
|
||||
disable_paste_summary: z.boolean().optional(),
|
||||
})
|
||||
.optional(),
|
||||
@@ -727,10 +631,7 @@ export namespace Config {
|
||||
if (provider && model) result.model = `${provider}/${model}`
|
||||
result["$schema"] = "https://opencode.ai/config.json"
|
||||
result = mergeDeep(result, rest)
|
||||
await Bun.write(
|
||||
path.join(Global.Path.config, "config.json"),
|
||||
JSON.stringify(result, null, 2),
|
||||
)
|
||||
await Bun.write(path.join(Global.Path.config, "config.json"), JSON.stringify(result, null, 2))
|
||||
await fs.unlink(path.join(Global.Path.config, "config"))
|
||||
})
|
||||
.catch(() => {})
|
||||
@@ -769,9 +670,7 @@ export namespace Config {
|
||||
if (filePath.startsWith("~/")) {
|
||||
filePath = path.join(os.homedir(), filePath.slice(2))
|
||||
}
|
||||
const resolvedPath = path.isAbsolute(filePath)
|
||||
? filePath
|
||||
: path.resolve(configDir, filePath)
|
||||
const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(configDir, filePath)
|
||||
const fileContent = (
|
||||
await Bun.file(resolvedPath)
|
||||
.text()
|
||||
|
||||
@@ -81,9 +81,7 @@ export namespace Fzf {
|
||||
})
|
||||
}
|
||||
if (config.extension === "zip") {
|
||||
const zipFileReader = new ZipReader(
|
||||
new BlobReader(new Blob([await Bun.file(archivePath).arrayBuffer()])),
|
||||
)
|
||||
const zipFileReader = new ZipReader(new BlobReader(new Blob([await Bun.file(archivePath).arrayBuffer()])))
|
||||
const entries = await zipFileReader.getEntries()
|
||||
let fzfEntry: any
|
||||
for (const entry of entries) {
|
||||
|
||||
@@ -165,11 +165,7 @@ export namespace File {
|
||||
const project = Instance.project
|
||||
if (project.vcs !== "git") return []
|
||||
|
||||
const diffOutput = await $`git diff --numstat HEAD`
|
||||
.cwd(Instance.directory)
|
||||
.quiet()
|
||||
.nothrow()
|
||||
.text()
|
||||
const diffOutput = await $`git diff --numstat HEAD`.cwd(Instance.directory).quiet().nothrow().text()
|
||||
|
||||
const changedFiles: Info[] = []
|
||||
|
||||
@@ -261,14 +257,9 @@ export namespace File {
|
||||
|
||||
if (project.vcs === "git") {
|
||||
let diff = await $`git diff ${file}`.cwd(Instance.directory).quiet().nothrow().text()
|
||||
if (!diff.trim())
|
||||
diff = await $`git diff --staged ${file}`.cwd(Instance.directory).quiet().nothrow().text()
|
||||
if (!diff.trim()) diff = await $`git diff --staged ${file}`.cwd(Instance.directory).quiet().nothrow().text()
|
||||
if (diff.trim()) {
|
||||
const original = await $`git show HEAD:${file}`
|
||||
.cwd(Instance.directory)
|
||||
.quiet()
|
||||
.nothrow()
|
||||
.text()
|
||||
const original = await $`git show HEAD:${file}`.cwd(Instance.directory).quiet().nothrow().text()
|
||||
const patch = structuredPatch(file, file, original, content, "old", "new", {
|
||||
context: Infinity,
|
||||
ignoreWhitespace: true,
|
||||
@@ -321,9 +312,7 @@ export namespace File {
|
||||
const limit = input.limit ?? 100
|
||||
const result = await state().then((x) => x.files())
|
||||
if (!input.query)
|
||||
return input.dirs !== false
|
||||
? result.dirs.toSorted().slice(0, limit)
|
||||
: result.files.slice(0, limit)
|
||||
return input.dirs !== false ? result.dirs.toSorted().slice(0, limit) : result.files.slice(0, limit)
|
||||
const items = input.dirs !== false ? [...result.files, ...result.dirs] : result.files
|
||||
const sorted = fuzzysort.go(input.query, items, { limit: limit }).map((r) => r.target)
|
||||
log.info("search", { query: input.query, results: sorted.length })
|
||||
|
||||
@@ -161,9 +161,7 @@ export namespace Ripgrep {
|
||||
}
|
||||
if (config.extension === "zip") {
|
||||
if (config.extension === "zip") {
|
||||
const zipFileReader = new ZipReader(
|
||||
new BlobReader(new Blob([await Bun.file(archivePath).arrayBuffer()])),
|
||||
)
|
||||
const zipFileReader = new ZipReader(new BlobReader(new Blob([await Bun.file(archivePath).arrayBuffer()])))
|
||||
const entries = await zipFileReader.getEntries()
|
||||
let rgEntry: any
|
||||
for (const entry of entries) {
|
||||
@@ -356,12 +354,7 @@ export namespace Ripgrep {
|
||||
return lines.join("\n")
|
||||
}
|
||||
|
||||
export async function search(input: {
|
||||
cwd: string
|
||||
pattern: string
|
||||
glob?: string[]
|
||||
limit?: number
|
||||
}) {
|
||||
export async function search(input: { cwd: string; pattern: string; glob?: string[]; limit?: number }) {
|
||||
const args = [`${await filepath()}`, "--json", "--hidden", "--glob='!.git/*'"]
|
||||
|
||||
if (input.glob) {
|
||||
|
||||
@@ -27,10 +27,7 @@ export namespace FileTime {
|
||||
|
||||
export async function assert(sessionID: string, filepath: string) {
|
||||
const time = get(sessionID, filepath)
|
||||
if (!time)
|
||||
throw new Error(
|
||||
`You must read the file ${filepath} before overwriting it. Use the Read tool first`,
|
||||
)
|
||||
if (!time) throw new Error(`You must read the file ${filepath} before overwriting it. Use the Read tool first`)
|
||||
const stats = await Bun.file(filepath).stat()
|
||||
if (stats.mtime.getTime() > time.getTime()) {
|
||||
throw new Error(
|
||||
|
||||
@@ -51,10 +51,8 @@ export namespace FileWatcher {
|
||||
for (const evt of evts) {
|
||||
log.info("event", evt)
|
||||
if (evt.type === "create") Bus.publish(Event.Updated, { file: evt.path, event: "add" })
|
||||
if (evt.type === "update")
|
||||
Bus.publish(Event.Updated, { file: evt.path, event: "change" })
|
||||
if (evt.type === "delete")
|
||||
Bus.publish(Event.Updated, { file: evt.path, event: "unlink" })
|
||||
if (evt.type === "update") Bus.publish(Event.Updated, { file: evt.path, event: "change" })
|
||||
if (evt.type === "delete") Bus.publish(Event.Updated, { file: evt.path, event: "unlink" })
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -132,21 +132,7 @@ export const zig: Info = {
|
||||
export const clang: Info = {
|
||||
name: "clang-format",
|
||||
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() {
|
||||
const items = await Filesystem.findUp(".clang-format", Instance.directory, Instance.worktree)
|
||||
return items.length > 0
|
||||
|
||||
@@ -49,11 +49,7 @@ export namespace Identifier {
|
||||
return result
|
||||
}
|
||||
|
||||
export function create(
|
||||
prefix: keyof typeof prefixes,
|
||||
descending: boolean,
|
||||
timestamp?: number,
|
||||
): string {
|
||||
export function create(prefix: keyof typeof prefixes, descending: boolean, timestamp?: number): string {
|
||||
const currentTimestamp = timestamp ?? Date.now()
|
||||
|
||||
if (currentTimestamp !== lastTimestamp) {
|
||||
|
||||
@@ -44,10 +44,7 @@ export namespace Ide {
|
||||
}
|
||||
|
||||
export function alreadyInstalled() {
|
||||
return (
|
||||
process.env["OPENCODE_CALLER"] === "vscode" ||
|
||||
process.env["OPENCODE_CALLER"] === "vscode-insiders"
|
||||
)
|
||||
return process.env["OPENCODE_CALLER"] === "vscode" || process.env["OPENCODE_CALLER"] === "vscode-insiders"
|
||||
}
|
||||
|
||||
export async function install(ide: (typeof SUPPORTED_IDES)[number]["name"]) {
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
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 { Log } from "../util/log"
|
||||
import { LANGUAGE_EXTENSIONS } from "./language"
|
||||
@@ -38,11 +34,7 @@ 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 l = log.clone().tag("serverID", input.serverID)
|
||||
l.info("starting client")
|
||||
|
||||
@@ -137,9 +129,7 @@ export namespace LSPClient {
|
||||
},
|
||||
notify: {
|
||||
async open(input: { path: string }) {
|
||||
input.path = path.isAbsolute(input.path)
|
||||
? input.path
|
||||
: path.resolve(Instance.directory, input.path)
|
||||
input.path = path.isAbsolute(input.path) ? input.path : path.resolve(Instance.directory, input.path)
|
||||
const file = Bun.file(input.path)
|
||||
const text = await file.text()
|
||||
const extension = path.extname(input.path)
|
||||
@@ -181,18 +171,13 @@ export namespace LSPClient {
|
||||
return diagnostics
|
||||
},
|
||||
async waitForDiagnostics(input: { path: string }) {
|
||||
input.path = path.isAbsolute(input.path)
|
||||
? input.path
|
||||
: path.resolve(Instance.directory, input.path)
|
||||
input.path = path.isAbsolute(input.path) ? input.path : path.resolve(Instance.directory, input.path)
|
||||
log.info("waiting for diagnostics", input)
|
||||
let unsub: () => void
|
||||
return await withTimeout(
|
||||
new Promise<void>((resolve) => {
|
||||
unsub = Bus.subscribe(Event.Diagnostics, (event) => {
|
||||
if (
|
||||
event.properties.path === input.path &&
|
||||
event.properties.serverID === result.serverID
|
||||
) {
|
||||
if (event.properties.path === input.path && event.properties.serverID === result.serverID) {
|
||||
log.info("got diagnostics", input)
|
||||
unsub?.()
|
||||
resolve()
|
||||
|
||||
@@ -197,9 +197,7 @@ export namespace LSP {
|
||||
await run(async (client) => {
|
||||
if (!clients.includes(client)) return
|
||||
|
||||
const wait = waitForDiagnostics
|
||||
? client.waitForDiagnostics({ path: input })
|
||||
: Promise.resolve()
|
||||
const wait = waitForDiagnostics ? client.waitForDiagnostics({ path: input }) : Promise.resolve()
|
||||
await client.notify.open({ path: input })
|
||||
return wait
|
||||
}).catch((err) => {
|
||||
|
||||
@@ -88,9 +88,7 @@ export namespace LSPServer {
|
||||
),
|
||||
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
|
||||
async spawn(root) {
|
||||
const tsserver = await Bun.resolve("typescript/lib/tsserver.js", Instance.directory).catch(
|
||||
() => {},
|
||||
)
|
||||
const tsserver = await Bun.resolve("typescript/lib/tsserver.js", Instance.directory).catch(() => {})
|
||||
if (!tsserver) return
|
||||
const proc = spawn(BunProc.which(), ["x", "typescript-language-server", "--stdio"], {
|
||||
cwd: root,
|
||||
@@ -113,13 +111,7 @@ export namespace LSPServer {
|
||||
export const Vue: Info = {
|
||||
id: "vue",
|
||||
extensions: [".vue"],
|
||||
root: NearestRoot([
|
||||
"package-lock.json",
|
||||
"bun.lockb",
|
||||
"bun.lock",
|
||||
"pnpm-lock.yaml",
|
||||
"yarn.lock",
|
||||
]),
|
||||
root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
|
||||
async spawn(root) {
|
||||
let binary = Bun.which("vue-language-server")
|
||||
const args: string[] = []
|
||||
@@ -167,31 +159,17 @@ export namespace LSPServer {
|
||||
|
||||
export const ESLint: Info = {
|
||||
id: "eslint",
|
||||
root: NearestRoot([
|
||||
"package-lock.json",
|
||||
"bun.lockb",
|
||||
"bun.lock",
|
||||
"pnpm-lock.yaml",
|
||||
"yarn.lock",
|
||||
]),
|
||||
root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
|
||||
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts", ".vue"],
|
||||
async spawn(root) {
|
||||
const eslint = await Bun.resolve("eslint", Instance.directory).catch(() => {})
|
||||
if (!eslint) return
|
||||
log.info("spawning eslint server")
|
||||
const serverPath = path.join(
|
||||
Global.Path.bin,
|
||||
"vscode-eslint",
|
||||
"server",
|
||||
"out",
|
||||
"eslintServer.js",
|
||||
)
|
||||
const serverPath = path.join(Global.Path.bin, "vscode-eslint", "server", "out", "eslintServer.js")
|
||||
if (!(await Bun.file(serverPath).exists())) {
|
||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||
log.info("downloading and building VS Code ESLint server")
|
||||
const response = await fetch(
|
||||
"https://github.com/microsoft/vscode-eslint/archive/refs/heads/main.zip",
|
||||
)
|
||||
const response = await fetch("https://github.com/microsoft/vscode-eslint/archive/refs/heads/main.zip")
|
||||
if (!response.ok) return
|
||||
|
||||
const zipPath = path.join(Global.Path.bin, "vscode-eslint.zip")
|
||||
@@ -316,25 +294,12 @@ export namespace LSPServer {
|
||||
export const Pyright: Info = {
|
||||
id: "pyright",
|
||||
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) {
|
||||
let binary = Bun.which("pyright-langserver")
|
||||
const args = []
|
||||
if (!binary) {
|
||||
const js = path.join(
|
||||
Global.Path.bin,
|
||||
"node_modules",
|
||||
"pyright",
|
||||
"dist",
|
||||
"pyright-langserver.js",
|
||||
)
|
||||
const js = path.join(Global.Path.bin, "node_modules", "pyright", "dist", "pyright-langserver.js")
|
||||
if (!(await Bun.file(js).exists())) {
|
||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||
await Bun.spawn([BunProc.which(), "install", "pyright"], {
|
||||
@@ -352,11 +317,9 @@ export namespace LSPServer {
|
||||
|
||||
const initialization: Record<string, string> = {}
|
||||
|
||||
const potentialVenvPaths = [
|
||||
process.env["VIRTUAL_ENV"],
|
||||
path.join(root, ".venv"),
|
||||
path.join(root, "venv"),
|
||||
].filter((p): p is string => p !== undefined)
|
||||
const potentialVenvPaths = [process.env["VIRTUAL_ENV"], path.join(root, ".venv"), path.join(root, "venv")].filter(
|
||||
(p): p is string => p !== undefined,
|
||||
)
|
||||
for (const venvPath of potentialVenvPaths) {
|
||||
const isWindows = process.platform === "win32"
|
||||
const potentialPythonPath = isWindows
|
||||
@@ -407,9 +370,7 @@ export namespace LSPServer {
|
||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||
log.info("downloading elixir-ls from GitHub releases")
|
||||
|
||||
const response = await fetch(
|
||||
"https://github.com/elixir-lsp/elixir-ls/archive/refs/heads/master.zip",
|
||||
)
|
||||
const response = await fetch("https://github.com/elixir-lsp/elixir-ls/archive/refs/heads/master.zip")
|
||||
if (!response.ok) return
|
||||
const zipPath = path.join(Global.Path.bin, "elixir-ls.zip")
|
||||
await Bun.file(zipPath).write(response)
|
||||
@@ -459,9 +420,7 @@ export namespace LSPServer {
|
||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||
log.info("downloading zls from GitHub releases")
|
||||
|
||||
const releaseResponse = await fetch(
|
||||
"https://api.github.com/repos/zigtools/zls/releases/latest",
|
||||
)
|
||||
const releaseResponse = await fetch("https://api.github.com/repos/zigtools/zls/releases/latest")
|
||||
if (!releaseResponse.ok) {
|
||||
log.error("Failed to fetch zls release info")
|
||||
return
|
||||
@@ -636,13 +595,7 @@ export namespace LSPServer {
|
||||
|
||||
export const Clangd: Info = {
|
||||
id: "clangd",
|
||||
root: NearestRoot([
|
||||
"compile_commands.json",
|
||||
"compile_flags.txt",
|
||||
".clangd",
|
||||
"CMakeLists.txt",
|
||||
"Makefile",
|
||||
]),
|
||||
root: NearestRoot(["compile_commands.json", "compile_flags.txt", ".clangd", "CMakeLists.txt", "Makefile"]),
|
||||
extensions: [".c", ".cpp", ".cc", ".cxx", ".c++", ".h", ".hpp", ".hh", ".hxx", ".h++"],
|
||||
async spawn(root) {
|
||||
let bin = Bun.which("clangd", {
|
||||
@@ -652,9 +605,7 @@ export namespace LSPServer {
|
||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||
log.info("downloading clangd from GitHub releases")
|
||||
|
||||
const releaseResponse = await fetch(
|
||||
"https://api.github.com/repos/clangd/clangd/releases/latest",
|
||||
)
|
||||
const releaseResponse = await fetch("https://api.github.com/repos/clangd/clangd/releases/latest")
|
||||
if (!releaseResponse.ok) {
|
||||
log.error("Failed to fetch clangd release info")
|
||||
return
|
||||
@@ -723,24 +674,12 @@ export namespace LSPServer {
|
||||
export const Svelte: Info = {
|
||||
id: "svelte",
|
||||
extensions: [".svelte"],
|
||||
root: NearestRoot([
|
||||
"package-lock.json",
|
||||
"bun.lockb",
|
||||
"bun.lock",
|
||||
"pnpm-lock.yaml",
|
||||
"yarn.lock",
|
||||
]),
|
||||
root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
|
||||
async spawn(root) {
|
||||
let binary = Bun.which("svelteserver")
|
||||
const args: string[] = []
|
||||
if (!binary) {
|
||||
const js = path.join(
|
||||
Global.Path.bin,
|
||||
"node_modules",
|
||||
"svelte-language-server",
|
||||
"bin",
|
||||
"server.js",
|
||||
)
|
||||
const js = path.join(Global.Path.bin, "node_modules", "svelte-language-server", "bin", "server.js")
|
||||
if (!(await Bun.file(js).exists())) {
|
||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||
await Bun.spawn([BunProc.which(), "install", "svelte-language-server"], {
|
||||
@@ -775,17 +714,9 @@ export namespace LSPServer {
|
||||
export const Astro: Info = {
|
||||
id: "astro",
|
||||
extensions: [".astro"],
|
||||
root: NearestRoot([
|
||||
"package-lock.json",
|
||||
"bun.lockb",
|
||||
"bun.lock",
|
||||
"pnpm-lock.yaml",
|
||||
"yarn.lock",
|
||||
]),
|
||||
root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
|
||||
async spawn(root) {
|
||||
const tsserver = await Bun.resolve("typescript/lib/tsserver.js", Instance.directory).catch(
|
||||
() => {},
|
||||
)
|
||||
const tsserver = await Bun.resolve("typescript/lib/tsserver.js", Instance.directory).catch(() => {})
|
||||
if (!tsserver) {
|
||||
log.info("typescript not found, required for Astro language server")
|
||||
return
|
||||
@@ -795,14 +726,7 @@ export namespace LSPServer {
|
||||
let binary = Bun.which("astro-ls")
|
||||
const args: string[] = []
|
||||
if (!binary) {
|
||||
const js = path.join(
|
||||
Global.Path.bin,
|
||||
"node_modules",
|
||||
"@astrojs",
|
||||
"language-server",
|
||||
"bin",
|
||||
"nodeServer.js",
|
||||
)
|
||||
const js = path.join(Global.Path.bin, "node_modules", "@astrojs", "language-server", "bin", "nodeServer.js")
|
||||
if (!(await Bun.file(js).exists())) {
|
||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||
await Bun.spawn([BunProc.which(), "install", "@astrojs/language-server"], {
|
||||
@@ -880,9 +804,7 @@ export namespace LSPServer {
|
||||
.then(({ stdout }) => stdout.toString().trim())
|
||||
const launcherJar = path.join(launcherDir, jarFileName)
|
||||
if (!(await fs.exists(launcherJar))) {
|
||||
log.error(
|
||||
`Failed to locate the JDTLS launcher module in the installed directory: ${distPath}.`,
|
||||
)
|
||||
log.error(`Failed to locate the JDTLS launcher module in the installed directory: ${distPath}.`)
|
||||
return
|
||||
}
|
||||
const configFile = path.join(
|
||||
@@ -948,9 +870,7 @@ export namespace LSPServer {
|
||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||
log.info("downloading lua-language-server from GitHub releases")
|
||||
|
||||
const releaseResponse = await fetch(
|
||||
"https://api.github.com/repos/LuaLS/lua-language-server/releases/latest",
|
||||
)
|
||||
const releaseResponse = await fetch("https://api.github.com/repos/LuaLS/lua-language-server/releases/latest")
|
||||
if (!releaseResponse.ok) {
|
||||
log.error("Failed to fetch lua-language-server release info")
|
||||
return
|
||||
@@ -987,9 +907,7 @@ export namespace LSPServer {
|
||||
|
||||
const assetSuffix = `${lualsPlatform}-${lualsArch}.${ext}`
|
||||
if (!supportedCombos.includes(assetSuffix)) {
|
||||
log.error(
|
||||
`Platform ${platform} and architecture ${arch} is not supported by lua-language-server`,
|
||||
)
|
||||
log.error(`Platform ${platform} and architecture ${arch} is not supported by lua-language-server`)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1012,10 +930,7 @@ export namespace LSPServer {
|
||||
// Unlike zls which is a single self-contained binary,
|
||||
// lua-language-server needs supporting files (meta/, locale/, etc.)
|
||||
// Extract entire archive to dedicated directory to preserve all files
|
||||
const installDir = path.join(
|
||||
Global.Path.bin,
|
||||
`lua-language-server-${lualsArch}-${lualsPlatform}`,
|
||||
)
|
||||
const installDir = path.join(Global.Path.bin, `lua-language-server-${lualsArch}-${lualsPlatform}`)
|
||||
|
||||
// Remove old installation if exists
|
||||
const stats = await fs.stat(installDir).catch(() => undefined)
|
||||
@@ -1040,11 +955,7 @@ export namespace LSPServer {
|
||||
await fs.rm(tempPath, { force: true })
|
||||
|
||||
// Binary is located in bin/ subdirectory within the extracted archive
|
||||
bin = path.join(
|
||||
installDir,
|
||||
"bin",
|
||||
"lua-language-server" + (platform === "win32" ? ".exe" : ""),
|
||||
)
|
||||
bin = path.join(installDir, "bin", "lua-language-server" + (platform === "win32" ? ".exe" : ""))
|
||||
|
||||
if (!(await Bun.file(bin).exists())) {
|
||||
log.error("Failed to extract lua-language-server binary")
|
||||
|
||||
@@ -104,10 +104,7 @@ export namespace Patch {
|
||||
return null
|
||||
}
|
||||
|
||||
function parseUpdateFileChunks(
|
||||
lines: string[],
|
||||
startIdx: number,
|
||||
): { chunks: UpdateFileChunk[]; nextIdx: number } {
|
||||
function parseUpdateFileChunks(lines: string[], startIdx: number): { chunks: UpdateFileChunk[]; nextIdx: number } {
|
||||
const chunks: UpdateFileChunk[] = []
|
||||
let i = startIdx
|
||||
|
||||
@@ -161,10 +158,7 @@ export namespace Patch {
|
||||
return { chunks, nextIdx: i }
|
||||
}
|
||||
|
||||
function parseAddFileContent(
|
||||
lines: string[],
|
||||
startIdx: number,
|
||||
): { content: string; nextIdx: number } {
|
||||
function parseAddFileContent(lines: string[], startIdx: number): { content: string; nextIdx: number } {
|
||||
let content = ""
|
||||
let i = startIdx
|
||||
|
||||
@@ -303,10 +297,7 @@ export namespace Patch {
|
||||
content: string
|
||||
}
|
||||
|
||||
export function deriveNewContentsFromChunks(
|
||||
filePath: string,
|
||||
chunks: UpdateFileChunk[],
|
||||
): ApplyPatchFileUpdate {
|
||||
export function deriveNewContentsFromChunks(filePath: string, chunks: UpdateFileChunk[]): ApplyPatchFileUpdate {
|
||||
// Read original file content
|
||||
let originalContent: string
|
||||
try {
|
||||
@@ -387,9 +378,7 @@ export namespace Patch {
|
||||
replacements.push([found, pattern.length, newSlice])
|
||||
lineIndex = found + pattern.length
|
||||
} else {
|
||||
throw new Error(
|
||||
`Failed to find expected lines in ${filePath}:\n${chunk.old_lines.join("\n")}`,
|
||||
)
|
||||
throw new Error(`Failed to find expected lines in ${filePath}:\n${chunk.old_lines.join("\n")}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -399,10 +388,7 @@ export namespace Patch {
|
||||
return replacements
|
||||
}
|
||||
|
||||
function applyReplacements(
|
||||
lines: string[],
|
||||
replacements: Array<[number, number, string[]]>,
|
||||
): string[] {
|
||||
function applyReplacements(lines: string[], replacements: Array<[number, number, string[]]>): string[] {
|
||||
// Apply replacements in reverse order to avoid index shifting
|
||||
const result = [...lines]
|
||||
|
||||
@@ -601,9 +587,7 @@ export namespace Patch {
|
||||
changes.set(resolvedPath, {
|
||||
type: "update",
|
||||
unified_diff: fileUpdate.unified_diff,
|
||||
move_path: hunk.move_path
|
||||
? path.resolve(effectiveCwd, hunk.move_path)
|
||||
: undefined,
|
||||
move_path: hunk.move_path ? path.resolve(effectiveCwd, hunk.move_path) : undefined,
|
||||
new_content: fileUpdate.content,
|
||||
})
|
||||
} catch (error) {
|
||||
|
||||
@@ -75,14 +75,7 @@ export namespace Permission {
|
||||
async (state) => {
|
||||
for (const pending of Object.values(state.pending)) {
|
||||
for (const item of Object.values(pending)) {
|
||||
item.reject(
|
||||
new RejectedError(
|
||||
item.info.sessionID,
|
||||
item.info.id,
|
||||
item.info.callID,
|
||||
item.info.metadata,
|
||||
),
|
||||
)
|
||||
item.reject(new RejectedError(item.info.sessionID, item.info.id, item.info.callID, item.info.metadata))
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -150,11 +143,7 @@ export namespace Permission {
|
||||
export const Response = z.enum(["once", "always", "reject"])
|
||||
export type Response = z.infer<typeof Response>
|
||||
|
||||
export function respond(input: {
|
||||
sessionID: Info["sessionID"]
|
||||
permissionID: Info["id"]
|
||||
response: Response
|
||||
}) {
|
||||
export function respond(input: { sessionID: Info["sessionID"]; permissionID: Info["id"]; response: Response }) {
|
||||
log.info("response", input)
|
||||
const { pending, approved } = state()
|
||||
const match = pending[input.sessionID]?.[input.permissionID]
|
||||
@@ -166,14 +155,7 @@ export namespace Permission {
|
||||
response: input.response,
|
||||
})
|
||||
if (input.response === "reject") {
|
||||
match.reject(
|
||||
new RejectedError(
|
||||
input.sessionID,
|
||||
input.permissionID,
|
||||
match.info.callID,
|
||||
match.info.metadata,
|
||||
),
|
||||
)
|
||||
match.reject(new RejectedError(input.sessionID, input.permissionID, match.info.callID, match.info.metadata))
|
||||
return
|
||||
}
|
||||
match.resolve()
|
||||
@@ -205,9 +187,7 @@ export namespace Permission {
|
||||
public readonly toolCallID?: string,
|
||||
public readonly metadata?: Record<string, any>,
|
||||
) {
|
||||
super(
|
||||
`The user rejected permission to use this specific tool call. You may try again with different parameters.`,
|
||||
)
|
||||
super(`The user rejected permission to use this specific tool call. You may try again with different parameters.`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,11 +12,7 @@ const context = Context.create<Context>("instance")
|
||||
const cache = new Map<string, Context>()
|
||||
|
||||
export const Instance = {
|
||||
async provide<R>(input: {
|
||||
directory: string
|
||||
init?: () => Promise<any>
|
||||
fn: () => R
|
||||
}): Promise<R> {
|
||||
async provide<R>(input: { directory: string; init?: () => Promise<any>; fn: () => R }): Promise<R> {
|
||||
let existing = cache.get(input.directory)
|
||||
if (!existing) {
|
||||
const project = await Project.fromDirectory(input.directory)
|
||||
|
||||
@@ -9,11 +9,7 @@ export namespace State {
|
||||
const log = Log.create({ service: "state" })
|
||||
const recordsByKey = new Map<string, Map<any, Entry>>()
|
||||
|
||||
export function create<S>(
|
||||
root: () => string,
|
||||
init: () => S,
|
||||
dispose?: (state: Awaited<S>) => Promise<void>,
|
||||
) {
|
||||
export function create<S>(root: () => string, init: () => S, dispose?: (state: Awaited<S>) => Promise<void>) {
|
||||
return () => {
|
||||
const key = root()
|
||||
let entries = recordsByKey.get(key)
|
||||
|
||||
@@ -78,18 +78,12 @@ export namespace Provider {
|
||||
}
|
||||
},
|
||||
"amazon-bedrock": async () => {
|
||||
if (
|
||||
!process.env["AWS_PROFILE"] &&
|
||||
!process.env["AWS_ACCESS_KEY_ID"] &&
|
||||
!process.env["AWS_BEARER_TOKEN_BEDROCK"]
|
||||
)
|
||||
if (!process.env["AWS_PROFILE"] && !process.env["AWS_ACCESS_KEY_ID"] && !process.env["AWS_BEARER_TOKEN_BEDROCK"])
|
||||
return { autoload: false }
|
||||
|
||||
const region = process.env["AWS_REGION"] ?? "us-east-1"
|
||||
|
||||
const { fromNodeProviderChain } = await import(
|
||||
await BunProc.install("@aws-sdk/credential-providers")
|
||||
)
|
||||
const { fromNodeProviderChain } = await import(await BunProc.install("@aws-sdk/credential-providers"))
|
||||
return {
|
||||
autoload: true,
|
||||
options: {
|
||||
@@ -125,13 +119,9 @@ export namespace Provider {
|
||||
"eu-south-1",
|
||||
"eu-south-2",
|
||||
].some((r) => region.includes(r))
|
||||
const modelRequiresPrefix = [
|
||||
"claude",
|
||||
"nova-lite",
|
||||
"nova-micro",
|
||||
"llama3",
|
||||
"pixtral",
|
||||
].some((m) => modelID.includes(m))
|
||||
const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "llama3", "pixtral"].some((m) =>
|
||||
modelID.includes(m),
|
||||
)
|
||||
if (regionRequiresPrefix && modelRequiresPrefix) {
|
||||
modelID = `${regionPrefix}.${modelID}`
|
||||
}
|
||||
@@ -141,15 +131,13 @@ export namespace Provider {
|
||||
const isAustraliaRegion = ["ap-southeast-2", "ap-southeast-4"].includes(region)
|
||||
if (
|
||||
isAustraliaRegion &&
|
||||
["anthropic.claude-sonnet-4-5", "anthropic.claude-haiku"].some((m) =>
|
||||
modelID.includes(m),
|
||||
)
|
||||
["anthropic.claude-sonnet-4-5", "anthropic.claude-haiku"].some((m) => modelID.includes(m))
|
||||
) {
|
||||
regionPrefix = "au"
|
||||
modelID = `${regionPrefix}.${modelID}`
|
||||
} else {
|
||||
const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "nova-pro"].some(
|
||||
(m) => modelID.includes(m),
|
||||
const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "nova-pro"].some((m) =>
|
||||
modelID.includes(m),
|
||||
)
|
||||
if (modelRequiresPrefix) {
|
||||
regionPrefix = "apac"
|
||||
@@ -187,12 +175,8 @@ export namespace Provider {
|
||||
}
|
||||
},
|
||||
"google-vertex": async () => {
|
||||
const project =
|
||||
process.env["GOOGLE_CLOUD_PROJECT"] ??
|
||||
process.env["GCP_PROJECT"] ??
|
||||
process.env["GCLOUD_PROJECT"]
|
||||
const location =
|
||||
process.env["GOOGLE_CLOUD_LOCATION"] ?? process.env["VERTEX_LOCATION"] ?? "us-east5"
|
||||
const project = process.env["GOOGLE_CLOUD_PROJECT"] ?? process.env["GCP_PROJECT"] ?? process.env["GCLOUD_PROJECT"]
|
||||
const location = process.env["GOOGLE_CLOUD_LOCATION"] ?? process.env["VERTEX_LOCATION"] ?? "us-east5"
|
||||
const autoload = Boolean(project)
|
||||
if (!autoload) return { autoload: false }
|
||||
return {
|
||||
@@ -208,12 +192,8 @@ export namespace Provider {
|
||||
}
|
||||
},
|
||||
"google-vertex-anthropic": async () => {
|
||||
const project =
|
||||
process.env["GOOGLE_CLOUD_PROJECT"] ??
|
||||
process.env["GCP_PROJECT"] ??
|
||||
process.env["GCLOUD_PROJECT"]
|
||||
const location =
|
||||
process.env["GOOGLE_CLOUD_LOCATION"] ?? process.env["VERTEX_LOCATION"] ?? "us-east5"
|
||||
const project = process.env["GOOGLE_CLOUD_PROJECT"] ?? process.env["GCP_PROJECT"] ?? process.env["GCLOUD_PROJECT"]
|
||||
const location = process.env["GOOGLE_CLOUD_LOCATION"] ?? process.env["VERTEX_LOCATION"] ?? "us-east5"
|
||||
const autoload = Boolean(project)
|
||||
if (!autoload) return { autoload: false }
|
||||
return {
|
||||
@@ -408,10 +388,7 @@ export namespace Provider {
|
||||
|
||||
// Load for the main provider if auth exists
|
||||
if (auth) {
|
||||
const options = await plugin.auth.loader(
|
||||
() => Auth.get(providerID) as any,
|
||||
database[plugin.auth.provider],
|
||||
)
|
||||
const options = await plugin.auth.loader(() => Auth.get(providerID) as any, database[plugin.auth.provider])
|
||||
mergeProvider(plugin.auth.provider, options ?? {}, "custom")
|
||||
}
|
||||
|
||||
@@ -442,14 +419,12 @@ export namespace Provider {
|
||||
// Filter out blacklisted models
|
||||
.filter(
|
||||
([modelID]) =>
|
||||
modelID !== "gpt-5-chat-latest" &&
|
||||
!(providerID === "openrouter" && modelID === "openai/gpt-5-chat"),
|
||||
modelID !== "gpt-5-chat-latest" && !(providerID === "openrouter" && modelID === "openai/gpt-5-chat"),
|
||||
)
|
||||
// Filter out experimental models
|
||||
.filter(
|
||||
([, model]) =>
|
||||
((!model.experimental && model.status !== "alpha") ||
|
||||
Flag.OPENCODE_ENABLE_EXPERIMENTAL_MODELS) &&
|
||||
((!model.experimental && model.status !== "alpha") || Flag.OPENCODE_ENABLE_EXPERIMENTAL_MODELS) &&
|
||||
model.status !== "deprecated",
|
||||
),
|
||||
)
|
||||
@@ -496,9 +471,7 @@ export namespace Provider {
|
||||
// In addition, Bun's dynamic import logic does not support subpath imports,
|
||||
// so we patch the import path to load directly from `dist`.
|
||||
const modPath =
|
||||
provider.id === "google-vertex-anthropic"
|
||||
? `${installedPath}/dist/anthropic/index.mjs`
|
||||
: installedPath
|
||||
provider.id === "google-vertex-anthropic" ? `${installedPath}/dist/anthropic/index.mjs` : installedPath
|
||||
const mod = await import(modPath)
|
||||
if (options["timeout"] !== undefined && options["timeout"] !== null) {
|
||||
// Preserve custom fetch if it exists, wrap it with timeout logic
|
||||
@@ -597,14 +570,7 @@ export namespace Provider {
|
||||
|
||||
const provider = await state().then((state) => state.providers[providerID])
|
||||
if (!provider) return
|
||||
let priority = [
|
||||
"claude-haiku-4-5",
|
||||
"claude-haiku-4.5",
|
||||
"3-5-haiku",
|
||||
"3.5-haiku",
|
||||
"gemini-2.5-flash",
|
||||
"gpt-5-nano",
|
||||
]
|
||||
let priority = ["claude-haiku-4-5", "claude-haiku-4.5", "3-5-haiku", "3.5-haiku", "gemini-2.5-flash", "gpt-5-nano"]
|
||||
// claude-haiku-4.5 is considered a premium model in github copilot, we shouldn't use premium requests for title gen
|
||||
if (providerID === "github-copilot") {
|
||||
priority = priority.filter((m) => m !== "claude-haiku-4.5")
|
||||
|
||||
@@ -3,19 +3,12 @@ import { unique } from "remeda"
|
||||
import type { JSONSchema } from "zod/v4/core"
|
||||
|
||||
export namespace ProviderTransform {
|
||||
function normalizeMessages(
|
||||
msgs: ModelMessage[],
|
||||
providerID: string,
|
||||
modelID: string,
|
||||
): ModelMessage[] {
|
||||
function normalizeMessages(msgs: ModelMessage[], providerID: string, modelID: string): ModelMessage[] {
|
||||
if (modelID.includes("claude")) {
|
||||
return msgs.map((msg) => {
|
||||
if ((msg.role === "assistant" || msg.role === "tool") && Array.isArray(msg.content)) {
|
||||
msg.content = msg.content.map((part) => {
|
||||
if (
|
||||
(part.type === "tool-call" || part.type === "tool-result") &&
|
||||
"toolCallId" in part
|
||||
) {
|
||||
if ((part.type === "tool-call" || part.type === "tool-result") && "toolCallId" in part) {
|
||||
return {
|
||||
...part,
|
||||
toolCallId: part.toolCallId.replace(/[^a-zA-Z0-9_-]/g, "_"),
|
||||
@@ -36,10 +29,7 @@ export namespace ProviderTransform {
|
||||
|
||||
if ((msg.role === "assistant" || msg.role === "tool") && Array.isArray(msg.content)) {
|
||||
msg.content = msg.content.map((part) => {
|
||||
if (
|
||||
(part.type === "tool-call" || part.type === "tool-result") &&
|
||||
"toolCallId" in part
|
||||
) {
|
||||
if ((part.type === "tool-call" || part.type === "tool-result") && "toolCallId" in part) {
|
||||
// Mistral requires alphanumeric tool call IDs with exactly 9 characters
|
||||
const normalizedId = part.toolCallId
|
||||
.replace(/[^a-zA-Z0-9]/g, "") // Remove non-alphanumeric characters
|
||||
@@ -96,8 +86,7 @@ export namespace ProviderTransform {
|
||||
}
|
||||
|
||||
for (const msg of unique([...system, ...final])) {
|
||||
const shouldUseContentOptions =
|
||||
providerID !== "anthropic" && Array.isArray(msg.content) && msg.content.length > 0
|
||||
const shouldUseContentOptions = providerID !== "anthropic" && Array.isArray(msg.content) && msg.content.length > 0
|
||||
|
||||
if (shouldUseContentOptions) {
|
||||
const lastContent = msg.content[msg.content.length - 1]
|
||||
@@ -139,11 +128,7 @@ export namespace ProviderTransform {
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function options(
|
||||
providerID: string,
|
||||
modelID: string,
|
||||
sessionID: string,
|
||||
): Record<string, any> | undefined {
|
||||
export function options(providerID: string, modelID: string, sessionID: string): Record<string, any> | undefined {
|
||||
const result: Record<string, any> = {}
|
||||
|
||||
if (providerID === "openai") {
|
||||
@@ -168,11 +153,7 @@ export namespace ProviderTransform {
|
||||
return result
|
||||
}
|
||||
|
||||
export function providerOptions(
|
||||
npm: string | undefined,
|
||||
providerID: string,
|
||||
options: { [x: string]: any },
|
||||
) {
|
||||
export function providerOptions(npm: string | undefined, providerID: string, options: { [x: string]: any }) {
|
||||
switch (npm) {
|
||||
case "@ai-sdk/openai":
|
||||
case "@ai-sdk/azure":
|
||||
@@ -205,8 +186,7 @@ export namespace ProviderTransform {
|
||||
|
||||
if (providerID === "anthropic") {
|
||||
const thinking = options?.["thinking"]
|
||||
const budgetTokens =
|
||||
typeof thinking?.["budgetTokens"] === "number" ? thinking["budgetTokens"] : 0
|
||||
const budgetTokens = typeof thinking?.["budgetTokens"] === "number" ? thinking["budgetTokens"] : 0
|
||||
const enabled = thinking?.["type"] === "enabled"
|
||||
if (enabled && budgetTokens > 0) {
|
||||
// Return text tokens so that text + thinking <= model cap, preferring 32k text when possible.
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import { Log } from "../util/log"
|
||||
import { Bus } from "../bus"
|
||||
import {
|
||||
describeRoute,
|
||||
generateSpecs,
|
||||
validator,
|
||||
resolver,
|
||||
openAPIRouteHandler,
|
||||
} from "hono-openapi"
|
||||
import { describeRoute, generateSpecs, validator, resolver, openAPIRouteHandler } from "hono-openapi"
|
||||
import { Hono } from "hono"
|
||||
import { cors } from "hono/cors"
|
||||
import { stream, streamSSE } from "hono/streaming"
|
||||
@@ -257,9 +251,7 @@ export namespace Server {
|
||||
id: t.id,
|
||||
description: t.description,
|
||||
// Handle both Zod schemas and plain JSON schemas
|
||||
parameters: (t.parameters as any)?._def
|
||||
? zodToJsonSchema(t.parameters as any)
|
||||
: t.parameters,
|
||||
parameters: (t.parameters as any)?._def ? zodToJsonSchema(t.parameters as any) : t.parameters,
|
||||
})),
|
||||
)
|
||||
},
|
||||
@@ -1086,10 +1078,7 @@ export namespace Server {
|
||||
const providers = await Provider.list().then((x) => mapValues(x, (item) => item.info))
|
||||
return c.json({
|
||||
providers: Object.values(providers),
|
||||
default: mapValues(
|
||||
providers,
|
||||
(item) => Provider.sort(Object.values(item.models))[0].id,
|
||||
),
|
||||
default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id),
|
||||
})
|
||||
},
|
||||
)
|
||||
@@ -1683,10 +1672,7 @@ export namespace Server {
|
||||
),
|
||||
async (c) => {
|
||||
const evt = c.req.valid("json")
|
||||
await Bus.publish(
|
||||
Object.values(TuiEvent).find((def) => def.type === evt.type)!,
|
||||
evt.properties,
|
||||
)
|
||||
await Bus.publish(Object.values(TuiEvent).find((def) => def.type === evt.type)!, evt.properties)
|
||||
return c.json(true)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -30,17 +30,12 @@ export namespace SessionCompaction {
|
||||
),
|
||||
}
|
||||
|
||||
export function isOverflow(input: {
|
||||
tokens: MessageV2.Assistant["tokens"]
|
||||
model: ModelsDev.Model
|
||||
}) {
|
||||
export function isOverflow(input: { tokens: MessageV2.Assistant["tokens"]; model: ModelsDev.Model }) {
|
||||
if (Flag.OPENCODE_DISABLE_AUTOCOMPACT) return false
|
||||
const context = input.model.limit.context
|
||||
if (context === 0) return false
|
||||
const count = input.tokens.input + input.tokens.cache.read + input.tokens.output
|
||||
const output =
|
||||
Math.min(input.model.limit.output, SessionPrompt.OUTPUT_TOKEN_MAX) ||
|
||||
SessionPrompt.OUTPUT_TOKEN_MAX
|
||||
const output = Math.min(input.model.limit.output, SessionPrompt.OUTPUT_TOKEN_MAX) || SessionPrompt.OUTPUT_TOKEN_MAX
|
||||
const usable = context - output
|
||||
return count > usable
|
||||
}
|
||||
@@ -92,15 +87,9 @@ export namespace SessionCompaction {
|
||||
}
|
||||
}
|
||||
|
||||
export async function run(input: {
|
||||
sessionID: string
|
||||
providerID: string
|
||||
modelID: string
|
||||
signal?: AbortSignal
|
||||
}) {
|
||||
export async function run(input: { sessionID: string; providerID: string; modelID: string; signal?: AbortSignal }) {
|
||||
if (!input.signal) SessionLock.assertUnlocked(input.sessionID)
|
||||
await using lock =
|
||||
input.signal === undefined ? SessionLock.acquire({ sessionID: input.sessionID }) : undefined
|
||||
await using lock = input.signal === undefined ? SessionLock.acquire({ sessionID: input.sessionID }) : undefined
|
||||
const signal = input.signal ?? lock!.signal
|
||||
|
||||
await Session.update(input.sessionID, (draft) => {
|
||||
@@ -160,11 +149,7 @@ export namespace SessionCompaction {
|
||||
// set to 0, we handle loop
|
||||
maxRetries: 0,
|
||||
model: model.language,
|
||||
providerOptions: ProviderTransform.providerOptions(
|
||||
model.npm,
|
||||
model.providerID,
|
||||
model.info.options,
|
||||
),
|
||||
providerOptions: ProviderTransform.providerOptions(model.npm, model.providerID, model.info.options),
|
||||
headers: model.info.headers,
|
||||
abortSignal: signal,
|
||||
onError(error) {
|
||||
@@ -244,11 +229,7 @@ export namespace SessionCompaction {
|
||||
error: e,
|
||||
})
|
||||
const error = MessageV2.fromError(e, { providerID: input.providerID })
|
||||
if (
|
||||
retries.count < retries.max &&
|
||||
MessageV2.APIError.isInstance(error) &&
|
||||
error.data.isRetryable
|
||||
) {
|
||||
if (retries.count < retries.max && MessageV2.APIError.isInstance(error) && error.data.isRetryable) {
|
||||
shouldRetry = true
|
||||
await Session.updatePart({
|
||||
id: Identifier.ascending("part"),
|
||||
@@ -287,9 +268,7 @@ export namespace SessionCompaction {
|
||||
})
|
||||
if (result.shouldRetry) {
|
||||
for (let retry = 1; retry < maxRetries; retry++) {
|
||||
const lastRetryPart = result.parts.findLast(
|
||||
(p): p is MessageV2.RetryPart => p.type === "retry",
|
||||
)
|
||||
const lastRetryPart = result.parts.findLast((p): p is MessageV2.RetryPart => p.type === "retry")
|
||||
|
||||
if (lastRetryPart) {
|
||||
const delayMs = SessionRetry.getRetryDelayInMs(lastRetryPart.error, retry)
|
||||
@@ -338,9 +317,7 @@ export namespace SessionCompaction {
|
||||
if (
|
||||
!msg.error ||
|
||||
(MessageV2.AbortedError.isInstance(msg.error) &&
|
||||
result.parts.some(
|
||||
(part): part is MessageV2.TextPart => part.type === "text" && part.text.length > 0,
|
||||
))
|
||||
result.parts.some((part): part is MessageV2.TextPart => part.type === "text" && part.text.length > 0))
|
||||
) {
|
||||
msg.summary = true
|
||||
Bus.publish(Event.Compacted, {
|
||||
|
||||
@@ -172,12 +172,7 @@ export namespace Session {
|
||||
})
|
||||
})
|
||||
|
||||
export async function createNext(input: {
|
||||
id?: string
|
||||
title?: string
|
||||
parentID?: string
|
||||
directory: string
|
||||
}) {
|
||||
export async function createNext(input: { id?: string; title?: string; parentID?: string; directory: string }) {
|
||||
const result: Info = {
|
||||
id: Identifier.descending("session", input.id),
|
||||
version: Installation.VERSION,
|
||||
@@ -400,9 +395,7 @@ export namespace Session {
|
||||
.add(new Decimal(tokens.input).mul(input.model.cost?.input ?? 0).div(1_000_000))
|
||||
.add(new Decimal(tokens.output).mul(input.model.cost?.output ?? 0).div(1_000_000))
|
||||
.add(new Decimal(tokens.cache.read).mul(input.model.cost?.cache_read ?? 0).div(1_000_000))
|
||||
.add(
|
||||
new Decimal(tokens.cache.write).mul(input.model.cost?.cache_write ?? 0).div(1_000_000),
|
||||
)
|
||||
.add(new Decimal(tokens.cache.write).mul(input.model.cost?.cache_write ?? 0).div(1_000_000))
|
||||
.toNumber(),
|
||||
tokens,
|
||||
}
|
||||
|
||||
@@ -2,13 +2,7 @@ import z from "zod"
|
||||
import { Bus } from "../bus"
|
||||
import { NamedError } from "../util/error"
|
||||
import { Message } from "./message"
|
||||
import {
|
||||
APICallError,
|
||||
convertToModelMessages,
|
||||
LoadAPIKeyError,
|
||||
type ModelMessage,
|
||||
type UIMessage,
|
||||
} from "ai"
|
||||
import { APICallError, convertToModelMessages, LoadAPIKeyError, type ModelMessage, type UIMessage } from "ai"
|
||||
import { Identifier } from "../id/id"
|
||||
import { LSP } from "../lsp"
|
||||
import { Snapshot } from "@/snapshot"
|
||||
@@ -17,10 +11,7 @@ import { Storage } from "@/storage/storage"
|
||||
|
||||
export namespace MessageV2 {
|
||||
export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({}))
|
||||
export const AbortedError = NamedError.create(
|
||||
"MessageAbortedError",
|
||||
z.object({ message: z.string() }),
|
||||
)
|
||||
export const AbortedError = NamedError.create("MessageAbortedError", z.object({ message: z.string() }))
|
||||
export const AuthError = NamedError.create(
|
||||
"ProviderAuthError",
|
||||
z.object({
|
||||
@@ -253,12 +244,7 @@ export namespace MessageV2 {
|
||||
export type ToolStateError = z.infer<typeof ToolStateError>
|
||||
|
||||
export const ToolState = z
|
||||
.discriminatedUnion("status", [
|
||||
ToolStatePending,
|
||||
ToolStateRunning,
|
||||
ToolStateCompleted,
|
||||
ToolStateError,
|
||||
])
|
||||
.discriminatedUnion("status", [ToolStatePending, ToolStateRunning, ToolStateCompleted, ToolStateError])
|
||||
.meta({
|
||||
ref: "ToolState",
|
||||
})
|
||||
@@ -454,8 +440,7 @@ export namespace MessageV2 {
|
||||
}
|
||||
}
|
||||
|
||||
const { title, time, ...metadata } =
|
||||
v1.metadata.tool[part.toolInvocation.toolCallId] ?? {}
|
||||
const { title, time, ...metadata } = v1.metadata.tool[part.toolInvocation.toolCallId] ?? {}
|
||||
if (part.toolInvocation.state === "call") {
|
||||
return {
|
||||
status: "running",
|
||||
@@ -556,11 +541,7 @@ export namespace MessageV2 {
|
||||
},
|
||||
]
|
||||
// text/plain and directory files are converted into text parts, ignore them
|
||||
if (
|
||||
part.type === "file" &&
|
||||
part.mime !== "text/plain" &&
|
||||
part.mime !== "application/x-directory"
|
||||
)
|
||||
if (part.type === "file" && part.mime !== "text/plain" && part.mime !== "application/x-directory")
|
||||
return [
|
||||
{
|
||||
type: "file",
|
||||
@@ -619,9 +600,7 @@ export namespace MessageV2 {
|
||||
state: "output-available",
|
||||
toolCallId: part.callID,
|
||||
input: part.state.input,
|
||||
output: part.state.time.compacted
|
||||
? "[Old tool result content cleared]"
|
||||
: part.state.output,
|
||||
output: part.state.time.compacted ? "[Old tool result content cleared]" : part.state.output,
|
||||
callProviderMetadata: part.metadata,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -51,11 +51,9 @@ export namespace Message {
|
||||
})
|
||||
export type ToolResult = z.infer<typeof ToolResult>
|
||||
|
||||
export const ToolInvocation = z
|
||||
.discriminatedUnion("state", [ToolCall, ToolPartialCall, ToolResult])
|
||||
.meta({
|
||||
ref: "ToolInvocation",
|
||||
})
|
||||
export const ToolInvocation = z.discriminatedUnion("state", [ToolCall, ToolPartialCall, ToolResult]).meta({
|
||||
ref: "ToolInvocation",
|
||||
})
|
||||
export type ToolInvocation = z.infer<typeof ToolInvocation>
|
||||
|
||||
export const TextPart = z
|
||||
@@ -124,14 +122,7 @@ export namespace Message {
|
||||
export type StepStartPart = z.infer<typeof StepStartPart>
|
||||
|
||||
export const MessagePart = z
|
||||
.discriminatedUnion("type", [
|
||||
TextPart,
|
||||
ReasoningPart,
|
||||
ToolInvocationPart,
|
||||
SourceUrlPart,
|
||||
FilePart,
|
||||
StepStartPart,
|
||||
])
|
||||
.discriminatedUnion("type", [TextPart, ReasoningPart, ToolInvocationPart, SourceUrlPart, FilePart, StepStartPart])
|
||||
.meta({
|
||||
ref: "MessagePart",
|
||||
})
|
||||
@@ -149,11 +140,7 @@ export namespace Message {
|
||||
completed: z.number().optional(),
|
||||
}),
|
||||
error: z
|
||||
.discriminatedUnion("name", [
|
||||
AuthError.Schema,
|
||||
NamedError.Unknown.Schema,
|
||||
OutputLengthError.Schema,
|
||||
])
|
||||
.discriminatedUnion("name", [AuthError.Schema, NamedError.Unknown.Schema, OutputLengthError.Schema])
|
||||
.optional(),
|
||||
sessionID: z.string(),
|
||||
tool: z.record(
|
||||
|
||||
@@ -301,11 +301,7 @@ export namespace SessionPrompt {
|
||||
OUTPUT_TOKEN_MAX,
|
||||
),
|
||||
abortSignal: abort.signal,
|
||||
providerOptions: ProviderTransform.providerOptions(
|
||||
model.npm,
|
||||
model.providerID,
|
||||
params.options,
|
||||
),
|
||||
providerOptions: ProviderTransform.providerOptions(model.npm, model.providerID, params.options),
|
||||
stopWhen: stepCountIs(1),
|
||||
temperature: params.temperature,
|
||||
topP: params.topP,
|
||||
@@ -340,11 +336,7 @@ export namespace SessionPrompt {
|
||||
async transformParams(args) {
|
||||
if (args.type === "stream") {
|
||||
// @ts-expect-error
|
||||
args.params.prompt = ProviderTransform.message(
|
||||
args.params.prompt,
|
||||
model.providerID,
|
||||
model.modelID,
|
||||
)
|
||||
args.params.prompt = ProviderTransform.message(args.params.prompt, model.providerID, model.modelID)
|
||||
}
|
||||
return args.params
|
||||
},
|
||||
@@ -362,9 +354,7 @@ export namespace SessionPrompt {
|
||||
})
|
||||
if (result.shouldRetry) {
|
||||
for (let retry = 1; retry < maxRetries; retry++) {
|
||||
const lastRetryPart = result.parts.findLast(
|
||||
(p): p is MessageV2.RetryPart => p.type === "retry",
|
||||
)
|
||||
const lastRetryPart = result.parts.findLast((p): p is MessageV2.RetryPart => p.type === "retry")
|
||||
|
||||
if (lastRetryPart) {
|
||||
const delayMs = SessionRetry.getRetryDelayInMs(lastRetryPart.error, retry)
|
||||
@@ -529,11 +519,7 @@ export namespace SessionPrompt {
|
||||
)
|
||||
for (const item of await ToolRegistry.tools(input.providerID, input.modelID)) {
|
||||
if (Wildcard.all(item.id, enabledTools) === false) continue
|
||||
const schema = ProviderTransform.schema(
|
||||
input.providerID,
|
||||
input.modelID,
|
||||
z.toJSONSchema(item.parameters),
|
||||
)
|
||||
const schema = ProviderTransform.schema(input.providerID, input.modelID, z.toJSONSchema(item.parameters))
|
||||
tools[item.id] = tool({
|
||||
id: item.id as any,
|
||||
description: item.description,
|
||||
@@ -853,9 +839,7 @@ export namespace SessionPrompt {
|
||||
messageID: info.id,
|
||||
sessionID: input.sessionID,
|
||||
type: "file",
|
||||
url:
|
||||
`data:${part.mime};base64,` +
|
||||
Buffer.from(await file.bytes()).toString("base64"),
|
||||
url: `data:${part.mime};base64,` + Buffer.from(await file.bytes()).toString("base64"),
|
||||
mime: part.mime,
|
||||
filename: part.filename!,
|
||||
source: part.source,
|
||||
@@ -929,9 +913,7 @@ export namespace SessionPrompt {
|
||||
synthetic: true,
|
||||
})
|
||||
}
|
||||
const wasPlan = input.messages.some(
|
||||
(msg) => msg.info.role === "assistant" && msg.info.mode === "plan",
|
||||
)
|
||||
const wasPlan = input.messages.some((msg) => msg.info.role === "assistant" && msg.info.mode === "plan")
|
||||
if (wasPlan && input.agent.name === "build") {
|
||||
userMessage.parts.push({
|
||||
id: Identifier.ascending("part"),
|
||||
@@ -1010,10 +992,7 @@ export namespace SessionPrompt {
|
||||
partFromToolCall(toolCallID: string) {
|
||||
return toolcalls[toolCallID]
|
||||
},
|
||||
async process(
|
||||
stream: StreamTextResult<Record<string, AITool>, never>,
|
||||
retries: { count: number; max: number },
|
||||
) {
|
||||
async process(stream: StreamTextResult<Record<string, AITool>, never>, retries: { count: number; max: number }) {
|
||||
log.info("process")
|
||||
if (!assistantMsg) throw new Error("call next() first before processing")
|
||||
let shouldRetry = false
|
||||
@@ -1169,10 +1148,7 @@ export namespace SessionPrompt {
|
||||
status: "error",
|
||||
input: value.input,
|
||||
error: (value.error as any).toString(),
|
||||
metadata:
|
||||
value.error instanceof Permission.RejectedError
|
||||
? value.error.metadata
|
||||
: undefined,
|
||||
metadata: value.error instanceof Permission.RejectedError ? value.error.metadata : undefined,
|
||||
time: {
|
||||
start: match.state.time.start,
|
||||
end: Date.now(),
|
||||
@@ -1296,11 +1272,7 @@ export namespace SessionPrompt {
|
||||
error: e,
|
||||
})
|
||||
const error = MessageV2.fromError(e, { providerID: input.providerID })
|
||||
if (
|
||||
retries.count < retries.max &&
|
||||
MessageV2.APIError.isInstance(error) &&
|
||||
error.data.isRetryable
|
||||
) {
|
||||
if (retries.count < retries.max && MessageV2.APIError.isInstance(error) && error.data.isRetryable) {
|
||||
shouldRetry = true
|
||||
await Session.updatePart({
|
||||
id: Identifier.ascending("part"),
|
||||
@@ -1323,11 +1295,7 @@ export namespace SessionPrompt {
|
||||
}
|
||||
const p = await MessageV2.parts(assistantMsg.id)
|
||||
for (const part of p) {
|
||||
if (
|
||||
part.type === "tool" &&
|
||||
part.state.status !== "completed" &&
|
||||
part.state.status !== "error"
|
||||
) {
|
||||
if (part.type === "tool" && part.state.status !== "completed" && part.state.status !== "error") {
|
||||
await Session.updatePart({
|
||||
...part,
|
||||
state: {
|
||||
@@ -1822,13 +1790,11 @@ export namespace SessionPrompt {
|
||||
if (input.session.parentID) return
|
||||
if (!Session.isDefaultTitle(input.session.title)) return
|
||||
const isFirst =
|
||||
input.history.filter(
|
||||
(m) => m.info.role === "user" && !m.parts.every((p) => "synthetic" in p && p.synthetic),
|
||||
).length === 1
|
||||
input.history.filter((m) => m.info.role === "user" && !m.parts.every((p) => "synthetic" in p && p.synthetic))
|
||||
.length === 1
|
||||
if (!isFirst) return
|
||||
const small =
|
||||
(await Provider.getSmallModel(input.providerID)) ??
|
||||
(await Provider.getModel(input.providerID, input.modelID))
|
||||
(await Provider.getSmallModel(input.providerID)) ?? (await Provider.getModel(input.providerID, input.modelID))
|
||||
const options = {
|
||||
...ProviderTransform.options(small.providerID, small.modelID, input.session.id),
|
||||
...small.info.options,
|
||||
|
||||
@@ -45,9 +45,7 @@ export namespace SessionRevert {
|
||||
if (!revert) {
|
||||
if ((msg.info.id === input.messageID && !input.partID) || part.id === input.partID) {
|
||||
// if no useful parts left in message, same as reverting whole message
|
||||
const partID = remaining.some((item) => ["text", "tool"].includes(item.type))
|
||||
? input.partID
|
||||
: undefined
|
||||
const partID = remaining.some((item) => ["text", "tool"].includes(item.type)) ? input.partID : undefined
|
||||
revert = {
|
||||
messageID: !partID && lastUser ? lastUser.id : msg.info.id,
|
||||
partID,
|
||||
|
||||
@@ -60,9 +60,7 @@ export namespace SessionSummary {
|
||||
|
||||
async function summarizeMessage(input: { messageID: string; messages: MessageV2.WithParts[] }) {
|
||||
const messages = input.messages.filter(
|
||||
(m) =>
|
||||
m.info.id === input.messageID ||
|
||||
(m.info.role === "assistant" && m.info.parentID === input.messageID),
|
||||
(m) => m.info.id === input.messageID || (m.info.role === "assistant" && m.info.parentID === input.messageID),
|
||||
)
|
||||
const msgWithParts = messages.find((m) => m.info.id === input.messageID)!
|
||||
const userMsg = msgWithParts.info as MessageV2.User
|
||||
@@ -73,14 +71,11 @@ export namespace SessionSummary {
|
||||
}
|
||||
await Session.updateMessage(userMsg)
|
||||
|
||||
const assistantMsg = messages.find((m) => m.info.role === "assistant")!
|
||||
.info as MessageV2.Assistant
|
||||
const assistantMsg = messages.find((m) => m.info.role === "assistant")!.info as MessageV2.Assistant
|
||||
const small = await Provider.getSmallModel(assistantMsg.providerID)
|
||||
if (!small) return
|
||||
|
||||
const textPart = msgWithParts.parts.find(
|
||||
(p) => p.type === "text" && !p.synthetic,
|
||||
) as MessageV2.TextPart
|
||||
const textPart = msgWithParts.parts.find((p) => p.type === "text" && !p.synthetic) as MessageV2.TextPart
|
||||
if (textPart && !userMsg.summary?.title) {
|
||||
const result = await generateText({
|
||||
maxOutputTokens: small.info.reasoning ? 1500 : 20,
|
||||
@@ -113,8 +108,7 @@ export namespace SessionSummary {
|
||||
if (
|
||||
messages.some(
|
||||
(m) =>
|
||||
m.info.role === "assistant" &&
|
||||
m.parts.some((p) => p.type === "step-finish" && p.reason !== "tool-calls"),
|
||||
m.info.role === "assistant" && m.parts.some((p) => p.type === "step-finish" && p.reason !== "tool-calls"),
|
||||
)
|
||||
) {
|
||||
let summary = messages
|
||||
|
||||
@@ -24,8 +24,7 @@ export namespace SystemPrompt {
|
||||
|
||||
export function provider(modelID: string) {
|
||||
if (modelID.includes("gpt-5")) return [PROMPT_CODEX]
|
||||
if (modelID.includes("gpt-") || modelID.includes("o1") || modelID.includes("o3"))
|
||||
return [PROMPT_BEAST]
|
||||
if (modelID.includes("gpt-") || modelID.includes("o1") || modelID.includes("o3")) return [PROMPT_BEAST]
|
||||
if (modelID.includes("gemini-")) return [PROMPT_GEMINI]
|
||||
if (modelID.includes("claude")) return [PROMPT_ANTHROPIC]
|
||||
return [PROMPT_ANTHROPIC_WITHOUT_TODO]
|
||||
@@ -100,11 +99,7 @@ export namespace SystemPrompt {
|
||||
}),
|
||||
).catch(() => [])
|
||||
} else {
|
||||
matches = await Filesystem.globUp(
|
||||
instruction,
|
||||
Instance.directory,
|
||||
Instance.worktree,
|
||||
).catch(() => [])
|
||||
matches = await Filesystem.globUp(instruction, Instance.directory, Instance.worktree).catch(() => [])
|
||||
}
|
||||
matches.forEach((path) => paths.add(path))
|
||||
}
|
||||
|
||||
@@ -6,9 +6,7 @@ export namespace Todo {
|
||||
export const Info = z
|
||||
.object({
|
||||
content: z.string().describe("Brief description of the task"),
|
||||
status: z
|
||||
.string()
|
||||
.describe("Current status of the task: pending, in_progress, completed, cancelled"),
|
||||
status: z.string().describe("Current status of the task: pending, in_progress, completed, cancelled"),
|
||||
priority: z.string().describe("Priority level of the task: high, medium, low"),
|
||||
id: z.string().describe("Unique identifier for the todo item"),
|
||||
})
|
||||
|
||||
@@ -50,10 +50,7 @@ export namespace Share {
|
||||
await sync("session/info/" + evt.properties.info.id, evt.properties.info)
|
||||
})
|
||||
Bus.subscribe(MessageV2.Event.Updated, async (evt) => {
|
||||
await sync(
|
||||
"session/message/" + evt.properties.info.sessionID + "/" + evt.properties.info.id,
|
||||
evt.properties.info,
|
||||
)
|
||||
await sync("session/message/" + evt.properties.info.sessionID + "/" + evt.properties.info.id, evt.properties.info)
|
||||
})
|
||||
Bus.subscribe(MessageV2.Event.PartUpdated, async (evt) => {
|
||||
await sync(
|
||||
@@ -70,9 +67,7 @@ export namespace Share {
|
||||
|
||||
export const URL =
|
||||
process.env["OPENCODE_API"] ??
|
||||
(Installation.isPreview() || Installation.isLocal()
|
||||
? "https://api.dev.opencode.ai"
|
||||
: "https://api.opencode.ai")
|
||||
(Installation.isPreview() || Installation.isLocal() ? "https://api.dev.opencode.ai" : "https://api.opencode.ai")
|
||||
|
||||
export async function create(sessionID: string) {
|
||||
return fetch(`${URL}/share_create`, {
|
||||
|
||||
@@ -27,11 +27,7 @@ export namespace Snapshot {
|
||||
log.info("initialized")
|
||||
}
|
||||
await $`git --git-dir ${git} add .`.quiet().cwd(Instance.directory).nothrow()
|
||||
const hash = await $`git --git-dir ${git} write-tree`
|
||||
.quiet()
|
||||
.cwd(Instance.directory)
|
||||
.nothrow()
|
||||
.text()
|
||||
const hash = await $`git --git-dir ${git} write-tree`.quiet().cwd(Instance.directory).nothrow().text()
|
||||
log.info("tracking", { hash, cwd: Instance.directory, git })
|
||||
return hash.trim()
|
||||
}
|
||||
@@ -45,10 +41,7 @@ export namespace Snapshot {
|
||||
export async function patch(hash: string): Promise<Patch> {
|
||||
const git = gitdir()
|
||||
await $`git --git-dir ${git} add .`.quiet().cwd(Instance.directory).nothrow()
|
||||
const result = await $`git --git-dir ${git} diff --name-only ${hash} -- .`
|
||||
.quiet()
|
||||
.cwd(Instance.directory)
|
||||
.nothrow()
|
||||
const result = await $`git --git-dir ${git} diff --name-only ${hash} -- .`.quiet().cwd(Instance.directory).nothrow()
|
||||
|
||||
// If git diff fails, return empty patch
|
||||
if (result.exitCode !== 0) {
|
||||
@@ -71,11 +64,10 @@ export namespace Snapshot {
|
||||
export async function restore(snapshot: string) {
|
||||
log.info("restore", { commit: snapshot })
|
||||
const git = gitdir()
|
||||
const result =
|
||||
await $`git --git-dir=${git} read-tree ${snapshot} && git --git-dir=${git} checkout-index -a -f`
|
||||
.quiet()
|
||||
.cwd(Instance.worktree)
|
||||
.nothrow()
|
||||
const result = await $`git --git-dir=${git} read-tree ${snapshot} && git --git-dir=${git} checkout-index -a -f`
|
||||
.quiet()
|
||||
.cwd(Instance.worktree)
|
||||
.nothrow()
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
log.error("failed to restore snapshot", {
|
||||
@@ -121,10 +113,7 @@ export namespace Snapshot {
|
||||
export async function diff(hash: string) {
|
||||
const git = gitdir()
|
||||
await $`git --git-dir ${git} add .`.quiet().cwd(Instance.directory).nothrow()
|
||||
const result = await $`git --git-dir=${git} diff ${hash} -- .`
|
||||
.quiet()
|
||||
.cwd(Instance.worktree)
|
||||
.nothrow()
|
||||
const result = await $`git --git-dir=${git} diff ${hash} -- .`.quiet().cwd(Instance.worktree).nothrow()
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
log.warn("failed to get diff", {
|
||||
|
||||
@@ -85,9 +85,7 @@ export namespace Storage {
|
||||
const session = await Bun.file(sessionFile).json()
|
||||
await Bun.write(dest, JSON.stringify(session))
|
||||
log.info(`migrating messages for session ${session.id}`)
|
||||
for await (const msgFile of new Bun.Glob(
|
||||
`storage/session/message/${session.id}/*.json`,
|
||||
).scan({
|
||||
for await (const msgFile of new Bun.Glob(`storage/session/message/${session.id}/*.json`).scan({
|
||||
cwd: fullProjectDir,
|
||||
absolute: true,
|
||||
})) {
|
||||
@@ -100,12 +98,12 @@ export namespace Storage {
|
||||
await Bun.write(dest, JSON.stringify(message))
|
||||
|
||||
log.info(`migrating parts for message ${message.id}`)
|
||||
for await (const partFile of new Bun.Glob(
|
||||
`storage/session/part/${session.id}/${message.id}/*.json`,
|
||||
).scan({
|
||||
cwd: fullProjectDir,
|
||||
absolute: true,
|
||||
})) {
|
||||
for await (const partFile of new Bun.Glob(`storage/session/part/${session.id}/${message.id}/*.json`).scan(
|
||||
{
|
||||
cwd: fullProjectDir,
|
||||
absolute: true,
|
||||
},
|
||||
)) {
|
||||
const dest = path.join(dir, "part", message.id, path.basename(partFile))
|
||||
const part = await Bun.file(partFile).json()
|
||||
log.info("copying", {
|
||||
@@ -128,9 +126,7 @@ export namespace Storage {
|
||||
if (!session.projectID) continue
|
||||
if (!session.summary?.diffs) continue
|
||||
const { diffs } = session.summary
|
||||
await Bun.file(path.join(dir, "session_diff", session.id + ".json")).write(
|
||||
JSON.stringify(diffs),
|
||||
)
|
||||
await Bun.file(path.join(dir, "session_diff", session.id + ".json")).write(JSON.stringify(diffs))
|
||||
await Bun.file(path.join(dir, "session", session.projectID, session.id + ".json")).write(
|
||||
JSON.stringify({
|
||||
...session,
|
||||
|
||||
@@ -51,9 +51,7 @@ export const BashTool = Tool.define("bash", {
|
||||
}),
|
||||
async execute(params, ctx) {
|
||||
if (params.timeout !== undefined && params.timeout < 0) {
|
||||
throw new Error(
|
||||
`Invalid timeout value: ${params.timeout}. Timeout must be a positive number.`,
|
||||
)
|
||||
throw new Error(`Invalid timeout value: ${params.timeout}. Timeout must be a positive number.`)
|
||||
}
|
||||
const timeout = Math.min(params.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT)
|
||||
const tree = await parser().then((p) => p.parse(params.command))
|
||||
@@ -101,10 +99,7 @@ export const BashTool = Tool.define("bash", {
|
||||
|
||||
// always allow cd if it passes above check
|
||||
if (command[0] !== "cd") {
|
||||
const action = Wildcard.allStructured(
|
||||
{ head: command[0], tail: command.slice(1) },
|
||||
permissions,
|
||||
)
|
||||
const action = Wildcard.allStructured({ head: command[0], tail: command.slice(1) }, permissions)
|
||||
if (action === "deny") {
|
||||
throw new Error(
|
||||
`The user has specifically restricted access to this command, you are not allowed to execute it. Here is the configuration: ${JSON.stringify(permissions)}`,
|
||||
|
||||
@@ -23,13 +23,8 @@ export const EditTool = Tool.define("edit", {
|
||||
parameters: z.object({
|
||||
filePath: z.string().describe("The absolute path to the file to modify"),
|
||||
oldString: z.string().describe("The text to replace"),
|
||||
newString: z
|
||||
.string()
|
||||
.describe("The text to replace it with (must be different from oldString)"),
|
||||
replaceAll: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Replace all occurrences of oldString (default false)"),
|
||||
newString: z.string().describe("The text to replace it with (must be different from oldString)"),
|
||||
replaceAll: z.boolean().optional().describe("Replace all occurrences of oldString (default false)"),
|
||||
}),
|
||||
async execute(params, ctx) {
|
||||
if (!params.filePath) {
|
||||
@@ -40,9 +35,7 @@ export const EditTool = Tool.define("edit", {
|
||||
throw new Error("oldString and newString must be different")
|
||||
}
|
||||
|
||||
const filePath = path.isAbsolute(params.filePath)
|
||||
? params.filePath
|
||||
: path.join(Instance.directory, params.filePath)
|
||||
const filePath = path.isAbsolute(params.filePath) ? params.filePath : path.join(Instance.directory, params.filePath)
|
||||
if (!Filesystem.contains(Instance.directory, filePath)) {
|
||||
const parentDir = path.dirname(filePath)
|
||||
await Permission.ask({
|
||||
@@ -179,11 +172,7 @@ function levenshtein(a: string, b: string): number {
|
||||
for (let i = 1; i <= a.length; i++) {
|
||||
for (let j = 1; j <= b.length; j++) {
|
||||
const cost = a[i - 1] === b[j - 1] ? 0 : 1
|
||||
matrix[i][j] = Math.min(
|
||||
matrix[i - 1][j] + 1,
|
||||
matrix[i][j - 1] + 1,
|
||||
matrix[i - 1][j - 1] + cost,
|
||||
)
|
||||
matrix[i][j] = Math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + cost)
|
||||
}
|
||||
}
|
||||
return matrix[a.length][b.length]
|
||||
@@ -385,9 +374,7 @@ export const WhitespaceNormalizedReplacer: Replacer = function* (content, find)
|
||||
// Find the actual substring in the original line that matches
|
||||
const words = find.trim().split(/\s+/)
|
||||
if (words.length > 0) {
|
||||
const pattern = words
|
||||
.map((word) => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
||||
.join("\\s+")
|
||||
const pattern = words.map((word) => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\s+")
|
||||
try {
|
||||
const regex = new RegExp(pattern)
|
||||
const match = line.match(regex)
|
||||
@@ -625,12 +612,7 @@ export function trimDiff(diff: string): string {
|
||||
return trimmedLines.join("\n")
|
||||
}
|
||||
|
||||
export function replace(
|
||||
content: string,
|
||||
oldString: string,
|
||||
newString: string,
|
||||
replaceAll = false,
|
||||
): string {
|
||||
export function replace(content: string, oldString: string, newString: string, replaceAll = false): string {
|
||||
if (oldString === newString) {
|
||||
throw new Error("oldString and newString must be different")
|
||||
}
|
||||
|
||||
@@ -9,14 +9,8 @@ export const GrepTool = Tool.define("grep", {
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
pattern: z.string().describe("The regex pattern to search for in file contents"),
|
||||
path: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("The directory to search in. Defaults to the current working directory."),
|
||||
include: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('File pattern to include in the search (e.g. "*.js", "*.{ts,tsx}")'),
|
||||
path: z.string().optional().describe("The directory to search in. Defaults to the current working directory."),
|
||||
include: z.string().optional().describe('File pattern to include in the search (e.g. "*.js", "*.{ts,tsx}")'),
|
||||
}),
|
||||
async execute(params) {
|
||||
if (!params.pattern) {
|
||||
|
||||
@@ -37,18 +37,13 @@ const LIMIT = 100
|
||||
export const ListTool = Tool.define("list", {
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
path: z
|
||||
.string()
|
||||
.describe("The absolute path to the directory to list (must be absolute, not relative)")
|
||||
.optional(),
|
||||
path: z.string().describe("The absolute path to the directory to list (must be absolute, not relative)").optional(),
|
||||
ignore: z.array(z.string()).describe("List of glob patterns to ignore").optional(),
|
||||
}),
|
||||
async execute(params) {
|
||||
const searchPath = path.resolve(Instance.directory, params.path || ".")
|
||||
|
||||
const ignoreGlobs = IGNORE_PATTERNS.map((p) => `!${p}*`).concat(
|
||||
params.ignore?.map((p) => `!${p}`) || [],
|
||||
)
|
||||
const ignoreGlobs = IGNORE_PATTERNS.map((p) => `!${p}*`).concat(params.ignore?.map((p) => `!${p}`) || [])
|
||||
const files = []
|
||||
for await (const file of Ripgrep.files({ cwd: searchPath, glob: ignoreGlobs })) {
|
||||
files.push(file)
|
||||
|
||||
@@ -11,9 +11,7 @@ export const LspDiagnosticTool = Tool.define("lsp_diagnostics", {
|
||||
path: z.string().describe("The path to the file to get diagnostics."),
|
||||
}),
|
||||
execute: async (args) => {
|
||||
const normalized = path.isAbsolute(args.path)
|
||||
? args.path
|
||||
: path.join(Instance.directory, args.path)
|
||||
const normalized = path.isAbsolute(args.path) ? args.path : path.join(Instance.directory, args.path)
|
||||
await LSP.touchFile(normalized, true)
|
||||
const diagnostics = await LSP.diagnostics()
|
||||
const file = diagnostics[normalized]
|
||||
|
||||
@@ -14,13 +14,8 @@ export const MultiEditTool = Tool.define("multiedit", {
|
||||
z.object({
|
||||
filePath: z.string().describe("The absolute path to the file to modify"),
|
||||
oldString: z.string().describe("The text to replace"),
|
||||
newString: z
|
||||
.string()
|
||||
.describe("The text to replace it with (must be different from oldString)"),
|
||||
replaceAll: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Replace all occurrences of oldString (default false)"),
|
||||
newString: z.string().describe("The text to replace it with (must be different from oldString)"),
|
||||
replaceAll: z.boolean().optional().describe("Replace all occurrences of oldString (default false)"),
|
||||
}),
|
||||
)
|
||||
.describe("Array of edit operations to perform sequentially on the file"),
|
||||
|
||||
@@ -18,10 +18,7 @@ export const ReadTool = Tool.define("read", {
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
filePath: z.string().describe("The path to the file to read"),
|
||||
offset: z.coerce
|
||||
.number()
|
||||
.describe("The line number to start reading from (0-based)")
|
||||
.optional(),
|
||||
offset: z.coerce.number().describe("The line number to start reading from (0-based)").optional(),
|
||||
limit: z.coerce.number().describe("The number of lines to read (defaults to 2000)").optional(),
|
||||
}),
|
||||
async execute(params, ctx) {
|
||||
@@ -56,16 +53,13 @@ export const ReadTool = Tool.define("read", {
|
||||
const suggestions = dirEntries
|
||||
.filter(
|
||||
(entry) =>
|
||||
entry.toLowerCase().includes(base.toLowerCase()) ||
|
||||
base.toLowerCase().includes(entry.toLowerCase()),
|
||||
entry.toLowerCase().includes(base.toLowerCase()) || base.toLowerCase().includes(entry.toLowerCase()),
|
||||
)
|
||||
.map((entry) => path.join(dir, entry))
|
||||
.slice(0, 3)
|
||||
|
||||
if (suggestions.length > 0) {
|
||||
throw new Error(
|
||||
`File not found: ${filepath}\n\nDid you mean one of these?\n${suggestions.join("\n")}`,
|
||||
)
|
||||
throw new Error(`File not found: ${filepath}\n\nDid you mean one of these?\n${suggestions.join("\n")}`)
|
||||
}
|
||||
|
||||
throw new Error(`File not found: ${filepath}`)
|
||||
|
||||
@@ -14,10 +14,7 @@ export const TaskTool = Tool.define("task", async () => {
|
||||
const description = DESCRIPTION.replace(
|
||||
"{agents}",
|
||||
agents
|
||||
.map(
|
||||
(a) =>
|
||||
`- ${a.name}: ${a.description ?? "This subagent should only be called manually by the user."}`,
|
||||
)
|
||||
.map((a) => `- ${a.name}: ${a.description ?? "This subagent should only be called manually by the user."}`)
|
||||
.join("\n"),
|
||||
)
|
||||
return {
|
||||
@@ -29,8 +26,7 @@ export const TaskTool = Tool.define("task", async () => {
|
||||
}),
|
||||
async execute(params, ctx) {
|
||||
const agent = await Agent.get(params.subagent_type)
|
||||
if (!agent)
|
||||
throw new Error(`Unknown agent type: ${params.subagent_type} is not a valid agent type`)
|
||||
if (!agent) throw new Error(`Unknown agent type: ${params.subagent_type} is not a valid agent type`)
|
||||
const session = await Session.create({
|
||||
parentID: ctx.sessionID,
|
||||
title: params.description + ` (@${agent.name} subagent)`,
|
||||
@@ -95,9 +91,7 @@ export const TaskTool = Tool.define("task", async () => {
|
||||
let all
|
||||
all = await Session.messages({ sessionID: session.id })
|
||||
all = all.filter((x) => x.info.role === "assistant")
|
||||
all = all.flatMap(
|
||||
(msg) => msg.parts.filter((x: any) => x.type === "tool") as MessageV2.ToolPart[],
|
||||
)
|
||||
all = all.flatMap((msg) => msg.parts.filter((x: any) => x.type === "tool") as MessageV2.ToolPart[])
|
||||
return {
|
||||
title: params.description,
|
||||
metadata: {
|
||||
|
||||
@@ -48,15 +48,13 @@ export const WebFetchTool = Tool.define("webfetch", {
|
||||
let acceptHeader = "*/*"
|
||||
switch (params.format) {
|
||||
case "markdown":
|
||||
acceptHeader =
|
||||
"text/markdown;q=1.0, text/x-markdown;q=0.9, text/plain;q=0.8, text/html;q=0.7, */*;q=0.1"
|
||||
acceptHeader = "text/markdown;q=1.0, text/x-markdown;q=0.9, text/plain;q=0.8, text/html;q=0.7, */*;q=0.1"
|
||||
break
|
||||
case "text":
|
||||
acceptHeader = "text/plain;q=1.0, text/markdown;q=0.9, text/html;q=0.8, */*;q=0.1"
|
||||
break
|
||||
case "html":
|
||||
acceptHeader =
|
||||
"text/html;q=1.0, application/xhtml+xml;q=0.9, text/plain;q=0.8, text/markdown;q=0.7, */*;q=0.1"
|
||||
acceptHeader = "text/html;q=1.0, application/xhtml+xml;q=0.9, text/plain;q=0.8, text/markdown;q=0.7, */*;q=0.1"
|
||||
break
|
||||
default:
|
||||
acceptHeader =
|
||||
@@ -160,9 +158,7 @@ async function extractTextFromHTML(html: string) {
|
||||
.on("*", {
|
||||
element(element) {
|
||||
// Reset skip flag when entering other elements
|
||||
if (
|
||||
!["script", "style", "noscript", "iframe", "object", "embed"].includes(element.tagName)
|
||||
) {
|
||||
if (!["script", "style", "noscript", "iframe", "object", "embed"].includes(element.tagName)) {
|
||||
skipContent = false
|
||||
}
|
||||
},
|
||||
|
||||
@@ -15,14 +15,10 @@ export const WriteTool = Tool.define("write", {
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
content: z.string().describe("The content to write to the file"),
|
||||
filePath: z
|
||||
.string()
|
||||
.describe("The absolute path to the file to write (must be absolute, not relative)"),
|
||||
filePath: z.string().describe("The absolute path to the file to write (must be absolute, not relative)"),
|
||||
}),
|
||||
async execute(params, ctx) {
|
||||
const filepath = path.isAbsolute(params.filePath)
|
||||
? params.filePath
|
||||
: path.join(Instance.directory, params.filePath)
|
||||
const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(Instance.directory, params.filePath)
|
||||
if (!Filesystem.contains(Instance.directory, filepath)) {
|
||||
const parentDir = path.dirname(filepath)
|
||||
await Permission.ask({
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
export namespace Binary {
|
||||
export function search<T>(
|
||||
array: T[],
|
||||
id: string,
|
||||
compare: (item: T) => string,
|
||||
): { found: boolean; index: number } {
|
||||
export function search<T>(array: T[], id: string, compare: (item: T) => string): { found: boolean; index: number } {
|
||||
let left = 0
|
||||
let right = array.length - 1
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
export function defer<T extends () => void | Promise<void>>(
|
||||
fn: T,
|
||||
): T extends () => Promise<void>
|
||||
? { [Symbol.asyncDispose]: () => Promise<void> }
|
||||
: { [Symbol.dispose]: () => void } {
|
||||
): T extends () => Promise<void> ? { [Symbol.asyncDispose]: () => Promise<void> } : { [Symbol.dispose]: () => void } {
|
||||
return {
|
||||
[Symbol.dispose]() {
|
||||
fn()
|
||||
|
||||
@@ -4,17 +4,11 @@ export namespace EventLoop {
|
||||
export async function wait() {
|
||||
return new Promise<void>((resolve) => {
|
||||
const check = () => {
|
||||
const active = [
|
||||
...(process as any)._getActiveHandles(),
|
||||
...(process as any)._getActiveRequests(),
|
||||
]
|
||||
const active = [...(process as any)._getActiveHandles(), ...(process as any)._getActiveRequests()]
|
||||
Log.Default.info("eventloop", {
|
||||
active,
|
||||
})
|
||||
if (
|
||||
(process as any)._getActiveHandles().length === 0 &&
|
||||
(process as any)._getActiveRequests().length === 0
|
||||
) {
|
||||
if ((process as any)._getActiveHandles().length === 0 && (process as any)._getActiveRequests().length === 0) {
|
||||
resolve()
|
||||
} else {
|
||||
setImmediate(check)
|
||||
|
||||
@@ -39,12 +39,7 @@ export namespace Lock {
|
||||
}
|
||||
|
||||
// Clean up empty locks
|
||||
if (
|
||||
lock.readers === 0 &&
|
||||
!lock.writer &&
|
||||
lock.waitingReaders.length === 0 &&
|
||||
lock.waitingWriters.length === 0
|
||||
) {
|
||||
if (lock.readers === 0 && !lock.writer && lock.waitingReaders.length === 0 && lock.waitingWriters.length === 0) {
|
||||
locks.delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,7 @@ import { Global } from "../global"
|
||||
import z from "zod"
|
||||
|
||||
export namespace Log {
|
||||
export const Level = z
|
||||
.enum(["DEBUG", "INFO", "WARN", "ERROR"])
|
||||
.meta({ ref: "LogLevel", description: "Log level" })
|
||||
export const Level = z.enum(["DEBUG", "INFO", "WARN", "ERROR"]).meta({ ref: "LogLevel", description: "Log level" })
|
||||
export type Level = z.infer<typeof Level>
|
||||
|
||||
const levelPriority: Record<Level, number> = {
|
||||
@@ -121,11 +119,7 @@ export namespace Log {
|
||||
const next = new Date()
|
||||
const diff = next.getTime() - last
|
||||
last = next.getTime()
|
||||
return (
|
||||
[next.toISOString().split(".")[0], "+" + diff + "ms", prefix, message]
|
||||
.filter(Boolean)
|
||||
.join(" ") + "\n"
|
||||
)
|
||||
return [next.toISOString().split(".")[0], "+" + diff + "ms", prefix, message].filter(Boolean).join(" ") + "\n"
|
||||
}
|
||||
const result: Logger = {
|
||||
debug(message?: any, extra?: Record<string, any>) {
|
||||
|
||||
@@ -30,10 +30,7 @@ export namespace Rpc {
|
||||
}
|
||||
}
|
||||
return {
|
||||
call<Method extends keyof T>(
|
||||
method: Method,
|
||||
input: Parameters<T[Method]>[0],
|
||||
): Promise<ReturnType<T[Method]>> {
|
||||
call<Method extends keyof T>(method: Method, input: Parameters<T[Method]>[0]): Promise<ReturnType<T[Method]>> {
|
||||
const requestId = id++
|
||||
return new Promise((resolve) => {
|
||||
pending.set(requestId, resolve)
|
||||
|
||||
@@ -15,11 +15,7 @@ export namespace Wildcard {
|
||||
}
|
||||
|
||||
export function all(input: string, patterns: Record<string, any>) {
|
||||
const sorted = pipe(
|
||||
patterns,
|
||||
Object.entries,
|
||||
sortBy([([key]) => key.length, "asc"], [([key]) => key, "asc"]),
|
||||
)
|
||||
const sorted = pipe(patterns, Object.entries, sortBy([([key]) => key.length, "asc"], [([key]) => key, "asc"]))
|
||||
let result = undefined
|
||||
for (const [pattern, value] of sorted) {
|
||||
if (match(input, pattern)) {
|
||||
@@ -30,15 +26,8 @@ export namespace Wildcard {
|
||||
return result
|
||||
}
|
||||
|
||||
export function allStructured(
|
||||
input: { head: string; tail: string[] },
|
||||
patterns: Record<string, any>,
|
||||
) {
|
||||
const sorted = pipe(
|
||||
patterns,
|
||||
Object.entries,
|
||||
sortBy([([key]) => key.length, "asc"], [([key]) => key, "asc"]),
|
||||
)
|
||||
export function allStructured(input: { head: string; tail: string[] }, patterns: Record<string, any>) {
|
||||
const sorted = pipe(patterns, Object.entries, sortBy([([key]) => key.length, "asc"], [([key]) => key, "asc"]))
|
||||
let result = undefined
|
||||
for (const [pattern, value] of sorted) {
|
||||
const parts = pattern.split(/\s+/)
|
||||
|
||||
@@ -381,11 +381,7 @@ test("resolves scoped npm plugins in config", async () => {
|
||||
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify(
|
||||
{ $schema: "https://opencode.ai/config.json", plugin: ["@scope/plugin"] },
|
||||
null,
|
||||
2,
|
||||
),
|
||||
JSON.stringify({ $schema: "https://opencode.ai/config.json", plugin: ["@scope/plugin"] }, null, 2),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -13,8 +13,7 @@ describe("ide", () => {
|
||||
|
||||
test("should detect Visual Studio Code", () => {
|
||||
process.env["TERM_PROGRAM"] = "vscode"
|
||||
process.env["GIT_ASKPASS"] =
|
||||
"/path/to/Visual Studio Code.app/Contents/Resources/app/extensions/git/dist/askpass.sh"
|
||||
process.env["GIT_ASKPASS"] = "/path/to/Visual Studio Code.app/Contents/Resources/app/extensions/git/dist/askpass.sh"
|
||||
|
||||
expect(Ide.ide()).toBe("Visual Studio Code")
|
||||
})
|
||||
@@ -29,24 +28,21 @@ describe("ide", () => {
|
||||
|
||||
test("should detect Cursor", () => {
|
||||
process.env["TERM_PROGRAM"] = "vscode"
|
||||
process.env["GIT_ASKPASS"] =
|
||||
"/path/to/Cursor.app/Contents/Resources/app/extensions/git/dist/askpass.sh"
|
||||
process.env["GIT_ASKPASS"] = "/path/to/Cursor.app/Contents/Resources/app/extensions/git/dist/askpass.sh"
|
||||
|
||||
expect(Ide.ide()).toBe("Cursor")
|
||||
})
|
||||
|
||||
test("should detect VSCodium", () => {
|
||||
process.env["TERM_PROGRAM"] = "vscode"
|
||||
process.env["GIT_ASKPASS"] =
|
||||
"/path/to/VSCodium.app/Contents/Resources/app/extensions/git/dist/askpass.sh"
|
||||
process.env["GIT_ASKPASS"] = "/path/to/VSCodium.app/Contents/Resources/app/extensions/git/dist/askpass.sh"
|
||||
|
||||
expect(Ide.ide()).toBe("VSCodium")
|
||||
})
|
||||
|
||||
test("should detect Windsurf", () => {
|
||||
process.env["TERM_PROGRAM"] = "vscode"
|
||||
process.env["GIT_ASKPASS"] =
|
||||
"/path/to/Windsurf.app/Contents/Resources/app/extensions/git/dist/askpass.sh"
|
||||
process.env["GIT_ASKPASS"] = "/path/to/Windsurf.app/Contents/Resources/app/extensions/git/dist/askpass.sh"
|
||||
|
||||
expect(Ide.ide()).toBe("Windsurf")
|
||||
})
|
||||
|
||||
@@ -13,9 +13,7 @@ function apiError(headers?: Record<string, string>): MessageV2.APIError {
|
||||
describe("session.retry.getRetryDelayInMs", () => {
|
||||
test("doubles delay on each attempt when headers missing", () => {
|
||||
const error = apiError()
|
||||
const delays = Array.from({ length: 7 }, (_, index) =>
|
||||
SessionRetry.getRetryDelayInMs(error, index + 1),
|
||||
)
|
||||
const delays = Array.from({ length: 7 }, (_, index) => SessionRetry.getRetryDelayInMs(error, index + 1))
|
||||
expect(delays).toStrictEqual([2000, 4000, 8000, 16000, 32000, 64000, 128000])
|
||||
})
|
||||
|
||||
|
||||
@@ -31,9 +31,7 @@ describe("tool.patch", () => {
|
||||
await Instance.provide({
|
||||
directory: "/tmp",
|
||||
fn: async () => {
|
||||
expect(patchTool.execute({ patchText: "invalid patch" }, ctx)).rejects.toThrow(
|
||||
"Failed to parse patch",
|
||||
)
|
||||
expect(patchTool.execute({ patchText: "invalid patch" }, ctx)).rejects.toThrow("Failed to parse patch")
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -45,9 +43,7 @@ describe("tool.patch", () => {
|
||||
const emptyPatch = `*** Begin Patch
|
||||
*** End Patch`
|
||||
|
||||
expect(patchTool.execute({ patchText: emptyPatch }, ctx)).rejects.toThrow(
|
||||
"No file changes found in patch",
|
||||
)
|
||||
expect(patchTool.execute({ patchText: emptyPatch }, ctx)).rejects.toThrow("No file changes found in patch")
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -116,9 +112,7 @@ describe("tool.patch", () => {
|
||||
// Verify file was created with correct content
|
||||
const filePath = path.join(fixture.path, "config.js")
|
||||
const content = await fs.readFile(filePath, "utf-8")
|
||||
expect(content).toBe(
|
||||
'const API_KEY = "test-key"\nconst DEBUG = false\nconst VERSION = "1.0"',
|
||||
)
|
||||
expect(content).toBe('const API_KEY = "test-key"\nconst DEBUG = false\nconst VERSION = "1.0"')
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -24,12 +24,9 @@ test("allStructured matches command sequences", () => {
|
||||
"git status*": "allow",
|
||||
}
|
||||
expect(Wildcard.allStructured({ head: "git", tail: ["status", "--short"] }, rules)).toBe("allow")
|
||||
expect(
|
||||
Wildcard.allStructured(
|
||||
{ head: "npm", tail: ["run", "build", "--watch"] },
|
||||
{ "npm run *": "allow" },
|
||||
),
|
||||
).toBe("allow")
|
||||
expect(Wildcard.allStructured({ head: "npm", tail: ["run", "build", "--watch"] }, { "npm run *": "allow" })).toBe(
|
||||
"allow",
|
||||
)
|
||||
expect(Wildcard.allStructured({ head: "ls", tail: ["-la"] }, rules)).toBeUndefined()
|
||||
})
|
||||
|
||||
@@ -54,7 +51,5 @@ test("allStructured handles sed flags", () => {
|
||||
expect(Wildcard.allStructured({ head: "sed", tail: ["-i", "file"] }, rules)).toBe("ask")
|
||||
expect(Wildcard.allStructured({ head: "sed", tail: ["-i.bak", "file"] }, rules)).toBe("ask")
|
||||
expect(Wildcard.allStructured({ head: "sed", tail: ["-n", "1p", "file"] }, rules)).toBe("allow")
|
||||
expect(
|
||||
Wildcard.allStructured({ head: "sed", tail: ["-i", "-n", "/./p", "myfile.txt"] }, rules),
|
||||
).toBe("ask")
|
||||
expect(Wildcard.allStructured({ head: "sed", tail: ["-i", "-n", "/./p", "myfile.txt"] }, rules)).toBe("ask")
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user