chore: format code

This commit is contained in:
GitHub Action
2025-11-08 01:59:02 +00:00
parent 16357e8041
commit 34ff87d504
182 changed files with 940 additions and 3646 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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\//, "")

View File

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

View File

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

View File

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

View File

@@ -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. ![Image](https://github.com/user-attachments/assets/xxxx)
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})&nbsp;&nbsp;|&nbsp;&nbsp;`
: ""
const shareUrl = shareId ? `[opencode session](${shareBaseUrl}/s/${shareId})&nbsp;&nbsp;|&nbsp;&nbsp;` : ""
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")
}

View File

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

View File

@@ -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)}%)`

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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()} />
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"]) {

View File

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

View File

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

View File

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

View File

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

View File

@@ -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.`)
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View 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}`)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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+/)

View File

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

View File

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

View File

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

View File

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

View File

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