mirror of
https://github.com/sst/opencode.git
synced 2025-08-04 05:28:16 +00:00
format
This commit is contained in:
parent
470c5b66fd
commit
8791920fb2
88 changed files with 819 additions and 3229 deletions
|
@ -38,10 +38,7 @@ export class SyncServer extends DurableObject<Env> {
|
|||
|
||||
async publish(key: string, content: any) {
|
||||
const sessionID = await this.getSessionID()
|
||||
if (
|
||||
!key.startsWith(`session/info/${sessionID}`) &&
|
||||
!key.startsWith(`session/message/${sessionID}/`)
|
||||
)
|
||||
if (!key.startsWith(`session/info/${sessionID}`) && !key.startsWith(`session/message/${sessionID}/`))
|
||||
return new Response("Error: Invalid key", { status: 400 })
|
||||
|
||||
// store message
|
||||
|
@ -184,8 +181,7 @@ export default {
|
|||
}
|
||||
const id = url.searchParams.get("id")
|
||||
console.log("share_poll", id)
|
||||
if (!id)
|
||||
return new Response("Error: Share ID is required", { status: 400 })
|
||||
if (!id) return new Response("Error: Share ID is required", { status: 400 })
|
||||
const stub = env.SYNC_SERVER.get(env.SYNC_SERVER.idFromName(id))
|
||||
return stub.fetch(request)
|
||||
}
|
||||
|
@ -193,8 +189,7 @@ export default {
|
|||
if (request.method === "GET" && method === "share_data") {
|
||||
const id = url.searchParams.get("id")
|
||||
console.log("share_data", id)
|
||||
if (!id)
|
||||
return new Response("Error: Share ID is required", { status: 400 })
|
||||
if (!id) return new Response("Error: Share ID is required", { status: 400 })
|
||||
const stub = env.SYNC_SERVER.get(env.SYNC_SERVER.idFromName(id))
|
||||
const data = await stub.getData()
|
||||
|
||||
|
|
|
@ -57,8 +57,7 @@ for (const [os, arch] of targets) {
|
|||
2,
|
||||
),
|
||||
)
|
||||
if (!dry)
|
||||
await $`cd dist/${name} && bun publish --access public --tag ${npmTag}`
|
||||
if (!dry) await $`cd dist/${name} && bun publish --access public --tag ${npmTag}`
|
||||
optionalDependencies[name] = version
|
||||
}
|
||||
|
||||
|
@ -82,8 +81,7 @@ await Bun.file(`./dist/${pkg.name}/package.json`).write(
|
|||
2,
|
||||
),
|
||||
)
|
||||
if (!dry)
|
||||
await $`cd ./dist/${pkg.name} && bun publish --access public --tag ${npmTag}`
|
||||
if (!dry) await $`cd ./dist/${pkg.name} && bun publish --access public --tag ${npmTag}`
|
||||
|
||||
if (!snapshot) {
|
||||
// Github Release
|
||||
|
@ -91,15 +89,11 @@ if (!snapshot) {
|
|||
await $`cd dist/${key}/bin && zip -r ../../${key}.zip *`
|
||||
}
|
||||
|
||||
const previous = await fetch(
|
||||
"https://api.github.com/repos/sst/opencode/releases/latest",
|
||||
)
|
||||
const previous = await fetch("https://api.github.com/repos/sst/opencode/releases/latest")
|
||||
.then((res) => res.json())
|
||||
.then((data) => data.tag_name)
|
||||
|
||||
const commits = await fetch(
|
||||
`https://api.github.com/repos/sst/opencode/compare/${previous}...HEAD`,
|
||||
)
|
||||
const commits = await fetch(`https://api.github.com/repos/sst/opencode/compare/${previous}...HEAD`)
|
||||
.then((res) => res.json())
|
||||
.then((data) => data.commits || [])
|
||||
|
||||
|
@ -117,26 +111,13 @@ if (!snapshot) {
|
|||
})
|
||||
.join("\n")
|
||||
|
||||
if (!dry)
|
||||
await $`gh release create v${version} --title "v${version}" --notes ${notes} ./dist/*.zip`
|
||||
if (!dry) await $`gh release create v${version} --title "v${version}" --notes ${notes} ./dist/*.zip`
|
||||
|
||||
// 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())
|
||||
|
||||
// AUR package
|
||||
const pkgbuild = [
|
||||
|
@ -170,9 +151,7 @@ if (!snapshot) {
|
|||
for (const pkg of ["opencode", "opencode-bin"]) {
|
||||
await $`rm -rf ./dist/aur-${pkg}`
|
||||
await $`git clone ssh://aur@aur.archlinux.org/${pkg}.git ./dist/aur-${pkg}`
|
||||
await Bun.file(`./dist/aur-${pkg}/PKGBUILD`).write(
|
||||
pkgbuild.replace("${pkg}", pkg),
|
||||
)
|
||||
await Bun.file(`./dist/aur-${pkg}/PKGBUILD`).write(pkgbuild.replace("${pkg}", pkg))
|
||||
await $`cd ./dist/aur-${pkg} && makepkg --printsrcinfo > .SRCINFO`
|
||||
await $`cd ./dist/aur-${pkg} && git add PKGBUILD .SRCINFO`
|
||||
await $`cd ./dist/aur-${pkg} && git commit -m "Update to v${version}"`
|
||||
|
|
|
@ -45,23 +45,14 @@ export namespace App {
|
|||
}
|
||||
|
||||
export const provideExisting = ctx.provide
|
||||
export async function provide<T>(
|
||||
input: Input,
|
||||
cb: (app: App.Info) => Promise<T>,
|
||||
) {
|
||||
export async function provide<T>(input: Input, cb: (app: App.Info) => Promise<T>) {
|
||||
log.info("creating", {
|
||||
cwd: input.cwd,
|
||||
})
|
||||
const git = await Filesystem.findUp(".git", input.cwd).then(([x]) =>
|
||||
x ? path.dirname(x) : undefined,
|
||||
)
|
||||
const git = await Filesystem.findUp(".git", input.cwd).then(([x]) => (x ? path.dirname(x) : undefined))
|
||||
log.info("git", { git })
|
||||
|
||||
const data = path.join(
|
||||
Global.Path.data,
|
||||
"project",
|
||||
git ? directory(git) : "global",
|
||||
)
|
||||
const data = path.join(Global.Path.data, "project", git ? directory(git) : "global")
|
||||
const stateFile = Bun.file(path.join(data, APP_JSON))
|
||||
const state = (await stateFile.json().catch(() => ({}))) as {
|
||||
initialized: number
|
||||
|
|
|
@ -10,14 +10,8 @@ export namespace AuthAnthropic {
|
|||
url.searchParams.set("code", "true")
|
||||
url.searchParams.set("client_id", CLIENT_ID)
|
||||
url.searchParams.set("response_type", "code")
|
||||
url.searchParams.set(
|
||||
"redirect_uri",
|
||||
"https://console.anthropic.com/oauth/code/callback",
|
||||
)
|
||||
url.searchParams.set(
|
||||
"scope",
|
||||
"org:create_api_key user:profile user:inference",
|
||||
)
|
||||
url.searchParams.set("redirect_uri", "https://console.anthropic.com/oauth/code/callback")
|
||||
url.searchParams.set("scope", "org:create_api_key user:profile user:inference")
|
||||
url.searchParams.set("code_challenge", pkce.challenge)
|
||||
url.searchParams.set("code_challenge_method", "S256")
|
||||
url.searchParams.set("state", pkce.verifier)
|
||||
|
@ -57,20 +51,17 @@ export namespace AuthAnthropic {
|
|||
const info = await Auth.get("anthropic")
|
||||
if (!info || info.type !== "oauth") return
|
||||
if (info.access && info.expires > Date.now()) return info.access
|
||||
const response = await fetch(
|
||||
"https://console.anthropic.com/v1/oauth/token",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: info.refresh,
|
||||
client_id: CLIENT_ID,
|
||||
}),
|
||||
const response = await fetch("https://console.anthropic.com/v1/oauth/token", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
)
|
||||
body: JSON.stringify({
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: info.refresh,
|
||||
client_id: CLIENT_ID,
|
||||
}),
|
||||
})
|
||||
if (!response.ok) return
|
||||
const json = await response.json()
|
||||
await Auth.set("anthropic", {
|
||||
|
|
|
@ -4,9 +4,7 @@ import path from "path"
|
|||
|
||||
export const AuthCopilot = lazy(async () => {
|
||||
const file = Bun.file(path.join(Global.Path.state, "plugin", "copilot.ts"))
|
||||
const response = fetch(
|
||||
"https://raw.githubusercontent.com/sst/opencode-github-copilot/refs/heads/main/auth.ts",
|
||||
)
|
||||
const response = fetch("https://raw.githubusercontent.com/sst/opencode-github-copilot/refs/heads/main/auth.ts")
|
||||
.then((x) => Bun.write(file, x))
|
||||
.catch(() => {})
|
||||
|
||||
|
|
|
@ -122,10 +122,7 @@ export namespace AuthGithubCopilot {
|
|||
return tokenData.token
|
||||
}
|
||||
|
||||
export const DeviceCodeError = NamedError.create(
|
||||
"DeviceCodeError",
|
||||
z.object({}),
|
||||
)
|
||||
export const DeviceCodeError = NamedError.create("DeviceCodeError", z.object({}))
|
||||
|
||||
export const TokenExchangeError = NamedError.create(
|
||||
"TokenExchangeError",
|
||||
|
|
|
@ -18,10 +18,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,
|
||||
|
@ -72,10 +69,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)
|
||||
}
|
||||
|
|
|
@ -5,10 +5,7 @@ import { Format } from "../format"
|
|||
import { LSP } from "../lsp"
|
||||
import { Share } from "../share/share"
|
||||
|
||||
export async function bootstrap<T>(
|
||||
input: App.Input,
|
||||
cb: (app: App.Info) => Promise<T>,
|
||||
) {
|
||||
export async function bootstrap<T>(input: App.Input, cb: (app: App.Info) => Promise<T>) {
|
||||
return App.provide(input, async (app) => {
|
||||
Share.init()
|
||||
Format.init()
|
||||
|
|
|
@ -15,11 +15,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() {},
|
||||
})
|
||||
|
||||
|
@ -31,9 +27,7 @@ export const AuthListCommand = cmd({
|
|||
UI.empty()
|
||||
const authPath = path.join(Global.Path.data, "auth.json")
|
||||
const homedir = os.homedir()
|
||||
const displayPath = authPath.startsWith(homedir)
|
||||
? authPath.replace(homedir, "~")
|
||||
: authPath
|
||||
const displayPath = authPath.startsWith(homedir) ? authPath.replace(homedir, "~") : authPath
|
||||
prompts.intro(`Credentials ${UI.Style.TEXT_DIM}${displayPath}`)
|
||||
const results = await Auth.all().then((x) => Object.entries(x))
|
||||
const database = await ModelsDev.get()
|
||||
|
@ -114,8 +108,7 @@ export const AuthLoginCommand = cmd({
|
|||
if (provider === "other") {
|
||||
provider = await prompts.text({
|
||||
message: "Enter provider id",
|
||||
validate: (x) =>
|
||||
x.match(/^[a-z-]+$/) ? undefined : "a-z and hyphens only",
|
||||
validate: (x) => (x.match(/^[a-z-]+$/) ? undefined : "a-z and hyphens only"),
|
||||
})
|
||||
if (prompts.isCancel(provider)) throw new UI.CancelledError()
|
||||
provider = provider.replace(/^@ai-sdk\//, "")
|
||||
|
@ -186,17 +179,13 @@ export const AuthLoginCommand = cmd({
|
|||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
const deviceInfo = await copilot.authorize()
|
||||
|
||||
prompts.note(
|
||||
`Please visit: ${deviceInfo.verification}\nEnter code: ${deviceInfo.user}`,
|
||||
)
|
||||
prompts.note(`Please visit: ${deviceInfo.verification}\nEnter code: ${deviceInfo.user}`)
|
||||
|
||||
const spinner = prompts.spinner()
|
||||
spinner.start("Waiting for authorization...")
|
||||
|
||||
while (true) {
|
||||
await new Promise((resolve) =>
|
||||
setTimeout(resolve, deviceInfo.interval * 1000),
|
||||
)
|
||||
await new Promise((resolve) => setTimeout(resolve, deviceInfo.interval * 1000))
|
||||
const response = await copilot.poll(deviceInfo.device)
|
||||
if (response.status === "pending") continue
|
||||
if (response.status === "success") {
|
||||
|
@ -248,12 +237,7 @@ export const AuthLogoutCommand = cmd({
|
|||
const providerID = await prompts.select({
|
||||
message: "Select provider",
|
||||
options: credentials.map(([key, value]) => ({
|
||||
label:
|
||||
(database[key]?.name || key) +
|
||||
UI.Style.TEXT_DIM +
|
||||
" (" +
|
||||
value.type +
|
||||
")",
|
||||
label: (database[key]?.name || key) + UI.Style.TEXT_DIM + " (" + value.type + ")",
|
||||
value: key,
|
||||
})),
|
||||
})
|
||||
|
|
|
@ -31,7 +31,6 @@ const FileStatusCommand = cmd({
|
|||
|
||||
export const FileCommand = cmd({
|
||||
command: "file",
|
||||
builder: (yargs) =>
|
||||
yargs.command(FileReadCommand).command(FileStatusCommand).demandCommand(),
|
||||
builder: (yargs) => yargs.command(FileReadCommand).command(FileStatusCommand).demandCommand(),
|
||||
async handler() {},
|
||||
})
|
||||
|
|
|
@ -17,9 +17,7 @@ export const DebugCommand = cmd({
|
|||
command: "wait",
|
||||
async handler() {
|
||||
await bootstrap({ cwd: process.cwd() }, async () => {
|
||||
await new Promise((resolve) =>
|
||||
setTimeout(resolve, 1_000 * 60 * 60 * 24),
|
||||
)
|
||||
await new Promise((resolve) => setTimeout(resolve, 1_000 * 60 * 60 * 24))
|
||||
})
|
||||
},
|
||||
})
|
||||
|
|
|
@ -5,15 +5,13 @@ import { Log } from "../../../util/log"
|
|||
|
||||
export const LSPCommand = cmd({
|
||||
command: "lsp",
|
||||
builder: (yargs) =>
|
||||
yargs.command(DiagnosticsCommand).command(SymbolsCommand).demandCommand(),
|
||||
builder: (yargs) => yargs.command(DiagnosticsCommand).command(SymbolsCommand).demandCommand(),
|
||||
async handler() {},
|
||||
})
|
||||
|
||||
const DiagnosticsCommand = cmd({
|
||||
command: "diagnostics <file>",
|
||||
builder: (yargs) =>
|
||||
yargs.positional("file", { type: "string", demandOption: true }),
|
||||
builder: (yargs) => yargs.positional("file", { type: "string", demandOption: true }),
|
||||
async handler(args) {
|
||||
await bootstrap({ cwd: process.cwd() }, async () => {
|
||||
await LSP.touchFile(args.file, true)
|
||||
|
@ -24,8 +22,7 @@ const DiagnosticsCommand = cmd({
|
|||
|
||||
export const SymbolsCommand = cmd({
|
||||
command: "symbols <query>",
|
||||
builder: (yargs) =>
|
||||
yargs.positional("query", { type: "string", demandOption: true }),
|
||||
builder: (yargs) => yargs.positional("query", { type: "string", demandOption: true }),
|
||||
async handler(args) {
|
||||
await bootstrap({ cwd: process.cwd() }, async () => {
|
||||
await LSP.touchFile("./src/index.ts", true)
|
||||
|
|
|
@ -5,12 +5,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() {},
|
||||
})
|
||||
|
||||
|
|
|
@ -4,11 +4,7 @@ import { cmd } from "../cmd"
|
|||
|
||||
export const SnapshotCommand = cmd({
|
||||
command: "snapshot",
|
||||
builder: (yargs) =>
|
||||
yargs
|
||||
.command(SnapshotCreateCommand)
|
||||
.command(SnapshotRestoreCommand)
|
||||
.demandCommand(),
|
||||
builder: (yargs) => yargs.command(SnapshotCreateCommand).command(SnapshotRestoreCommand).demandCommand(),
|
||||
async handler() {},
|
||||
})
|
||||
|
||||
|
|
|
@ -10,9 +10,6 @@ export const GenerateCommand = {
|
|||
const dir = "gen"
|
||||
await fs.rmdir(dir, { recursive: true }).catch(() => {})
|
||||
await fs.mkdir(dir, { recursive: true })
|
||||
await Bun.write(
|
||||
path.join(dir, "openapi.json"),
|
||||
JSON.stringify(specs, null, 2),
|
||||
)
|
||||
await Bun.write(path.join(dir, "openapi.json"), JSON.stringify(specs, null, 2))
|
||||
},
|
||||
} satisfies CommandModule
|
||||
|
|
|
@ -84,21 +84,12 @@ export const RunCommand = cmd({
|
|||
const cfg = await Config.get()
|
||||
if (cfg.autoshare || Flag.OPENCODE_AUTO_SHARE || args.share) {
|
||||
await Session.share(session.id)
|
||||
UI.println(
|
||||
UI.Style.TEXT_INFO_BOLD +
|
||||
"~ https://opencode.ai/s/" +
|
||||
session.id.slice(-8),
|
||||
)
|
||||
UI.println(UI.Style.TEXT_INFO_BOLD + "~ https://opencode.ai/s/" + session.id.slice(-8))
|
||||
}
|
||||
UI.empty()
|
||||
|
||||
const { providerID, modelID } = args.model
|
||||
? Provider.parseModel(args.model)
|
||||
: await Provider.defaultModel()
|
||||
UI.println(
|
||||
UI.Style.TEXT_NORMAL_BOLD + "@ ",
|
||||
UI.Style.TEXT_NORMAL + `${providerID}/${modelID}`,
|
||||
)
|
||||
const { providerID, modelID } = args.model ? Provider.parseModel(args.model) : await Provider.defaultModel()
|
||||
UI.println(UI.Style.TEXT_NORMAL_BOLD + "@ ", UI.Style.TEXT_NORMAL + `${providerID}/${modelID}`)
|
||||
UI.empty()
|
||||
|
||||
function printEvent(color: string, type: string, title: string) {
|
||||
|
@ -115,10 +106,7 @@ export const RunCommand = cmd({
|
|||
const part = evt.properties.part
|
||||
|
||||
if (part.type === "tool" && part.state.status === "completed") {
|
||||
const [tool, color] = TOOL[part.tool] ?? [
|
||||
part.tool,
|
||||
UI.Style.TEXT_INFO_BOLD,
|
||||
]
|
||||
const [tool, color] = TOOL[part.tool] ?? [part.tool, UI.Style.TEXT_INFO_BOLD]
|
||||
printEvent(color, tool, part.state.title || "Unknown")
|
||||
}
|
||||
|
||||
|
|
|
@ -38,9 +38,7 @@ export const ServeCommand = cmd({
|
|||
hostname,
|
||||
})
|
||||
|
||||
console.log(
|
||||
`opencode server listening on http://${server.hostname}:${server.port}`,
|
||||
)
|
||||
console.log(`opencode server listening on http://${server.hostname}:${server.port}`)
|
||||
|
||||
await new Promise(() => {})
|
||||
|
||||
|
|
|
@ -40,9 +40,7 @@ export const TuiCommand = cmd({
|
|||
})
|
||||
|
||||
let cmd = ["go", "run", "./main.go"]
|
||||
let cwd = Bun.fileURLToPath(
|
||||
new URL("../../../../tui/cmd/opencode", import.meta.url),
|
||||
)
|
||||
let cwd = Bun.fileURLToPath(new URL("../../../../tui/cmd/opencode", import.meta.url))
|
||||
if (Bun.embeddedFiles.length > 0) {
|
||||
const blob = Bun.embeddedFiles[0] as File
|
||||
let binaryName = blob.name
|
||||
|
|
|
@ -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 seems to be managed by a package manager`,
|
||||
)
|
||||
prompts.log.error(`opencode is installed to ${process.execPath} and seems to be managed by a package manager`)
|
||||
prompts.outro("Done")
|
||||
return
|
||||
}
|
||||
|
@ -37,9 +35,7 @@ export const UpgradeCommand = {
|
|||
const target = args.target ?? (await Installation.latest())
|
||||
|
||||
if (Installation.VERSION === target) {
|
||||
prompts.log.warn(
|
||||
`opencode upgrade skipped: ${target} is already installed`,
|
||||
)
|
||||
prompts.log.warn(`opencode upgrade skipped: ${target} is already installed`)
|
||||
prompts.outro("Done")
|
||||
return
|
||||
}
|
||||
|
@ -50,8 +46,7 @@ export const UpgradeCommand = {
|
|||
const err = await Installation.upgrade(method, target).catch((err) => err)
|
||||
if (err) {
|
||||
spinner.stop("Upgrade failed")
|
||||
if (err instanceof Installation.UpgradeFailedError)
|
||||
prompts.log.error(err.data.stderr)
|
||||
if (err instanceof Installation.UpgradeFailedError) prompts.log.error(err.data.stderr)
|
||||
else if (err instanceof Error) prompts.log.error(err.message)
|
||||
prompts.outro("Done")
|
||||
return
|
||||
|
|
|
@ -5,14 +5,11 @@ import { UI } from "./ui"
|
|||
export function FormatError(input: unknown) {
|
||||
if (MCP.Failed.isInstance(input))
|
||||
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`
|
||||
if (Config.JsonError.isInstance(input)) return `Config file at ${input.data.path} is not valid JSON`
|
||||
if (Config.InvalidError.isInstance(input))
|
||||
return [
|
||||
`Config file at ${input.data.path} is invalid`,
|
||||
...(input.data.issues?.map(
|
||||
(issue) => "↳ " + issue.message + " " + issue.path.join("."),
|
||||
) ?? []),
|
||||
...(input.data.issues?.map((issue) => "↳ " + issue.message + " " + issue.path.join(".")) ?? []),
|
||||
].join("\n")
|
||||
|
||||
if (UI.CancelledError.isInstance(input)) return ""
|
||||
|
|
|
@ -29,18 +29,12 @@ export namespace Config {
|
|||
export const McpLocal = z
|
||||
.object({
|
||||
type: z.literal("local").describe("Type of MCP server connection"),
|
||||
command: z
|
||||
.string()
|
||||
.array()
|
||||
.describe("Command and arguments to run the MCP server"),
|
||||
command: z.string().array().describe("Command and arguments to run the MCP server"),
|
||||
environment: z
|
||||
.record(z.string(), z.string())
|
||||
.optional()
|
||||
.describe("Environment variables to set when running the MCP server"),
|
||||
enabled: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Enable or disable the MCP server on startup"),
|
||||
enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"),
|
||||
})
|
||||
.strict()
|
||||
.openapi({
|
||||
|
@ -51,10 +45,7 @@ export namespace Config {
|
|||
.object({
|
||||
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"),
|
||||
enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"),
|
||||
})
|
||||
.strict()
|
||||
.openapi({
|
||||
|
@ -66,67 +57,31 @@ export namespace Config {
|
|||
|
||||
export const Keybinds = z
|
||||
.object({
|
||||
leader: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Leader key for keybind combinations"),
|
||||
leader: z.string().optional().describe("Leader key for keybind combinations"),
|
||||
help: z.string().optional().describe("Show help dialog"),
|
||||
editor_open: z.string().optional().describe("Open external editor"),
|
||||
session_new: z.string().optional().describe("Create a new session"),
|
||||
session_list: z.string().optional().describe("List all sessions"),
|
||||
session_share: z.string().optional().describe("Share current session"),
|
||||
session_interrupt: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Interrupt current session"),
|
||||
session_compact: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Toggle compact mode for session"),
|
||||
session_interrupt: z.string().optional().describe("Interrupt current session"),
|
||||
session_compact: z.string().optional().describe("Toggle compact mode for session"),
|
||||
tool_details: z.string().optional().describe("Show tool details"),
|
||||
model_list: z.string().optional().describe("List available models"),
|
||||
theme_list: z.string().optional().describe("List available themes"),
|
||||
project_init: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Initialize project configuration"),
|
||||
project_init: z.string().optional().describe("Initialize project configuration"),
|
||||
input_clear: z.string().optional().describe("Clear input field"),
|
||||
input_paste: z.string().optional().describe("Paste from clipboard"),
|
||||
input_submit: z.string().optional().describe("Submit input"),
|
||||
input_newline: z.string().optional().describe("Insert newline in input"),
|
||||
history_previous: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Navigate to previous history item"),
|
||||
history_next: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Navigate to next history item"),
|
||||
messages_page_up: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Scroll messages up by one page"),
|
||||
messages_page_down: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Scroll messages down by one page"),
|
||||
messages_half_page_up: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Scroll messages up by half page"),
|
||||
messages_half_page_down: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Scroll messages down by half page"),
|
||||
messages_previous: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Navigate to previous message"),
|
||||
history_previous: z.string().optional().describe("Navigate to previous history item"),
|
||||
history_next: z.string().optional().describe("Navigate to next history item"),
|
||||
messages_page_up: z.string().optional().describe("Scroll messages up by one page"),
|
||||
messages_page_down: z.string().optional().describe("Scroll messages down by one page"),
|
||||
messages_half_page_up: z.string().optional().describe("Scroll messages up by half page"),
|
||||
messages_half_page_down: z.string().optional().describe("Scroll messages down by half page"),
|
||||
messages_previous: z.string().optional().describe("Navigate to previous message"),
|
||||
messages_next: z.string().optional().describe("Navigate to next message"),
|
||||
messages_first: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Navigate to first message"),
|
||||
messages_first: z.string().optional().describe("Navigate to first message"),
|
||||
messages_last: z.string().optional().describe("Navigate to last message"),
|
||||
app_exit: z.string().optional().describe("Exit the application"),
|
||||
})
|
||||
|
@ -136,33 +91,13 @@ export namespace Config {
|
|||
})
|
||||
export const Info = z
|
||||
.object({
|
||||
$schema: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("JSON schema reference for configuration validation"),
|
||||
theme: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Theme name to use for the interface"),
|
||||
$schema: z.string().optional().describe("JSON schema reference for configuration validation"),
|
||||
theme: z.string().optional().describe("Theme name to use for the interface"),
|
||||
keybinds: Keybinds.optional().describe("Custom keybind configurations"),
|
||||
autoshare: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("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(),
|
||||
autoshare: z.boolean().optional().describe("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(),
|
||||
provider: z
|
||||
.record(
|
||||
ModelsDev.Provider.partial().extend({
|
||||
|
@ -172,14 +107,8 @@ 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"),
|
||||
instructions: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe("Additional instruction files or patterns to include"),
|
||||
mcp: z.record(z.string(), Mcp).optional().describe("MCP (Model Context Protocol) server configurations"),
|
||||
instructions: z.array(z.string()).optional().describe("Additional instruction files or patterns to include"),
|
||||
experimental: z
|
||||
.object({
|
||||
hook: z
|
||||
|
@ -227,10 +156,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(() => {})
|
||||
|
|
|
@ -22,9 +22,7 @@ export namespace ConfigHooks {
|
|||
command: item.command,
|
||||
})
|
||||
Bun.spawn({
|
||||
cmd: item.command.map((x) =>
|
||||
x.replace("$FILE", payload.properties.file),
|
||||
),
|
||||
cmd: item.command.map((x) => x.replace("$FILE", payload.properties.file)),
|
||||
env: item.environment,
|
||||
cwd: app.path.cwd,
|
||||
stdout: "ignore",
|
||||
|
|
|
@ -45,10 +45,7 @@ export namespace Fzf {
|
|||
log.info("found", { filepath })
|
||||
return { filepath }
|
||||
}
|
||||
filepath = path.join(
|
||||
Global.Path.bin,
|
||||
"fzf" + (process.platform === "win32" ? ".exe" : ""),
|
||||
)
|
||||
filepath = path.join(Global.Path.bin, "fzf" + (process.platform === "win32" ? ".exe" : ""))
|
||||
|
||||
const file = Bun.file(filepath)
|
||||
if (!(await file.exists())) {
|
||||
|
@ -56,18 +53,15 @@ export namespace Fzf {
|
|||
const arch = archMap[process.arch as keyof typeof archMap] ?? "amd64"
|
||||
|
||||
const config = PLATFORM[process.platform as keyof typeof PLATFORM]
|
||||
if (!config)
|
||||
throw new UnsupportedPlatformError({ platform: process.platform })
|
||||
if (!config) throw new UnsupportedPlatformError({ platform: process.platform })
|
||||
|
||||
const version = VERSION
|
||||
const platformName =
|
||||
process.platform === "win32" ? "windows" : process.platform
|
||||
const platformName = process.platform === "win32" ? "windows" : process.platform
|
||||
const filename = `fzf-${version}-${platformName}_${arch}.${config.extension}`
|
||||
const url = `https://github.com/junegunn/fzf/releases/download/v${version}/${filename}`
|
||||
|
||||
const response = await fetch(url)
|
||||
if (!response.ok)
|
||||
throw new DownloadFailedError({ url, status: response.status })
|
||||
if (!response.ok) throw new DownloadFailedError({ url, status: response.status })
|
||||
|
||||
const buffer = await response.arrayBuffer()
|
||||
const archivePath = path.join(Global.Path.bin, filename)
|
||||
|
@ -86,14 +80,11 @@ export namespace Fzf {
|
|||
})
|
||||
}
|
||||
if (config.extension === "zip") {
|
||||
const proc = Bun.spawn(
|
||||
["unzip", "-j", archivePath, "fzf.exe", "-d", Global.Path.bin],
|
||||
{
|
||||
cwd: Global.Path.bin,
|
||||
stderr: "pipe",
|
||||
stdout: "ignore",
|
||||
},
|
||||
)
|
||||
const proc = Bun.spawn(["unzip", "-j", archivePath, "fzf.exe", "-d", Global.Path.bin], {
|
||||
cwd: Global.Path.bin,
|
||||
stderr: "pipe",
|
||||
stdout: "ignore",
|
||||
})
|
||||
await proc.exited
|
||||
if (proc.exitCode !== 0)
|
||||
throw new ExtractionFailedError({
|
||||
|
|
|
@ -24,11 +24,7 @@ export namespace File {
|
|||
const app = App.info()
|
||||
if (!app.git) return []
|
||||
|
||||
const diffOutput = await $`git diff --numstat HEAD`
|
||||
.cwd(app.path.cwd)
|
||||
.quiet()
|
||||
.nothrow()
|
||||
.text()
|
||||
const diffOutput = await $`git diff --numstat HEAD`.cwd(app.path.cwd).quiet().nothrow().text()
|
||||
|
||||
const changedFiles = []
|
||||
|
||||
|
@ -45,19 +41,13 @@ export namespace File {
|
|||
}
|
||||
}
|
||||
|
||||
const untrackedOutput = await $`git ls-files --others --exclude-standard`
|
||||
.cwd(app.path.cwd)
|
||||
.quiet()
|
||||
.nothrow()
|
||||
.text()
|
||||
const untrackedOutput = await $`git ls-files --others --exclude-standard`.cwd(app.path.cwd).quiet().nothrow().text()
|
||||
|
||||
if (untrackedOutput.trim()) {
|
||||
const untrackedFiles = untrackedOutput.trim().split("\n")
|
||||
for (const filepath of untrackedFiles) {
|
||||
try {
|
||||
const content = await Bun.file(
|
||||
path.join(app.path.root, filepath),
|
||||
).text()
|
||||
const content = await Bun.file(path.join(app.path.root, filepath)).text()
|
||||
const lines = content.split("\n").length
|
||||
changedFiles.push({
|
||||
file: filepath,
|
||||
|
@ -72,11 +62,7 @@ export namespace File {
|
|||
}
|
||||
|
||||
// Get deleted files
|
||||
const deletedOutput = await $`git diff --name-only --diff-filter=D HEAD`
|
||||
.cwd(app.path.cwd)
|
||||
.quiet()
|
||||
.nothrow()
|
||||
.text()
|
||||
const deletedOutput = await $`git diff --name-only --diff-filter=D HEAD`.cwd(app.path.cwd).quiet().nothrow().text()
|
||||
|
||||
if (deletedOutput.trim()) {
|
||||
const deletedFiles = deletedOutput.trim().split("\n")
|
||||
|
@ -112,11 +98,7 @@ export namespace File {
|
|||
filepath: rel,
|
||||
})
|
||||
if (diff !== "unmodified") {
|
||||
const original = await $`git show HEAD:${rel}`
|
||||
.cwd(app.path.root)
|
||||
.quiet()
|
||||
.nothrow()
|
||||
.text()
|
||||
const original = await $`git show HEAD:${rel}`.cwd(app.path.root).quiet().nothrow().text()
|
||||
const patch = createPatch(file, original, content, "old", "new", {
|
||||
context: Infinity,
|
||||
})
|
||||
|
|
|
@ -122,15 +122,11 @@ export namespace Ripgrep {
|
|||
const state = lazy(async () => {
|
||||
let filepath = Bun.which("rg")
|
||||
if (filepath) return { filepath }
|
||||
filepath = path.join(
|
||||
Global.Path.bin,
|
||||
"rg" + (process.platform === "win32" ? ".exe" : ""),
|
||||
)
|
||||
filepath = path.join(Global.Path.bin, "rg" + (process.platform === "win32" ? ".exe" : ""))
|
||||
|
||||
const file = Bun.file(filepath)
|
||||
if (!(await file.exists())) {
|
||||
const platformKey =
|
||||
`${process.arch}-${process.platform}` as keyof typeof PLATFORM
|
||||
const platformKey = `${process.arch}-${process.platform}` as keyof typeof PLATFORM
|
||||
const config = PLATFORM[platformKey]
|
||||
if (!config) throw new UnsupportedPlatformError({ platform: platformKey })
|
||||
|
||||
|
@ -139,8 +135,7 @@ export namespace Ripgrep {
|
|||
const url = `https://github.com/BurntSushi/ripgrep/releases/download/${version}/${filename}`
|
||||
|
||||
const response = await fetch(url)
|
||||
if (!response.ok)
|
||||
throw new DownloadFailedError({ url, status: response.status })
|
||||
if (!response.ok) throw new DownloadFailedError({ url, status: response.status })
|
||||
|
||||
const buffer = await response.arrayBuffer()
|
||||
const archivePath = path.join(Global.Path.bin, filename)
|
||||
|
@ -164,14 +159,11 @@ export namespace Ripgrep {
|
|||
})
|
||||
}
|
||||
if (config.extension === "zip") {
|
||||
const proc = Bun.spawn(
|
||||
["unzip", "-j", archivePath, "*/rg.exe", "-d", Global.Path.bin],
|
||||
{
|
||||
cwd: Global.Path.bin,
|
||||
stderr: "pipe",
|
||||
stdout: "ignore",
|
||||
},
|
||||
)
|
||||
const proc = Bun.spawn(["unzip", "-j", archivePath, "*/rg.exe", "-d", Global.Path.bin], {
|
||||
cwd: Global.Path.bin,
|
||||
stderr: "pipe",
|
||||
stdout: "ignore",
|
||||
})
|
||||
await proc.exited
|
||||
if (proc.exitCode !== 0)
|
||||
throw new ExtractionFailedError({
|
||||
|
@ -193,17 +185,11 @@ export namespace Ripgrep {
|
|||
return filepath
|
||||
}
|
||||
|
||||
export async function files(input: {
|
||||
cwd: string
|
||||
query?: string
|
||||
glob?: string
|
||||
limit?: number
|
||||
}) {
|
||||
export async function files(input: { cwd: string; query?: string; glob?: string; limit?: number }) {
|
||||
const commands = [
|
||||
`${await filepath()} --files --hidden --glob='!.git/*' ${input.glob ? `--glob='${input.glob}'` : ``}`,
|
||||
]
|
||||
if (input.query)
|
||||
commands.push(`${await Fzf.filepath()} --filter=${input.query}`)
|
||||
if (input.query) commands.push(`${await Fzf.filepath()} --filter=${input.query}`)
|
||||
if (input.limit) commands.push(`head -n ${input.limit}`)
|
||||
const joined = commands.join(" | ")
|
||||
const result = await $`${{ raw: joined }}`.cwd(input.cwd).nothrow().text()
|
||||
|
@ -310,18 +296,8 @@ export namespace Ripgrep {
|
|||
return lines.join("\n")
|
||||
}
|
||||
|
||||
export async function search(input: {
|
||||
cwd: string
|
||||
pattern: string
|
||||
glob?: string[]
|
||||
limit?: number
|
||||
}) {
|
||||
const args = [
|
||||
`${await filepath()}`,
|
||||
"--json",
|
||||
"--hidden",
|
||||
"--glob='!.git/*'",
|
||||
]
|
||||
export async function search(input: { cwd: string; pattern: string; glob?: string[]; limit?: number }) {
|
||||
const args = [`${await filepath()}`, "--json", "--hidden", "--glob='!.git/*'"]
|
||||
|
||||
if (input.glob) {
|
||||
for (const g of input.glob) {
|
||||
|
|
|
@ -27,10 +27,7 @@ export namespace FileTime {
|
|||
|
||||
export async function assert(sessionID: string, filepath: string) {
|
||||
const time = get(sessionID, filepath)
|
||||
if (!time)
|
||||
throw new Error(
|
||||
`You must read the file ${filepath} before overwriting it. Use the Read tool first`,
|
||||
)
|
||||
if (!time) throw new Error(`You must read the file ${filepath} before overwriting it. Use the Read tool first`)
|
||||
const stats = await Bun.file(filepath).stat()
|
||||
if (stats.mtime.getTime() > time.getTime()) {
|
||||
throw new Error(
|
||||
|
|
|
@ -94,21 +94,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() {
|
||||
return Bun.which("clang-format") !== null
|
||||
},
|
||||
|
|
|
@ -26,11 +26,7 @@ export namespace Identifier {
|
|||
return generateID(prefix, true, given)
|
||||
}
|
||||
|
||||
function generateID(
|
||||
prefix: keyof typeof prefixes,
|
||||
descending: boolean,
|
||||
given?: string,
|
||||
): string {
|
||||
function generateID(prefix: keyof typeof prefixes, descending: boolean, given?: string): string {
|
||||
if (!given) {
|
||||
return generateNewID(prefix, descending)
|
||||
}
|
||||
|
@ -42,8 +38,7 @@ export namespace Identifier {
|
|||
}
|
||||
|
||||
function randomBase62(length: number): string {
|
||||
const chars =
|
||||
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||
const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||
let result = ""
|
||||
const bytes = randomBytes(length)
|
||||
for (let i = 0; i < length; i++) {
|
||||
|
@ -52,10 +47,7 @@ export namespace Identifier {
|
|||
return result
|
||||
}
|
||||
|
||||
function generateNewID(
|
||||
prefix: keyof typeof prefixes,
|
||||
descending: boolean,
|
||||
): string {
|
||||
function generateNewID(prefix: keyof typeof prefixes, descending: boolean): string {
|
||||
const currentTimestamp = Date.now()
|
||||
|
||||
if (currentTimestamp !== lastTimestamp) {
|
||||
|
@ -73,11 +65,6 @@ export namespace Identifier {
|
|||
timeBytes[i] = Number((now >> BigInt(40 - 8 * i)) & BigInt(0xff))
|
||||
}
|
||||
|
||||
return (
|
||||
prefixes[prefix] +
|
||||
"_" +
|
||||
timeBytes.toString("hex") +
|
||||
randomBase62(LENGTH - 12)
|
||||
)
|
||||
return prefixes[prefix] + "_" + timeBytes.toString("hex") + randomBase62(LENGTH - 12)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,10 +55,7 @@ const cli = yargs(hideBin(process.argv))
|
|||
.command(ServeCommand)
|
||||
.command(ModelsCommand)
|
||||
.fail((msg) => {
|
||||
if (
|
||||
msg.startsWith("Unknown argument") ||
|
||||
msg.startsWith("Not enough non-option arguments")
|
||||
) {
|
||||
if (msg.startsWith("Unknown argument") || msg.startsWith("Not enough non-option arguments")) {
|
||||
cli.showHelp("log")
|
||||
}
|
||||
})
|
||||
|
@ -97,10 +94,7 @@ try {
|
|||
Log.Default.error("fatal", data)
|
||||
const formatted = FormatError(e)
|
||||
if (formatted) UI.error(formatted)
|
||||
if (formatted === undefined)
|
||||
UI.error(
|
||||
"Unexpected error, check log file at " + Log.file() + " for more details",
|
||||
)
|
||||
if (formatted === undefined) UI.error("Unexpected error, check log file at " + Log.file() + " for more details")
|
||||
process.exitCode = 1
|
||||
}
|
||||
|
||||
|
|
|
@ -135,8 +135,7 @@ export namespace Installation {
|
|||
})
|
||||
}
|
||||
|
||||
export const VERSION =
|
||||
typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "dev"
|
||||
export const VERSION = typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "dev"
|
||||
|
||||
export async function latest() {
|
||||
return fetch("https://api.github.com/repos/sst/opencode/releases/latest")
|
||||
|
|
|
@ -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 { App } from "../app/app"
|
||||
import { Log } from "../util/log"
|
||||
|
@ -121,9 +117,7 @@ export namespace LSPClient {
|
|||
},
|
||||
notify: {
|
||||
async open(input: { path: string }) {
|
||||
input.path = path.isAbsolute(input.path)
|
||||
? input.path
|
||||
: path.resolve(app.path.cwd, input.path)
|
||||
input.path = path.isAbsolute(input.path) ? input.path : path.resolve(app.path.cwd, input.path)
|
||||
const file = Bun.file(input.path)
|
||||
const text = await file.text()
|
||||
const version = files[input.path]
|
||||
|
@ -155,18 +149,13 @@ export namespace LSPClient {
|
|||
return diagnostics
|
||||
},
|
||||
async waitForDiagnostics(input: { path: string }) {
|
||||
input.path = path.isAbsolute(input.path)
|
||||
? input.path
|
||||
: path.resolve(app.path.cwd, input.path)
|
||||
input.path = path.isAbsolute(input.path) ? input.path : path.resolve(app.path.cwd, 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()
|
||||
|
|
|
@ -46,9 +46,7 @@ export namespace LSP {
|
|||
if (!file) continue
|
||||
const handle = await server.spawn(App.info())
|
||||
if (!handle) break
|
||||
const client = await LSPClient.create(server.id, handle).catch(
|
||||
(err) => log.error("", { error: err }),
|
||||
)
|
||||
const client = await LSPClient.create(server.id, handle).catch((err) => log.error("", { error: err }))
|
||||
if (!client) break
|
||||
clients.set(server.id, client)
|
||||
break
|
||||
|
@ -77,9 +75,7 @@ export namespace LSP {
|
|||
.map((x) => x.id)
|
||||
await run(async (client) => {
|
||||
if (!matches.includes(client.serverID)) 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
|
||||
})
|
||||
|
@ -97,11 +93,7 @@ export namespace LSP {
|
|||
return results
|
||||
}
|
||||
|
||||
export async function hover(input: {
|
||||
file: string
|
||||
line: number
|
||||
character: number
|
||||
}) {
|
||||
export async function hover(input: { file: string; line: number; character: number }) {
|
||||
return run((client) => {
|
||||
return client.connection.sendRequest("textDocument/hover", {
|
||||
textDocument: {
|
||||
|
@ -123,9 +115,7 @@ export namespace LSP {
|
|||
).then((result) => result.flat() as LSP.Symbol[])
|
||||
}
|
||||
|
||||
async function run<T>(
|
||||
input: (client: LSPClient.Info) => Promise<T>,
|
||||
): Promise<T[]> {
|
||||
async function run<T>(input: (client: LSPClient.Info) => Promise<T>): Promise<T[]> {
|
||||
const clients = await state().then((x) => [...x.clients.values()])
|
||||
const tasks = clients.map((x) => input(x))
|
||||
return Promise.all(tasks)
|
||||
|
|
|
@ -25,21 +25,14 @@ export namespace LSPServer {
|
|||
id: "typescript",
|
||||
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
|
||||
async spawn(app) {
|
||||
const tsserver = await Bun.resolve(
|
||||
"typescript/lib/tsserver.js",
|
||||
app.path.cwd,
|
||||
).catch(() => {})
|
||||
const tsserver = await Bun.resolve("typescript/lib/tsserver.js", app.path.cwd).catch(() => {})
|
||||
if (!tsserver) return
|
||||
const proc = spawn(
|
||||
BunProc.which(),
|
||||
["x", "typescript-language-server", "--stdio"],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
BUN_BE_BUN: "1",
|
||||
},
|
||||
const proc = spawn(BunProc.which(), ["x", "typescript-language-server", "--stdio"], {
|
||||
env: {
|
||||
...process.env,
|
||||
BUN_BE_BUN: "1",
|
||||
},
|
||||
)
|
||||
})
|
||||
return {
|
||||
process: proc,
|
||||
initialization: {
|
||||
|
@ -73,10 +66,7 @@ export namespace LSPServer {
|
|||
log.error("Failed to install gopls")
|
||||
return
|
||||
}
|
||||
bin = path.join(
|
||||
Global.Path.bin,
|
||||
"gopls" + (process.platform === "win32" ? ".exe" : ""),
|
||||
)
|
||||
bin = path.join(Global.Path.bin, "gopls" + (process.platform === "win32" ? ".exe" : ""))
|
||||
log.info(`installed gopls`, {
|
||||
bin,
|
||||
})
|
||||
|
@ -113,10 +103,7 @@ export namespace LSPServer {
|
|||
log.error("Failed to install ruby-lsp")
|
||||
return
|
||||
}
|
||||
bin = path.join(
|
||||
Global.Path.bin,
|
||||
"ruby-lsp" + (process.platform === "win32" ? ".exe" : ""),
|
||||
)
|
||||
bin = path.join(Global.Path.bin, "ruby-lsp" + (process.platform === "win32" ? ".exe" : ""))
|
||||
log.info(`installed ruby-lsp`, {
|
||||
bin,
|
||||
})
|
||||
|
@ -131,16 +118,12 @@ export namespace LSPServer {
|
|||
id: "pyright",
|
||||
extensions: [".py", ".pyi"],
|
||||
async spawn() {
|
||||
const proc = spawn(
|
||||
BunProc.which(),
|
||||
["x", "pyright-langserver", "--stdio"],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
BUN_BE_BUN: "1",
|
||||
},
|
||||
const proc = spawn(BunProc.which(), ["x", "pyright-langserver", "--stdio"], {
|
||||
env: {
|
||||
...process.env,
|
||||
BUN_BE_BUN: "1",
|
||||
},
|
||||
)
|
||||
})
|
||||
return {
|
||||
process: proc,
|
||||
}
|
||||
|
@ -158,9 +141,7 @@ export namespace LSPServer {
|
|||
Global.Path.bin,
|
||||
"elixir-ls-master",
|
||||
"release",
|
||||
process.platform === "win32"
|
||||
? "language_server.bar"
|
||||
: "language_server.sh",
|
||||
process.platform === "win32" ? "language_server.bar" : "language_server.sh",
|
||||
)
|
||||
|
||||
if (!(await Bun.file(binary).exists())) {
|
||||
|
@ -172,9 +153,7 @@ export namespace LSPServer {
|
|||
|
||||
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)
|
||||
|
|
|
@ -91,8 +91,7 @@ export namespace Provider {
|
|||
if (!info || info.type !== "oauth") return
|
||||
if (!info.access || info.expires < Date.now()) {
|
||||
const tokens = await copilot.access(info.refresh)
|
||||
if (!tokens)
|
||||
throw new Error("GitHub Copilot authentication expired")
|
||||
if (!tokens) throw new Error("GitHub Copilot authentication expired")
|
||||
await Auth.set("github-copilot", {
|
||||
type: "oauth",
|
||||
...tokens,
|
||||
|
@ -101,15 +100,9 @@ export namespace Provider {
|
|||
}
|
||||
let isAgentCall = false
|
||||
try {
|
||||
const body =
|
||||
typeof init.body === "string"
|
||||
? JSON.parse(init.body)
|
||||
: init.body
|
||||
const body = typeof init.body === "string" ? JSON.parse(init.body) : init.body
|
||||
if (body?.messages) {
|
||||
isAgentCall = body.messages.some(
|
||||
(msg: any) =>
|
||||
msg.role && ["tool", "assistant"].includes(msg.role),
|
||||
)
|
||||
isAgentCall = body.messages.some((msg: any) => msg.role && ["tool", "assistant"].includes(msg.role))
|
||||
}
|
||||
} catch {}
|
||||
const headers = {
|
||||
|
@ -138,14 +131,11 @@ export namespace Provider {
|
|||
}
|
||||
},
|
||||
"amazon-bedrock": async () => {
|
||||
if (!process.env["AWS_PROFILE"] && !process.env["AWS_ACCESS_KEY_ID"])
|
||||
return { autoload: false }
|
||||
if (!process.env["AWS_PROFILE"] && !process.env["AWS_ACCESS_KEY_ID"]) 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: {
|
||||
|
@ -157,9 +147,7 @@ export namespace Provider {
|
|||
|
||||
switch (regionPrefix) {
|
||||
case "us": {
|
||||
const modelRequiresPrefix = ["claude", "deepseek"].some((m) =>
|
||||
modelID.includes(m),
|
||||
)
|
||||
const modelRequiresPrefix = ["claude", "deepseek"].some((m) => modelID.includes(m))
|
||||
if (modelRequiresPrefix) {
|
||||
modelID = `${regionPrefix}.${modelID}`
|
||||
}
|
||||
|
@ -174,25 +162,18 @@ 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}`
|
||||
}
|
||||
break
|
||||
}
|
||||
case "ap": {
|
||||
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"
|
||||
modelID = `${regionPrefix}.${modelID}`
|
||||
|
@ -230,10 +211,7 @@ export namespace Provider {
|
|||
options: Record<string, any>
|
||||
}
|
||||
} = {}
|
||||
const models = new Map<
|
||||
string,
|
||||
{ info: ModelsDev.Model; language: LanguageModel }
|
||||
>()
|
||||
const models = new Map<string, { info: ModelsDev.Model; language: LanguageModel }>()
|
||||
const sdk = new Map<string, SDK>()
|
||||
|
||||
log.info("init")
|
||||
|
@ -308,9 +286,7 @@ export namespace Provider {
|
|||
database[providerID] = parsed
|
||||
}
|
||||
|
||||
const disabled = await Config.get().then(
|
||||
(cfg) => new Set(cfg.disabled_providers ?? []),
|
||||
)
|
||||
const disabled = await Config.get().then((cfg) => new Set(cfg.disabled_providers ?? []))
|
||||
// load env
|
||||
for (const [providerID, provider] of Object.entries(database)) {
|
||||
if (disabled.has(providerID)) continue
|
||||
|
@ -337,12 +313,7 @@ export namespace Provider {
|
|||
if (disabled.has(providerID)) continue
|
||||
const result = await fn(database[providerID])
|
||||
if (result && (result.autoload || providers[providerID])) {
|
||||
mergeProvider(
|
||||
providerID,
|
||||
result.options ?? {},
|
||||
"custom",
|
||||
result.getModel,
|
||||
)
|
||||
mergeProvider(providerID, result.options ?? {}, "custom", result.getModel)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -406,9 +377,7 @@ export namespace Provider {
|
|||
const sdk = await getSDK(provider.info)
|
||||
|
||||
try {
|
||||
const language = provider.getModel
|
||||
? await provider.getModel(sdk, modelID)
|
||||
: sdk.languageModel(modelID)
|
||||
const language = provider.getModel ? await provider.getModel(sdk, modelID) : sdk.languageModel(modelID)
|
||||
log.info("found", { providerID, modelID })
|
||||
s.models.set(key, {
|
||||
info,
|
||||
|
@ -435,10 +404,7 @@ export namespace Provider {
|
|||
export function sort(models: ModelsDev.Model[]) {
|
||||
return sortBy(
|
||||
models,
|
||||
[
|
||||
(model) => priority.findIndex((filter) => model.id.includes(filter)),
|
||||
"desc",
|
||||
],
|
||||
[(model) => priority.findIndex((filter) => model.id.includes(filter)), "desc"],
|
||||
[(model) => (model.id.includes("latest") ? 0 : 1), "asc"],
|
||||
[(model) => model.id, "desc"],
|
||||
)
|
||||
|
@ -449,11 +415,7 @@ export namespace Provider {
|
|||
if (cfg.model) return parseModel(cfg.model)
|
||||
const provider = await list()
|
||||
.then((val) => Object.values(val))
|
||||
.then((x) =>
|
||||
x.find(
|
||||
(p) => !cfg.provider || Object.keys(cfg.provider).includes(p.info.id),
|
||||
),
|
||||
)
|
||||
.then((x) => x.find((p) => !cfg.provider || Object.keys(cfg.provider).includes(p.info.id)))
|
||||
if (!provider) throw new Error("no providers found")
|
||||
const [model] = sort(Object.values(provider.info.models))
|
||||
if (!model) throw new Error("no models found")
|
||||
|
@ -536,9 +498,11 @@ export namespace Provider {
|
|||
|
||||
if (schema instanceof z.ZodUnion) {
|
||||
return z.union(
|
||||
schema.options.map((option: z.ZodTypeAny) =>
|
||||
optionalToNullable(option),
|
||||
) as [z.ZodTypeAny, z.ZodTypeAny, ...z.ZodTypeAny[]],
|
||||
schema.options.map((option: z.ZodTypeAny) => optionalToNullable(option)) as [
|
||||
z.ZodTypeAny,
|
||||
z.ZodTypeAny,
|
||||
...z.ZodTypeAny[],
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -2,11 +2,7 @@ import type { ModelMessage } from "ai"
|
|||
import { unique } from "remeda"
|
||||
|
||||
export namespace ProviderTransform {
|
||||
export function message(
|
||||
msgs: ModelMessage[],
|
||||
providerID: string,
|
||||
modelID: string,
|
||||
) {
|
||||
export function message(msgs: ModelMessage[], providerID: string, modelID: string) {
|
||||
if (providerID === "anthropic" || modelID.includes("anthropic")) {
|
||||
const system = msgs.filter((msg) => msg.role === "system").slice(0, 2)
|
||||
const final = msgs.filter((msg) => msg.role !== "system").slice(-2)
|
||||
|
|
|
@ -51,12 +51,9 @@ export namespace Server {
|
|||
status: 400,
|
||||
})
|
||||
}
|
||||
return c.json(
|
||||
new NamedError.Unknown({ message: err.toString() }).toObject(),
|
||||
{
|
||||
status: 400,
|
||||
},
|
||||
)
|
||||
return c.json(new NamedError.Unknown({ message: err.toString() }).toObject(), {
|
||||
status: 400,
|
||||
})
|
||||
})
|
||||
.use(async (c, next) => {
|
||||
log.info("request", {
|
||||
|
@ -481,15 +478,10 @@ export namespace Server {
|
|||
},
|
||||
}),
|
||||
async (c) => {
|
||||
const providers = await Provider.list().then((x) =>
|
||||
mapValues(x, (item) => item.info),
|
||||
)
|
||||
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),
|
||||
})
|
||||
},
|
||||
)
|
||||
|
|
|
@ -3,10 +3,7 @@ import { Provider } from "../provider/provider"
|
|||
import { NamedError } from "../util/error"
|
||||
|
||||
export namespace Message {
|
||||
export const OutputLengthError = NamedError.create(
|
||||
"MessageOutputLengthError",
|
||||
z.object({}),
|
||||
)
|
||||
export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({}))
|
||||
|
||||
export const ToolCall = z
|
||||
.object({
|
||||
|
@ -48,11 +45,9 @@ export namespace Message {
|
|||
})
|
||||
export type ToolResult = z.infer<typeof ToolResult>
|
||||
|
||||
export const ToolInvocation = z
|
||||
.discriminatedUnion("state", [ToolCall, ToolPartialCall, ToolResult])
|
||||
.openapi({
|
||||
ref: "ToolInvocation",
|
||||
})
|
||||
export const ToolInvocation = z.discriminatedUnion("state", [ToolCall, ToolPartialCall, ToolResult]).openapi({
|
||||
ref: "ToolInvocation",
|
||||
})
|
||||
export type ToolInvocation = z.infer<typeof ToolInvocation>
|
||||
|
||||
export const TextPart = z
|
||||
|
@ -121,14 +116,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])
|
||||
.openapi({
|
||||
ref: "MessagePart",
|
||||
})
|
||||
|
|
|
@ -53,9 +53,7 @@ export namespace Share {
|
|||
|
||||
export const URL =
|
||||
process.env["OPENCODE_API"] ??
|
||||
(Installation.isSnapshot() || Installation.isDev()
|
||||
? "https://api.dev.opencode.ai"
|
||||
: "https://api.opencode.ai")
|
||||
(Installation.isSnapshot() || Installation.isDev() ? "https://api.dev.opencode.ai" : "https://api.opencode.ai")
|
||||
|
||||
export async function create(sessionID: string) {
|
||||
return fetch(`${URL}/share_create`, {
|
||||
|
|
|
@ -55,9 +55,7 @@ export namespace Snapshot {
|
|||
log.info("restore", { commit })
|
||||
const app = App.info()
|
||||
const git = gitdir(sessionID)
|
||||
await $`git --git-dir=${git} checkout ${commit} --force`
|
||||
.quiet()
|
||||
.cwd(app.path.root)
|
||||
await $`git --git-dir=${git} checkout ${commit} --force`.quiet().cwd(app.path.root)
|
||||
}
|
||||
|
||||
function gitdir(sessionID: string) {
|
||||
|
|
|
@ -12,12 +12,7 @@ export const BashTool = Tool.define({
|
|||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
command: z.string().describe("The command to execute"),
|
||||
timeout: z
|
||||
.number()
|
||||
.min(0)
|
||||
.max(MAX_TIMEOUT)
|
||||
.describe("Optional timeout in milliseconds")
|
||||
.optional(),
|
||||
timeout: z.number().min(0).max(MAX_TIMEOUT).describe("Optional timeout in milliseconds").optional(),
|
||||
description: z
|
||||
.string()
|
||||
.describe(
|
||||
|
@ -48,14 +43,7 @@ export const BashTool = Tool.define({
|
|||
exit: process.exitCode,
|
||||
description: params.description,
|
||||
},
|
||||
output: [
|
||||
`<stdout>`,
|
||||
stdout ?? "",
|
||||
`</stdout>`,
|
||||
`<stderr>`,
|
||||
stderr ?? "",
|
||||
`</stderr>`,
|
||||
].join("\n"),
|
||||
output: [`<stdout>`, stdout ?? "", `</stdout>`, `<stderr>`, stderr ?? "", `</stderr>`].join("\n"),
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
|
@ -20,15 +20,8 @@ export const EditTool = Tool.define({
|
|||
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 old_string)",
|
||||
),
|
||||
replaceAll: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Replace all occurrences of old_string (default false)"),
|
||||
newString: z.string().describe("The text to replace it with (must be different from old_string)"),
|
||||
replaceAll: z.boolean().optional().describe("Replace all occurrences of old_string (default false)"),
|
||||
}),
|
||||
async execute(params, ctx) {
|
||||
if (!params.filePath) {
|
||||
|
@ -40,9 +33,7 @@ export const EditTool = Tool.define({
|
|||
}
|
||||
|
||||
const app = App.info()
|
||||
const filepath = path.isAbsolute(params.filePath)
|
||||
? params.filePath
|
||||
: path.join(app.path.cwd, params.filePath)
|
||||
const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(app.path.cwd, params.filePath)
|
||||
|
||||
await Permission.ask({
|
||||
id: "edit",
|
||||
|
@ -70,17 +61,11 @@ export const EditTool = Tool.define({
|
|||
const file = Bun.file(filepath)
|
||||
const stats = await file.stat().catch(() => {})
|
||||
if (!stats) throw new Error(`File ${filepath} not found`)
|
||||
if (stats.isDirectory())
|
||||
throw new Error(`Path is a directory, not a file: ${filepath}`)
|
||||
if (stats.isDirectory()) throw new Error(`Path is a directory, not a file: ${filepath}`)
|
||||
await FileTime.assert(ctx.sessionID, filepath)
|
||||
contentOld = await file.text()
|
||||
|
||||
contentNew = replace(
|
||||
contentOld,
|
||||
params.oldString,
|
||||
params.newString,
|
||||
params.replaceAll,
|
||||
)
|
||||
contentNew = replace(contentOld, params.oldString, params.newString, params.replaceAll)
|
||||
await file.write(contentNew)
|
||||
await Bus.publish(File.Event.Edited, {
|
||||
file: filepath,
|
||||
|
@ -88,9 +73,7 @@ export const EditTool = Tool.define({
|
|||
contentNew = await file.text()
|
||||
})()
|
||||
|
||||
const diff = trimDiff(
|
||||
createTwoFilesPatch(filepath, filepath, contentOld, contentNew),
|
||||
)
|
||||
const diff = trimDiff(createTwoFilesPatch(filepath, filepath, contentOld, contentNew))
|
||||
|
||||
FileTime.read(ctx.sessionID, filepath)
|
||||
|
||||
|
@ -117,10 +100,7 @@ export const EditTool = Tool.define({
|
|||
},
|
||||
})
|
||||
|
||||
export type Replacer = (
|
||||
content: string,
|
||||
find: string,
|
||||
) => Generator<string, void, unknown>
|
||||
export type Replacer = (content: string, find: string) => Generator<string, void, unknown>
|
||||
|
||||
export const SimpleReplacer: Replacer = function* (_content, find) {
|
||||
yield find
|
||||
|
@ -208,10 +188,7 @@ export const BlockAnchorReplacer: Replacer = function* (content, find) {
|
|||
}
|
||||
}
|
||||
|
||||
export const WhitespaceNormalizedReplacer: Replacer = function* (
|
||||
content,
|
||||
find,
|
||||
) {
|
||||
export const WhitespaceNormalizedReplacer: Replacer = function* (content, find) {
|
||||
const normalizeWhitespace = (text: string) => text.replace(/\s+/g, " ").trim()
|
||||
const normalizedFind = normalizeWhitespace(find)
|
||||
|
||||
|
@ -229,9 +206,7 @@ export const WhitespaceNormalizedReplacer: Replacer = function* (
|
|||
// 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)
|
||||
|
@ -270,9 +245,7 @@ export const IndentationFlexibleReplacer: Replacer = function* (content, find) {
|
|||
}),
|
||||
)
|
||||
|
||||
return lines
|
||||
.map((line) => (line.trim().length === 0 ? line : line.slice(minIndent)))
|
||||
.join("\n")
|
||||
return lines.map((line) => (line.trim().length === 0 ? line : line.slice(minIndent))).join("\n")
|
||||
}
|
||||
|
||||
const normalizedFind = removeIndentation(find)
|
||||
|
@ -423,10 +396,7 @@ export const ContextAwareReplacer: Replacer = function* (content, find) {
|
|||
}
|
||||
}
|
||||
|
||||
if (
|
||||
totalNonEmptyLines === 0 ||
|
||||
matchingLines / totalNonEmptyLines >= 0.5
|
||||
) {
|
||||
if (totalNonEmptyLines === 0 || matchingLines / totalNonEmptyLines >= 0.5) {
|
||||
yield block
|
||||
break // Only match the first occurrence
|
||||
}
|
||||
|
@ -473,12 +443,7 @@ 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")
|
||||
}
|
||||
|
@ -502,11 +467,7 @@ export function replace(
|
|||
}
|
||||
const lastIndex = content.lastIndexOf(search)
|
||||
if (index !== lastIndex) continue
|
||||
return (
|
||||
content.substring(0, index) +
|
||||
newString +
|
||||
content.substring(index + search.length)
|
||||
)
|
||||
return content.substring(0, index) + newString + content.substring(index + search.length)
|
||||
}
|
||||
}
|
||||
throw new Error("oldString not found in content or was found multiple times")
|
||||
|
|
|
@ -20,9 +20,7 @@ export const GlobTool = Tool.define({
|
|||
async execute(params) {
|
||||
const app = App.info()
|
||||
let search = params.path ?? app.path.cwd
|
||||
search = path.isAbsolute(search)
|
||||
? search
|
||||
: path.resolve(app.path.cwd, search)
|
||||
search = path.isAbsolute(search) ? search : path.resolve(app.path.cwd, search)
|
||||
|
||||
const limit = 100
|
||||
const files = []
|
||||
|
@ -53,9 +51,7 @@ export const GlobTool = Tool.define({
|
|||
output.push(...files.map((f) => f.path))
|
||||
if (truncated) {
|
||||
output.push("")
|
||||
output.push(
|
||||
"(Results are truncated. Consider using a more specific path or pattern.)",
|
||||
)
|
||||
output.push("(Results are truncated. Consider using a more specific path or pattern.)")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,21 +9,9 @@ export const GrepTool = Tool.define({
|
|||
id: "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}")',
|
||||
),
|
||||
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}")'),
|
||||
}),
|
||||
async execute(params) {
|
||||
if (!params.pattern) {
|
||||
|
@ -116,9 +104,7 @@ export const GrepTool = Tool.define({
|
|||
|
||||
if (truncated) {
|
||||
outputLines.push("")
|
||||
outputLines.push(
|
||||
"(Results are truncated. Consider using a more specific path or pattern.)",
|
||||
)
|
||||
outputLines.push("(Results are truncated. Consider using a more specific path or pattern.)")
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
@ -24,16 +24,8 @@ export const ListTool = Tool.define({
|
|||
id: "list",
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
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(),
|
||||
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 app = App.info()
|
||||
|
@ -44,8 +36,7 @@ export const ListTool = Tool.define({
|
|||
|
||||
for await (const file of glob.scan({ cwd: searchPath, dot: true })) {
|
||||
if (IGNORE_PATTERNS.some((p) => file.includes(p))) continue
|
||||
if (params.ignore?.some((pattern) => new Bun.Glob(pattern).match(file)))
|
||||
continue
|
||||
if (params.ignore?.some((pattern) => new Bun.Glob(pattern).match(file))) continue
|
||||
files.push(file)
|
||||
if (files.length >= LIMIT) break
|
||||
}
|
||||
|
|
|
@ -13,9 +13,7 @@ export const LspDiagnosticTool = Tool.define({
|
|||
}),
|
||||
execute: async (args) => {
|
||||
const app = App.info()
|
||||
const normalized = path.isAbsolute(args.path)
|
||||
? args.path
|
||||
: path.join(app.path.cwd, args.path)
|
||||
const normalized = path.isAbsolute(args.path) ? args.path : path.join(app.path.cwd, args.path)
|
||||
await LSP.touchFile(normalized, true)
|
||||
const diagnostics = await LSP.diagnostics()
|
||||
const file = diagnostics[normalized]
|
||||
|
@ -24,9 +22,7 @@ export const LspDiagnosticTool = Tool.define({
|
|||
metadata: {
|
||||
diagnostics,
|
||||
},
|
||||
output: file?.length
|
||||
? file.map(LSP.Diagnostic.pretty).join("\n")
|
||||
: "No errors found",
|
||||
output: file?.length ? file.map(LSP.Diagnostic.pretty).join("\n") : "No errors found",
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
|
@ -15,9 +15,7 @@ export const LspHoverTool = Tool.define({
|
|||
}),
|
||||
execute: async (args) => {
|
||||
const app = App.info()
|
||||
const file = path.isAbsolute(args.file)
|
||||
? args.file
|
||||
: path.join(app.path.cwd, args.file)
|
||||
const file = path.isAbsolute(args.file) ? args.file : path.join(app.path.cwd, args.file)
|
||||
await LSP.touchFile(file, true)
|
||||
const result = await LSP.hover({
|
||||
...args,
|
||||
|
@ -25,12 +23,7 @@ export const LspHoverTool = Tool.define({
|
|||
})
|
||||
|
||||
return {
|
||||
title:
|
||||
path.relative(app.path.root, file) +
|
||||
":" +
|
||||
args.line +
|
||||
":" +
|
||||
args.character,
|
||||
title: path.relative(app.path.root, file) + ":" + args.line + ":" + args.character,
|
||||
metadata: {
|
||||
result,
|
||||
},
|
||||
|
|
|
@ -10,9 +10,7 @@ export const MultiEditTool = Tool.define({
|
|||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
filePath: z.string().describe("The absolute path to the file to modify"),
|
||||
edits: z
|
||||
.array(EditTool.parameters)
|
||||
.describe("Array of edit operations to perform sequentially on the file"),
|
||||
edits: z.array(EditTool.parameters).describe("Array of edit operations to perform sequentially on the file"),
|
||||
}),
|
||||
async execute(params, ctx) {
|
||||
const results = []
|
||||
|
|
|
@ -6,9 +6,7 @@ import { FileTime } from "../file/time"
|
|||
import DESCRIPTION from "./patch.txt"
|
||||
|
||||
const PatchParams = z.object({
|
||||
patchText: z
|
||||
.string()
|
||||
.describe("The full patch text that describes all changes to be made"),
|
||||
patchText: z.string().describe("The full patch text that describes all changes to be made"),
|
||||
})
|
||||
|
||||
interface Change {
|
||||
|
@ -42,10 +40,7 @@ function identifyFilesNeeded(patchText: string): string[] {
|
|||
const files: string[] = []
|
||||
const lines = patchText.split("\n")
|
||||
for (const line of lines) {
|
||||
if (
|
||||
line.startsWith("*** Update File:") ||
|
||||
line.startsWith("*** Delete File:")
|
||||
) {
|
||||
if (line.startsWith("*** Update File:") || line.startsWith("*** Delete File:")) {
|
||||
const filePath = line.split(":", 2)[1]?.trim()
|
||||
if (filePath) files.push(filePath)
|
||||
}
|
||||
|
@ -65,10 +60,7 @@ function identifyFilesAdded(patchText: string): string[] {
|
|||
return files
|
||||
}
|
||||
|
||||
function textToPatch(
|
||||
patchText: string,
|
||||
_currentFiles: Record<string, string>,
|
||||
): [PatchOperation[], number] {
|
||||
function textToPatch(patchText: string, _currentFiles: Record<string, string>): [PatchOperation[], number] {
|
||||
const operations: PatchOperation[] = []
|
||||
const lines = patchText.split("\n")
|
||||
let i = 0
|
||||
|
@ -93,11 +85,7 @@ function textToPatch(
|
|||
const changes: PatchChange[] = []
|
||||
i++
|
||||
|
||||
while (
|
||||
i < lines.length &&
|
||||
!lines[i].startsWith("@@") &&
|
||||
!lines[i].startsWith("***")
|
||||
) {
|
||||
while (i < lines.length && !lines[i].startsWith("@@") && !lines[i].startsWith("***")) {
|
||||
const changeLine = lines[i]
|
||||
if (changeLine.startsWith(" ")) {
|
||||
changes.push({ type: "keep", content: changeLine.substring(1) })
|
||||
|
@ -151,10 +139,7 @@ function textToPatch(
|
|||
return [operations, fuzz]
|
||||
}
|
||||
|
||||
function patchToCommit(
|
||||
operations: PatchOperation[],
|
||||
currentFiles: Record<string, string>,
|
||||
): Commit {
|
||||
function patchToCommit(operations: PatchOperation[], currentFiles: Record<string, string>): Commit {
|
||||
const changes: Record<string, Change> = {}
|
||||
|
||||
for (const op of operations) {
|
||||
|
@ -173,9 +158,7 @@ function patchToCommit(
|
|||
const lines = originalContent.split("\n")
|
||||
|
||||
for (const hunk of op.hunks) {
|
||||
const contextIndex = lines.findIndex((line) =>
|
||||
line.includes(hunk.contextLine),
|
||||
)
|
||||
const contextIndex = lines.findIndex((line) => line.includes(hunk.contextLine))
|
||||
if (contextIndex === -1) {
|
||||
throw new Error(`Context line not found: ${hunk.contextLine}`)
|
||||
}
|
||||
|
@ -204,11 +187,7 @@ function patchToCommit(
|
|||
return { changes }
|
||||
}
|
||||
|
||||
function generateDiff(
|
||||
oldContent: string,
|
||||
newContent: string,
|
||||
filePath: string,
|
||||
): [string, number, number] {
|
||||
function generateDiff(oldContent: string, newContent: string, filePath: string): [string, number, number] {
|
||||
// Mock implementation - would need actual diff generation
|
||||
const lines1 = oldContent.split("\n")
|
||||
const lines2 = newContent.split("\n")
|
||||
|
@ -296,9 +275,7 @@ export const PatchTool = Tool.define({
|
|||
// Process the patch
|
||||
const [patch, fuzz] = textToPatch(params.patchText, currentFiles)
|
||||
if (fuzz > 3) {
|
||||
throw new Error(
|
||||
`patch contains fuzzy matches (fuzz level: ${fuzz}). Please make your context lines more precise`,
|
||||
)
|
||||
throw new Error(`patch contains fuzzy matches (fuzz level: ${fuzz}). Please make your context lines more precise`)
|
||||
}
|
||||
|
||||
// Convert patch to commit
|
||||
|
@ -343,11 +320,7 @@ export const PatchTool = Tool.define({
|
|||
const newContent = change.new_content || ""
|
||||
|
||||
// Calculate diff statistics
|
||||
const [, additions, removals] = generateDiff(
|
||||
oldContent,
|
||||
newContent,
|
||||
filePath,
|
||||
)
|
||||
const [, additions, removals] = generateDiff(oldContent, newContent, filePath)
|
||||
totalAdditions += additions
|
||||
totalRemovals += removals
|
||||
|
||||
|
|
|
@ -16,14 +16,8 @@ export const ReadTool = Tool.define({
|
|||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
filePath: z.string().describe("The path to the file to read"),
|
||||
offset: z
|
||||
.number()
|
||||
.describe("The line number to start reading from (0-based)")
|
||||
.optional(),
|
||||
limit: z
|
||||
.number()
|
||||
.describe("The number of lines to read (defaults to 2000)")
|
||||
.optional(),
|
||||
offset: z.number().describe("The line number to start reading from (0-based)").optional(),
|
||||
limit: z.number().describe("The number of lines to read (defaults to 2000)").optional(),
|
||||
}),
|
||||
async execute(params, ctx) {
|
||||
let filePath = params.filePath
|
||||
|
@ -40,16 +34,13 @@ export const ReadTool = Tool.define({
|
|||
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}`)
|
||||
|
@ -57,21 +48,14 @@ export const ReadTool = Tool.define({
|
|||
const stats = await file.stat()
|
||||
|
||||
if (stats.size > MAX_READ_SIZE)
|
||||
throw new Error(
|
||||
`File is too large (${stats.size} bytes). Maximum size is ${MAX_READ_SIZE} bytes`,
|
||||
)
|
||||
throw new Error(`File is too large (${stats.size} bytes). Maximum size is ${MAX_READ_SIZE} bytes`)
|
||||
const limit = params.limit ?? DEFAULT_READ_LIMIT
|
||||
const offset = params.offset || 0
|
||||
const isImage = isImageFile(filePath)
|
||||
if (isImage)
|
||||
throw new Error(
|
||||
`This is an image file of type: ${isImage}\nUse a different tool to process images`,
|
||||
)
|
||||
if (isImage) throw new Error(`This is an image file of type: ${isImage}\nUse a different tool to process images`)
|
||||
const lines = await file.text().then((text) => text.split("\n"))
|
||||
const raw = lines.slice(offset, offset + limit).map((line) => {
|
||||
return line.length > MAX_LINE_LENGTH
|
||||
? line.substring(0, MAX_LINE_LENGTH) + "..."
|
||||
: line
|
||||
return line.length > MAX_LINE_LENGTH ? line.substring(0, MAX_LINE_LENGTH) + "..." : line
|
||||
})
|
||||
const content = raw.map((line, index) => {
|
||||
return `${(index + offset + 1).toString().padStart(5, "0")}| ${line}`
|
||||
|
@ -82,9 +66,7 @@ export const ReadTool = Tool.define({
|
|||
output += content.join("\n")
|
||||
|
||||
if (lines.length > offset + content.length) {
|
||||
output += `\n\n(File has more lines. Use 'offset' parameter to read beyond line ${
|
||||
offset + content.length
|
||||
})`
|
||||
output += `\n\n(File has more lines. Use 'offset' parameter to read beyond line ${offset + content.length})`
|
||||
}
|
||||
output += "\n</file>"
|
||||
|
||||
|
|
|
@ -9,17 +9,12 @@ export const TaskTool = Tool.define({
|
|||
id: "task",
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
description: z
|
||||
.string()
|
||||
.describe("A short (3-5 words) description of the task"),
|
||||
description: z.string().describe("A short (3-5 words) description of the task"),
|
||||
prompt: z.string().describe("The task for the agent to perform"),
|
||||
}),
|
||||
async execute(params, ctx) {
|
||||
const session = await Session.create(ctx.sessionID)
|
||||
const msg = (await Session.getMessage(
|
||||
ctx.sessionID,
|
||||
ctx.messageID,
|
||||
)) as MessageV2.Assistant
|
||||
const msg = (await Session.getMessage(ctx.sessionID, ctx.messageID)) as MessageV2.Assistant
|
||||
|
||||
function summary(input: MessageV2.Info) {
|
||||
const result = []
|
||||
|
|
|
@ -5,12 +5,8 @@ import { App } from "../app/app"
|
|||
|
||||
const TodoInfo = z.object({
|
||||
content: z.string().min(1).describe("Brief description of the task"),
|
||||
status: z
|
||||
.enum(["pending", "in_progress", "completed"])
|
||||
.describe("Current status of the task"),
|
||||
priority: z
|
||||
.enum(["high", "medium", "low"])
|
||||
.describe("Priority level of the task"),
|
||||
status: z.enum(["pending", "in_progress", "completed"]).describe("Current status of the task"),
|
||||
priority: z.enum(["high", "medium", "low"]).describe("Priority level of the task"),
|
||||
id: z.string().describe("Unique identifier for the todo item"),
|
||||
})
|
||||
type TodoInfo = z.infer<typeof TodoInfo>
|
||||
|
|
|
@ -10,10 +10,7 @@ export namespace Tool {
|
|||
abort: AbortSignal
|
||||
metadata(input: { title?: string; metadata?: M }): void
|
||||
}
|
||||
export interface Info<
|
||||
Parameters extends StandardSchemaV1 = StandardSchemaV1,
|
||||
M extends Metadata = Metadata,
|
||||
> {
|
||||
export interface Info<Parameters extends StandardSchemaV1 = StandardSchemaV1, M extends Metadata = Metadata> {
|
||||
id: string
|
||||
description: string
|
||||
parameters: Parameters
|
||||
|
@ -27,10 +24,9 @@ export namespace Tool {
|
|||
}>
|
||||
}
|
||||
|
||||
export function define<
|
||||
Parameters extends StandardSchemaV1,
|
||||
Result extends Metadata,
|
||||
>(input: Info<Parameters, Result>): Info<Parameters, Result> {
|
||||
export function define<Parameters extends StandardSchemaV1, Result extends Metadata>(
|
||||
input: Info<Parameters, Result>,
|
||||
): Info<Parameters, Result> {
|
||||
return input
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,9 +14,7 @@ export const WebFetchTool = Tool.define({
|
|||
url: z.string().describe("The URL to fetch content from"),
|
||||
format: z
|
||||
.enum(["text", "markdown", "html"])
|
||||
.describe(
|
||||
"The format to return the content in (text, markdown, or html)",
|
||||
),
|
||||
.describe("The format to return the content in (text, markdown, or html)"),
|
||||
timeout: z
|
||||
.number()
|
||||
.min(0)
|
||||
|
@ -26,17 +24,11 @@ export const WebFetchTool = Tool.define({
|
|||
}),
|
||||
async execute(params, ctx) {
|
||||
// Validate URL
|
||||
if (
|
||||
!params.url.startsWith("http://") &&
|
||||
!params.url.startsWith("https://")
|
||||
) {
|
||||
if (!params.url.startsWith("http://") && !params.url.startsWith("https://")) {
|
||||
throw new Error("URL must start with http:// or https://")
|
||||
}
|
||||
|
||||
const timeout = Math.min(
|
||||
(params.timeout ?? DEFAULT_TIMEOUT / 1000) * 1000,
|
||||
MAX_TIMEOUT,
|
||||
)
|
||||
const timeout = Math.min((params.timeout ?? DEFAULT_TIMEOUT / 1000) * 1000, MAX_TIMEOUT)
|
||||
|
||||
const controller = new AbortController()
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeout)
|
||||
|
@ -46,8 +38,7 @@ export const WebFetchTool = Tool.define({
|
|||
headers: {
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||
Accept:
|
||||
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
|
||||
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
|
||||
"Accept-Language": "en-US,en;q=0.9",
|
||||
},
|
||||
})
|
||||
|
@ -137,16 +128,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
|
||||
}
|
||||
},
|
||||
|
|
|
@ -13,18 +13,12 @@ export const WriteTool = Tool.define({
|
|||
id: "write",
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
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)"),
|
||||
content: z.string().describe("The content to write to the file"),
|
||||
}),
|
||||
async execute(params, ctx) {
|
||||
const app = App.info()
|
||||
const filepath = path.isAbsolute(params.filePath)
|
||||
? params.filePath
|
||||
: path.join(app.path.cwd, params.filePath)
|
||||
const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(app.path.cwd, params.filePath)
|
||||
|
||||
const file = Bun.file(filepath)
|
||||
const exists = await file.exists()
|
||||
|
@ -33,9 +27,7 @@ export const WriteTool = Tool.define({
|
|||
await Permission.ask({
|
||||
id: "write",
|
||||
sessionID: ctx.sessionID,
|
||||
title: exists
|
||||
? "Overwrite this file: " + filepath
|
||||
: "Create new file: " + filepath,
|
||||
title: exists ? "Overwrite this file: " + filepath : "Create new file: " + filepath,
|
||||
metadata: {
|
||||
filePath: filepath,
|
||||
content: params.content,
|
||||
|
|
|
@ -7,10 +7,7 @@ export abstract class NamedError extends Error {
|
|||
abstract schema(): ZodSchema
|
||||
abstract toObject(): { name: string; data: any }
|
||||
|
||||
static create<Name extends string, Data extends ZodSchema>(
|
||||
name: Name,
|
||||
data: Data,
|
||||
) {
|
||||
static create<Name extends string, Data extends ZodSchema>(name: Name, data: Data) {
|
||||
const schema = z
|
||||
.object({
|
||||
name: z.literal(name),
|
||||
|
|
|
@ -19,10 +19,7 @@ export namespace Log {
|
|||
await fs.mkdir(dir, { recursive: true })
|
||||
cleanup(dir)
|
||||
if (options.print) return
|
||||
logpath = path.join(
|
||||
dir,
|
||||
new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log",
|
||||
)
|
||||
logpath = path.join(dir, new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log")
|
||||
const logfile = Bun.file(logpath)
|
||||
await fs.truncate(logpath).catch(() => {})
|
||||
const writer = logfile.writer()
|
||||
|
@ -43,9 +40,7 @@ export namespace Log {
|
|||
|
||||
const filesToDelete = files.slice(0, -10)
|
||||
|
||||
await Promise.all(
|
||||
filesToDelete.map((file) => fs.unlink(file).catch(() => {})),
|
||||
)
|
||||
await Promise.all(filesToDelete.map((file) => fs.unlink(file).catch(() => {})))
|
||||
}
|
||||
|
||||
let last = Date.now()
|
||||
|
@ -63,11 +58,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 = {
|
||||
info(message?: any, extra?: Record<string, any>) {
|
||||
|
|
|
@ -17,12 +17,7 @@ const testCases: TestCase[] = [
|
|||
replace: 'console.log("universe");',
|
||||
},
|
||||
{
|
||||
content: [
|
||||
"if (condition) {",
|
||||
" doSomething();",
|
||||
" doSomethingElse();",
|
||||
"}",
|
||||
].join("\n"),
|
||||
content: ["if (condition) {", " doSomething();", " doSomethingElse();", "}"].join("\n"),
|
||||
find: [" doSomething();", " doSomethingElse();"].join("\n"),
|
||||
replace: [" doNewThing();", " doAnotherThing();"].join("\n"),
|
||||
},
|
||||
|
@ -53,15 +48,8 @@ const testCases: TestCase[] = [
|
|||
" return result;",
|
||||
"}",
|
||||
].join("\n"),
|
||||
find: [
|
||||
"function calculate(a, b) {",
|
||||
" // different middle content",
|
||||
" return result;",
|
||||
"}",
|
||||
].join("\n"),
|
||||
replace: ["function calculate(a, b) {", " return a * b * 2;", "}"].join(
|
||||
"\n",
|
||||
),
|
||||
find: ["function calculate(a, b) {", " // different middle content", " return result;", "}"].join("\n"),
|
||||
replace: ["function calculate(a, b) {", " return a * b * 2;", "}"].join("\n"),
|
||||
},
|
||||
{
|
||||
content: [
|
||||
|
@ -76,13 +64,7 @@ const testCases: TestCase[] = [
|
|||
"}",
|
||||
].join("\n"),
|
||||
find: ["class MyClass {", " // different implementation", "}"].join("\n"),
|
||||
replace: [
|
||||
"class MyClass {",
|
||||
" constructor() {",
|
||||
" this.value = 42;",
|
||||
" }",
|
||||
"}",
|
||||
].join("\n"),
|
||||
replace: ["class MyClass {", " constructor() {", " this.value = 42;", " }", "}"].join("\n"),
|
||||
},
|
||||
|
||||
// WhitespaceNormalizedReplacer cases
|
||||
|
@ -104,48 +86,21 @@ const testCases: TestCase[] = [
|
|||
|
||||
// IndentationFlexibleReplacer cases
|
||||
{
|
||||
content: [
|
||||
" function nested() {",
|
||||
' console.log("deeply nested");',
|
||||
" return true;",
|
||||
" }",
|
||||
].join("\n"),
|
||||
find: [
|
||||
"function nested() {",
|
||||
' console.log("deeply nested");',
|
||||
" return true;",
|
||||
"}",
|
||||
].join("\n"),
|
||||
replace: [
|
||||
"function nested() {",
|
||||
' console.log("updated");',
|
||||
" return false;",
|
||||
"}",
|
||||
].join("\n"),
|
||||
content: [" function nested() {", ' console.log("deeply nested");', " return true;", " }"].join(
|
||||
"\n",
|
||||
),
|
||||
find: ["function nested() {", ' console.log("deeply nested");', " return true;", "}"].join("\n"),
|
||||
replace: ["function nested() {", ' console.log("updated");', " return false;", "}"].join("\n"),
|
||||
},
|
||||
{
|
||||
content: [
|
||||
" if (true) {",
|
||||
' console.log("level 1");',
|
||||
' console.log("level 2");',
|
||||
" }",
|
||||
].join("\n"),
|
||||
find: [
|
||||
"if (true) {",
|
||||
'console.log("level 1");',
|
||||
' console.log("level 2");',
|
||||
"}",
|
||||
].join("\n"),
|
||||
content: [" if (true) {", ' console.log("level 1");', ' console.log("level 2");', " }"].join("\n"),
|
||||
find: ["if (true) {", 'console.log("level 1");', ' console.log("level 2");', "}"].join("\n"),
|
||||
replace: ["if (true) {", 'console.log("updated");', "}"].join("\n"),
|
||||
},
|
||||
|
||||
// replaceAll option cases
|
||||
{
|
||||
content: [
|
||||
'console.log("test");',
|
||||
'console.log("test");',
|
||||
'console.log("test");',
|
||||
].join("\n"),
|
||||
content: ['console.log("test");', 'console.log("test");', 'console.log("test");'].join("\n"),
|
||||
find: 'console.log("test");',
|
||||
replace: 'console.log("updated");',
|
||||
all: true,
|
||||
|
@ -213,9 +168,7 @@ const testCases: TestCase[] = [
|
|||
|
||||
// MultiOccurrenceReplacer cases (with replaceAll)
|
||||
{
|
||||
content: ["debug('start');", "debug('middle');", "debug('end');"].join(
|
||||
"\n",
|
||||
),
|
||||
content: ["debug('start');", "debug('middle');", "debug('end');"].join("\n"),
|
||||
find: "debug",
|
||||
replace: "log",
|
||||
all: true,
|
||||
|
@ -239,9 +192,7 @@ const testCases: TestCase[] = [
|
|||
replace: "const value = 24;",
|
||||
},
|
||||
{
|
||||
content: ["", " if (condition) {", " doSomething();", " }", ""].join(
|
||||
"\n",
|
||||
),
|
||||
content: ["", " if (condition) {", " doSomething();", " }", ""].join("\n"),
|
||||
find: ["if (condition) {", " doSomething();", "}"].join("\n"),
|
||||
replace: ["if (condition) {", " doNothing();", "}"].join("\n"),
|
||||
},
|
||||
|
@ -262,9 +213,7 @@ const testCases: TestCase[] = [
|
|||
" return result;",
|
||||
"}",
|
||||
].join("\n"),
|
||||
replace: ["function calculate(a, b) {", " return (a + b) * 2;", "}"].join(
|
||||
"\n",
|
||||
),
|
||||
replace: ["function calculate(a, b) {", " return (a + b) * 2;", "}"].join("\n"),
|
||||
},
|
||||
{
|
||||
content: [
|
||||
|
@ -278,15 +227,8 @@ const testCases: TestCase[] = [
|
|||
" }",
|
||||
"}",
|
||||
].join("\n"),
|
||||
find: [
|
||||
"class TestClass {",
|
||||
" // different implementation",
|
||||
" // with multiple lines",
|
||||
"}",
|
||||
].join("\n"),
|
||||
replace: ["class TestClass {", " getValue() { return 42; }", "}"].join(
|
||||
"\n",
|
||||
),
|
||||
find: ["class TestClass {", " // different implementation", " // with multiple lines", "}"].join("\n"),
|
||||
replace: ["class TestClass {", " getValue() { return 42; }", "}"].join("\n"),
|
||||
},
|
||||
|
||||
// Combined edge cases for new replacers
|
||||
|
@ -296,9 +238,7 @@ const testCases: TestCase[] = [
|
|||
replace: 'console.log("updated");',
|
||||
},
|
||||
{
|
||||
content: [" ", "function test() {", " return 'value';", "}", " "].join(
|
||||
"\n",
|
||||
),
|
||||
content: [" ", "function test() {", " return 'value';", "}", " "].join("\n"),
|
||||
find: ["function test() {", "return 'value';", "}"].join("\n"),
|
||||
replace: ["function test() {", "return 'new value';", "}"].join("\n"),
|
||||
},
|
||||
|
@ -346,13 +286,7 @@ const testCases: TestCase[] = [
|
|||
|
||||
// ContextAwareReplacer - test with trailing newline in find string
|
||||
{
|
||||
content: [
|
||||
"class Test {",
|
||||
" method1() {",
|
||||
" return 1;",
|
||||
" }",
|
||||
"}",
|
||||
].join("\n"),
|
||||
content: ["class Test {", " method1() {", " return 1;", " }", "}"].join("\n"),
|
||||
find: [
|
||||
"class Test {",
|
||||
" // different content",
|
||||
|
@ -401,12 +335,7 @@ describe("EditTool Replacers", () => {
|
|||
replace(testCase.content, testCase.find, testCase.replace, testCase.all)
|
||||
}).toThrow()
|
||||
} else {
|
||||
const result = replace(
|
||||
testCase.content,
|
||||
testCase.find,
|
||||
testCase.replace,
|
||||
testCase.all,
|
||||
)
|
||||
const result = replace(testCase.content, testCase.find, testCase.replace, testCase.all)
|
||||
expect(result).toContain(testCase.replace)
|
||||
}
|
||||
})
|
||||
|
|
|
@ -42,10 +42,7 @@ describe("tool.glob", () => {
|
|||
describe("tool.ls", () => {
|
||||
test("basic", async () => {
|
||||
const result = await App.provide({ cwd: process.cwd() }, async () => {
|
||||
return await ListTool.execute(
|
||||
{ path: "./example", ignore: [".git"] },
|
||||
ctx,
|
||||
)
|
||||
return await ListTool.execute({ path: "./example", ignore: [".git"] }, ctx)
|
||||
})
|
||||
expect(result.output).toMatchSnapshot()
|
||||
})
|
||||
|
|
14
packages/tui/sdk/.github/workflows/ci.yml
vendored
14
packages/tui/sdk/.github/workflows/ci.yml
vendored
|
@ -2,15 +2,15 @@ name: CI
|
|||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'generated'
|
||||
- 'codegen/**'
|
||||
- 'integrated/**'
|
||||
- 'stl-preview-head/**'
|
||||
- 'stl-preview-base/**'
|
||||
- "generated"
|
||||
- "codegen/**"
|
||||
- "integrated/**"
|
||||
- "stl-preview-head/**"
|
||||
- "stl-preview-base/**"
|
||||
pull_request:
|
||||
branches-ignore:
|
||||
- 'stl-preview-head/**'
|
||||
- 'stl-preview-base/**'
|
||||
- "stl-preview-head/**"
|
||||
- "stl-preview-base/**"
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
".": "0.1.0-alpha.8"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ Full Changelog: [v0.1.0-alpha.7...v0.1.0-alpha.8](https://github.com/sst/opencod
|
|||
|
||||
### Features
|
||||
|
||||
* **api:** update via SDK Studio ([651e937](https://github.com/sst/opencode-sdk-go/commit/651e937c334e1caba3b968e6cac865c219879519))
|
||||
- **api:** update via SDK Studio ([651e937](https://github.com/sst/opencode-sdk-go/commit/651e937c334e1caba3b968e6cac865c219879519))
|
||||
|
||||
## 0.1.0-alpha.7 (2025-06-30)
|
||||
|
||||
|
@ -14,13 +14,12 @@ Full Changelog: [v0.1.0-alpha.6...v0.1.0-alpha.7](https://github.com/sst/opencod
|
|||
|
||||
### Features
|
||||
|
||||
* **api:** update via SDK Studio ([13550a5](https://github.com/sst/opencode-sdk-go/commit/13550a5c65d77325e945ed99fe0799cd1107b775))
|
||||
* **api:** update via SDK Studio ([7b73730](https://github.com/sst/opencode-sdk-go/commit/7b73730c7fa62ba966dda3541c3e97b49be8d2bf))
|
||||
|
||||
- **api:** update via SDK Studio ([13550a5](https://github.com/sst/opencode-sdk-go/commit/13550a5c65d77325e945ed99fe0799cd1107b775))
|
||||
- **api:** update via SDK Studio ([7b73730](https://github.com/sst/opencode-sdk-go/commit/7b73730c7fa62ba966dda3541c3e97b49be8d2bf))
|
||||
|
||||
### Chores
|
||||
|
||||
* **ci:** only run for pushes and fork pull requests ([bea59b8](https://github.com/sst/opencode-sdk-go/commit/bea59b886800ef555f89c47a9256d6392ed2e53d))
|
||||
- **ci:** only run for pushes and fork pull requests ([bea59b8](https://github.com/sst/opencode-sdk-go/commit/bea59b886800ef555f89c47a9256d6392ed2e53d))
|
||||
|
||||
## 0.1.0-alpha.6 (2025-06-28)
|
||||
|
||||
|
@ -28,7 +27,7 @@ Full Changelog: [v0.1.0-alpha.5...v0.1.0-alpha.6](https://github.com/sst/opencod
|
|||
|
||||
### Bug Fixes
|
||||
|
||||
* don't try to deserialize as json when ResponseBodyInto is []byte ([5988d04](https://github.com/sst/opencode-sdk-go/commit/5988d04839cb78b6613057280b91b72a60fef33d))
|
||||
- don't try to deserialize as json when ResponseBodyInto is []byte ([5988d04](https://github.com/sst/opencode-sdk-go/commit/5988d04839cb78b6613057280b91b72a60fef33d))
|
||||
|
||||
## 0.1.0-alpha.5 (2025-06-27)
|
||||
|
||||
|
@ -36,7 +35,7 @@ Full Changelog: [v0.1.0-alpha.4...v0.1.0-alpha.5](https://github.com/sst/opencod
|
|||
|
||||
### Features
|
||||
|
||||
* **api:** update via SDK Studio ([9e39a59](https://github.com/sst/opencode-sdk-go/commit/9e39a59b3d5d1bd5e64633732521fb28362cc70e))
|
||||
- **api:** update via SDK Studio ([9e39a59](https://github.com/sst/opencode-sdk-go/commit/9e39a59b3d5d1bd5e64633732521fb28362cc70e))
|
||||
|
||||
## 0.1.0-alpha.4 (2025-06-27)
|
||||
|
||||
|
@ -44,7 +43,7 @@ Full Changelog: [v0.1.0-alpha.3...v0.1.0-alpha.4](https://github.com/sst/opencod
|
|||
|
||||
### Features
|
||||
|
||||
* **api:** update via SDK Studio ([9609d1b](https://github.com/sst/opencode-sdk-go/commit/9609d1b1db7806d00cb846c9914cb4935cdedf52))
|
||||
- **api:** update via SDK Studio ([9609d1b](https://github.com/sst/opencode-sdk-go/commit/9609d1b1db7806d00cb846c9914cb4935cdedf52))
|
||||
|
||||
## 0.1.0-alpha.3 (2025-06-27)
|
||||
|
||||
|
@ -52,7 +51,7 @@ Full Changelog: [v0.1.0-alpha.2...v0.1.0-alpha.3](https://github.com/sst/opencod
|
|||
|
||||
### Features
|
||||
|
||||
* **api:** update via SDK Studio ([57f3230](https://github.com/sst/opencode-sdk-go/commit/57f32309023cc1f0f20c20d02a3907e390a71f61))
|
||||
- **api:** update via SDK Studio ([57f3230](https://github.com/sst/opencode-sdk-go/commit/57f32309023cc1f0f20c20d02a3907e390a71f61))
|
||||
|
||||
## 0.1.0-alpha.2 (2025-06-27)
|
||||
|
||||
|
@ -60,7 +59,7 @@ Full Changelog: [v0.1.0-alpha.1...v0.1.0-alpha.2](https://github.com/sst/opencod
|
|||
|
||||
### Features
|
||||
|
||||
* **api:** update via SDK Studio ([a766f1c](https://github.com/sst/opencode-sdk-go/commit/a766f1c54f02bbc1380151b0e22d97cc2c5892e6))
|
||||
- **api:** update via SDK Studio ([a766f1c](https://github.com/sst/opencode-sdk-go/commit/a766f1c54f02bbc1380151b0e22d97cc2c5892e6))
|
||||
|
||||
## 0.1.0-alpha.1 (2025-06-27)
|
||||
|
||||
|
@ -68,6 +67,6 @@ Full Changelog: [v0.0.1-alpha.0...v0.1.0-alpha.1](https://github.com/sst/opencod
|
|||
|
||||
### Features
|
||||
|
||||
* **api:** update via SDK Studio ([27b7376](https://github.com/sst/opencode-sdk-go/commit/27b7376310466ee17a63f2104f546b53a2b8361a))
|
||||
* **api:** update via SDK Studio ([0a73e04](https://github.com/sst/opencode-sdk-go/commit/0a73e04c23c90b2061611edaa8fd6282dc0ce397))
|
||||
* **api:** update via SDK Studio ([9b7883a](https://github.com/sst/opencode-sdk-go/commit/9b7883a144eeac526d9d04538e0876a9d18bb844))
|
||||
- **api:** update via SDK Studio ([27b7376](https://github.com/sst/opencode-sdk-go/commit/27b7376310466ee17a63f2104f546b53a2b8361a))
|
||||
- **api:** update via SDK Studio ([0a73e04](https://github.com/sst/opencode-sdk-go/commit/0a73e04c23c90b2061611edaa8fd6282dc0ce397))
|
||||
- **api:** update via SDK Studio ([9b7883a](https://github.com/sst/opencode-sdk-go/commit/9b7883a144eeac526d9d04538e0876a9d18bb844))
|
||||
|
|
|
@ -60,8 +60,5 @@
|
|||
}
|
||||
],
|
||||
"release-type": "go",
|
||||
"extra-files": [
|
||||
"internal/version.go",
|
||||
"README.md"
|
||||
]
|
||||
}
|
||||
"extra-files": ["internal/version.go", "README.md"]
|
||||
}
|
||||
|
|
|
@ -24,10 +24,7 @@ export default defineConfig({
|
|||
host: "0.0.0.0",
|
||||
},
|
||||
markdown: {
|
||||
rehypePlugins: [
|
||||
rehypeHeadingIds,
|
||||
[rehypeAutolinkHeadings, { behavior: "wrap" }],
|
||||
],
|
||||
rehypePlugins: [rehypeHeadingIds, [rehypeAutolinkHeadings, { behavior: "wrap" }]],
|
||||
},
|
||||
integrations: [
|
||||
solidJs(),
|
||||
|
@ -36,7 +33,7 @@ export default defineConfig({
|
|||
expressiveCode: { themes: ["github-light", "github-dark"] },
|
||||
social: [
|
||||
{ icon: "github", label: "GitHub", href: config.github },
|
||||
{ icon: "discord", label: "Dscord", href: config.discord }
|
||||
{ icon: "discord", label: "Dscord", href: config.discord },
|
||||
],
|
||||
head: [
|
||||
{
|
||||
|
|
|
@ -88,14 +88,7 @@
|
|||
"syntaxOperator": { "$ref": "#/definitions/colorValue" },
|
||||
"syntaxPunctuation": { "$ref": "#/definitions/colorValue" }
|
||||
},
|
||||
"required": [
|
||||
"primary",
|
||||
"secondary",
|
||||
"accent",
|
||||
"text",
|
||||
"textMuted",
|
||||
"background"
|
||||
],
|
||||
"required": ["primary", "secondary", "accent", "text", "textMuted", "background"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
|
|
|
@ -12,29 +12,15 @@ import {
|
|||
} from "solid-js"
|
||||
import { DateTime } from "luxon"
|
||||
import { createStore, reconcile } from "solid-js/store"
|
||||
import {
|
||||
IconOpenAI,
|
||||
IconGemini,
|
||||
IconOpencode,
|
||||
IconAnthropic,
|
||||
} from "./icons/custom"
|
||||
import {
|
||||
IconSparkles,
|
||||
IconArrowDown,
|
||||
} from "./icons"
|
||||
import { IconOpenAI, IconGemini, IconOpencode, IconAnthropic } from "./icons/custom"
|
||||
import { IconSparkles, IconArrowDown } from "./icons"
|
||||
import styles from "./share.module.css"
|
||||
import type { MessageV2 } from "opencode/session/message-v2"
|
||||
import type { Message } from "opencode/session/message"
|
||||
import type { Session } from "opencode/session/index"
|
||||
import { Part } from "./share/part"
|
||||
|
||||
|
||||
type Status =
|
||||
| "disconnected"
|
||||
| "connecting"
|
||||
| "connected"
|
||||
| "error"
|
||||
| "reconnecting"
|
||||
type Status = "disconnected" | "connecting" | "connected" | "error" | "reconnecting"
|
||||
|
||||
function scrollToAnchor(id: string) {
|
||||
const el = document.getElementById(id)
|
||||
|
@ -43,7 +29,6 @@ function scrollToAnchor(id: string) {
|
|||
el.scrollIntoView({ behavior: "smooth" })
|
||||
}
|
||||
|
||||
|
||||
function getStatusText(status: [Status, string?]): string {
|
||||
switch (status[0]) {
|
||||
case "connected":
|
||||
|
@ -61,7 +46,6 @@ function getStatusText(status: [Status, string?]): string {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
function ProviderIcon(props: { provider: string; size?: number }) {
|
||||
const size = props.size || 16
|
||||
return (
|
||||
|
@ -79,8 +63,6 @@ function ProviderIcon(props: { provider: string; size?: number }) {
|
|||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default function Share(props: {
|
||||
id: string
|
||||
api: string
|
||||
|
@ -105,12 +87,8 @@ export default function Share(props: {
|
|||
info?: Session.Info
|
||||
messages: Record<string, MessageV2.Info | Message.Info>
|
||||
}>({ info: props.info, messages: props.messages })
|
||||
const messages = createMemo(() =>
|
||||
Object.values(store.messages).toSorted((a, b) => a.id?.localeCompare(b.id)),
|
||||
)
|
||||
const [connectionStatus, setConnectionStatus] = createSignal<
|
||||
[Status, string?]
|
||||
>(["disconnected", "Disconnected"])
|
||||
const messages = createMemo(() => Object.values(store.messages).toSorted((a, b) => a.id?.localeCompare(b.id)))
|
||||
const [connectionStatus, setConnectionStatus] = createSignal<[Status, string?]>(["disconnected", "Disconnected"])
|
||||
|
||||
onMount(() => {
|
||||
const apiUrl = props.api
|
||||
|
@ -185,10 +163,7 @@ export default function Share(props: {
|
|||
|
||||
// Try to reconnect after 2 seconds
|
||||
clearTimeout(reconnectTimer)
|
||||
reconnectTimer = window.setTimeout(
|
||||
setupWebSocket,
|
||||
2000,
|
||||
) as unknown as number
|
||||
reconnectTimer = window.setTimeout(setupWebSocket, 2000) as unknown as number
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -310,10 +285,7 @@ export default function Share(props: {
|
|||
result.tokens.output += msg.tokens.output
|
||||
result.tokens.reasoning += msg.tokens.reasoning
|
||||
|
||||
result.models[`${msg.providerID} ${msg.modelID}`] = [
|
||||
msg.providerID,
|
||||
msg.modelID,
|
||||
]
|
||||
result.models[`${msg.providerID} ${msg.modelID}`] = [msg.providerID, msg.modelID]
|
||||
|
||||
if (msg.path.root) {
|
||||
result.rootDir = msg.path.root
|
||||
|
@ -329,7 +301,7 @@ export default function Share(props: {
|
|||
})
|
||||
|
||||
return (
|
||||
<main classList={{ [styles.root]: true, "not-content": true }} >
|
||||
<main classList={{ [styles.root]: true, "not-content": true }}>
|
||||
<div data-component="header">
|
||||
<h1 data-component="header-title">{store.info?.title}</h1>
|
||||
<div data-component="header-details">
|
||||
|
@ -362,58 +334,50 @@ export default function Share(props: {
|
|||
</ul>
|
||||
<div
|
||||
data-component="header-time"
|
||||
title={DateTime.fromMillis(data().created || 0).toLocaleString(
|
||||
DateTime.DATETIME_FULL_WITH_SECONDS,
|
||||
)}
|
||||
title={DateTime.fromMillis(data().created || 0).toLocaleString(DateTime.DATETIME_FULL_WITH_SECONDS)}
|
||||
>
|
||||
{DateTime.fromMillis(data().created || 0).toLocaleString(
|
||||
DateTime.DATETIME_MED,
|
||||
)}
|
||||
{DateTime.fromMillis(data().created || 0).toLocaleString(DateTime.DATETIME_MED)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Show
|
||||
when={data().messages.length > 0}
|
||||
fallback={<p>Waiting for messages...</p>}
|
||||
>
|
||||
<Show when={data().messages.length > 0} fallback={<p>Waiting for messages...</p>}>
|
||||
<div class={styles.parts}>
|
||||
<SuspenseList revealOrder="forwards">
|
||||
<For each={data().messages}>
|
||||
{(msg, msgIndex) => (
|
||||
<Suspense>
|
||||
<For each={msg.parts.filter((x, index) => {
|
||||
if (x.type === "step-start" && index > 0) return false
|
||||
if (x.type === "tool" && x.tool === "todoread") return false
|
||||
if (x.type === "text" && !x.text) return false
|
||||
if (x.type === "tool" && (x.state.status === "pending" || x.state.status === "running")) return false
|
||||
return true
|
||||
})}>
|
||||
<For
|
||||
each={msg.parts.filter((x, index) => {
|
||||
if (x.type === "step-start" && index > 0) return false
|
||||
if (x.type === "tool" && x.tool === "todoread") return false
|
||||
if (x.type === "text" && !x.text) return false
|
||||
if (x.type === "tool" && (x.state.status === "pending" || x.state.status === "running"))
|
||||
return false
|
||||
return true
|
||||
})}
|
||||
>
|
||||
{(part, partIndex) => {
|
||||
const last = createMemo(
|
||||
() =>
|
||||
data().messages.length === msgIndex() + 1 &&
|
||||
msg.parts.length === partIndex() + 1,
|
||||
() => data().messages.length === msgIndex() + 1 && msg.parts.length === partIndex() + 1,
|
||||
)
|
||||
|
||||
onMount(() => {
|
||||
const hash = window.location.hash.slice(1)
|
||||
// Wait till all parts are loaded
|
||||
if (
|
||||
hash !== ""
|
||||
&& !hasScrolledToAnchor
|
||||
&& msg.parts.length === partIndex() + 1
|
||||
&& data().messages.length === msgIndex() + 1
|
||||
hash !== "" &&
|
||||
!hasScrolledToAnchor &&
|
||||
msg.parts.length === partIndex() + 1 &&
|
||||
data().messages.length === msgIndex() + 1
|
||||
) {
|
||||
hasScrolledToAnchor = true
|
||||
scrollToAnchor(hash)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<Part last={last()} part={part} index={partIndex()} message={msg} />
|
||||
)
|
||||
return <Part last={last()} part={part} index={partIndex()} message={msg} />
|
||||
}}
|
||||
</For>
|
||||
</Suspense>
|
||||
|
@ -438,19 +402,11 @@ export default function Share(props: {
|
|||
</li>
|
||||
<li>
|
||||
<span data-element-label>Input Tokens</span>
|
||||
{data().tokens.input ? (
|
||||
<span>{data().tokens.input}</span>
|
||||
) : (
|
||||
<span data-placeholder>—</span>
|
||||
)}
|
||||
{data().tokens.input ? <span>{data().tokens.input}</span> : <span data-placeholder>—</span>}
|
||||
</li>
|
||||
<li>
|
||||
<span data-element-label>Output Tokens</span>
|
||||
{data().tokens.output ? (
|
||||
<span>{data().tokens.output}</span>
|
||||
) : (
|
||||
<span data-placeholder>—</span>
|
||||
)}
|
||||
{data().tokens.output ? <span>{data().tokens.output}</span> : <span data-placeholder>—</span>}
|
||||
</li>
|
||||
<li>
|
||||
<span data-element-label>Reasoning Tokens</span>
|
||||
|
@ -476,10 +432,7 @@ export default function Share(props: {
|
|||
"overflow-y": "auto",
|
||||
}}
|
||||
>
|
||||
<Show
|
||||
when={data().messages.length > 0}
|
||||
fallback={<p>Waiting for messages...</p>}
|
||||
>
|
||||
<Show when={data().messages.length > 0} fallback={<p>Waiting for messages...</p>}>
|
||||
<ul style={{ "list-style-type": "none", padding: 0 }}>
|
||||
<For each={data().messages}>
|
||||
{(msg) => (
|
||||
|
@ -507,9 +460,7 @@ export default function Share(props: {
|
|||
<button
|
||||
type="button"
|
||||
class={styles["scroll-button"]}
|
||||
onClick={() =>
|
||||
document.body.scrollIntoView({ behavior: "smooth", block: "end" })
|
||||
}
|
||||
onClick={() => document.body.scrollIntoView({ behavior: "smooth", block: "end" })}
|
||||
onMouseEnter={() => {
|
||||
setIsButtonHovered(true)
|
||||
if (scrollTimeout) {
|
||||
|
@ -583,8 +534,7 @@ export function fromV1(v1: Message.Info): MessageV2.Info {
|
|||
}
|
||||
}
|
||||
|
||||
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",
|
||||
|
|
|
@ -8,4 +8,3 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -39,7 +39,12 @@ export function IconGemini(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
|||
export function IconOpencode(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox="0 0 70 70" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 13H35V58H0V13ZM26.25 22.1957H8.75V48.701H26.25V22.1957Z" fill="currentColor" />
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M0 13H35V58H0V13ZM26.25 22.1957H8.75V48.701H26.25V22.1957Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path d="M43.75 13H70V22.1957H52.5V48.701H70V57.8967H43.75V13Z" fill="currentColor" />
|
||||
</svg>
|
||||
)
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -24,7 +24,6 @@
|
|||
@media (max-width: 30rem) {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
[data-component="header-title"] {
|
||||
|
@ -59,7 +58,6 @@
|
|||
gap: 0.5rem 0.875rem;
|
||||
flex-wrap: wrap;
|
||||
|
||||
|
||||
[data-slot="item"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -120,8 +118,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.parts {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -131,7 +127,7 @@
|
|||
display: flex;
|
||||
gap: 0.625rem;
|
||||
|
||||
&>[data-section="decoration"] {
|
||||
& > [data-section="decoration"] {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -209,7 +205,6 @@
|
|||
|
||||
a,
|
||||
a:hover {
|
||||
|
||||
svg:nth-child(1),
|
||||
svg:nth-child(2) {
|
||||
display: none;
|
||||
|
@ -230,7 +225,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
&>[data-section="content"] {
|
||||
& > [data-section="content"] {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
padding: 0 0 0.375rem;
|
||||
|
@ -283,20 +278,20 @@
|
|||
max-width: var(--md-tool-width);
|
||||
gap: 0.25rem 0.375rem;
|
||||
|
||||
&>div:nth-child(3n + 1) {
|
||||
& > div:nth-child(3n + 1) {
|
||||
width: 8px;
|
||||
height: 2px;
|
||||
border-radius: 1px;
|
||||
background: var(--sl-color-divider);
|
||||
}
|
||||
|
||||
&>div:nth-child(3n + 2),
|
||||
&>div:nth-child(3n + 3) {
|
||||
& > div:nth-child(3n + 2),
|
||||
& > div:nth-child(3n + 3) {
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
&>div:nth-child(3n + 3) {
|
||||
& > div:nth-child(3n + 3) {
|
||||
padding-left: 0.125rem;
|
||||
word-break: break-word;
|
||||
color: var(--sl-color-text-secondary);
|
||||
|
@ -327,7 +322,7 @@
|
|||
[data-part-type="ai-model"],
|
||||
[data-part-type="system-text"],
|
||||
[data-part-type="fallback"] {
|
||||
&>[data-section="content"] {
|
||||
& > [data-section="content"] {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
@ -338,13 +333,13 @@
|
|||
[data-part-type="tool-edit"],
|
||||
[data-part-type="tool-write"],
|
||||
[data-part-type="tool-fetch"] {
|
||||
&>[data-section="content"]>[data-part-tool-body] {
|
||||
& > [data-section="content"] > [data-part-tool-body] {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
[data-part-type="tool-grep"] {
|
||||
&:not(:has([data-part-tool-args]))>[data-section="content"]>[data-part-tool-body] {
|
||||
&:not(:has([data-part-tool-args])) > [data-section="content"] > [data-part-tool-body] {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
@ -371,7 +366,7 @@
|
|||
}
|
||||
|
||||
[data-part-type="summary"] {
|
||||
&>[data-section="decoration"] {
|
||||
& > [data-section="decoration"] {
|
||||
span:first-child {
|
||||
flex: 0 0 auto;
|
||||
display: block;
|
||||
|
@ -403,7 +398,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
&>[data-section="content"] {
|
||||
& > [data-section="content"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
|
@ -597,7 +592,7 @@
|
|||
width: 100%;
|
||||
max-width: var(--sm-tool-width);
|
||||
|
||||
&>[data-section="body"] {
|
||||
& > [data-section="body"] {
|
||||
width: 100%;
|
||||
border: 1px solid var(--sl-color-divider);
|
||||
border-radius: 0.25rem;
|
||||
|
@ -610,7 +605,7 @@
|
|||
text-align: center;
|
||||
padding: 0 3.25rem;
|
||||
|
||||
&>span {
|
||||
& > span {
|
||||
max-width: min(100%, 140ch);
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
|
@ -622,7 +617,7 @@
|
|||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
content: "";
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
top: 8px;
|
||||
|
@ -746,7 +741,7 @@
|
|||
border-bottom: none;
|
||||
}
|
||||
|
||||
&>span {
|
||||
& > span {
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
left: 0.5rem;
|
||||
|
@ -756,7 +751,8 @@
|
|||
border: 1px solid var(--sl-color-divider);
|
||||
border-radius: 0.15rem;
|
||||
|
||||
&::before {}
|
||||
&::before {
|
||||
}
|
||||
}
|
||||
|
||||
&[data-status="pending"] {
|
||||
|
@ -766,11 +762,11 @@
|
|||
&[data-status="in_progress"] {
|
||||
color: var(--sl-color-text);
|
||||
|
||||
&>span {
|
||||
& > span {
|
||||
border-color: var(--sl-color-orange);
|
||||
}
|
||||
|
||||
&>span::before {
|
||||
& > span::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
|
@ -784,11 +780,11 @@
|
|||
&[data-status="completed"] {
|
||||
color: var(--sl-color-text-secondary);
|
||||
|
||||
&>span {
|
||||
& > span {
|
||||
border-color: var(--sl-color-green-low);
|
||||
}
|
||||
|
||||
&>span::before {
|
||||
& > span::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
|
@ -818,7 +814,9 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.15s ease, opacity 0.5s ease;
|
||||
transition:
|
||||
all 0.15s ease,
|
||||
opacity 0.5s ease;
|
||||
z-index: 100;
|
||||
appearance: none;
|
||||
opacity: 1;
|
||||
|
|
|
@ -10,12 +10,7 @@ export function AnchorIcon(props: AnchorProps) {
|
|||
const [copied, setCopied] = createSignal(false)
|
||||
|
||||
return (
|
||||
<div
|
||||
{...rest}
|
||||
data-element-anchor
|
||||
title="Link to this message"
|
||||
data-status={copied() ? "copied" : ""}
|
||||
>
|
||||
<div {...rest} data-element-anchor title="Link to this message" data-status={copied() ? "copied" : ""}>
|
||||
<a
|
||||
href={`#${local.id}`}
|
||||
onClick={(e) => {
|
||||
|
@ -60,6 +55,6 @@ export function createOverflow() {
|
|||
onCleanup(() => {
|
||||
ro.disconnect()
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,13 +2,13 @@
|
|||
max-width: var(--md-tool-width);
|
||||
border: 1px solid var(--sl-color-divider);
|
||||
background-color: var(--sl-color-bg-surface);
|
||||
border-radius: .25rem;
|
||||
padding: .5rem calc(.5rem + 3px);
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.5rem calc(0.5rem + 3px);
|
||||
|
||||
&[data-flush="true"] {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
padding: 0
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
|
|
|
@ -30,4 +30,3 @@ export function ContentCode(props: Props) {
|
|||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -23,8 +23,6 @@
|
|||
grid-template-columns: 1fr 1fr;
|
||||
align-items: stretch;
|
||||
|
||||
|
||||
|
||||
[data-slot="before"],
|
||||
[data-slot="after"] {
|
||||
position: relative;
|
||||
|
@ -77,13 +75,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.diff>.row:first-child [data-section="cell"]:first-child {
|
||||
.diff > .row:first-child [data-section="cell"]:first-child {
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
.diff>.row:last-child [data-section="cell"]:last-child {
|
||||
.diff > .row:last-child [data-section="cell"]:last-child {
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
|
@ -108,7 +104,7 @@
|
|||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
|
||||
code>span:empty::before {
|
||||
code > span:empty::before {
|
||||
content: "\00a0";
|
||||
white-space: pre;
|
||||
display: inline-block;
|
||||
|
@ -117,7 +113,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 40rem) {
|
||||
[data-slot="desktop"] {
|
||||
display: none;
|
||||
|
|
|
@ -160,18 +160,10 @@ export function ContentDiff(props: Props) {
|
|||
{rows().map((r) => (
|
||||
<div data-component="diff-row" data-type={r.type}>
|
||||
<div data-slot="before" data-diff-type={r.type === "removed" || r.type === "modified" ? "removed" : ""}>
|
||||
<ContentCode
|
||||
code={r.left}
|
||||
flush
|
||||
lang={props.lang}
|
||||
/>
|
||||
<ContentCode code={r.left} flush lang={props.lang} />
|
||||
</div>
|
||||
<div data-slot="after" data-diff-type={r.type === "added" || r.type === "modified" ? "added" : ""}>
|
||||
<ContentCode
|
||||
code={r.right}
|
||||
lang={props.lang}
|
||||
flush
|
||||
/>
|
||||
<ContentCode code={r.right} lang={props.lang} flush />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
@ -195,7 +187,6 @@ export function ContentDiff(props: Props) {
|
|||
)
|
||||
}
|
||||
|
||||
|
||||
// const testDiff = `--- combined_before.txt 2025-06-24 16:38:08
|
||||
// +++ combined_after.txt 2025-06-24 16:38:12
|
||||
// @@ -1,21 +1,25 @@
|
||||
|
|
|
@ -67,7 +67,7 @@
|
|||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
&>*:last-child {
|
||||
& > *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
|
|
|
@ -40,11 +40,11 @@ export function ContentMarkdown(props: Props) {
|
|||
<div
|
||||
class={style.root}
|
||||
data-highlight={props.highlight === true ? true : undefined}
|
||||
data-expanded={(expanded() || props.expand === true) ? true : undefined}
|
||||
data-expanded={expanded() || props.expand === true ? true : undefined}
|
||||
>
|
||||
<div data-slot="markdown" ref={overflow.ref} innerHTML={html()} />
|
||||
|
||||
{(!props.expand && overflow.status) && (
|
||||
{!props.expand && overflow.status && (
|
||||
<button
|
||||
type="button"
|
||||
data-component="text-button"
|
||||
|
|
|
@ -35,7 +35,6 @@
|
|||
flex: 0 0 auto;
|
||||
padding: 2px 0;
|
||||
font-size: 0.75rem;
|
||||
|
||||
}
|
||||
|
||||
&[data-theme="invert"] {
|
||||
|
|
|
@ -14,10 +14,12 @@ export function ContentText(props: Props) {
|
|||
return (
|
||||
<div
|
||||
class={style.root}
|
||||
data-expanded={(expanded() || props.expand === true) ? true : undefined}
|
||||
data-expanded={expanded() || props.expand === true ? true : undefined}
|
||||
data-compact={props.compact === true ? true : undefined}
|
||||
>
|
||||
<pre data-slot="text" ref={overflow.ref}>{props.text}</pre>
|
||||
<pre data-slot="text" ref={overflow.ref}>
|
||||
{props.text}
|
||||
</pre>
|
||||
{((!props.expand && overflow.status) || expanded()) && (
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
@ -45,10 +45,8 @@
|
|||
}
|
||||
|
||||
[data-copied] & {
|
||||
|
||||
a,
|
||||
a:hover {
|
||||
|
||||
svg:nth-child(1),
|
||||
svg:nth-child(2) {
|
||||
display: none;
|
||||
|
@ -227,7 +225,7 @@
|
|||
border-bottom: none;
|
||||
}
|
||||
|
||||
&>span {
|
||||
& > span {
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
left: 0.5rem;
|
||||
|
@ -237,7 +235,8 @@
|
|||
border: 1px solid var(--sl-color-divider);
|
||||
border-radius: 0.15rem;
|
||||
|
||||
&::before {}
|
||||
&::before {
|
||||
}
|
||||
}
|
||||
|
||||
&[data-status="pending"] {
|
||||
|
@ -247,11 +246,11 @@
|
|||
&[data-status="in_progress"] {
|
||||
color: var(--sl-color-text);
|
||||
|
||||
&>span {
|
||||
& > span {
|
||||
border-color: var(--sl-color-orange);
|
||||
}
|
||||
|
||||
&>span::before {
|
||||
& > span::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
|
@ -265,11 +264,11 @@
|
|||
&[data-status="completed"] {
|
||||
color: var(--sl-color-text-secondary);
|
||||
|
||||
&>span {
|
||||
& > span {
|
||||
border-color: var(--sl-color-green-low);
|
||||
}
|
||||
|
||||
&>span::before {
|
||||
& > span::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
|
@ -305,13 +304,13 @@
|
|||
text-align: center;
|
||||
padding: 0 3.25rem;
|
||||
|
||||
>span {
|
||||
> span {
|
||||
max-width: min(100%, 140ch);
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
line-height: 1.625rem;
|
||||
font-size: .75rem;
|
||||
font-size: 0.75rem;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--sl-color-text-dimmed);
|
||||
}
|
||||
|
@ -323,7 +322,7 @@
|
|||
top: 8px;
|
||||
left: 10px;
|
||||
width: 2rem;
|
||||
height: .5rem;
|
||||
height: 0.5rem;
|
||||
line-height: 0;
|
||||
background-color: var(--sl-color-hairline);
|
||||
mask-image: var(--term-icon);
|
||||
|
@ -334,13 +333,13 @@
|
|||
[data-slot="content"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: .5rem calc(.5rem + 3px);
|
||||
padding: 0.5rem calc(0.5rem + 3px);
|
||||
|
||||
pre {
|
||||
--shiki-dark-bg: var(--sl-color-bg) !important;
|
||||
background-color: var(--sl-color-bg) !important;
|
||||
line-height: 1.6;
|
||||
font-size: .75rem;
|
||||
font-size: 0.75rem;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
@ -354,20 +353,20 @@
|
|||
max-width: var(--md-tool-width);
|
||||
gap: 0.25rem 0.375rem;
|
||||
|
||||
&>div:nth-child(3n + 1) {
|
||||
& > div:nth-child(3n + 1) {
|
||||
width: 8px;
|
||||
height: 2px;
|
||||
border-radius: 1px;
|
||||
background: var(--sl-color-divider);
|
||||
}
|
||||
|
||||
&>div:nth-child(3n + 2),
|
||||
&>div:nth-child(3n + 3) {
|
||||
& > div:nth-child(3n + 2),
|
||||
& > div:nth-child(3n + 3) {
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
&>div:nth-child(3n + 3) {
|
||||
& > div:nth-child(3n + 3) {
|
||||
padding-left: 0.125rem;
|
||||
word-break: break-word;
|
||||
color: var(--sl-color-text-secondary);
|
||||
|
|
|
@ -39,12 +39,12 @@ opencode run Explain the use of context in Go
|
|||
|
||||
#### Flags
|
||||
|
||||
| Flag | Short | Description |
|
||||
| ----------------- | ----- | --------------------- |
|
||||
| `--continue` | `-c` | Continue the last session |
|
||||
| `--session` | `-s` | Session ID to continue |
|
||||
| `--share` | | Share the session |
|
||||
| `--model` | `-m` | Model to use in the form of provider/model |
|
||||
| Flag | Short | Description |
|
||||
| ------------ | ----- | ------------------------------------------ |
|
||||
| `--continue` | `-c` | Continue the last session |
|
||||
| `--session` | `-s` | Session ID to continue |
|
||||
| `--share` | | Share the session |
|
||||
| `--model` | `-m` | Model to use in the form of provider/model |
|
||||
|
||||
---
|
||||
|
||||
|
@ -122,8 +122,8 @@ opencode upgrade v0.1.48
|
|||
|
||||
The opencode CLI takes the following flags.
|
||||
|
||||
| Flag | Short | Description |
|
||||
| ----------------- | ----- | --------------------- |
|
||||
| `--help` | `-h` | Display help |
|
||||
| `--version` | | Print version number |
|
||||
| `--print-logs` | | Print logs to stderr |
|
||||
| Flag | Short | Description |
|
||||
| -------------- | ----- | -------------------- |
|
||||
| `--help` | `-h` | Display help |
|
||||
| `--version` | | Print version number |
|
||||
| `--print-logs` | | Print logs to stderr |
|
||||
|
|
|
@ -39,7 +39,7 @@ You can configure the providers and models you want to use in your opencode conf
|
|||
```json title="opencode.json"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"provider": { },
|
||||
"provider": {},
|
||||
"model": ""
|
||||
}
|
||||
```
|
||||
|
@ -70,7 +70,7 @@ You can customize your keybinds through the `keybinds` option.
|
|||
```json title="opencode.json"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"keybinds": { }
|
||||
"keybinds": {}
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -85,7 +85,7 @@ You can configure MCP servers you want to use through the `mcp` option.
|
|||
```json title="opencode.json"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"mcp": { }
|
||||
"mcp": {}
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -105,6 +105,7 @@ You can disable providers that are loaded automatically through the `disabled_pr
|
|||
```
|
||||
|
||||
The `disabled_providers` option accepts an array of provider IDs. When a provider is disabled:
|
||||
|
||||
- It won't be loaded even if environment variables are set
|
||||
- It won't be loaded even if API keys are configured through `opencode auth login`
|
||||
- The provider's models won't appear in the model selection list
|
||||
|
|
|
@ -3,7 +3,7 @@ title: Intro
|
|||
description: Get started with opencode.
|
||||
---
|
||||
|
||||
import { Tabs, TabItem } from '@astrojs/starlight/components';
|
||||
import { Tabs, TabItem } from "@astrojs/starlight/components"
|
||||
|
||||
[**opencode**](/) is an AI coding agent built for the terminal. It features:
|
||||
|
||||
|
@ -21,26 +21,10 @@ import { Tabs, TabItem } from '@astrojs/starlight/components';
|
|||
## Install
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="npm">
|
||||
```bash
|
||||
npm install -g opencode-ai
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="Bun">
|
||||
```bash
|
||||
bun install -g opencode-ai
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="pnpm">
|
||||
```bash
|
||||
pnpm install -g opencode-ai
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="Yarn">
|
||||
```bash
|
||||
yarn global add opencode-ai
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="npm">```bash npm install -g opencode-ai ```</TabItem>
|
||||
<TabItem label="Bun">```bash bun install -g opencode-ai ```</TabItem>
|
||||
<TabItem label="pnpm">```bash pnpm install -g opencode-ai ```</TabItem>
|
||||
<TabItem label="Yarn">```bash yarn global add opencode-ai ```</TabItem>
|
||||
</Tabs>
|
||||
|
||||
You can also install the opencode binary through the following.
|
||||
|
|
|
@ -31,17 +31,20 @@ You can also just create this file manually. Here's an example of some things yo
|
|||
This is an SST v3 monorepo with TypeScript. The project uses bun workspaces for package management.
|
||||
|
||||
## Project Structure
|
||||
|
||||
- `packages/` - Contains all workspace packages (functions, core, web, etc.)
|
||||
- `infra/` - Infrastructure definitions split by service (storage.ts, api.ts, web.ts)
|
||||
- `sst.config.ts` - Main SST configuration with dynamic imports
|
||||
|
||||
## Code Standards
|
||||
|
||||
- Use TypeScript with strict mode enabled
|
||||
- Shared code goes in `packages/core/` with proper exports configuration
|
||||
- Functions go in `packages/functions/`
|
||||
- Infrastructure should be split into logical files in `infra/`
|
||||
|
||||
## Monorepo Conventions
|
||||
|
||||
- Import shared modules using workspace names: `@my-app/core/example`
|
||||
```
|
||||
|
||||
|
|
|
@ -13,18 +13,18 @@ By default, opencode uses our own `opencode` theme.
|
|||
|
||||
opencode comes with several built-in themes.
|
||||
|
||||
| Name | Description |
|
||||
| --- | --- |
|
||||
| `system` | Adapts to your terminal's background color |
|
||||
| `tokyonight` | Based on the Tokyonight theme |
|
||||
| `everforest` | Based on the Everforest theme |
|
||||
| `ayu` | Based on the Ayu dark theme |
|
||||
| `catppuccin` | Based on the Catppuccin theme |
|
||||
| `gruvbox` | Based on the Gruvbox theme |
|
||||
| `kanagawa` | Based on the Kanagawa theme |
|
||||
| `nord` | Based on the Nord theme |
|
||||
| `matrix` | Hacker-style green on black theme |
|
||||
| `one-dark` | Based on the Atom One Dark theme |
|
||||
| Name | Description |
|
||||
| ------------ | ------------------------------------------ |
|
||||
| `system` | Adapts to your terminal's background color |
|
||||
| `tokyonight` | Based on the Tokyonight theme |
|
||||
| `everforest` | Based on the Everforest theme |
|
||||
| `ayu` | Based on the Ayu dark theme |
|
||||
| `catppuccin` | Based on the Catppuccin theme |
|
||||
| `gruvbox` | Based on the Gruvbox theme |
|
||||
| `kanagawa` | Based on the Kanagawa theme |
|
||||
| `nord` | Based on the Nord theme |
|
||||
| `matrix` | Hacker-style green on black theme |
|
||||
| `one-dark` | Based on the Atom One Dark theme |
|
||||
|
||||
And more, we are constantly adding new themes.
|
||||
|
||||
|
@ -61,7 +61,7 @@ You can select a theme by bringing up the theme select with the `/theme` command
|
|||
|
||||
## Custom themes
|
||||
|
||||
opencode supports a flexible JSON-based theme system that allows users to create and customize themes easily.
|
||||
opencode supports a flexible JSON-based theme system that allows users to create and customize themes easily.
|
||||
|
||||
---
|
||||
|
||||
|
|
12
packages/web/src/types/lang-map.d.ts
vendored
12
packages/web/src/types/lang-map.d.ts
vendored
|
@ -2,9 +2,9 @@ declare module "lang-map" {
|
|||
/** Returned by calling `map()` */
|
||||
export interface MapReturn {
|
||||
/** All extensions keyed by language name */
|
||||
extensions: Record<string, string[]>;
|
||||
extensions: Record<string, string[]>
|
||||
/** All languages keyed by file-extension */
|
||||
languages: Record<string, string[]>;
|
||||
languages: Record<string, string[]>
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -14,14 +14,14 @@ declare module "lang-map" {
|
|||
* const { extensions, languages } = map();
|
||||
* ```
|
||||
*/
|
||||
function map(): MapReturn;
|
||||
function map(): MapReturn
|
||||
|
||||
/** Static method: get extensions for a given language */
|
||||
namespace map {
|
||||
function extensions(language: string): string[];
|
||||
function extensions(language: string): string[]
|
||||
/** Static method: get languages for a given extension */
|
||||
function languages(extension: string): string[];
|
||||
function languages(extension: string): string[]
|
||||
}
|
||||
|
||||
export = map;
|
||||
export = map
|
||||
}
|
||||
|
|
2
packages/web/sst-env.d.ts
vendored
2
packages/web/sst-env.d.ts
vendored
|
@ -6,4 +6,4 @@
|
|||
/// <reference path="../../sst-env.d.ts" />
|
||||
|
||||
import "sst"
|
||||
export {}
|
||||
export {}
|
||||
|
|
|
@ -26,13 +26,9 @@ async function fetchNpmDownloads(packageName: string): Promise<number> {
|
|||
// Use a range from 2020 to current year + 5 years to ensure it works forever
|
||||
const currentYear = new Date().getFullYear()
|
||||
const endYear = currentYear + 5
|
||||
const response = await fetch(
|
||||
`https://api.npmjs.org/downloads/range/2020-01-01:${endYear}-12-31/${packageName}`,
|
||||
)
|
||||
const response = await fetch(`https://api.npmjs.org/downloads/range/2020-01-01:${endYear}-12-31/${packageName}`)
|
||||
if (!response.ok) {
|
||||
console.warn(
|
||||
`Failed to fetch npm downloads for ${packageName}: ${response.status}`,
|
||||
)
|
||||
console.warn(`Failed to fetch npm downloads for ${packageName}: ${response.status}`)
|
||||
return 0
|
||||
}
|
||||
const data: NpmDownloadsRange = await response.json()
|
||||
|
@ -53,9 +49,7 @@ async function fetchReleases(): Promise<Release[]> {
|
|||
|
||||
const response = await fetch(url)
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`GitHub API error: ${response.status} ${response.statusText}`,
|
||||
)
|
||||
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const batch: Release[] = await response.json()
|
||||
|
@ -115,11 +109,7 @@ async function save(githubTotal: number, npmDownloads: number) {
|
|||
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
const line = lines[i].trim()
|
||||
if (
|
||||
line.startsWith("|") &&
|
||||
!line.includes("Date") &&
|
||||
!line.includes("---")
|
||||
) {
|
||||
if (line.startsWith("|") && !line.includes("Date") && !line.includes("---")) {
|
||||
const match = line.match(
|
||||
/\|\s*[\d-]+\s*\|\s*([\d,]+)\s*(?:\([^)]*\))?\s*\|\s*([\d,]+)\s*(?:\([^)]*\))?\s*\|\s*([\d,]+)\s*(?:\([^)]*\))?\s*\|/,
|
||||
)
|
||||
|
@ -147,11 +137,7 @@ async function save(githubTotal: number, npmDownloads: number) {
|
|||
? ` (${githubChange.toLocaleString()})`
|
||||
: " (+0)"
|
||||
const npmChangeStr =
|
||||
npmChange > 0
|
||||
? ` (+${npmChange.toLocaleString()})`
|
||||
: npmChange < 0
|
||||
? ` (${npmChange.toLocaleString()})`
|
||||
: " (+0)"
|
||||
npmChange > 0 ? ` (+${npmChange.toLocaleString()})` : npmChange < 0 ? ` (${npmChange.toLocaleString()})` : " (+0)"
|
||||
const totalChangeStr =
|
||||
totalChange > 0
|
||||
? ` (+${totalChange.toLocaleString()})`
|
||||
|
@ -182,9 +168,7 @@ const { total: githubTotal, stats } = calculate(releases)
|
|||
|
||||
console.log("Fetching npm all-time downloads for opencode-ai...\n")
|
||||
const npmDownloads = await fetchNpmDownloads("opencode-ai")
|
||||
console.log(
|
||||
`Fetched npm all-time downloads: ${npmDownloads.toLocaleString()}\n`,
|
||||
)
|
||||
console.log(`Fetched npm all-time downloads: ${npmDownloads.toLocaleString()}\n`)
|
||||
|
||||
await save(githubTotal, npmDownloads)
|
||||
|
||||
|
@ -202,24 +186,18 @@ console.log("-".repeat(60))
|
|||
stats
|
||||
.sort((a, b) => b.downloads - a.downloads)
|
||||
.forEach((release) => {
|
||||
console.log(
|
||||
`${release.tag.padEnd(15)} ${release.downloads.toLocaleString().padStart(10)} downloads`,
|
||||
)
|
||||
console.log(`${release.tag.padEnd(15)} ${release.downloads.toLocaleString().padStart(10)} downloads`)
|
||||
|
||||
if (release.assets.length > 1) {
|
||||
release.assets
|
||||
.sort((a, b) => b.downloads - a.downloads)
|
||||
.forEach((asset) => {
|
||||
console.log(
|
||||
` └─ ${asset.name.padEnd(25)} ${asset.downloads.toLocaleString().padStart(8)}`,
|
||||
)
|
||||
console.log(` └─ ${asset.name.padEnd(25)} ${asset.downloads.toLocaleString().padStart(8)}`)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
console.log("-".repeat(60))
|
||||
console.log(
|
||||
`GitHub Total: ${githubTotal.toLocaleString()} downloads across ${releases.length} releases`,
|
||||
)
|
||||
console.log(`GitHub Total: ${githubTotal.toLocaleString()} downloads across ${releases.length} releases`)
|
||||
console.log(`npm Total: ${npmDownloads.toLocaleString()} downloads`)
|
||||
console.log(`Combined Total: ${totalDownloads.toLocaleString()} downloads`)
|
||||
|
|
18
sst-env.d.ts
vendored
18
sst-env.d.ts
vendored
|
@ -5,20 +5,20 @@
|
|||
|
||||
declare module "sst" {
|
||||
export interface Resource {
|
||||
"Api": {
|
||||
"type": "sst.cloudflare.Worker"
|
||||
"url": string
|
||||
Api: {
|
||||
type: "sst.cloudflare.Worker"
|
||||
url: string
|
||||
}
|
||||
"Bucket": {
|
||||
"type": "sst.cloudflare.Bucket"
|
||||
Bucket: {
|
||||
type: "sst.cloudflare.Bucket"
|
||||
}
|
||||
"Web": {
|
||||
"type": "sst.cloudflare.Astro"
|
||||
"url": string
|
||||
Web: {
|
||||
type: "sst.cloudflare.Astro"
|
||||
url: string
|
||||
}
|
||||
}
|
||||
}
|
||||
/// <reference path="sst-env.d.ts" />
|
||||
|
||||
import "sst"
|
||||
export {}
|
||||
export {}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue