Merge branch 'dev' into project
This commit is contained in:
commit
11afe56f63
178 changed files with 4236 additions and 6777 deletions
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode/function",
|
||||
"version": "0.5.18",
|
||||
"version": "0.5.29",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
|
|
|||
4
packages/function/sst-env.d.ts
vendored
4
packages/function/sst-env.d.ts
vendored
|
|
@ -14,6 +14,10 @@ declare module "sst" {
|
|||
"type": "sst.sst.Linkable"
|
||||
"value": string
|
||||
}
|
||||
"Console": {
|
||||
"type": "sst.cloudflare.SolidStart"
|
||||
"url": string
|
||||
}
|
||||
"DATABASE_PASSWORD": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "0.5.18",
|
||||
"version": "0.5.29",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
|
|
@ -41,7 +41,7 @@
|
|||
"gray-matter": "4.0.3",
|
||||
"hono": "catalog:",
|
||||
"hono-openapi": "0.4.8",
|
||||
"isomorphic-git": "1.32.1",
|
||||
"ignore": "7.0.5",
|
||||
"jsonc-parser": "3.3.1",
|
||||
"minimatch": "10.0.3",
|
||||
"open": "10.1.2",
|
||||
|
|
|
|||
|
|
@ -97,11 +97,11 @@ if (!snapshot) {
|
|||
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 pkgbuild = [
|
||||
const binaryPkgbuild = [
|
||||
"# Maintainer: dax",
|
||||
"# Maintainer: adam",
|
||||
"",
|
||||
"pkgname='${pkg}'",
|
||||
"pkgname='opencode-bin'",
|
||||
`pkgver=${version.split("-")[0]}`,
|
||||
"options=('!debug' '!strip')",
|
||||
"pkgrel=1",
|
||||
|
|
@ -125,11 +125,58 @@ if (!snapshot) {
|
|||
"",
|
||||
].join("\n")
|
||||
|
||||
for (const pkg of ["opencode-bin"]) {
|
||||
// Source-based PKGBUILD for opencode
|
||||
const sourcePkgbuild = [
|
||||
"# Maintainer: dax",
|
||||
"# Maintainer: adam",
|
||||
"",
|
||||
"pkgname='opencode'",
|
||||
`pkgver=${version.split("-")[0]}`,
|
||||
"options=('!debug' '!strip')",
|
||||
"pkgrel=1",
|
||||
"pkgdesc='The AI coding agent built for the terminal.'",
|
||||
"url='https://github.com/sst/opencode'",
|
||||
"arch=('aarch64' 'x86_64')",
|
||||
"license=('MIT')",
|
||||
"provides=('opencode')",
|
||||
"conflicts=('opencode-bin')",
|
||||
"depends=('fzf' 'ripgrep')",
|
||||
"makedepends=('git' 'bun-bin' 'go')",
|
||||
"",
|
||||
`source=("opencode-\${pkgver}.tar.gz::https://github.com/sst/opencode/archive/v${version}.tar.gz")`,
|
||||
`sha256sums=('SKIP')`,
|
||||
"",
|
||||
"build() {",
|
||||
` cd "opencode-\${pkgver}"`,
|
||||
` bun install`,
|
||||
" cd packages/tui",
|
||||
` CGO_ENABLED=0 go build -ldflags="-s -w -X main.Version=\${pkgver}" -o tui cmd/opencode/main.go`,
|
||||
" cd ../opencode",
|
||||
` bun build --define OPENCODE_TUI_PATH="'$(realpath ../tui/tui)'" --define OPENCODE_VERSION="'\${pkgver}'" --compile --target=bun-linux-x64 --outfile=opencode ./src/index.ts`,
|
||||
"}",
|
||||
"",
|
||||
"package() {",
|
||||
` cd "opencode-\${pkgver}/packages/opencode"`,
|
||||
' install -Dm755 ./opencode "${pkgdir}/usr/bin/opencode"',
|
||||
"}",
|
||||
"",
|
||||
].join("\n")
|
||||
|
||||
for (const [pkg, pkgbuild] of [
|
||||
["opencode-bin", binaryPkgbuild],
|
||||
["opencode", sourcePkgbuild],
|
||||
]) {
|
||||
await $`rm -rf ./dist/aur-${pkg}`
|
||||
await $`git clone ssh://aur@aur.archlinux.org/${pkg}.git ./dist/aur-${pkg}`
|
||||
while (true) {
|
||||
try {
|
||||
await $`git clone ssh://aur@aur.archlinux.org/${pkg}.git ./dist/aur-${pkg}`
|
||||
break
|
||||
} catch (e) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
await $`cd ./dist/aur-${pkg} && git checkout master`
|
||||
await Bun.file(`./dist/aur-${pkg}/PKGBUILD`).write(pkgbuild.replace("${pkg}", pkg))
|
||||
await Bun.file(`./dist/aur-${pkg}/PKGBUILD`).write(pkgbuild)
|
||||
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}"`
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ export namespace App {
|
|||
hostname: z.string(),
|
||||
git: z.boolean(),
|
||||
path: z.object({
|
||||
home: z.string(),
|
||||
config: z.string(),
|
||||
data: z.string(),
|
||||
root: z.string(),
|
||||
|
|
@ -77,6 +78,7 @@ export namespace App {
|
|||
},
|
||||
git: git !== undefined,
|
||||
path: {
|
||||
home: os.homedir(),
|
||||
config: Global.Path.config,
|
||||
state: Global.Path.state,
|
||||
data,
|
||||
|
|
|
|||
|
|
@ -245,7 +245,7 @@ export const AuthLoginCommand = cmd({
|
|||
}
|
||||
|
||||
if (provider === "vercel") {
|
||||
prompts.log.info("You can create an api key in the dashboard")
|
||||
prompts.log.info("You can create an api key at https://vercel.link/ai-gateway-token")
|
||||
}
|
||||
|
||||
const key = await prompts.password({
|
||||
|
|
|
|||
20
packages/opencode/src/cli/cmd/debug/app.ts
Normal file
20
packages/opencode/src/cli/cmd/debug/app.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { App } from "../../../app/app"
|
||||
import { bootstrap } from "../../bootstrap"
|
||||
import { cmd } from "../cmd"
|
||||
|
||||
const AppInfoCommand = cmd({
|
||||
command: "info",
|
||||
builder: (yargs) => yargs,
|
||||
async handler() {
|
||||
await bootstrap({ cwd: process.cwd() }, async () => {
|
||||
const app = App.info()
|
||||
console.log(JSON.stringify(app, null, 2))
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const AppCommand = cmd({
|
||||
command: "app",
|
||||
builder: (yargs) => yargs.command(AppInfoCommand).demandCommand(),
|
||||
async handler() {},
|
||||
})
|
||||
|
|
@ -29,8 +29,25 @@ const FileStatusCommand = cmd({
|
|||
},
|
||||
})
|
||||
|
||||
const FileListCommand = cmd({
|
||||
command: "list <path>",
|
||||
builder: (yargs) =>
|
||||
yargs.positional("path", {
|
||||
type: "string",
|
||||
demandOption: true,
|
||||
description: "File path to list",
|
||||
}),
|
||||
async handler(args) {
|
||||
await bootstrap({ cwd: process.cwd() }, async () => {
|
||||
const files = await File.list(args.path)
|
||||
console.log(JSON.stringify(files, null, 2))
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const FileCommand = cmd({
|
||||
command: "file",
|
||||
builder: (yargs) => yargs.command(FileReadCommand).command(FileStatusCommand).demandCommand(),
|
||||
builder: (yargs) =>
|
||||
yargs.command(FileReadCommand).command(FileStatusCommand).command(FileListCommand).demandCommand(),
|
||||
async handler() {},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { Global } from "../../../global"
|
||||
import { bootstrap } from "../../bootstrap"
|
||||
import { cmd } from "../cmd"
|
||||
import { AppCommand } from "./app"
|
||||
import { FileCommand } from "./file"
|
||||
import { LSPCommand } from "./lsp"
|
||||
import { RipgrepCommand } from "./ripgrep"
|
||||
|
|
@ -11,6 +12,7 @@ export const DebugCommand = cmd({
|
|||
command: "debug",
|
||||
builder: (yargs) =>
|
||||
yargs
|
||||
.command(AppCommand)
|
||||
.command(LSPCommand)
|
||||
.command(RipgrepCommand)
|
||||
.command(FileCommand)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ const DiagnosticsCommand = cmd({
|
|||
async handler(args) {
|
||||
await bootstrap({ cwd: process.cwd() }, async () => {
|
||||
await LSP.touchFile(args.file, true)
|
||||
console.log(await LSP.diagnostics())
|
||||
console.log(JSON.stringify(await LSP.diagnostics(), null, 2))
|
||||
})
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -64,6 +64,11 @@ export const RunCommand = cmd({
|
|||
|
||||
if (!process.stdin.isTTY) message += "\n" + (await Bun.stdin.text())
|
||||
|
||||
if (message.trim().length === 0) {
|
||||
UI.error("Message cannot be empty")
|
||||
return
|
||||
}
|
||||
|
||||
await bootstrap({ cwd: process.cwd() }, async () => {
|
||||
const session = await (async () => {
|
||||
if (args.continue) {
|
||||
|
|
@ -171,12 +176,8 @@ export const RunCommand = cmd({
|
|||
const result = await Session.chat({
|
||||
sessionID: session.id,
|
||||
messageID,
|
||||
...(agent.model
|
||||
? agent.model
|
||||
: {
|
||||
providerID,
|
||||
modelID,
|
||||
}),
|
||||
providerID,
|
||||
modelID,
|
||||
agent: agent.name,
|
||||
parts: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -342,7 +342,10 @@ export namespace Config {
|
|||
theme: z.string().optional().describe("Theme name to use for the interface"),
|
||||
keybinds: Keybinds.optional().describe("Custom keybind configurations"),
|
||||
tui: TUI.optional().describe("TUI specific settings"),
|
||||
command: z.record(z.string(), Command).optional(),
|
||||
command: z
|
||||
.record(z.string(), Command)
|
||||
.optional()
|
||||
.describe("Command configuration, see https://opencode.ai/docs/commands"),
|
||||
plugin: z.string().array().optional(),
|
||||
snapshot: z.boolean().optional(),
|
||||
share: z
|
||||
|
|
|
|||
|
|
@ -3,8 +3,9 @@ import { Bus } from "../bus"
|
|||
import { $ } from "bun"
|
||||
import { createPatch } from "diff"
|
||||
import path from "path"
|
||||
import * as git from "isomorphic-git"
|
||||
import { App } from "../app/app"
|
||||
import fs from "fs"
|
||||
import ignore from "ignore"
|
||||
import { Log } from "../util/log"
|
||||
import { Instance } from "../project/instance"
|
||||
import { Project } from "../project/project"
|
||||
|
|
@ -25,6 +26,18 @@ export namespace File {
|
|||
|
||||
export type Info = z.infer<typeof Info>
|
||||
|
||||
export const Node = z
|
||||
.object({
|
||||
name: z.string(),
|
||||
path: z.string(),
|
||||
type: z.enum(["file", "directory"]),
|
||||
ignored: z.boolean(),
|
||||
})
|
||||
.openapi({
|
||||
ref: "FileNode",
|
||||
})
|
||||
export type Node = z.infer<typeof Node>
|
||||
|
||||
export const Event = {
|
||||
Edited: Bus.event(
|
||||
"file.edited",
|
||||
|
|
@ -114,12 +127,8 @@ export namespace File {
|
|||
.then((x) => x.trim())
|
||||
if (project.vcs === "git") {
|
||||
const rel = path.relative(Instance.worktree, full)
|
||||
const diff = await git.status({
|
||||
fs,
|
||||
dir: Instance.worktree,
|
||||
filepath: rel,
|
||||
})
|
||||
if (diff !== "unmodified") {
|
||||
const diff = await $`git diff ${rel}`.cwd(Instance.worktree).quiet().nothrow().text()
|
||||
if (diff.trim()) {
|
||||
const original = await $`git show HEAD:${rel}`.cwd(Instance.worktree).quiet().nothrow().text()
|
||||
const patch = createPatch(file, original, content, "old", "new", {
|
||||
context: Infinity,
|
||||
|
|
@ -129,4 +138,38 @@ export namespace File {
|
|||
}
|
||||
return { type: "raw", content }
|
||||
}
|
||||
|
||||
export async function list(dir?: string) {
|
||||
const exclude = [".git", ".DS_Store"]
|
||||
const app = App.info()
|
||||
let ignored = (_: string) => false
|
||||
if (app.git) {
|
||||
const gitignore = Bun.file(path.join(app.path.root, ".gitignore"))
|
||||
if (await gitignore.exists()) {
|
||||
const ig = ignore().add(await gitignore.text())
|
||||
ignored = ig.ignores.bind(ig)
|
||||
}
|
||||
}
|
||||
const resolved = dir ? path.join(app.path.cwd, dir) : app.path.cwd
|
||||
const nodes: Node[] = []
|
||||
for (const entry of await fs.promises.readdir(resolved, { withFileTypes: true })) {
|
||||
if (exclude.includes(entry.name)) continue
|
||||
const fullPath = path.join(resolved, entry.name)
|
||||
const relativePath = path.relative(app.path.cwd, fullPath)
|
||||
const relativeToRoot = path.relative(app.path.root, fullPath)
|
||||
const type = entry.isDirectory() ? "directory" : "file"
|
||||
nodes.push({
|
||||
name: entry.name,
|
||||
path: relativePath,
|
||||
type,
|
||||
ignored: ignored(type === "directory" ? relativeToRoot + "/" : relativeToRoot),
|
||||
})
|
||||
}
|
||||
return nodes.sort((a, b) => {
|
||||
if (a.type !== b.type) {
|
||||
return a.type === "directory" ? -1 : 1
|
||||
}
|
||||
return a.name.localeCompare(b.name)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -139,6 +139,7 @@ export namespace LSP {
|
|||
s.broken.add(root + server.id)
|
||||
handle.process.kill()
|
||||
log.error(`Failed to initialize LSP client ${server.id}`, { error: err })
|
||||
return undefined
|
||||
})
|
||||
if (!client) continue
|
||||
s.clients.push(client)
|
||||
|
|
|
|||
|
|
@ -148,6 +148,7 @@ export namespace LSPServer {
|
|||
async spawn(root) {
|
||||
const eslint = await Bun.resolve("eslint", Instance.directory).catch(() => {})
|
||||
if (!eslint) return
|
||||
log.info("spawning eslint server")
|
||||
const serverPath = path.join(Global.Path.bin, "vscode-eslint", "server", "out", "eslintServer.js")
|
||||
if (!(await Bun.file(serverPath).exists())) {
|
||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||
|
|
@ -164,7 +165,9 @@ export namespace LSPServer {
|
|||
const extractedPath = path.join(Global.Path.bin, "vscode-eslint-main")
|
||||
const finalPath = path.join(Global.Path.bin, "vscode-eslint")
|
||||
|
||||
if (await Bun.file(finalPath).exists()) {
|
||||
const stats = await fs.stat(finalPath).catch(() => undefined)
|
||||
if (stats) {
|
||||
log.info("removing old eslint installation", { path: finalPath })
|
||||
await fs.rm(finalPath, { force: true, recursive: true })
|
||||
}
|
||||
await fs.rename(extractedPath, finalPath)
|
||||
|
|
@ -512,7 +515,35 @@ export namespace LSPServer {
|
|||
|
||||
export const RustAnalyzer: Info = {
|
||||
id: "rust",
|
||||
root: NearestRoot(["Cargo.toml", "Cargo.lock"]),
|
||||
root: async (file, app) => {
|
||||
const crateRoot = await NearestRoot(["Cargo.toml", "Cargo.lock"])(file, app)
|
||||
if (crateRoot === undefined) {
|
||||
return undefined
|
||||
}
|
||||
let currentDir = crateRoot
|
||||
|
||||
while (currentDir !== path.dirname(currentDir)) {
|
||||
// Stop at filesystem root
|
||||
const cargoTomlPath = path.join(currentDir, "Cargo.toml")
|
||||
try {
|
||||
const cargoTomlContent = await Bun.file(cargoTomlPath).text()
|
||||
if (cargoTomlContent.includes("[workspace]")) {
|
||||
return currentDir
|
||||
}
|
||||
} catch (err) {
|
||||
// File doesn't exist or can't be read, continue searching up
|
||||
}
|
||||
|
||||
const parentDir = path.dirname(currentDir)
|
||||
if (parentDir === currentDir) break // Reached filesystem root
|
||||
currentDir = parentDir
|
||||
|
||||
// Stop if we've gone above the app root
|
||||
if (!currentDir.startsWith(app.path.root)) break
|
||||
}
|
||||
|
||||
return crateRoot
|
||||
},
|
||||
extensions: [".rs"],
|
||||
async spawn(root) {
|
||||
const bin = Bun.which("rust-analyzer")
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ export namespace MCP {
|
|||
let lastError: Error | undefined
|
||||
for (const { name, transport } of transports) {
|
||||
const client = await experimental_createMCPClient({
|
||||
name: key,
|
||||
name: "opencode",
|
||||
transport,
|
||||
}).catch((error) => {
|
||||
lastError = error instanceof Error ? error : new Error(String(error))
|
||||
|
|
@ -91,7 +91,7 @@ export namespace MCP {
|
|||
if (mcp.type === "local") {
|
||||
const [cmd, ...args] = mcp.command
|
||||
const client = await experimental_createMCPClient({
|
||||
name: key,
|
||||
name: "opencode",
|
||||
transport: new StdioClientTransport({
|
||||
stderr: "ignore",
|
||||
command: cmd,
|
||||
|
|
|
|||
|
|
@ -936,6 +936,34 @@ export namespace Server {
|
|||
)
|
||||
.get(
|
||||
"/file",
|
||||
describeRoute({
|
||||
description: "List files and directories",
|
||||
operationId: "file.list",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Files and directories",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(File.Node.array()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
zValidator(
|
||||
"query",
|
||||
z.object({
|
||||
path: z.string(),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const path = c.req.valid("query").path
|
||||
const content = await File.list(path)
|
||||
return c.json(content)
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/file/content",
|
||||
describeRoute({
|
||||
description: "Read a file",
|
||||
operationId: "file.read",
|
||||
|
|
@ -964,10 +992,6 @@ export namespace Server {
|
|||
async (c) => {
|
||||
const path = c.req.valid("query").path
|
||||
const content = await File.read(path)
|
||||
log.info("read file", {
|
||||
path,
|
||||
content: content.content,
|
||||
})
|
||||
return c.json(content)
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import path from "path"
|
||||
import os from "os"
|
||||
import { spawn } from "child_process"
|
||||
import { Decimal } from "decimal.js"
|
||||
import { z, ZodSchema } from "zod"
|
||||
|
|
@ -721,7 +722,9 @@ export namespace Session {
|
|||
draft.title = title.trim()
|
||||
})
|
||||
})
|
||||
.catch(() => {})
|
||||
.catch((error) => {
|
||||
log.error("failed to generate title", { error, model: small.info.id })
|
||||
})
|
||||
}
|
||||
|
||||
const agent = await Agent.get(inputAgent)
|
||||
|
|
@ -866,11 +869,31 @@ export namespace Session {
|
|||
const execute = item.execute
|
||||
if (!execute) continue
|
||||
item.execute = async (args, opts) => {
|
||||
await Plugin.trigger(
|
||||
"tool.execute.before",
|
||||
{
|
||||
tool: key,
|
||||
sessionID: input.sessionID,
|
||||
callID: opts.toolCallId,
|
||||
},
|
||||
{
|
||||
args,
|
||||
},
|
||||
)
|
||||
const result = await execute(args, opts)
|
||||
const output = result.content
|
||||
.filter((x: any) => x.type === "text")
|
||||
.map((x: any) => x.text)
|
||||
.join("\n\n")
|
||||
await Plugin.trigger(
|
||||
"tool.execute.after",
|
||||
{
|
||||
tool: key,
|
||||
sessionID: input.sessionID,
|
||||
callID: opts.toolCallId,
|
||||
},
|
||||
result,
|
||||
)
|
||||
|
||||
return {
|
||||
output,
|
||||
|
|
@ -1041,6 +1064,25 @@ export namespace Session {
|
|||
export type ShellInput = z.infer<typeof ShellInput>
|
||||
export async function shell(input: ShellInput) {
|
||||
using abort = lock(input.sessionID)
|
||||
const userMsg: MessageV2.User = {
|
||||
id: Identifier.ascending("message"),
|
||||
sessionID: input.sessionID,
|
||||
time: {
|
||||
created: Date.now(),
|
||||
},
|
||||
role: "user",
|
||||
}
|
||||
await updateMessage(userMsg)
|
||||
const userPart: MessageV2.Part = {
|
||||
type: "text",
|
||||
id: Identifier.ascending("part"),
|
||||
messageID: userMsg.id,
|
||||
sessionID: input.sessionID,
|
||||
text: "The following tool was executed by the user",
|
||||
synthetic: true,
|
||||
}
|
||||
await updatePart(userPart)
|
||||
|
||||
const msg: MessageV2.Assistant = {
|
||||
id: Identifier.ascending("message"),
|
||||
sessionID: input.sessionID,
|
||||
|
|
@ -1177,14 +1219,22 @@ export namespace Session {
|
|||
|
||||
export async function command(input: CommandInput) {
|
||||
const command = await Command.get(input.command)
|
||||
const agent = input.agent ?? command.agent ?? "build"
|
||||
const agent = command.agent ?? input.agent ?? "build"
|
||||
const fmtModel = (model: { providerID: string; modelID: string }) => `${model.providerID}/${model.modelID}`
|
||||
|
||||
const model =
|
||||
input.model ??
|
||||
command.model ??
|
||||
(await Agent.get(agent).then((x) => (x.model ? `${x.model.providerID}/${x.model.modelID}` : undefined))) ??
|
||||
(await Provider.defaultModel().then((x) => `${x.providerID}/${x.modelID}`))
|
||||
(command.agent && (await Agent.get(command.agent).then((x) => (x.model ? fmtModel(x.model) : undefined)))) ??
|
||||
input.model ??
|
||||
(input.agent && (await Agent.get(input.agent).then((x) => (x.model ? fmtModel(x.model) : undefined)))) ??
|
||||
fmtModel(await Provider.defaultModel())
|
||||
|
||||
let template = command.template.replace("$ARGUMENTS", input.arguments)
|
||||
|
||||
// intentionally doing match regex doing bash regex replacements
|
||||
// this is because bash commands can output "@" references
|
||||
const fileMatches = template.matchAll(fileRegex)
|
||||
|
||||
const bash = Array.from(template.matchAll(bashRegex))
|
||||
if (bash.length > 0) {
|
||||
const results = await Promise.all(
|
||||
|
|
@ -1207,15 +1257,18 @@ export namespace Session {
|
|||
},
|
||||
] as ChatInput["parts"]
|
||||
|
||||
const matches = template.matchAll(fileRegex)
|
||||
const app = App.info()
|
||||
|
||||
for (const match of matches) {
|
||||
const file = path.join(app.path.cwd, match[1])
|
||||
for (const match of fileMatches) {
|
||||
const filename = match[1]
|
||||
const filepath = filename.startsWith("~/")
|
||||
? path.join(os.homedir(), filename.slice(2))
|
||||
: path.join(app.path.cwd, filename)
|
||||
|
||||
parts.push({
|
||||
type: "file",
|
||||
url: `file://${file}`,
|
||||
filename: match[1],
|
||||
url: `file://${filepath}`,
|
||||
filename,
|
||||
mime: "text/plain",
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,6 +109,9 @@ IMPORTANT: When the user asks you to create a pull request, follow these steps c
|
|||
gh pr create --title "the pr title" --body "$(cat <<'EOF'
|
||||
## Summary
|
||||
<1-3 bullet points>
|
||||
EOF
|
||||
)"
|
||||
</example>
|
||||
|
||||
Important:
|
||||
- NEVER update the git config
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "0.5.18",
|
||||
"version": "0.5.29",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit"
|
||||
|
|
@ -19,7 +19,6 @@
|
|||
"@opencode-ai/sdk": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hey-api/openapi-ts": "0.81.0",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "0.5.18",
|
||||
"version": "0.5.29",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit"
|
||||
|
|
@ -29,6 +29,6 @@
|
|||
"@tsconfig/node22": "catalog:"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hey-api/openapi-ts": "0.80.1"
|
||||
"@hey-api/openapi-ts": "0.81.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
export * from "./gen/types.gen.js"
|
||||
export { type Config as OpencodeClientConfig, OpencodeClient }
|
||||
|
||||
import { createClient } from "./gen/client/client.js"
|
||||
import { type Config } from "./gen/client/types.js"
|
||||
import { createClient } from "./gen/client/client.gen.js"
|
||||
import { type Config } from "./gen/client/types.gen.js"
|
||||
import { OpencodeClient } from "./gen/sdk.gen.js"
|
||||
|
||||
export function createOpencodeClient(config?: Config) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
import type { Client, Config, RequestOptions } from "./types.js"
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
import { createSseClient } from "../core/serverSentEvents.gen.js"
|
||||
import type { Client, Config, RequestOptions, ResolvedRequestOptions } from "./types.gen.js"
|
||||
import {
|
||||
buildUrl,
|
||||
createConfig,
|
||||
|
|
@ -7,7 +10,7 @@ import {
|
|||
mergeConfigs,
|
||||
mergeHeaders,
|
||||
setAuthParams,
|
||||
} from "./utils.js"
|
||||
} from "./utils.gen.js"
|
||||
|
||||
type ReqInit = Omit<RequestInit, "body" | "headers"> & {
|
||||
body?: any
|
||||
|
|
@ -24,14 +27,15 @@ export const createClient = (config: Config = {}): Client => {
|
|||
return getConfig()
|
||||
}
|
||||
|
||||
const interceptors = createInterceptors<Request, Response, unknown, RequestOptions>()
|
||||
const interceptors = createInterceptors<Request, Response, unknown, ResolvedRequestOptions>()
|
||||
|
||||
const request: Client["request"] = async (options) => {
|
||||
const beforeRequest = async (options: RequestOptions) => {
|
||||
const opts = {
|
||||
..._config,
|
||||
...options,
|
||||
fetch: options.fetch ?? _config.fetch ?? globalThis.fetch,
|
||||
headers: mergeHeaders(_config.headers, options.headers),
|
||||
serializedBody: undefined,
|
||||
}
|
||||
|
||||
if (opts.security) {
|
||||
|
|
@ -46,18 +50,26 @@ export const createClient = (config: Config = {}): Client => {
|
|||
}
|
||||
|
||||
if (opts.body && opts.bodySerializer) {
|
||||
opts.body = opts.bodySerializer(opts.body)
|
||||
opts.serializedBody = opts.bodySerializer(opts.body)
|
||||
}
|
||||
|
||||
// remove Content-Type header if body is empty to avoid sending invalid requests
|
||||
if (opts.body === undefined || opts.body === "") {
|
||||
if (opts.serializedBody === undefined || opts.serializedBody === "") {
|
||||
opts.headers.delete("Content-Type")
|
||||
}
|
||||
|
||||
const url = buildUrl(opts)
|
||||
|
||||
return { opts, url }
|
||||
}
|
||||
|
||||
const request: Client["request"] = async (options) => {
|
||||
// @ts-expect-error
|
||||
const { opts, url } = await beforeRequest(options)
|
||||
const requestInit: ReqInit = {
|
||||
redirect: "follow",
|
||||
...opts,
|
||||
body: opts.serializedBody,
|
||||
}
|
||||
|
||||
let request = new Request(url, requestInit)
|
||||
|
|
@ -166,20 +178,35 @@ export const createClient = (config: Config = {}): Client => {
|
|||
}
|
||||
}
|
||||
|
||||
const makeMethod = (method: Required<Config>["method"]) => {
|
||||
const fn = (options: RequestOptions) => request({ ...options, method })
|
||||
fn.sse = async (options: RequestOptions) => {
|
||||
const { opts, url } = await beforeRequest(options)
|
||||
return createSseClient({
|
||||
...opts,
|
||||
body: opts.body as BodyInit | null | undefined,
|
||||
headers: opts.headers as unknown as Record<string, string>,
|
||||
method,
|
||||
url,
|
||||
})
|
||||
}
|
||||
return fn
|
||||
}
|
||||
|
||||
return {
|
||||
buildUrl,
|
||||
connect: (options) => request({ ...options, method: "CONNECT" }),
|
||||
delete: (options) => request({ ...options, method: "DELETE" }),
|
||||
get: (options) => request({ ...options, method: "GET" }),
|
||||
connect: makeMethod("CONNECT"),
|
||||
delete: makeMethod("DELETE"),
|
||||
get: makeMethod("GET"),
|
||||
getConfig,
|
||||
head: (options) => request({ ...options, method: "HEAD" }),
|
||||
head: makeMethod("HEAD"),
|
||||
interceptors,
|
||||
options: (options) => request({ ...options, method: "OPTIONS" }),
|
||||
patch: (options) => request({ ...options, method: "PATCH" }),
|
||||
post: (options) => request({ ...options, method: "POST" }),
|
||||
put: (options) => request({ ...options, method: "PUT" }),
|
||||
options: makeMethod("OPTIONS"),
|
||||
patch: makeMethod("PATCH"),
|
||||
post: makeMethod("POST"),
|
||||
put: makeMethod("PUT"),
|
||||
request,
|
||||
setConfig,
|
||||
trace: (options) => request({ ...options, method: "TRACE" }),
|
||||
}
|
||||
trace: makeMethod("TRACE"),
|
||||
} as Client
|
||||
}
|
||||
|
|
@ -1,8 +1,14 @@
|
|||
export type { Auth } from "../core/auth.js"
|
||||
export type { QuerySerializerOptions } from "../core/bodySerializer.js"
|
||||
export { formDataBodySerializer, jsonBodySerializer, urlSearchParamsBodySerializer } from "../core/bodySerializer.js"
|
||||
export { buildClientParams } from "../core/params.js"
|
||||
export { createClient } from "./client.js"
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
export type { Auth } from "../core/auth.gen.js"
|
||||
export type { QuerySerializerOptions } from "../core/bodySerializer.gen.js"
|
||||
export {
|
||||
formDataBodySerializer,
|
||||
jsonBodySerializer,
|
||||
urlSearchParamsBodySerializer,
|
||||
} from "../core/bodySerializer.gen.js"
|
||||
export { buildClientParams } from "../core/params.gen.js"
|
||||
export { createClient } from "./client.gen.js"
|
||||
export type {
|
||||
Client,
|
||||
ClientOptions,
|
||||
|
|
@ -12,7 +18,8 @@ export type {
|
|||
OptionsLegacyParser,
|
||||
RequestOptions,
|
||||
RequestResult,
|
||||
ResolvedRequestOptions,
|
||||
ResponseStyle,
|
||||
TDataShape,
|
||||
} from "./types.js"
|
||||
export { createConfig, mergeHeaders } from "./utils.js"
|
||||
} from "./types.gen.js"
|
||||
export { createConfig, mergeHeaders } from "./utils.gen.js"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import type { Auth } from "../core/auth.js"
|
||||
import type { Client as CoreClient, Config as CoreConfig } from "../core/types.js"
|
||||
import type { Middleware } from "./utils.js"
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
import type { Auth } from "../core/auth.gen.js"
|
||||
import type { ServerSentEventsOptions, ServerSentEventsResult } from "../core/serverSentEvents.gen.js"
|
||||
import type { Client as CoreClient, Config as CoreConfig } from "../core/types.gen.js"
|
||||
import type { Middleware } from "./utils.gen.js"
|
||||
|
||||
export type ResponseStyle = "data" | "fields"
|
||||
|
||||
|
|
@ -49,13 +52,18 @@ export interface Config<T extends ClientOptions = ClientOptions>
|
|||
}
|
||||
|
||||
export interface RequestOptions<
|
||||
TData = unknown,
|
||||
TResponseStyle extends ResponseStyle = "fields",
|
||||
ThrowOnError extends boolean = boolean,
|
||||
Url extends string = string,
|
||||
> extends Config<{
|
||||
responseStyle: TResponseStyle
|
||||
throwOnError: ThrowOnError
|
||||
}> {
|
||||
responseStyle: TResponseStyle
|
||||
throwOnError: ThrowOnError
|
||||
}>,
|
||||
Pick<
|
||||
ServerSentEventsOptions<TData>,
|
||||
"onSseError" | "onSseEvent" | "sseDefaultRetryDelay" | "sseMaxRetryAttempts" | "sseMaxRetryDelay"
|
||||
> {
|
||||
/**
|
||||
* Any body that you want to add to your request.
|
||||
*
|
||||
|
|
@ -71,6 +79,14 @@ export interface RequestOptions<
|
|||
url: Url
|
||||
}
|
||||
|
||||
export interface ResolvedRequestOptions<
|
||||
TResponseStyle extends ResponseStyle = "fields",
|
||||
ThrowOnError extends boolean = boolean,
|
||||
Url extends string = string,
|
||||
> extends RequestOptions<unknown, TResponseStyle, ThrowOnError, Url> {
|
||||
serializedBody?: string
|
||||
}
|
||||
|
||||
export type RequestResult<
|
||||
TData = unknown,
|
||||
TError = unknown,
|
||||
|
|
@ -112,23 +128,36 @@ export interface ClientOptions {
|
|||
throwOnError?: boolean
|
||||
}
|
||||
|
||||
type MethodFn = <
|
||||
type MethodFnBase = <
|
||||
TData = unknown,
|
||||
TError = unknown,
|
||||
ThrowOnError extends boolean = false,
|
||||
TResponseStyle extends ResponseStyle = "fields",
|
||||
>(
|
||||
options: Omit<RequestOptions<TResponseStyle, ThrowOnError>, "method">,
|
||||
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, "method">,
|
||||
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>
|
||||
|
||||
type MethodFnServerSentEvents = <
|
||||
TData = unknown,
|
||||
TError = unknown,
|
||||
ThrowOnError extends boolean = false,
|
||||
TResponseStyle extends ResponseStyle = "fields",
|
||||
>(
|
||||
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, "method">,
|
||||
) => Promise<ServerSentEventsResult<TData, TError>>
|
||||
|
||||
type MethodFn = MethodFnBase & {
|
||||
sse: MethodFnServerSentEvents
|
||||
}
|
||||
|
||||
type RequestFn = <
|
||||
TData = unknown,
|
||||
TError = unknown,
|
||||
ThrowOnError extends boolean = false,
|
||||
TResponseStyle extends ResponseStyle = "fields",
|
||||
>(
|
||||
options: Omit<RequestOptions<TResponseStyle, ThrowOnError>, "method"> &
|
||||
Pick<Required<RequestOptions<TResponseStyle, ThrowOnError>>, "method">,
|
||||
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, "method"> &
|
||||
Pick<Required<RequestOptions<TData, TResponseStyle, ThrowOnError>>, "method">,
|
||||
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>
|
||||
|
||||
type BuildUrlFn = <
|
||||
|
|
@ -143,7 +172,7 @@ type BuildUrlFn = <
|
|||
) => string
|
||||
|
||||
export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn> & {
|
||||
interceptors: Middleware<Request, Response, unknown, RequestOptions>
|
||||
interceptors: Middleware<Request, Response, unknown, ResolvedRequestOptions>
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -171,8 +200,10 @@ type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>
|
|||
export type Options<
|
||||
TData extends TDataShape = TDataShape,
|
||||
ThrowOnError extends boolean = boolean,
|
||||
TResponse = unknown,
|
||||
TResponseStyle extends ResponseStyle = "fields",
|
||||
> = OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, "body" | "path" | "query" | "url"> & Omit<TData, "url">
|
||||
> = OmitKeys<RequestOptions<TResponse, TResponseStyle, ThrowOnError>, "body" | "path" | "query" | "url"> &
|
||||
Omit<TData, "url">
|
||||
|
||||
export type OptionsLegacyParser<
|
||||
TData = unknown,
|
||||
|
|
@ -180,12 +211,12 @@ export type OptionsLegacyParser<
|
|||
TResponseStyle extends ResponseStyle = "fields",
|
||||
> = TData extends { body?: any }
|
||||
? TData extends { headers?: any }
|
||||
? OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, "body" | "headers" | "url"> & TData
|
||||
: OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, "body" | "url"> &
|
||||
? OmitKeys<RequestOptions<unknown, TResponseStyle, ThrowOnError>, "body" | "headers" | "url"> & TData
|
||||
: OmitKeys<RequestOptions<unknown, TResponseStyle, ThrowOnError>, "body" | "url"> &
|
||||
TData &
|
||||
Pick<RequestOptions<TResponseStyle, ThrowOnError>, "headers">
|
||||
Pick<RequestOptions<unknown, TResponseStyle, ThrowOnError>, "headers">
|
||||
: TData extends { headers?: any }
|
||||
? OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, "headers" | "url"> &
|
||||
? OmitKeys<RequestOptions<unknown, TResponseStyle, ThrowOnError>, "headers" | "url"> &
|
||||
TData &
|
||||
Pick<RequestOptions<TResponseStyle, ThrowOnError>, "body">
|
||||
: OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, "url"> & TData
|
||||
Pick<RequestOptions<unknown, TResponseStyle, ThrowOnError>, "body">
|
||||
: OmitKeys<RequestOptions<unknown, TResponseStyle, ThrowOnError>, "url"> & TData
|
||||
|
|
@ -1,84 +1,11 @@
|
|||
import { getAuthToken } from "../core/auth.js"
|
||||
import type { QuerySerializer, QuerySerializerOptions } from "../core/bodySerializer.js"
|
||||
import { jsonBodySerializer } from "../core/bodySerializer.js"
|
||||
import { serializeArrayParam, serializeObjectParam, serializePrimitiveParam } from "../core/pathSerializer.js"
|
||||
import type { Client, ClientOptions, Config, RequestOptions } from "./types.js"
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
interface PathSerializer {
|
||||
path: Record<string, unknown>
|
||||
url: string
|
||||
}
|
||||
|
||||
const PATH_PARAM_RE = /\{[^{}]+\}/g
|
||||
|
||||
type ArrayStyle = "form" | "spaceDelimited" | "pipeDelimited"
|
||||
type MatrixStyle = "label" | "matrix" | "simple"
|
||||
type ArraySeparatorStyle = ArrayStyle | MatrixStyle
|
||||
|
||||
const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
|
||||
let url = _url
|
||||
const matches = _url.match(PATH_PARAM_RE)
|
||||
if (matches) {
|
||||
for (const match of matches) {
|
||||
let explode = false
|
||||
let name = match.substring(1, match.length - 1)
|
||||
let style: ArraySeparatorStyle = "simple"
|
||||
|
||||
if (name.endsWith("*")) {
|
||||
explode = true
|
||||
name = name.substring(0, name.length - 1)
|
||||
}
|
||||
|
||||
if (name.startsWith(".")) {
|
||||
name = name.substring(1)
|
||||
style = "label"
|
||||
} else if (name.startsWith(";")) {
|
||||
name = name.substring(1)
|
||||
style = "matrix"
|
||||
}
|
||||
|
||||
const value = path[name]
|
||||
|
||||
if (value === undefined || value === null) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
url = url.replace(match, serializeArrayParam({ explode, name, style, value }))
|
||||
continue
|
||||
}
|
||||
|
||||
if (typeof value === "object") {
|
||||
url = url.replace(
|
||||
match,
|
||||
serializeObjectParam({
|
||||
explode,
|
||||
name,
|
||||
style,
|
||||
value: value as Record<string, unknown>,
|
||||
valueOnly: true,
|
||||
}),
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
if (style === "matrix") {
|
||||
url = url.replace(
|
||||
match,
|
||||
`;${serializePrimitiveParam({
|
||||
name,
|
||||
value: value as string,
|
||||
})}`,
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
const replaceValue = encodeURIComponent(style === "label" ? `.${value as string}` : (value as string))
|
||||
url = url.replace(match, replaceValue)
|
||||
}
|
||||
}
|
||||
return url
|
||||
}
|
||||
import { getAuthToken } from "../core/auth.gen.js"
|
||||
import type { QuerySerializerOptions } from "../core/bodySerializer.gen.js"
|
||||
import { jsonBodySerializer } from "../core/bodySerializer.gen.js"
|
||||
import { serializeArrayParam, serializeObjectParam, serializePrimitiveParam } from "../core/pathSerializer.gen.js"
|
||||
import { getUrl } from "../core/utils.gen.js"
|
||||
import type { Client, ClientOptions, Config, RequestOptions } from "./types.gen.js"
|
||||
|
||||
export const createQuerySerializer = <T = unknown>({ allowReserved, array, object }: QuerySerializerOptions = {}) => {
|
||||
const querySerializer = (queryParams: T) => {
|
||||
|
|
@ -161,6 +88,21 @@ export const getParseAs = (contentType: string | null): Exclude<Config["parseAs"
|
|||
return
|
||||
}
|
||||
|
||||
const checkForExistence = (
|
||||
options: Pick<RequestOptions, "auth" | "query"> & {
|
||||
headers: Headers
|
||||
},
|
||||
name?: string,
|
||||
): boolean => {
|
||||
if (!name) {
|
||||
return false
|
||||
}
|
||||
if (options.headers.has(name) || options.query?.[name] || options.headers.get("Cookie")?.includes(`${name}=`)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export const setAuthParams = async ({
|
||||
security,
|
||||
...options
|
||||
|
|
@ -169,6 +111,10 @@ export const setAuthParams = async ({
|
|||
headers: Headers
|
||||
}) => {
|
||||
for (const auth of security) {
|
||||
if (checkForExistence(options, auth.name)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const token = await getAuthToken(auth, options.auth)
|
||||
|
||||
if (!token) {
|
||||
|
|
@ -192,13 +138,11 @@ export const setAuthParams = async ({
|
|||
options.headers.set(name, token)
|
||||
break
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
export const buildUrl: Client["buildUrl"] = (options) => {
|
||||
const url = getUrl({
|
||||
export const buildUrl: Client["buildUrl"] = (options) =>
|
||||
getUrl({
|
||||
baseUrl: options.baseUrl as string,
|
||||
path: options.path,
|
||||
query: options.query,
|
||||
|
|
@ -208,36 +152,6 @@ export const buildUrl: Client["buildUrl"] = (options) => {
|
|||
: createQuerySerializer(options.querySerializer),
|
||||
url: options.url,
|
||||
})
|
||||
return url
|
||||
}
|
||||
|
||||
export const getUrl = ({
|
||||
baseUrl,
|
||||
path,
|
||||
query,
|
||||
querySerializer,
|
||||
url: _url,
|
||||
}: {
|
||||
baseUrl?: string
|
||||
path?: Record<string, unknown>
|
||||
query?: Record<string, unknown>
|
||||
querySerializer: QuerySerializer
|
||||
url: string
|
||||
}) => {
|
||||
const pathUrl = _url.startsWith("/") ? _url : `/${_url}`
|
||||
let url = (baseUrl ?? "") + pathUrl
|
||||
if (path) {
|
||||
url = defaultPathSerializer({ path, url })
|
||||
}
|
||||
let search = query ? querySerializer(query) : ""
|
||||
if (search.startsWith("?")) {
|
||||
search = search.substring(1)
|
||||
}
|
||||
if (search) {
|
||||
url += `?${search}`
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
export const mergeConfigs = (a: Config, b: Config): Config => {
|
||||
const config = { ...a, ...b }
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
export type AuthToken = string | undefined
|
||||
|
||||
export interface Auth {
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
import type { ArrayStyle, ObjectStyle, SerializerOptions } from "./pathSerializer.js"
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
import type { ArrayStyle, ObjectStyle, SerializerOptions } from "./pathSerializer.gen.js"
|
||||
|
||||
export type QuerySerializer = (query: Record<string, unknown>) => string
|
||||
|
||||
|
|
@ -13,6 +15,8 @@ export interface QuerySerializerOptions {
|
|||
const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => {
|
||||
if (typeof value === "string" || value instanceof Blob) {
|
||||
data.append(key, value)
|
||||
} else if (value instanceof Date) {
|
||||
data.append(key, value.toISOString())
|
||||
} else {
|
||||
data.append(key, JSON.stringify(value))
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
type Slot = "body" | "headers" | "path" | "query"
|
||||
|
||||
export type Field =
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
interface SerializeOptions<T> extends SerializePrimitiveOptions, SerializerOptions<T> {}
|
||||
|
||||
interface SerializePrimitiveOptions {
|
||||
210
packages/sdk/js/src/gen/core/serverSentEvents.gen.ts
Normal file
210
packages/sdk/js/src/gen/core/serverSentEvents.gen.ts
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
import type { Config } from "./types.gen.js"
|
||||
|
||||
export type ServerSentEventsOptions<TData = unknown> = Omit<RequestInit, "method"> &
|
||||
Pick<Config, "method" | "responseTransformer" | "responseValidator"> & {
|
||||
/**
|
||||
* Callback invoked when a network or parsing error occurs during streaming.
|
||||
*
|
||||
* This option applies only if the endpoint returns a stream of events.
|
||||
*
|
||||
* @param error The error that occurred.
|
||||
*/
|
||||
onSseError?: (error: unknown) => void
|
||||
/**
|
||||
* Callback invoked when an event is streamed from the server.
|
||||
*
|
||||
* This option applies only if the endpoint returns a stream of events.
|
||||
*
|
||||
* @param event Event streamed from the server.
|
||||
* @returns Nothing (void).
|
||||
*/
|
||||
onSseEvent?: (event: StreamEvent<TData>) => void
|
||||
/**
|
||||
* Default retry delay in milliseconds.
|
||||
*
|
||||
* This option applies only if the endpoint returns a stream of events.
|
||||
*
|
||||
* @default 3000
|
||||
*/
|
||||
sseDefaultRetryDelay?: number
|
||||
/**
|
||||
* Maximum number of retry attempts before giving up.
|
||||
*/
|
||||
sseMaxRetryAttempts?: number
|
||||
/**
|
||||
* Maximum retry delay in milliseconds.
|
||||
*
|
||||
* Applies only when exponential backoff is used.
|
||||
*
|
||||
* This option applies only if the endpoint returns a stream of events.
|
||||
*
|
||||
* @default 30000
|
||||
*/
|
||||
sseMaxRetryDelay?: number
|
||||
/**
|
||||
* Optional sleep function for retry backoff.
|
||||
*
|
||||
* Defaults to using `setTimeout`.
|
||||
*/
|
||||
sseSleepFn?: (ms: number) => Promise<void>
|
||||
url: string
|
||||
}
|
||||
|
||||
export interface StreamEvent<TData = unknown> {
|
||||
data: TData
|
||||
event?: string
|
||||
id?: string
|
||||
retry?: number
|
||||
}
|
||||
|
||||
export type ServerSentEventsResult<TData = unknown, TReturn = void, TNext = unknown> = {
|
||||
stream: AsyncGenerator<TData extends Record<string, unknown> ? TData[keyof TData] : TData, TReturn, TNext>
|
||||
}
|
||||
|
||||
export const createSseClient = <TData = unknown>({
|
||||
onSseError,
|
||||
onSseEvent,
|
||||
responseTransformer,
|
||||
responseValidator,
|
||||
sseDefaultRetryDelay,
|
||||
sseMaxRetryAttempts,
|
||||
sseMaxRetryDelay,
|
||||
sseSleepFn,
|
||||
url,
|
||||
...options
|
||||
}: ServerSentEventsOptions): ServerSentEventsResult<TData> => {
|
||||
let lastEventId: string | undefined
|
||||
|
||||
const sleep = sseSleepFn ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms)))
|
||||
|
||||
const createStream = async function* () {
|
||||
let retryDelay: number = sseDefaultRetryDelay ?? 3000
|
||||
let attempt = 0
|
||||
const signal = options.signal ?? new AbortController().signal
|
||||
|
||||
while (true) {
|
||||
if (signal.aborted) break
|
||||
|
||||
attempt++
|
||||
|
||||
const headers =
|
||||
options.headers instanceof Headers
|
||||
? options.headers
|
||||
: new Headers(options.headers as Record<string, string> | undefined)
|
||||
|
||||
if (lastEventId !== undefined) {
|
||||
headers.set("Last-Event-ID", lastEventId)
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url, { ...options, headers, signal })
|
||||
|
||||
if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`)
|
||||
|
||||
if (!response.body) throw new Error("No body in SSE response")
|
||||
|
||||
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader()
|
||||
|
||||
let buffer = ""
|
||||
|
||||
const abortHandler = () => {
|
||||
try {
|
||||
reader.cancel()
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
signal.addEventListener("abort", abortHandler)
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
buffer += value
|
||||
|
||||
const chunks = buffer.split("\n\n")
|
||||
buffer = chunks.pop() ?? ""
|
||||
|
||||
for (const chunk of chunks) {
|
||||
const lines = chunk.split("\n")
|
||||
const dataLines: Array<string> = []
|
||||
let eventName: string | undefined
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("data:")) {
|
||||
dataLines.push(line.replace(/^data:\s*/, ""))
|
||||
} else if (line.startsWith("event:")) {
|
||||
eventName = line.replace(/^event:\s*/, "")
|
||||
} else if (line.startsWith("id:")) {
|
||||
lastEventId = line.replace(/^id:\s*/, "")
|
||||
} else if (line.startsWith("retry:")) {
|
||||
const parsed = Number.parseInt(line.replace(/^retry:\s*/, ""), 10)
|
||||
if (!Number.isNaN(parsed)) {
|
||||
retryDelay = parsed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let data: unknown
|
||||
let parsedJson = false
|
||||
|
||||
if (dataLines.length) {
|
||||
const rawData = dataLines.join("\n")
|
||||
try {
|
||||
data = JSON.parse(rawData)
|
||||
parsedJson = true
|
||||
} catch {
|
||||
data = rawData
|
||||
}
|
||||
}
|
||||
|
||||
if (parsedJson) {
|
||||
if (responseValidator) {
|
||||
await responseValidator(data)
|
||||
}
|
||||
|
||||
if (responseTransformer) {
|
||||
data = await responseTransformer(data)
|
||||
}
|
||||
}
|
||||
|
||||
onSseEvent?.({
|
||||
data,
|
||||
event: eventName,
|
||||
id: lastEventId,
|
||||
retry: retryDelay,
|
||||
})
|
||||
|
||||
if (dataLines.length) {
|
||||
yield data as any
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
signal.removeEventListener("abort", abortHandler)
|
||||
reader.releaseLock()
|
||||
}
|
||||
|
||||
break // exit loop on normal completion
|
||||
} catch (error) {
|
||||
// connection failed or aborted; retry after delay
|
||||
onSseError?.(error)
|
||||
|
||||
if (sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts) {
|
||||
break // stop after firing error
|
||||
}
|
||||
|
||||
// exponential backoff: double retry each attempt, cap at 30s
|
||||
const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000)
|
||||
await sleep(backoff)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const stream = createStream()
|
||||
|
||||
return { stream }
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
import type { Auth, AuthToken } from "./auth.js"
|
||||
import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from "./bodySerializer.js"
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
import type { Auth, AuthToken } from "./auth.gen.js"
|
||||
import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from "./bodySerializer.gen.js"
|
||||
|
||||
export interface Client<RequestFn = never, Config = unknown, MethodFn = never, BuildUrlFn = never> {
|
||||
/**
|
||||
109
packages/sdk/js/src/gen/core/utils.gen.ts
Normal file
109
packages/sdk/js/src/gen/core/utils.gen.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
import type { QuerySerializer } from "./bodySerializer.gen.js"
|
||||
import {
|
||||
type ArraySeparatorStyle,
|
||||
serializeArrayParam,
|
||||
serializeObjectParam,
|
||||
serializePrimitiveParam,
|
||||
} from "./pathSerializer.gen.js"
|
||||
|
||||
export interface PathSerializer {
|
||||
path: Record<string, unknown>
|
||||
url: string
|
||||
}
|
||||
|
||||
export const PATH_PARAM_RE = /\{[^{}]+\}/g
|
||||
|
||||
export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
|
||||
let url = _url
|
||||
const matches = _url.match(PATH_PARAM_RE)
|
||||
if (matches) {
|
||||
for (const match of matches) {
|
||||
let explode = false
|
||||
let name = match.substring(1, match.length - 1)
|
||||
let style: ArraySeparatorStyle = "simple"
|
||||
|
||||
if (name.endsWith("*")) {
|
||||
explode = true
|
||||
name = name.substring(0, name.length - 1)
|
||||
}
|
||||
|
||||
if (name.startsWith(".")) {
|
||||
name = name.substring(1)
|
||||
style = "label"
|
||||
} else if (name.startsWith(";")) {
|
||||
name = name.substring(1)
|
||||
style = "matrix"
|
||||
}
|
||||
|
||||
const value = path[name]
|
||||
|
||||
if (value === undefined || value === null) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
url = url.replace(match, serializeArrayParam({ explode, name, style, value }))
|
||||
continue
|
||||
}
|
||||
|
||||
if (typeof value === "object") {
|
||||
url = url.replace(
|
||||
match,
|
||||
serializeObjectParam({
|
||||
explode,
|
||||
name,
|
||||
style,
|
||||
value: value as Record<string, unknown>,
|
||||
valueOnly: true,
|
||||
}),
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
if (style === "matrix") {
|
||||
url = url.replace(
|
||||
match,
|
||||
`;${serializePrimitiveParam({
|
||||
name,
|
||||
value: value as string,
|
||||
})}`,
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
const replaceValue = encodeURIComponent(style === "label" ? `.${value as string}` : (value as string))
|
||||
url = url.replace(match, replaceValue)
|
||||
}
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
export const getUrl = ({
|
||||
baseUrl,
|
||||
path,
|
||||
query,
|
||||
querySerializer,
|
||||
url: _url,
|
||||
}: {
|
||||
baseUrl?: string
|
||||
path?: Record<string, unknown>
|
||||
query?: Record<string, unknown>
|
||||
querySerializer: QuerySerializer
|
||||
url: string
|
||||
}) => {
|
||||
const pathUrl = _url.startsWith("/") ? _url : `/${_url}`
|
||||
let url = (baseUrl ?? "") + pathUrl
|
||||
if (path) {
|
||||
url = defaultPathSerializer({ path, url })
|
||||
}
|
||||
let search = query ? querySerializer(query) : ""
|
||||
if (search.startsWith("?")) {
|
||||
search = search.substring(1)
|
||||
}
|
||||
if (search) {
|
||||
url += `?${search}`
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
|
@ -59,6 +59,8 @@ import type {
|
|||
FindFilesResponses,
|
||||
FindSymbolsData,
|
||||
FindSymbolsResponses,
|
||||
FileListData,
|
||||
FileListResponses,
|
||||
FileReadData,
|
||||
FileReadResponses,
|
||||
FileStatusData,
|
||||
|
|
@ -123,7 +125,7 @@ class Event extends _HeyApiClient {
|
|||
* Get events
|
||||
*/
|
||||
public subscribe<ThrowOnError extends boolean = false>(options?: Options<EventSubscribeData, ThrowOnError>) {
|
||||
return (options?.client ?? this._client).get<EventSubscribeResponses, unknown, ThrowOnError>({
|
||||
return (options?.client ?? this._client).get.sse<EventSubscribeResponses, unknown, ThrowOnError>({
|
||||
url: "/event",
|
||||
...options,
|
||||
})
|
||||
|
|
@ -457,12 +459,22 @@ class Find extends _HeyApiClient {
|
|||
}
|
||||
|
||||
class File extends _HeyApiClient {
|
||||
/**
|
||||
* List files and directories
|
||||
*/
|
||||
public list<ThrowOnError extends boolean = false>(options: Options<FileListData, ThrowOnError>) {
|
||||
return (options.client ?? this._client).get<FileListResponses, unknown, ThrowOnError>({
|
||||
url: "/file",
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a file
|
||||
*/
|
||||
public read<ThrowOnError extends boolean = false>(options: Options<FileReadData, ThrowOnError>) {
|
||||
return (options.client ?? this._client).get<FileReadResponses, unknown, ThrowOnError>({
|
||||
url: "/file",
|
||||
url: "/file/content",
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -554,6 +554,7 @@ export type App = {
|
|||
hostname: string
|
||||
git: boolean
|
||||
path: {
|
||||
home: string
|
||||
config: string
|
||||
data: string
|
||||
root: string
|
||||
|
|
@ -587,6 +588,9 @@ export type Config = {
|
|||
*/
|
||||
scroll_speed: number
|
||||
}
|
||||
/**
|
||||
* Command configuration, see https://opencode.ai/docs/commands
|
||||
*/
|
||||
command?: {
|
||||
[key: string]: {
|
||||
template: string
|
||||
|
|
@ -1137,6 +1141,13 @@ export type Symbol = {
|
|||
}
|
||||
}
|
||||
|
||||
export type FileNode = {
|
||||
name: string
|
||||
path: string
|
||||
type: "file" | "directory"
|
||||
ignored: boolean
|
||||
}
|
||||
|
||||
export type File = {
|
||||
path: string
|
||||
added: number
|
||||
|
|
@ -1803,7 +1814,7 @@ export type FindSymbolsResponses = {
|
|||
|
||||
export type FindSymbolsResponse = FindSymbolsResponses[keyof FindSymbolsResponses]
|
||||
|
||||
export type FileReadData = {
|
||||
export type FileListData = {
|
||||
body?: never
|
||||
path?: never
|
||||
query: {
|
||||
|
|
@ -1812,6 +1823,24 @@ export type FileReadData = {
|
|||
url: "/file"
|
||||
}
|
||||
|
||||
export type FileListResponses = {
|
||||
/**
|
||||
* Files and directories
|
||||
*/
|
||||
200: Array<FileNode>
|
||||
}
|
||||
|
||||
export type FileListResponse = FileListResponses[keyof FileListResponses]
|
||||
|
||||
export type FileReadData = {
|
||||
body?: never
|
||||
path?: never
|
||||
query: {
|
||||
path: string
|
||||
}
|
||||
url: "/file/content"
|
||||
}
|
||||
|
||||
export type FileReadResponses = {
|
||||
/**
|
||||
* File content
|
||||
|
|
|
|||
|
|
@ -48,7 +48,6 @@ resources:
|
|||
app:
|
||||
models:
|
||||
app: App
|
||||
logLevel: LogLevel
|
||||
provider: Provider
|
||||
model: Model
|
||||
agent: Agent
|
||||
|
|
@ -61,7 +60,6 @@ resources:
|
|||
|
||||
find:
|
||||
models:
|
||||
match: Match
|
||||
symbol: Symbol
|
||||
methods:
|
||||
text: get /find
|
||||
|
|
@ -71,8 +69,11 @@ resources:
|
|||
file:
|
||||
models:
|
||||
file: File
|
||||
fileNode: FileNode
|
||||
|
||||
methods:
|
||||
read: get /file
|
||||
list: get /file
|
||||
read: get /file/content
|
||||
status: get /file/status
|
||||
|
||||
config:
|
||||
|
|
|
|||
|
|
@ -820,11 +820,13 @@ func (a *App) SendCommand(ctx context.Context, command string, args string) (*Ap
|
|||
opencode.SessionCommandParams{
|
||||
Command: opencode.F(command),
|
||||
Arguments: opencode.F(args),
|
||||
Agent: opencode.F(a.Agents[a.AgentIndex].Name),
|
||||
Model: opencode.F(a.State.Provider + "/" + a.State.Model),
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
slog.Error("Failed to execute command", "error", err)
|
||||
return toast.NewErrorToast("Failed to execute command")
|
||||
return toast.NewErrorToast(fmt.Sprintf("Failed to execute command: %v", err))()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
|
@ -856,7 +858,7 @@ func (a *App) SendShell(ctx context.Context, command string) (*App, tea.Cmd) {
|
|||
)
|
||||
if err != nil {
|
||||
slog.Error("Failed to submit shell command", "error", err)
|
||||
return toast.NewErrorToast("Failed to submit shell command")()
|
||||
return toast.NewErrorToast(fmt.Sprintf("Failed to submit shell command: %v", err))()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
|
|
|||
|
|
@ -92,6 +92,12 @@ func (r CommandRegistry) Sorted() []Command {
|
|||
if b.Name == AppExitCommand {
|
||||
return -1
|
||||
}
|
||||
if a.Custom && !b.Custom {
|
||||
return 1
|
||||
}
|
||||
if !a.Custom && b.Custom {
|
||||
return -1
|
||||
}
|
||||
|
||||
return strings.Compare(string(a.Name), string(b.Name))
|
||||
})
|
||||
|
|
|
|||
|
|
@ -92,7 +92,41 @@ func (c *CommandCompletionProvider) GetChildEntries(
|
|||
}
|
||||
|
||||
matches := fuzzy.RankFindFold(query, commandNames)
|
||||
sort.Sort(matches)
|
||||
|
||||
// Custom sort to prioritize exact matches
|
||||
sort.Slice(matches, func(i, j int) bool {
|
||||
// Check for exact match (case-insensitive)
|
||||
iExact := strings.EqualFold(matches[i].Target, query)
|
||||
jExact := strings.EqualFold(matches[j].Target, query)
|
||||
|
||||
// Exact matches come first
|
||||
if iExact && !jExact {
|
||||
return true
|
||||
}
|
||||
if !iExact && jExact {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check for prefix match (case-insensitive)
|
||||
iPrefix := strings.HasPrefix(strings.ToLower(matches[i].Target), strings.ToLower(query))
|
||||
jPrefix := strings.HasPrefix(strings.ToLower(matches[j].Target), strings.ToLower(query))
|
||||
|
||||
// Prefix matches come before fuzzy matches
|
||||
if iPrefix && !jPrefix {
|
||||
return true
|
||||
}
|
||||
if !iPrefix && jPrefix {
|
||||
return false
|
||||
}
|
||||
|
||||
// Otherwise, sort by fuzzy match score (lower distance is better)
|
||||
if matches[i].Distance != matches[j].Distance {
|
||||
return matches[i].Distance < matches[j].Distance
|
||||
}
|
||||
|
||||
// If distances are equal, sort by original index (stable sort)
|
||||
return matches[i].OriginalIndex < matches[j].OriginalIndex
|
||||
})
|
||||
|
||||
// Convert matches to completion items, deduplicating by command name
|
||||
items := []CompletionSuggestion{}
|
||||
|
|
|
|||
|
|
@ -489,11 +489,22 @@ func (m *editorComponent) Submit() (tea.Model, tea.Cmd) {
|
|||
|
||||
var cmds []tea.Cmd
|
||||
if strings.HasPrefix(value, "/") {
|
||||
value = value[1:]
|
||||
commandName := strings.Split(value, " ")[0]
|
||||
// Expand attachments in the value to get actual content
|
||||
expandedValue := value
|
||||
attachments := m.textarea.GetAttachments()
|
||||
for _, att := range attachments {
|
||||
if att.Type == "text" && att.Source != nil {
|
||||
if textSource, ok := att.Source.(*attachment.TextSource); ok {
|
||||
expandedValue = strings.Replace(expandedValue, att.Display, textSource.Value, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expandedValue = expandedValue[1:] // Remove the "/"
|
||||
commandName := strings.Split(expandedValue, " ")[0]
|
||||
command := m.app.Commands[commands.CommandName(commandName)]
|
||||
if command.Custom {
|
||||
args := strings.TrimPrefix(value, command.PrimaryTrigger()+" ")
|
||||
args := strings.TrimPrefix(expandedValue, command.PrimaryTrigger()+" ")
|
||||
cmds = append(
|
||||
cmds,
|
||||
util.CmdHandler(app.SendCommand{Command: string(command.Name), Args: args}),
|
||||
|
|
|
|||
|
|
@ -1173,10 +1173,10 @@ func (m *messagesComponent) UndoLastMessage() (tea.Model, tea.Cmd) {
|
|||
)
|
||||
if err != nil {
|
||||
slog.Error("Failed to undo message", "error", err)
|
||||
return toast.NewErrorToast("Failed to undo message")
|
||||
return toast.NewErrorToast("Failed to undo message")()
|
||||
}
|
||||
if response == nil {
|
||||
return toast.NewErrorToast("Failed to undo message")
|
||||
return toast.NewErrorToast("Failed to undo message")()
|
||||
}
|
||||
return app.MessageRevertedMsg{Session: *response, Message: revertedMessage}
|
||||
}
|
||||
|
|
@ -1241,10 +1241,10 @@ func (m *messagesComponent) RedoLastMessage() (tea.Model, tea.Cmd) {
|
|||
)
|
||||
if err != nil {
|
||||
slog.Error("Failed to unrevert session", "error", err)
|
||||
return toast.NewErrorToast("Failed to redo message")
|
||||
return toast.NewErrorToast("Failed to redo message")()
|
||||
}
|
||||
if response == nil {
|
||||
return toast.NewErrorToast("Failed to redo message")
|
||||
return toast.NewErrorToast("Failed to redo message")()
|
||||
}
|
||||
return app.SessionUnrevertedMsg{Session: *response}
|
||||
}
|
||||
|
|
@ -1261,10 +1261,10 @@ func (m *messagesComponent) RedoLastMessage() (tea.Model, tea.Cmd) {
|
|||
)
|
||||
if err != nil {
|
||||
slog.Error("Failed to redo message", "error", err)
|
||||
return toast.NewErrorToast("Failed to redo message")
|
||||
return toast.NewErrorToast("Failed to redo message")()
|
||||
}
|
||||
if response == nil {
|
||||
return toast.NewErrorToast("Failed to redo message")
|
||||
return toast.NewErrorToast("Failed to redo message")()
|
||||
}
|
||||
return app.MessageRevertedMsg{Session: *response, Message: revertedMessage}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
)
|
||||
if err != nil {
|
||||
slog.Error("Failed to respond to permission request", "error", err)
|
||||
return toast.NewErrorToast("Failed to respond to permission request")
|
||||
return toast.NewErrorToast("Failed to respond to permission request")()
|
||||
}
|
||||
slog.Debug("Responded to permission request", "response", resp)
|
||||
return nil
|
||||
|
|
@ -915,9 +915,10 @@ func (a Model) Cleanup() {
|
|||
func (a Model) home() (string, int, int) {
|
||||
t := theme.CurrentTheme()
|
||||
effectiveWidth := a.width - 4
|
||||
baseStyle := styles.NewStyle().Background(t.Background())
|
||||
baseStyle := styles.NewStyle().Foreground(t.Text()).Background(t.Background())
|
||||
base := baseStyle.Render
|
||||
muted := styles.NewStyle().Foreground(t.TextMuted()).Background(t.Background()).Render
|
||||
highlight := styles.NewStyle().Foreground(t.Accent()).Background(t.Background()).Render
|
||||
|
||||
open := `
|
||||
█▀▀█ █▀▀█ █▀▀ █▀▀▄
|
||||
|
|
@ -952,9 +953,9 @@ func (a Model) home() (string, int, int) {
|
|||
)
|
||||
|
||||
// Use limit of 4 for vscode, 6 for others
|
||||
limit := 6
|
||||
limit := 4
|
||||
if util.IsVSCode() {
|
||||
limit = 4
|
||||
limit = 2
|
||||
}
|
||||
|
||||
showVscode := util.IsVSCode()
|
||||
|
|
@ -971,15 +972,23 @@ func (a Model) home() (string, int, int) {
|
|||
styles.WhitespaceStyle(t.Background()),
|
||||
)
|
||||
|
||||
grok := highlight("Grok Code is free for a limited time")
|
||||
grok = lipgloss.PlaceHorizontal(
|
||||
effectiveWidth,
|
||||
lipgloss.Center,
|
||||
grok,
|
||||
styles.WhitespaceStyle(t.Background()),
|
||||
)
|
||||
|
||||
lines := []string{}
|
||||
lines = append(lines, "")
|
||||
lines = append(lines, "")
|
||||
lines = append(lines, logoAndVersion)
|
||||
lines = append(lines, "")
|
||||
lines = append(lines, "")
|
||||
lines = append(lines, cmds)
|
||||
lines = append(lines, "")
|
||||
lines = append(lines, "")
|
||||
lines = append(lines, grok)
|
||||
lines = append(lines, "")
|
||||
|
||||
mainHeight := lipgloss.Height(strings.Join(lines, "\n"))
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,10 @@ import (
|
|||
"github.com/sst/opencode/internal/styles"
|
||||
)
|
||||
|
||||
var shimmerStart = time.Now()
|
||||
var (
|
||||
shimmerStart = time.Now()
|
||||
trueColorSupport = hasTrueColor()
|
||||
)
|
||||
|
||||
// Shimmer renders text with a moving foreground highlight.
|
||||
// bg is the background color, dim is the base text color, bright is the highlight color.
|
||||
|
|
@ -32,7 +35,7 @@ func Shimmer(s string, bg compat.AdaptiveColor, _ compat.AdaptiveColor, _ compat
|
|||
elapsed := time.Since(shimmerStart).Seconds()
|
||||
pos := (math.Mod(elapsed, sweep) / sweep) * period
|
||||
|
||||
half := 4.0
|
||||
half := 2.0
|
||||
|
||||
type seg struct {
|
||||
useHex bool
|
||||
|
|
@ -41,60 +44,52 @@ func Shimmer(s string, bg compat.AdaptiveColor, _ compat.AdaptiveColor, _ compat
|
|||
faint bool
|
||||
text string
|
||||
}
|
||||
var segs []seg
|
||||
segs := make([]seg, 0, n/4)
|
||||
|
||||
useHex := hasTrueColor()
|
||||
useHex := trueColorSupport
|
||||
for i, r := range runes {
|
||||
ip := float64(i + pad)
|
||||
dist := math.Abs(ip - pos)
|
||||
t := 0.0
|
||||
if dist <= half {
|
||||
x := math.Pi * (dist / half)
|
||||
t = 0.5 * (1.0 + math.Cos(x))
|
||||
}
|
||||
// Cosine brightness: base + amp*t (quantized for grouping)
|
||||
base := 0.55
|
||||
amp := 0.45
|
||||
brightness := base
|
||||
if t > 0 {
|
||||
brightness = base + amp*t
|
||||
}
|
||||
lvl := int(math.Round(brightness * 255.0))
|
||||
if !useHex {
|
||||
step := 24 // ~11 steps across range for non-truecolor
|
||||
lvl = int(math.Round(float64(lvl)/float64(step))) * step
|
||||
}
|
||||
|
||||
bold := lvl >= 208
|
||||
faint := lvl <= 128
|
||||
|
||||
// truecolor if possible; else fallback to modifiers only
|
||||
bold := false
|
||||
faint := true
|
||||
hex := ""
|
||||
if useHex {
|
||||
if lvl < 0 {
|
||||
lvl = 0
|
||||
|
||||
if dist <= half {
|
||||
// Simple 3-level brightness based on distance
|
||||
if dist <= half/3 {
|
||||
// Center: brightest
|
||||
bold = true
|
||||
faint = false
|
||||
if useHex {
|
||||
hex = "#ffffff"
|
||||
}
|
||||
} else {
|
||||
// Edge: medium bright
|
||||
bold = false
|
||||
faint = false
|
||||
if useHex {
|
||||
hex = "#cccccc"
|
||||
}
|
||||
}
|
||||
if lvl > 255 {
|
||||
lvl = 255
|
||||
}
|
||||
hex = rgbHex(lvl, lvl, lvl)
|
||||
}
|
||||
|
||||
if len(segs) == 0 {
|
||||
if len(segs) == 0 ||
|
||||
segs[len(segs)-1].useHex != useHex ||
|
||||
segs[len(segs)-1].hex != hex ||
|
||||
segs[len(segs)-1].bold != bold ||
|
||||
segs[len(segs)-1].faint != faint {
|
||||
segs = append(segs, seg{useHex: useHex, hex: hex, bold: bold, faint: faint, text: string(r)})
|
||||
} else {
|
||||
last := &segs[len(segs)-1]
|
||||
if last.useHex == useHex && last.hex == hex && last.bold == bold && last.faint == faint {
|
||||
last.text += string(r)
|
||||
} else {
|
||||
segs = append(segs, seg{useHex: useHex, hex: hex, bold: bold, faint: faint, text: string(r)})
|
||||
}
|
||||
segs[len(segs)-1].text += string(r)
|
||||
}
|
||||
}
|
||||
|
||||
baseStyle := styles.NewStyle().Background(bg)
|
||||
var b strings.Builder
|
||||
b.Grow(len(s) * 2)
|
||||
for _, g := range segs {
|
||||
st := styles.NewStyle().Background(bg)
|
||||
st := baseStyle
|
||||
if g.useHex && g.hex != "" {
|
||||
c := compat.AdaptiveColor{Dark: lipgloss.Color(g.hex), Light: lipgloss.Color(g.hex)}
|
||||
st = st.Foreground(c)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@opencode/web",
|
||||
"type": "module",
|
||||
"version": "0.5.18",
|
||||
"version": "0.5.29",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"dev:remote": "sst shell --stage=dev --target=Web astro dev",
|
||||
|
|
@ -30,7 +30,7 @@
|
|||
"remeda": "2.26.0",
|
||||
"sharp": "0.32.5",
|
||||
"shiki": "3.4.2",
|
||||
"solid-js": "1.9.7",
|
||||
"solid-js": "catalog:",
|
||||
"toolbeam-docs-theme": "0.4.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import style from "./content-text.module.css"
|
||||
import { createSignal } from "solid-js"
|
||||
import { createOverflow } from "./common"
|
||||
import { CopyButton } from "./copy-button"
|
||||
|
||||
interface Props {
|
||||
text: string
|
||||
|
|
@ -30,6 +31,7 @@ export function ContentText(props: Props) {
|
|||
{expanded() ? "Show less" : "Show more"}
|
||||
</button>
|
||||
)}
|
||||
<CopyButton text={props.text} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@
|
|||
background: none;
|
||||
border: none;
|
||||
padding: 0.125rem;
|
||||
background-color: var(--sl-color-bg);
|
||||
color: var(--sl-color-text-secondary);
|
||||
|
||||
svg {
|
||||
|
|
|
|||
|
|
@ -126,6 +126,12 @@
|
|||
gap: 1rem;
|
||||
flex-grow: 1;
|
||||
max-width: var(--md-tool-width);
|
||||
position: relative;
|
||||
|
||||
[data-component="copy-button"] {
|
||||
top: 0.5rem;
|
||||
right: calc(0.5rem - 1px);
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="assistant-reasoning"] {
|
||||
|
|
|
|||
|
|
@ -58,12 +58,11 @@ Build is the **default** primary agent with all tools enabled. This is the stand
|
|||
|
||||
_Mode_: `primary`
|
||||
|
||||
A restricted agent designed for planning and analysis. In the plan agent, the following tools are disabled by default:
|
||||
A restricted agent designed for planning and analysis. We use a permission system to give you more control and prevent unintended changes.
|
||||
By default, all of the following are set to `ask`:
|
||||
|
||||
- `write` - Cannot create new files
|
||||
- `edit` - Cannot modify existing files
|
||||
- `patch` - Cannot apply patches
|
||||
- `bash` - Cannot execute shell commands
|
||||
- `file edits`: All writes, patches, and edits
|
||||
- `bash`: All bash commands
|
||||
|
||||
This agent is useful when you want the LLM to analyze code, suggest changes, or create plans without making any actual modifications to your codebase.
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,13 @@ title: Commands
|
|||
description: Create custom commands for repetitive tasks.
|
||||
---
|
||||
|
||||
Define custom commands to automate repetitive coding tasks.
|
||||
Custom commands let you specify a prompt you want to run when that command is executed in the TUI.
|
||||
|
||||
```bash frame="none"
|
||||
/my-command
|
||||
```
|
||||
|
||||
Custom commands are in addition to the built-in commands like `/init`, `/undo`, `/redo`, `/share`, `/help`. [Learn more](/docs/tui#commands).
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -34,12 +40,78 @@ Use the command by typing `/` followed by the command name.
|
|||
|
||||
---
|
||||
|
||||
## Use arguments
|
||||
## Configure
|
||||
|
||||
You can add custom commands through the opencode config or by creating markdown files in the `command/` directory.
|
||||
|
||||
---
|
||||
|
||||
### JSON
|
||||
|
||||
Use the `command` option in your opencode [config](/docs/config):
|
||||
|
||||
```json title="opencode.jsonc" {4-12}
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"command": {
|
||||
// This becomes the name of the command
|
||||
"test": {
|
||||
// This is the prompt that will be sent to the LLM
|
||||
"template": "Run the full test suite with coverage report and show any failures.\nFocus on the failing tests and suggest fixes.",
|
||||
// This is show as the description in the TUI
|
||||
"description": "Run tests with coverage",
|
||||
"agent": "build",
|
||||
"model": "anthropic/claude-3-5-sonnet-20241022"
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now you can run this command in the TUI:
|
||||
|
||||
```bash frame="none"
|
||||
/test
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Markdown
|
||||
|
||||
You can also define commands using markdown files. Place them in:
|
||||
|
||||
- Global: `~/.config/opencode/command/`
|
||||
- Per-project: `.opencode/command/`
|
||||
|
||||
```markdown title="~/.config/opencode/command/test.md"
|
||||
---
|
||||
description: Run tests with coverage
|
||||
agent: build
|
||||
model: anthropic/claude-3-5-sonnet-20241022
|
||||
---
|
||||
|
||||
Run the full test suite with coverage report and show any failures.
|
||||
Focus on the failing tests and suggest fixes.
|
||||
```
|
||||
|
||||
The markdown file name becomes the command name. For example, `test.md` lets
|
||||
you run:
|
||||
|
||||
```bash frame="none"
|
||||
/test
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Prompt config
|
||||
|
||||
The prompts for the custom commands support several special placeholders and syntax.
|
||||
|
||||
---
|
||||
|
||||
### Arguments
|
||||
|
||||
Pass arguments to commands using the `$ARGUMENTS` placeholder.
|
||||
|
||||
Create `.opencode/command/component.md`:
|
||||
|
||||
```md title=".opencode/command/component.md"
|
||||
---
|
||||
description: Create a new component
|
||||
|
|
@ -52,16 +124,18 @@ Include proper typing and basic structure.
|
|||
Run the command with arguments:
|
||||
|
||||
```bash frame="none"
|
||||
"/component Button"
|
||||
/component Button
|
||||
```
|
||||
|
||||
And `$ARGUMENTS` will be replaced with `Button`.
|
||||
|
||||
---
|
||||
|
||||
## Inject shell output
|
||||
### Shell output
|
||||
|
||||
Use `!command` to inject shell command output into your prompt.
|
||||
Use _!`command`_ to inject [bash command](/docs/tui#bash-commands) output into your prompt.
|
||||
|
||||
Create `.opencode/command/analyze-coverage.md`:
|
||||
For example, to create a custom command that analyzes test coverage:
|
||||
|
||||
```md title=".opencode/command/analyze-coverage.md"
|
||||
---
|
||||
|
|
@ -69,12 +143,12 @@ description: Analyze test coverage
|
|||
---
|
||||
|
||||
Here are the current test results:
|
||||
`!npm test`
|
||||
!`npm test`
|
||||
|
||||
Based on these results, suggest improvements to increase coverage.
|
||||
```
|
||||
|
||||
Create `.opencode/command/review-changes.md`:
|
||||
Or to review recent changes:
|
||||
|
||||
```md title=".opencode/command/review-changes.md"
|
||||
---
|
||||
|
|
@ -82,7 +156,7 @@ description: Review recent changes
|
|||
---
|
||||
|
||||
Recent git commits:
|
||||
`!git log --oneline -10`
|
||||
!`git log --oneline -10`
|
||||
|
||||
Review these changes and suggest any improvements.
|
||||
```
|
||||
|
|
@ -91,12 +165,10 @@ Commands run in your project's root directory and their output becomes part of t
|
|||
|
||||
---
|
||||
|
||||
## Reference files
|
||||
### File references
|
||||
|
||||
Include files in your command using `@` followed by the filename.
|
||||
|
||||
Create `.opencode/command/review-component.md`:
|
||||
|
||||
```md title=".opencode/command/review-component.md"
|
||||
---
|
||||
description: Review component
|
||||
|
|
@ -110,47 +182,90 @@ The file content gets included in the prompt automatically.
|
|||
|
||||
---
|
||||
|
||||
## Command properties
|
||||
## Options
|
||||
|
||||
Configure commands with these optional frontmatter properties:
|
||||
Let's look at the configuration options in detail.
|
||||
|
||||
- **description**: Brief explanation of what the command does
|
||||
- **agent**: Agent to use (defaults to "build")
|
||||
- **model**: Specific model to use for this command
|
||||
|
||||
Create `.opencode/command/code-review.md`:
|
||||
|
||||
```md title=".opencode/command/code-review.md"
|
||||
---
|
||||
description: Code review assistant
|
||||
agent: build
|
||||
model: anthropic/claude-3-5-sonnet-20241022
|
||||
---
|
||||
|
||||
Review the code for best practices and suggest improvements.
|
||||
### Template
|
||||
|
||||
The `template` option defines the prompt that will be sent to the LLM when the command is executed.
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"command": {
|
||||
"test": {
|
||||
"template": "Run the full test suite with coverage report and show any failures.\nFocus on the failing tests and suggest fixes."
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Command directory
|
||||
|
||||
Store command files in these locations:
|
||||
|
||||
- `.opencode/command/` - Project-specific commands
|
||||
- `command/` - Global commands in config directory
|
||||
|
||||
Project commands take precedence over global ones.
|
||||
This is a **required** config option.
|
||||
|
||||
---
|
||||
|
||||
## Built-in commands
|
||||
### Description
|
||||
|
||||
opencode includes several built-in commands:
|
||||
Use the `description` option to provide a brief description of what the command does.
|
||||
|
||||
- `/init` - Initialize project and create AGENTS.md
|
||||
- `/undo` - Revert the last changes
|
||||
- `/redo` - Restore reverted changes
|
||||
- `/share` - Share the current conversation
|
||||
- `/help` - Show available commands and keybinds
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"command": {
|
||||
"test": {
|
||||
"description": "Run tests with coverage"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Use `/help` to see all available commands in your setup.
|
||||
This is shown as the description in the TUI when you type in the command.
|
||||
|
||||
---
|
||||
|
||||
### Agent
|
||||
|
||||
Use the `agent` config to optionally specify which [agent](/docs/agents) should execute this command.
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"command": {
|
||||
"review": {
|
||||
"agent": "plan"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is an **optional** config option. If not specified, defaults to "build".
|
||||
|
||||
---
|
||||
|
||||
### Model
|
||||
|
||||
Use the `model` config to override the default model for this command.
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"command": {
|
||||
"analyze": {
|
||||
"model": "anthropic/claude-3-5-sonnet-20241022"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is an **optional** config option.
|
||||
|
||||
---
|
||||
|
||||
## Built-in
|
||||
|
||||
opencode includes several built-in commands like `/init`, `/undo`, `/redo`, `/share`, `/help`; [learn more](/docs/tui#commands).
|
||||
|
||||
:::note
|
||||
Custom commands can override built-in commands.
|
||||
:::
|
||||
|
||||
If you define a custom command with the same name, it will override the built-in command.
|
||||
|
|
|
|||
|
|
@ -152,6 +152,32 @@ By default, sharing is set to manual mode where you need to explicitly share con
|
|||
|
||||
---
|
||||
|
||||
### Commands
|
||||
|
||||
You can configure custom commands for repetitive tasks through the `command` option.
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"command": {
|
||||
"test": {
|
||||
"template": "Run the full test suite with coverage report and show any failures.\nFocus on the failing tests and suggest fixes.",
|
||||
"description": "Run tests with coverage",
|
||||
"agent": "build",
|
||||
"model": "anthropic/claude-3-5-sonnet-20241022"
|
||||
},
|
||||
"component": {
|
||||
"template": "Create a new React component named $ARGUMENTS with TypeScript support.\nInclude proper typing and basic structure.",
|
||||
"description": "Create a new component"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can also define commands using markdown files in `~/.config/opencode/command/` or `.opencode/command/`. [Learn more here](/docs/commands).
|
||||
|
||||
---
|
||||
|
||||
### Keybinds
|
||||
|
||||
You can customize your keybinds through the `keybinds` option.
|
||||
|
|
@ -220,6 +246,11 @@ You can configure permissions to control what AI agents can do in your codebase
|
|||
}
|
||||
```
|
||||
|
||||
This allows you to configure explicit approval requirements for sensitive operations:
|
||||
|
||||
- `edit` - Controls whether file editing operations require user approval (`"ask"` or `"allow"`)
|
||||
- `bash` - Controls whether bash commands require user approval (can be `"ask"`/`"allow"` or a pattern map)
|
||||
|
||||
[Learn more about permissions here](/docs/permissions).
|
||||
|
||||
---
|
||||
|
|
@ -259,13 +290,6 @@ about rules here](/docs/rules).
|
|||
|
||||
You can disable providers that are loaded automatically through the `disabled_providers` option. This is useful when you want to prevent certain providers from being loaded even if their credentials are available.
|
||||
|
||||
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
|
||||
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
|
|
@ -273,12 +297,11 @@ The `disabled_providers` option accepts an array of provider IDs. When a provide
|
|||
}
|
||||
```
|
||||
|
||||
The permissions system allows you to configure explicit approval requirements for sensitive operations:
|
||||
The `disabled_providers` option accepts an array of provider IDs. When a provider is disabled:
|
||||
|
||||
- `edit` - Controls whether file editing operations require user approval (`"ask"` or `"allow"`)
|
||||
- `bash` - Controls whether bash commands require user approval (can be `"ask"`/`"allow"` or a pattern map)
|
||||
|
||||
[Learn more about permissions here](/docs/permissions).
|
||||
- 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.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ you to use opencode at your organization.
|
|||
To get started, we recommend:
|
||||
|
||||
1. Do a trial internally with your team.
|
||||
2. [**Contact us**](mailto:hello@sst.dev) to discuss pricing and implementation options.
|
||||
2. [**Contact us**](mailto:hello@anoma.ly) to discuss pricing and implementation options.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -55,7 +55,7 @@ We recommend you disable this for your trial.
|
|||
## Deployment
|
||||
|
||||
Once you have completed your trial and you are ready to self-host opencode at
|
||||
your organization, you can [**contact us**](mailto:hello@sst.dev) to discuss
|
||||
your organization, you can [**contact us**](mailto:hello@anoma.ly) to discuss
|
||||
pricing and implementation options.
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -11,19 +11,26 @@ opencode integrates with your Language Server Protocol (LSP) to help the LLM int
|
|||
|
||||
opencode comes with several built-in LSP servers for popular languages:
|
||||
|
||||
| LSP Server | Extensions | Requirements |
|
||||
| ---------- | -------------------------------------------- | ----------------------------------- |
|
||||
| typescript | .ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, .cts | `typescript` dependency in project |
|
||||
| eslint | .ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, .cts | `eslint` dependency in project |
|
||||
| gopls | .go | `go` command available |
|
||||
| ruby-lsp | .rb, .rake, .gemspec, .ru | `ruby` and `gem` commands available |
|
||||
| pyright | .py, .pyi | `pyright` dependency installed |
|
||||
| elixir-ls | .ex, .exs | `elixir` command available |
|
||||
| zls | .zig, .zon | `zig` command available |
|
||||
| csharp | .cs | `.NET SDK` installed |
|
||||
| LSP Server | Extensions | Requirements |
|
||||
| ---------- | ---------------------------------------------------- | ----------------------------------- |
|
||||
| typescript | .ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, .cts | `typescript` dependency in project |
|
||||
| eslint | .ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, .cts, .vue | `eslint` dependency in project |
|
||||
| gopls | .go | `go` command available |
|
||||
| ruby-lsp | .rb, .rake, .gemspec, .ru | `ruby` and `gem` commands available |
|
||||
| pyright | .py, .pyi | `pyright` dependency installed |
|
||||
| elixir-ls | .ex, .exs | `elixir` command available |
|
||||
| zls | .zig, .zon | `zig` command available |
|
||||
| csharp | .cs | `.NET SDK` installed |
|
||||
| vue | .vue | Auto-installs for Vue projects |
|
||||
| rust | .rs | `rust-analyzer` command available |
|
||||
| clangd | .c, .cpp, .cc, .cxx, .c++, .h, .hpp, .hh, .hxx, .h++ | Auto-installs for C/C++ projects |
|
||||
|
||||
LSP servers are automatically enabled when one of the above file extensions are detected and the requirements are met.
|
||||
|
||||
:::note
|
||||
You can disable automatic LSP server downloads by setting the `OPENCODE_DISABLE_LSP_DOWNLOAD` environment variable to `true`.
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## How It Works
|
||||
|
|
|
|||
|
|
@ -91,3 +91,38 @@ Local and remote servers can be used together within the same `mcp` config objec
|
|||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Per agent
|
||||
|
||||
If you have a large number of MCP servers you may want to only enable them per
|
||||
agent and disable them globally. To do this:
|
||||
|
||||
1. Configure the MCP server.
|
||||
2. Disable it as a tool globally.
|
||||
3. In your [agent config](/docs/agents#tools) enable the MCP server as a tool.
|
||||
|
||||
```json title="opencode.json" {11, 14-17}
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"mcp": {
|
||||
"my-mcp": {
|
||||
"type": "local",
|
||||
"command": ["bun", "x", "my-mcp-command"],
|
||||
"enabled": true
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"my-mcp*": false
|
||||
},
|
||||
"agent": {
|
||||
"my-agent": {
|
||||
"tools": {
|
||||
"my-mcp*": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ If you've configured a [custom provider](/docs/providers#custom), the `provider_
|
|||
|
||||
You can globally configure a model's options through the config.
|
||||
|
||||
```jsonc title="opencode.jsonc" {7-11}
|
||||
```jsonc title="opencode.jsonc" {7-12,19-24}
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"provider": {
|
||||
|
|
@ -79,16 +79,28 @@ You can globally configure a model's options through the config.
|
|||
"reasoningEffort": "high",
|
||||
"textVerbosity": "low",
|
||||
"reasoningSummary": "auto",
|
||||
"include": ["reasoning.encrypted_content"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"include": ["reasoning.encrypted_content"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"anthropic": {
|
||||
"models": {
|
||||
"claude-sonnet-4-20250514": {
|
||||
"options": {
|
||||
"thinking": {
|
||||
"type": "enabled",
|
||||
"budgetTokens": 16000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Here we are setting global options for the `gpt-5` model when used through the `openai` provider.
|
||||
Here we're configuring global settings for two models: `gpt-5` when accessed via the `openai` provider, and `claude-sonnet-4-20250514` when accessed via the `anthropic` provider.
|
||||
|
||||
You can also configure these options for any agents that you are using. The agent config overrides any global options here. [Learn more](/docs/agents/#additional).
|
||||
|
||||
|
|
|
|||
|
|
@ -759,6 +759,23 @@ In this example:
|
|||
|
||||
---
|
||||
|
||||
### xAI
|
||||
|
||||
For a limited time, you can use xAI's Grok Code for free with opencode.
|
||||
|
||||
:::tip
|
||||
Grok Code is available for free for a limited time on opencode.
|
||||
:::
|
||||
|
||||
1. Make sure you are on the latest version of opencode.
|
||||
|
||||
2. Run the `/models` command and select **Grok Code Free**.
|
||||
|
||||
As a part of the trial period, the xAI team will be using the request logs to
|
||||
monitor and improve Grok Code.
|
||||
|
||||
---
|
||||
|
||||
### Z.AI
|
||||
|
||||
1. Head over to the [Z.AI API console](https://z.ai/manage-apikey/apikey-list), create an account, and click **Create a new API key**.
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ const client = createOpencodeClient({
|
|||
|
||||
You can also programmatically start an opencode server:
|
||||
|
||||
````javascript
|
||||
```javascript
|
||||
import { createOpencodeServer } from "@opencode-ai/sdk"
|
||||
|
||||
const server = await createOpencodeServer({
|
||||
|
|
@ -61,7 +61,7 @@ const server = await createOpencodeServer({
|
|||
console.log(`Server running at ${server.url}`)
|
||||
|
||||
server.close()
|
||||
}
|
||||
```
|
||||
|
||||
#### Options
|
||||
|
||||
|
|
@ -307,8 +307,8 @@ await client.auth.set({
|
|||
|
||||
```javascript
|
||||
// Listen to real-time events
|
||||
const eventStream = await client.event.subscribe()
|
||||
for await (const event of eventStream) {
|
||||
const events = await client.event.subscribe()
|
||||
for await (const event of events.stream) {
|
||||
console.log("Event:", event.type, event.properties)
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -25,6 +25,12 @@ Once you're in the TUI, you can prompt it with a message.
|
|||
Give me a quick summary of the codebase.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File references
|
||||
|
||||
You can reference files in your messages using `@`. This does a fuzzy file search in the current working directory.
|
||||
|
||||
:::tip
|
||||
You can also use `@` to reference files in your messages.
|
||||
:::
|
||||
|
|
@ -33,6 +39,20 @@ You can also use `@` to reference files in your messages.
|
|||
How is auth handled in @packages/functions/src/api/index.ts?
|
||||
```
|
||||
|
||||
The content of the file is added to the conversation automatically.
|
||||
|
||||
---
|
||||
|
||||
## Bash commands
|
||||
|
||||
Start a message with `!` to run a shell command.
|
||||
|
||||
```bash frame="none"
|
||||
!ls -la
|
||||
```
|
||||
|
||||
The output of the command is added to the conversation as a tool result.
|
||||
|
||||
---
|
||||
|
||||
## Commands
|
||||
|
|
@ -235,18 +255,6 @@ Unshare current session. [Learn more](/docs/share#un-sharing).
|
|||
|
||||
---
|
||||
|
||||
## Bash commands
|
||||
|
||||
Start a message with `!` to run a shell command.
|
||||
|
||||
```bash frame="none"
|
||||
!ls -la
|
||||
```
|
||||
|
||||
The output of the command is added to the conversation as a tool result.
|
||||
|
||||
---
|
||||
|
||||
## Editor setup
|
||||
|
||||
Both the `/editor` and `/export` commands use the editor specified in your `EDITOR` environment variable.
|
||||
|
|
@ -254,37 +262,54 @@ Both the `/editor` and `/export` commands use the editor specified in your `EDIT
|
|||
<Tabs>
|
||||
<TabItem label="Linux/macOS">
|
||||
```bash
|
||||
export EDITOR=nano # or vim, code, etc.
|
||||
# Example for nano or vim
|
||||
export EDITOR=nano
|
||||
export EDITOR=vim
|
||||
|
||||
# For GUI editors (VS Code, Cursor, VSCodium, Windsurf, Zed, etc.) include --wait
|
||||
export EDITOR="code --wait"
|
||||
```
|
||||
|
||||
To make it permanent, add this to your shell profile;
|
||||
`~/.bashrc`, `~/.zshrc`, etc.
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem label="Windows (CMD)">
|
||||
```bash
|
||||
set EDITOR=notepad # or code, vim, etc.
|
||||
set EDITOR=notepad
|
||||
|
||||
# For GUI editors (VS Code, Cursor, VSCodium, Windsurf, Zed, etc.) include --wait
|
||||
set EDITOR=code --wait
|
||||
```
|
||||
|
||||
To make it permanent, use **System Properties** > **Environment
|
||||
Variables**.
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem label="Windows (PowerShell)">
|
||||
```bash
|
||||
$env:EDITOR = "notepad" # or "code", "vim", etc.
|
||||
```powershell
|
||||
$env:EDITOR = "notepad"
|
||||
|
||||
# For GUI editors (VS Code, Cursor, VSCodium, Windsurf, Zed, etc.) include --wait
|
||||
$env:EDITOR = "code --wait"
|
||||
```
|
||||
|
||||
To make it permanent, add this to your PowerShell
|
||||
profile.
|
||||
|
||||
To make it permanent, add this to your PowerShell profile.
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Popular editor options include:
|
||||
|
||||
- `code` - Visual Studio Code
|
||||
- `cursor` - Cursor
|
||||
- `windsurf` - Windsurf
|
||||
- `vim` - Vim editor
|
||||
- `nano` - Nano editor
|
||||
- `notepad` - Windows Notepad
|
||||
- `subl` - Sublime Text
|
||||
|
||||
:::note
|
||||
Some editors like VS Code need to be started with the `--wait` flag.
|
||||
:::
|
||||
|
||||
Some editors need command-line arguments to run in blocking mode. The `--wait` flag makes the editor process block until closed.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue