mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-10 03:19:59 -04:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 434c0ff0d7 | |||
| 7a7060ef15 | |||
| f9af9fc221 | |||
| 1bf1b93404 | |||
| bc6f4aed2b | |||
| 2af3f19397 | |||
| 9275665868 | |||
| 09bb819064 | |||
| 6f0028644e | |||
| aec44abcf6 | |||
| b41e573886 | |||
| 737ddab300 | |||
| 90fc3ddb02 | |||
| 15d7eebb92 | |||
| 33301c94df | |||
| d341d26e37 | |||
| d49b1b25d1 | |||
| 25eb100210 | |||
| 9886353715 | |||
| f501501791 | |||
| c103052f93 | |||
| 68039d4c71 | |||
| d3566d3b1a | |||
| b275e18d28 | |||
| af9a1797b5 |
@@ -11,3 +11,5 @@ playground
|
||||
tmp
|
||||
dist
|
||||
.turbo
|
||||
**/.serena
|
||||
.serena/
|
||||
|
||||
@@ -129,3 +129,4 @@
|
||||
| 2025-11-01 | 636,100 (+9,488) | 581,806 (+17,227) | 1,217,906 (+26,715) |
|
||||
| 2025-11-02 | 644,067 (+7,967) | 590,004 (+8,198) | 1,234,071 (+16,165) |
|
||||
| 2025-11-03 | 653,130 (+9,063) | 597,139 (+7,135) | 1,250,269 (+16,198) |
|
||||
| 2025-11-04 | 663,912 (+10,782) | 608,056 (+10,917) | 1,271,968 (+21,699) |
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.0.19",
|
||||
"version": "1.0.22",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -66,7 +66,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.0.19",
|
||||
"version": "1.0.22",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "2.0.0",
|
||||
"@ai-sdk/openai": "2.0.2",
|
||||
@@ -90,7 +90,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.0.19",
|
||||
"version": "1.0.22",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -111,7 +111,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.0.19",
|
||||
"version": "1.0.22",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -150,7 +150,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.0.19",
|
||||
"version": "1.0.22",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "22.0.0",
|
||||
@@ -166,7 +166,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.0.19",
|
||||
"version": "1.0.22",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -243,7 +243,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.0.19",
|
||||
"version": "1.0.22",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"zod": "catalog:",
|
||||
@@ -263,7 +263,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.0.19",
|
||||
"version": "1.0.22",
|
||||
"devDependencies": {
|
||||
"@hey-api/openapi-ts": "0.81.0",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
@@ -274,7 +274,7 @@
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.0.19",
|
||||
"version": "1.0.22",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
@@ -287,7 +287,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.0.19",
|
||||
"version": "1.0.22",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -317,7 +317,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.0.19",
|
||||
"version": "1.0.22",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev",
|
||||
"build": "vinxi build && ../../opencode/script/schema.ts ./.output/public/config.json",
|
||||
"start": "vinxi start",
|
||||
"version": "1.0.19"
|
||||
"version": "1.0.22"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ibm/plex": "6.4.1",
|
||||
|
||||
@@ -1081,7 +1081,8 @@ export default function Home() {
|
||||
</svg>
|
||||
</div>
|
||||
<span>
|
||||
<figure>Fig 2.</figure> <strong>230</strong> Contributors
|
||||
<figure>Fig 2.</figure> <strong>{config.stats.contributors}</strong>{" "}
|
||||
Contributors
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -1129,7 +1130,8 @@ export default function Home() {
|
||||
</svg>
|
||||
</div>
|
||||
<span>
|
||||
<figure>Fig 3.</figure> <strong>250K</strong> Monthly Devs
|
||||
<figure>Fig 3.</figure> <strong>{config.stats.monthlyUsers}</strong> Monthly
|
||||
Devs
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -79,17 +79,19 @@ export default function Home() {
|
||||
<strong>LSP enabled</strong> Automatically loads the right LSPs for the LLM
|
||||
</li>
|
||||
<li>
|
||||
<strong>opencode zen</strong> A <a href="/docs/zen">curated list of models</a> provided by opencode{" "}
|
||||
<label>New</label>
|
||||
<strong>opencode zen</strong> A <a href="/docs/zen">curated list of models</a>{" "}
|
||||
provided by opencode <label>New</label>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Multi-session</strong> Start multiple agents in parallel on the same project
|
||||
</li>
|
||||
<li>
|
||||
<strong>Shareable links</strong> Share a link to any sessions for reference or to debug
|
||||
<strong>Shareable links</strong> Share a link to any sessions for reference or to
|
||||
debug
|
||||
</li>
|
||||
<li>
|
||||
<strong>Claude Pro</strong> Log in with Anthropic to use your Claude Pro or Max account
|
||||
<strong>Claude Pro</strong> Log in with Anthropic to use your Claude Pro or Max
|
||||
account
|
||||
</li>
|
||||
<li>
|
||||
<strong>Use any model</strong> Supports 75+ LLM providers through{" "}
|
||||
@@ -121,7 +123,7 @@ export default function Home() {
|
||||
<h3 data-component="title">homebrew</h3>
|
||||
<button data-copy data-slot="button">
|
||||
<span>
|
||||
brew install <strong>sst/tap/opencode</strong>
|
||||
brew install <strong>opencode</strong>
|
||||
</span>
|
||||
<CopyStatus />
|
||||
</button>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.0.19",
|
||||
"version": "1.0.22",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.0.19",
|
||||
"version": "1.0.22",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.0.19",
|
||||
"version": "1.0.22",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.0.19",
|
||||
"version": "1.0.22",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -12,7 +12,9 @@ import Home from "@/pages"
|
||||
const host = import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "127.0.0.1"
|
||||
const port = import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"
|
||||
|
||||
const url = new URLSearchParams(document.location.search).get("url") || `http://${host}:${port}`
|
||||
const url =
|
||||
new URLSearchParams(document.location.search).get("url") ||
|
||||
(location.hostname.includes("opencode.ai") ? `http://${host}:${port}` : "/")
|
||||
|
||||
const root = document.getElementById("root")
|
||||
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.0.19",
|
||||
"version": "1.0.22",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.0.19",
|
||||
"version": "1.0.22",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
|
||||
@@ -41,7 +41,9 @@ for (const [os, arch] of targets) {
|
||||
|
||||
const opentui = `@opentui/core-${os === "windows" ? "win32" : os}-${arch.replace("-baseline", "")}`
|
||||
await $`mkdir -p ../../node_modules/${opentui}`
|
||||
await $`npm pack ${opentui}@${pkg.dependencies["@opentui/core"]}`.cwd(path.join(dir, "../../node_modules"))
|
||||
await $`npm pack ${opentui}@${pkg.dependencies["@opentui/core"]}`.cwd(
|
||||
path.join(dir, "../../node_modules"),
|
||||
)
|
||||
await $`tar -xf ../../node_modules/${opentui.replace("@opentui/", "opentui-")}-*.tgz -C ../../node_modules/${opentui} --strip-components=1`
|
||||
|
||||
const watcher = `@parcel/watcher-${os === "windows" ? "win32" : os}-${arch.replace("-baseline", "")}${os === "linux" ? "-glibc" : ""}`
|
||||
@@ -49,7 +51,11 @@ for (const [os, arch] of targets) {
|
||||
await $`npm pack ${watcher}`.cwd(path.join(dir, "../../node_modules")).quiet()
|
||||
await $`tar -xf ../../node_modules/${watcher.replace("@parcel/", "parcel-")}-*.tgz -C ../../node_modules/${watcher} --strip-components=1`
|
||||
|
||||
const parserWorker = fs.realpathSync(path.resolve(dir, "./node_modules/@opentui/core/parser.worker.js"))
|
||||
const parserWorker = fs.realpathSync(
|
||||
path.resolve(dir, "./node_modules/@opentui/core/parser.worker.js"),
|
||||
)
|
||||
const workerPath = "./src/cli/cmd/tui/worker.ts"
|
||||
|
||||
await Bun.build({
|
||||
conditions: ["browser"],
|
||||
tsconfig: "./tsconfig.json",
|
||||
@@ -61,10 +67,11 @@ for (const [os, arch] of targets) {
|
||||
execArgv: [`--user-agent=opencode/${Script.version}`, `--env-file=""`, `--`],
|
||||
windows: {},
|
||||
},
|
||||
entrypoints: ["./src/index.ts", parserWorker, "./src/cli/cmd/tui/worker.ts"],
|
||||
entrypoints: ["./src/index.ts", parserWorker, workerPath],
|
||||
define: {
|
||||
OPENCODE_VERSION: `'${Script.version}'`,
|
||||
OTUI_TREE_SITTER_WORKER_PATH: "/$bunfs/root/" + path.relative(dir, parserWorker),
|
||||
OPENCODE_WORKER_PATH: workerPath,
|
||||
OPENCODE_CHANNEL: `'${Script.channel}'`,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,222 +0,0 @@
|
||||
import { Global } from "../../global"
|
||||
import { Provider } from "../../provider/provider"
|
||||
import { Server } from "../../server/server"
|
||||
import { UI } from "../ui"
|
||||
import { cmd } from "./cmd"
|
||||
import path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { Installation } from "../../installation"
|
||||
import { Config } from "../../config/config"
|
||||
import { Bus } from "../../bus"
|
||||
import { Log } from "../../util/log"
|
||||
import { Ide } from "../../ide"
|
||||
|
||||
import { Flag } from "../../flag/flag"
|
||||
import { Session } from "../../session"
|
||||
import { $ } from "bun"
|
||||
import { bootstrap } from "../bootstrap"
|
||||
|
||||
declare global {
|
||||
const OPENCODE_TUI_PATH: string
|
||||
}
|
||||
|
||||
if (typeof OPENCODE_TUI_PATH !== "undefined") {
|
||||
await import(OPENCODE_TUI_PATH as string, {
|
||||
with: { type: "file" },
|
||||
})
|
||||
}
|
||||
|
||||
export const TuiCommand = cmd({
|
||||
command: "$0 [project]",
|
||||
describe: "start opencode tui",
|
||||
builder: (yargs) =>
|
||||
yargs
|
||||
.positional("project", {
|
||||
type: "string",
|
||||
describe: "path to start opencode in",
|
||||
})
|
||||
.option("model", {
|
||||
type: "string",
|
||||
alias: ["m"],
|
||||
describe: "model to use in the format of provider/model",
|
||||
})
|
||||
.option("continue", {
|
||||
alias: ["c"],
|
||||
describe: "continue the last session",
|
||||
type: "boolean",
|
||||
})
|
||||
.option("session", {
|
||||
alias: ["s"],
|
||||
describe: "session id to continue",
|
||||
type: "string",
|
||||
})
|
||||
.option("prompt", {
|
||||
alias: ["p"],
|
||||
type: "string",
|
||||
describe: "prompt to use",
|
||||
})
|
||||
.option("agent", {
|
||||
type: "string",
|
||||
describe: "agent to use",
|
||||
})
|
||||
.option("port", {
|
||||
type: "number",
|
||||
describe: "port to listen on",
|
||||
default: 0,
|
||||
})
|
||||
.option("hostname", {
|
||||
type: "string",
|
||||
describe: "hostname to listen on",
|
||||
default: "127.0.0.1",
|
||||
}),
|
||||
handler: async (args) => {
|
||||
while (true) {
|
||||
const cwd = args.project ? path.resolve(args.project) : process.cwd()
|
||||
try {
|
||||
process.chdir(cwd)
|
||||
} catch (e) {
|
||||
UI.error("Failed to change directory to " + cwd)
|
||||
return
|
||||
}
|
||||
const result = await bootstrap(cwd, async () => {
|
||||
const sessionID = await (async () => {
|
||||
if (args.continue) {
|
||||
const it = Session.list()
|
||||
try {
|
||||
for await (const s of it) {
|
||||
if (s.parentID === undefined) {
|
||||
return s.id
|
||||
}
|
||||
}
|
||||
return
|
||||
} finally {
|
||||
await it.return()
|
||||
}
|
||||
}
|
||||
if (args.session) {
|
||||
return args.session
|
||||
}
|
||||
return undefined
|
||||
})()
|
||||
const providers = await Provider.list()
|
||||
if (Object.keys(providers).length === 0) {
|
||||
return "needs_provider"
|
||||
}
|
||||
|
||||
const server = Server.listen({
|
||||
port: args.port,
|
||||
hostname: args.hostname,
|
||||
})
|
||||
|
||||
let cmd = [] as string[]
|
||||
const tui = Bun.embeddedFiles.find((item) => (item as File).name.includes("tui")) as File
|
||||
if (tui) {
|
||||
let binaryName = tui.name
|
||||
if (process.platform === "win32" && !binaryName.endsWith(".exe")) {
|
||||
binaryName += ".exe"
|
||||
}
|
||||
const binary = path.join(Global.Path.cache, "tui", binaryName)
|
||||
const file = Bun.file(binary)
|
||||
if (!(await file.exists())) {
|
||||
await Bun.write(file, tui, { mode: 0o755 })
|
||||
if (process.platform !== "win32") await fs.chmod(binary, 0o755)
|
||||
}
|
||||
cmd = [binary]
|
||||
}
|
||||
if (!tui) {
|
||||
const dir = Bun.fileURLToPath(new URL("../../../../tui/cmd/opencode", import.meta.url))
|
||||
let binaryName = `./dist/tui${process.platform === "win32" ? ".exe" : ""}`
|
||||
await $`go build -o ${binaryName} ./main.go`.cwd(dir)
|
||||
cmd = [path.join(dir, binaryName)]
|
||||
}
|
||||
Log.Default.info("tui", {
|
||||
cmd,
|
||||
})
|
||||
const proc = Bun.spawn({
|
||||
cmd: [
|
||||
...cmd,
|
||||
...(args.model ? ["--model", args.model] : []),
|
||||
...(args.prompt ? ["--prompt", args.prompt] : []),
|
||||
...(args.agent ? ["--agent", args.agent] : []),
|
||||
...(sessionID ? ["--session", sessionID] : []),
|
||||
],
|
||||
cwd,
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
stdin: "inherit",
|
||||
env: {
|
||||
...process.env,
|
||||
CGO_ENABLED: "0",
|
||||
OPENCODE_SERVER: server.url.toString(),
|
||||
},
|
||||
onExit: () => {
|
||||
server.stop()
|
||||
},
|
||||
})
|
||||
|
||||
;(async () => {
|
||||
// if (Installation.isLocal()) return
|
||||
const config = await Config.get()
|
||||
if (config.autoupdate === false || Flag.OPENCODE_DISABLE_AUTOUPDATE) return
|
||||
const latest = await Installation.latest().catch(() => {})
|
||||
if (!latest) return
|
||||
if (Installation.VERSION === latest) return
|
||||
const method = await Installation.method()
|
||||
if (method === "unknown") return
|
||||
await Installation.upgrade(method, latest)
|
||||
.then(() => Bus.publish(Installation.Event.Updated, { version: latest }))
|
||||
.catch(() => {})
|
||||
})()
|
||||
;(async () => {
|
||||
if (Ide.alreadyInstalled()) return
|
||||
const ide = Ide.ide()
|
||||
if (ide === "unknown") return
|
||||
await Ide.install(ide)
|
||||
.then(() => Bus.publish(Ide.Event.Installed, { ide }))
|
||||
.catch(() => {})
|
||||
})()
|
||||
|
||||
await proc.exited
|
||||
server.stop()
|
||||
|
||||
return "done"
|
||||
})
|
||||
if (result === "done") break
|
||||
if (result === "needs_provider") {
|
||||
UI.empty()
|
||||
UI.println(UI.logo(" "))
|
||||
const result = await Bun.spawn({
|
||||
cmd: [...getOpencodeCommand(), "auth", "login"],
|
||||
cwd: process.cwd(),
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
stdin: "inherit",
|
||||
}).exited
|
||||
if (result !== 0) return
|
||||
UI.empty()
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Get the correct command to run opencode CLI
|
||||
* In development: ["bun", "run", "packages/opencode/src/index.ts"]
|
||||
* In production: ["/path/to/opencode"]
|
||||
*/
|
||||
function getOpencodeCommand(): string[] {
|
||||
// Check if OPENCODE_BIN_PATH is set (used by shell wrapper scripts)
|
||||
if (process.env["OPENCODE_BIN_PATH"]) {
|
||||
return [process.env["OPENCODE_BIN_PATH"]]
|
||||
}
|
||||
|
||||
const execPath = process.execPath.toLowerCase()
|
||||
|
||||
if (Installation.isLocal()) {
|
||||
// In development, use bun to run the TypeScript entry point
|
||||
return [execPath, "run", process.argv[1]]
|
||||
}
|
||||
|
||||
// In production, use the current executable path
|
||||
return [process.execPath]
|
||||
}
|
||||
@@ -30,6 +30,9 @@ import { TuiEvent } from "./event"
|
||||
import { KVProvider, useKV } from "./context/kv"
|
||||
|
||||
async function getTerminalBackgroundColor(): Promise<"dark" | "light"> {
|
||||
// can't set raw mode if not a TTY
|
||||
if (!process.stdin.isTTY) return "dark"
|
||||
|
||||
return new Promise((resolve) => {
|
||||
let timeout: NodeJS.Timeout
|
||||
|
||||
@@ -355,6 +358,7 @@ function App() {
|
||||
|
||||
event.on(SessionApi.Event.Deleted.type, (evt) => {
|
||||
if (route.data.type === "session" && route.data.sessionID === evt.properties.info.id) {
|
||||
dialog.clear()
|
||||
route.navigate({ type: "home" })
|
||||
toast.show({
|
||||
variant: "info",
|
||||
|
||||
@@ -72,6 +72,7 @@ export function DialogSessionList() {
|
||||
},
|
||||
})
|
||||
setToDelete(undefined)
|
||||
// dialog.clear()
|
||||
return
|
||||
}
|
||||
setToDelete(option.value)
|
||||
|
||||
@@ -52,14 +52,14 @@ export function Autocomplete(props: {
|
||||
// Track props.value to make memo reactive to text changes
|
||||
props.value // <- there surely is a better way to do this, like making .input() reactive
|
||||
|
||||
const val = props.input().getTextRange(store.index + 1, props.input().visualCursor.offset + 1)
|
||||
const val = props.input().getTextRange(store.index + 1, props.input().cursorOffset + 1)
|
||||
|
||||
return val
|
||||
})
|
||||
|
||||
function insertPart(text: string, part: PromptInfo["parts"][number]) {
|
||||
const input = props.input()
|
||||
const currentCursorOffset = input.visualCursor.offset
|
||||
const currentCursorOffset = input.cursorOffset
|
||||
|
||||
const charAfterCursor = props.value.at(currentCursorOffset)
|
||||
const needsSpace = charAfterCursor !== " "
|
||||
@@ -204,7 +204,10 @@ export function Autocomplete(props: {
|
||||
{
|
||||
display: "/undo",
|
||||
description: "undo the last message",
|
||||
onSelect: () => command.trigger("session.undo"),
|
||||
onSelect: () => {
|
||||
hide()
|
||||
command.trigger("session.undo")
|
||||
},
|
||||
},
|
||||
{
|
||||
display: "/redo",
|
||||
@@ -266,6 +269,7 @@ export function Autocomplete(props: {
|
||||
},
|
||||
{
|
||||
display: "/status",
|
||||
aliases: ["/mcp"],
|
||||
description: "show status",
|
||||
onSelect: () => command.trigger("opencode.status"),
|
||||
},
|
||||
@@ -344,7 +348,7 @@ export function Autocomplete(props: {
|
||||
command.keybinds(false)
|
||||
setStore({
|
||||
visible: mode,
|
||||
index: props.input().visualCursor.offset,
|
||||
index: props.input().cursorOffset,
|
||||
position: {
|
||||
x: props.anchor().x,
|
||||
y: props.anchor().y,
|
||||
@@ -368,8 +372,8 @@ export function Autocomplete(props: {
|
||||
get visible() {
|
||||
return store.visible
|
||||
},
|
||||
onInput(value: string) {
|
||||
if (store.visible && Bun.stringWidth(value) <= store.index) hide()
|
||||
onInput() {
|
||||
if (store.visible && props.input().cursorOffset <= store.index) hide()
|
||||
},
|
||||
onKeyDown(e: KeyEvent) {
|
||||
if (store.visible) {
|
||||
@@ -381,23 +385,20 @@ export function Autocomplete(props: {
|
||||
}
|
||||
if (!store.visible) {
|
||||
if (e.name === "@") {
|
||||
const cursorOffset = props.input().visualCursor.offset
|
||||
const cursorOffset = props.input().cursorOffset
|
||||
const charBeforeCursor =
|
||||
cursorOffset === 0
|
||||
? undefined
|
||||
: props.input().getTextRange(cursorOffset - 1, cursorOffset)
|
||||
|
||||
if (
|
||||
charBeforeCursor === " " ||
|
||||
charBeforeCursor === "\n" ||
|
||||
charBeforeCursor === undefined
|
||||
) {
|
||||
show("@")
|
||||
}
|
||||
const canTrigger =
|
||||
charBeforeCursor === undefined ||
|
||||
charBeforeCursor === "" ||
|
||||
/\s/.test(charBeforeCursor)
|
||||
if (canTrigger) show("@")
|
||||
}
|
||||
|
||||
if (e.name === "/") {
|
||||
if (props.input().visualCursor.offset === 0) show("/")
|
||||
if (props.input().cursorOffset === 0) show("/")
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -587,8 +587,12 @@ export function Prompt(props: PromptProps) {
|
||||
return
|
||||
}
|
||||
|
||||
if (e.name === "up" && input.visualCursor.visualRow === 0) input.cursorOffset = 0
|
||||
if (e.name === "down" && input.visualCursor.visualRow === input.height - 1)
|
||||
if (keybind.match("history_previous", e) && input.visualCursor.visualRow === 0)
|
||||
input.cursorOffset = 0
|
||||
if (
|
||||
keybind.match("history_next", e) &&
|
||||
input.visualCursor.visualRow === input.height - 1
|
||||
)
|
||||
input.cursorOffset = input.plainText.length
|
||||
}
|
||||
if (!autocomplete.visible) {
|
||||
@@ -639,7 +643,7 @@ export function Prompt(props: PromptProps) {
|
||||
} catch {}
|
||||
|
||||
const lineCount = (pastedContent.match(/\n/g)?.length ?? 0) + 1
|
||||
if (lineCount >= 5) {
|
||||
if (lineCount >= 5 && !sync.data.config.experimental?.disable_paste_summary) {
|
||||
event.preventDefault()
|
||||
const currentOffset = input.visualCursor.offset
|
||||
const virtualText = `[Pasted ~${lineCount} lines]`
|
||||
@@ -712,7 +716,8 @@ export function Prompt(props: PromptProps) {
|
||||
<Match when={props.hint}>{props.hint!}</Match>
|
||||
<Match when={true}>
|
||||
<text fg={theme.text}>
|
||||
{keybind.print("command_list")} <span style={{ fg: theme.textMuted }}>commands</span>
|
||||
{keybind.print("command_list")}{" "}
|
||||
<span style={{ fg: theme.textMuted }}>commands</span>
|
||||
</text>
|
||||
</Match>
|
||||
</Switch>
|
||||
|
||||
@@ -150,6 +150,16 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
})
|
||||
|
||||
const fallbackModel = createMemo(() => {
|
||||
if (props.initialModel) {
|
||||
const { providerID, modelID } = Provider.parseModel(props.initialModel)
|
||||
if (isModelValid({ providerID, modelID })) {
|
||||
return {
|
||||
providerID,
|
||||
modelID,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sync.data.config.model) {
|
||||
const { providerID, modelID } = Provider.parseModel(sync.data.config.model)
|
||||
if (isModelValid({ providerID, modelID })) {
|
||||
|
||||
@@ -480,22 +480,26 @@ export function Session() {
|
||||
const diffText = s.revert?.diff || ""
|
||||
if (!diffText) return []
|
||||
|
||||
const patches = parsePatch(diffText)
|
||||
return patches.map((patch) => {
|
||||
const filename = patch.newFileName || patch.oldFileName || "unknown"
|
||||
const cleanFilename = filename.replace(/^[ab]\//, "")
|
||||
return {
|
||||
filename: cleanFilename,
|
||||
additions: patch.hunks.reduce(
|
||||
(sum, hunk) => sum + hunk.lines.filter((line) => line.startsWith("+")).length,
|
||||
0,
|
||||
),
|
||||
deletions: patch.hunks.reduce(
|
||||
(sum, hunk) => sum + hunk.lines.filter((line) => line.startsWith("-")).length,
|
||||
0,
|
||||
),
|
||||
}
|
||||
})
|
||||
try {
|
||||
const patches = parsePatch(diffText)
|
||||
return patches.map((patch) => {
|
||||
const filename = patch.newFileName || patch.oldFileName || "unknown"
|
||||
const cleanFilename = filename.replace(/^[ab]\//, "")
|
||||
return {
|
||||
filename: cleanFilename,
|
||||
additions: patch.hunks.reduce(
|
||||
(sum, hunk) => sum + hunk.lines.filter((line) => line.startsWith("+")).length,
|
||||
0,
|
||||
),
|
||||
deletions: patch.hunks.reduce(
|
||||
(sum, hunk) => sum + hunk.lines.filter((line) => line.startsWith("-")).length,
|
||||
0,
|
||||
),
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
return []
|
||||
}
|
||||
})()
|
||||
|
||||
return {
|
||||
@@ -915,7 +919,7 @@ function ToolPart(props: { part: ToolPart; message: AssistantMessage }) {
|
||||
const render = ToolRegistry.render(props.part.tool) ?? GenericTool
|
||||
|
||||
const metadata = props.part.state.status === "pending" ? {} : (props.part.state.metadata ?? {})
|
||||
const input = props.part.state.input
|
||||
const input = props.part.state.input ?? {}
|
||||
const container = ToolRegistry.container(props.part.tool)
|
||||
const permissions = sync.data.permission[props.message.sessionID] ?? []
|
||||
const permissionIndex = permissions.findIndex((x) => x.callID === props.part.callID)
|
||||
@@ -1245,58 +1249,63 @@ ToolRegistry.register<typeof EditTool>({
|
||||
const diff = createMemo(() => {
|
||||
const diff = props.metadata.diff ?? props.permission["diff"]
|
||||
if (!diff) return null
|
||||
const patches = parsePatch(diff)
|
||||
if (patches.length === 0) return null
|
||||
|
||||
const patch = patches[0]
|
||||
const oldLines: string[] = []
|
||||
const newLines: string[] = []
|
||||
try {
|
||||
const patches = parsePatch(diff)
|
||||
if (patches.length === 0) return null
|
||||
|
||||
for (const hunk of patch.hunks) {
|
||||
let i = 0
|
||||
while (i < hunk.lines.length) {
|
||||
const line = hunk.lines[i]
|
||||
const patch = patches[0]
|
||||
const oldLines: string[] = []
|
||||
const newLines: string[] = []
|
||||
|
||||
if (line.startsWith("-")) {
|
||||
const removedLines: string[] = []
|
||||
while (i < hunk.lines.length && hunk.lines[i].startsWith("-")) {
|
||||
removedLines.push("- " + hunk.lines[i].slice(1))
|
||||
for (const hunk of patch.hunks) {
|
||||
let i = 0
|
||||
while (i < hunk.lines.length) {
|
||||
const line = hunk.lines[i]
|
||||
|
||||
if (line.startsWith("-")) {
|
||||
const removedLines: string[] = []
|
||||
while (i < hunk.lines.length && hunk.lines[i].startsWith("-")) {
|
||||
removedLines.push("- " + hunk.lines[i].slice(1))
|
||||
i++
|
||||
}
|
||||
|
||||
const addedLines: string[] = []
|
||||
while (i < hunk.lines.length && hunk.lines[i].startsWith("+")) {
|
||||
addedLines.push("+ " + hunk.lines[i].slice(1))
|
||||
i++
|
||||
}
|
||||
|
||||
const maxLen = Math.max(removedLines.length, addedLines.length)
|
||||
for (let j = 0; j < maxLen; j++) {
|
||||
oldLines.push(removedLines[j] ?? "")
|
||||
newLines.push(addedLines[j] ?? "")
|
||||
}
|
||||
} else if (line.startsWith("+")) {
|
||||
const addedLines: string[] = []
|
||||
while (i < hunk.lines.length && hunk.lines[i].startsWith("+")) {
|
||||
addedLines.push("+ " + hunk.lines[i].slice(1))
|
||||
i++
|
||||
}
|
||||
|
||||
for (const added of addedLines) {
|
||||
oldLines.push("")
|
||||
newLines.push(added)
|
||||
}
|
||||
} else {
|
||||
oldLines.push(" " + line.slice(1))
|
||||
newLines.push(" " + line.slice(1))
|
||||
i++
|
||||
}
|
||||
|
||||
const addedLines: string[] = []
|
||||
while (i < hunk.lines.length && hunk.lines[i].startsWith("+")) {
|
||||
addedLines.push("+ " + hunk.lines[i].slice(1))
|
||||
i++
|
||||
}
|
||||
|
||||
const maxLen = Math.max(removedLines.length, addedLines.length)
|
||||
for (let j = 0; j < maxLen; j++) {
|
||||
oldLines.push(removedLines[j] ?? "")
|
||||
newLines.push(addedLines[j] ?? "")
|
||||
}
|
||||
} else if (line.startsWith("+")) {
|
||||
const addedLines: string[] = []
|
||||
while (i < hunk.lines.length && hunk.lines[i].startsWith("+")) {
|
||||
addedLines.push("+ " + hunk.lines[i].slice(1))
|
||||
i++
|
||||
}
|
||||
|
||||
for (const added of addedLines) {
|
||||
oldLines.push("")
|
||||
newLines.push(added)
|
||||
}
|
||||
} else {
|
||||
oldLines.push(" " + line.slice(1))
|
||||
newLines.push(" " + line.slice(1))
|
||||
i++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
oldContent: oldLines.join("\n"),
|
||||
newContent: newLines.join("\n"),
|
||||
return {
|
||||
oldContent: oldLines.join("\n"),
|
||||
newContent: newLines.join("\n"),
|
||||
}
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -8,6 +8,10 @@ import { bootstrap } from "@/cli/bootstrap"
|
||||
import path from "path"
|
||||
import { UI } from "@/cli/ui"
|
||||
|
||||
declare global {
|
||||
const OPENCODE_WORKER_PATH: string
|
||||
}
|
||||
|
||||
export const TuiThreadCommand = cmd({
|
||||
command: "$0 [project]",
|
||||
describe: "start opencode tui",
|
||||
@@ -52,13 +56,27 @@ export const TuiThreadCommand = cmd({
|
||||
default: "127.0.0.1",
|
||||
}),
|
||||
handler: async (args) => {
|
||||
const cwd = args.project ? path.resolve(args.project) : process.cwd()
|
||||
const prompt = await (async () => {
|
||||
const piped = !process.stdin.isTTY ? await Bun.stdin.text() : undefined
|
||||
if (!args.prompt) return piped
|
||||
return piped ? piped + "\n" + args.prompt : args.prompt
|
||||
})()
|
||||
|
||||
// Resolve relative paths against PWD to preserve behavior when using --cwd flag
|
||||
const baseCwd = process.env.PWD ?? process.cwd()
|
||||
const cwd = args.project ? path.resolve(baseCwd, args.project) : process.cwd()
|
||||
let workerPath: string | URL = new URL("./worker.ts", import.meta.url)
|
||||
|
||||
if (typeof OPENCODE_WORKER_PATH !== "undefined") {
|
||||
workerPath = OPENCODE_WORKER_PATH
|
||||
}
|
||||
try {
|
||||
process.chdir(cwd)
|
||||
} catch (e) {
|
||||
UI.error("Failed to change directory to " + cwd)
|
||||
return
|
||||
}
|
||||
|
||||
await bootstrap(cwd, async () => {
|
||||
upgrade()
|
||||
|
||||
@@ -82,7 +100,13 @@ export const TuiThreadCommand = cmd({
|
||||
return undefined
|
||||
})()
|
||||
|
||||
const worker = new Worker("./src/cli/cmd/tui/worker.ts")
|
||||
const worker = new Worker(workerPath, {
|
||||
env: Object.fromEntries(
|
||||
Object.entries(process.env).filter(
|
||||
(entry): entry is [string, string] => entry[1] !== undefined,
|
||||
),
|
||||
),
|
||||
})
|
||||
worker.onerror = console.error
|
||||
const client = Rpc.client<typeof rpc>(worker)
|
||||
process.on("uncaughtException", (e) => {
|
||||
@@ -100,7 +124,7 @@ export const TuiThreadCommand = cmd({
|
||||
sessionID,
|
||||
model: args.model,
|
||||
agent: args.agent,
|
||||
prompt: args.prompt,
|
||||
prompt,
|
||||
onExit: async () => {
|
||||
await client.call("shutdown", undefined)
|
||||
},
|
||||
|
||||
@@ -26,12 +26,15 @@ export const WebCommand = cmd({
|
||||
port,
|
||||
hostname,
|
||||
})
|
||||
const url = `https://desktop.dev.opencode.ai?url=${server.url}`
|
||||
UI.empty()
|
||||
UI.println(UI.logo(" "))
|
||||
UI.empty()
|
||||
UI.println(UI.Style.TEXT_INFO_BOLD + " Web interface: ", UI.Style.TEXT_NORMAL, url)
|
||||
open(url).catch(() => {})
|
||||
UI.println(
|
||||
UI.Style.TEXT_INFO_BOLD + " Web interface: ",
|
||||
UI.Style.TEXT_NORMAL,
|
||||
server.url.toString(),
|
||||
)
|
||||
open(server.url.toString()).catch(() => {})
|
||||
await new Promise(() => {})
|
||||
await server.stop()
|
||||
},
|
||||
|
||||
@@ -473,7 +473,7 @@ export namespace Config {
|
||||
.default("shift+return,ctrl+j")
|
||||
.describe("Insert newline in input"),
|
||||
history_previous: z.string().optional().default("up").describe("Previous history item"),
|
||||
history_next: z.string().optional().default("down").describe("Previous history item"),
|
||||
history_next: z.string().optional().default("down").describe("Next history item"),
|
||||
session_child_cycle: z
|
||||
.string()
|
||||
.optional()
|
||||
|
||||
@@ -62,7 +62,8 @@ const cli = yargs(hideBin(process.argv))
|
||||
})(),
|
||||
})
|
||||
|
||||
process.env["OPENCODE"] = "1"
|
||||
process.env.AGENT = "1"
|
||||
process.env.OPENCODE = "1"
|
||||
|
||||
Log.Default.info("opencode", {
|
||||
version: Installation.VERSION,
|
||||
|
||||
@@ -73,7 +73,7 @@ export namespace Installation {
|
||||
},
|
||||
{
|
||||
name: "brew" as const,
|
||||
command: () => $`brew list --formula opencode-ai`.throws(false).text(),
|
||||
command: () => $`brew list --formula opencode`.throws(false).text(),
|
||||
},
|
||||
]
|
||||
|
||||
@@ -87,7 +87,7 @@ export namespace Installation {
|
||||
|
||||
for (const check of checks) {
|
||||
const output = await check.command()
|
||||
if (output.includes("opencode-ai")) {
|
||||
if (output.includes(check.name === "brew" ? "opencode" : "opencode-ai")) {
|
||||
return check.name
|
||||
}
|
||||
}
|
||||
@@ -102,28 +102,42 @@ export namespace Installation {
|
||||
}),
|
||||
)
|
||||
|
||||
async function getBrewFormula() {
|
||||
const tapFormula = await $`brew list --formula sst/tap/opencode`.throws(false).text()
|
||||
if (tapFormula.includes("opencode")) return "sst/tap/opencode"
|
||||
const coreFormula = await $`brew list --formula opencode`.throws(false).text()
|
||||
if (coreFormula.includes("opencode")) return "opencode"
|
||||
return "opencode"
|
||||
}
|
||||
|
||||
export async function upgrade(method: Method, target: string) {
|
||||
const cmd = (() => {
|
||||
switch (method) {
|
||||
case "curl":
|
||||
return $`curl -fsSL https://opencode.ai/install | bash`.env({
|
||||
...process.env,
|
||||
VERSION: target,
|
||||
})
|
||||
case "npm":
|
||||
return $`npm install -g opencode-ai@${target}`
|
||||
case "pnpm":
|
||||
return $`pnpm install -g opencode-ai@${target}`
|
||||
case "bun":
|
||||
return $`bun install -g opencode-ai@${target}`
|
||||
case "brew":
|
||||
return $`brew install sst/tap/opencode`.env({
|
||||
HOMEBREW_NO_AUTO_UPDATE: "1",
|
||||
})
|
||||
default:
|
||||
throw new Error(`Unknown method: ${method}`)
|
||||
let cmd
|
||||
switch (method) {
|
||||
case "curl":
|
||||
cmd = $`curl -fsSL https://opencode.ai/install | bash`.env({
|
||||
...process.env,
|
||||
VERSION: target,
|
||||
})
|
||||
break
|
||||
case "npm":
|
||||
cmd = $`npm install -g opencode-ai@${target}`
|
||||
break
|
||||
case "pnpm":
|
||||
cmd = $`pnpm install -g opencode-ai@${target}`
|
||||
break
|
||||
case "bun":
|
||||
cmd = $`bun install -g opencode-ai@${target}`
|
||||
break
|
||||
case "brew": {
|
||||
const formula = await getBrewFormula()
|
||||
cmd = $`brew install ${formula}`.env({
|
||||
HOMEBREW_NO_AUTO_UPDATE: "1",
|
||||
})
|
||||
break
|
||||
}
|
||||
})()
|
||||
default:
|
||||
throw new Error(`Unknown method: ${method}`)
|
||||
}
|
||||
const result = await cmd.quiet().throws(false)
|
||||
log.info("upgraded", {
|
||||
method,
|
||||
|
||||
@@ -34,8 +34,10 @@ export namespace Plugin {
|
||||
for (let plugin of plugins) {
|
||||
log.info("loading plugin", { path: plugin })
|
||||
if (!plugin.startsWith("file://")) {
|
||||
const [pkg, version] = plugin.split("@")
|
||||
plugin = await BunProc.install(pkg, version ?? "latest")
|
||||
const lastAtIndex = plugin.lastIndexOf("@")
|
||||
const pkg = lastAtIndex > 0 ? plugin.substring(0, lastAtIndex) : plugin
|
||||
const version = lastAtIndex > 0 ? plugin.substring(lastAtIndex + 1) : "latest"
|
||||
plugin = await BunProc.install(pkg, version)
|
||||
}
|
||||
const mod = await import(plugin)
|
||||
for (const [_name, fn] of Object.entries<PluginInstance>(mod)) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import { Hono } from "hono"
|
||||
import { cors } from "hono/cors"
|
||||
import { stream, streamSSE } from "hono/streaming"
|
||||
import { proxy } from "hono/proxy"
|
||||
import { Session } from "../session"
|
||||
import z from "zod"
|
||||
import { Provider } from "../provider/provider"
|
||||
@@ -757,6 +758,34 @@ export namespace Server {
|
||||
return c.json(messages)
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/session/:id/diff",
|
||||
describeRoute({
|
||||
description: "Get the diff for this session",
|
||||
operationId: "session.diff",
|
||||
responses: {
|
||||
200: {
|
||||
description: "List of diffs",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Snapshot.FileDiff.array()),
|
||||
},
|
||||
},
|
||||
},
|
||||
...errors(400, 404),
|
||||
},
|
||||
}),
|
||||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
id: z.string().meta({ description: "Session ID" }),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const diff = await Session.diff(c.req.valid("param").id)
|
||||
return c.json(diff)
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/session/:id/message/:messageID",
|
||||
describeRoute({
|
||||
@@ -1696,7 +1725,15 @@ export namespace Server {
|
||||
})
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
.all("/*", async (c) => {
|
||||
return proxy(`https://desktop.dev.opencode.ai${c.req.path}`, {
|
||||
...c.req,
|
||||
headers: {
|
||||
host: "desktop.dev.opencode.ai",
|
||||
},
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
export async function openapi() {
|
||||
|
||||
@@ -15,8 +15,8 @@ import { MessageV2 } from "./message-v2"
|
||||
import { Instance } from "../project/instance"
|
||||
import { SessionPrompt } from "./prompt"
|
||||
import { fn } from "@/util/fn"
|
||||
import { Snapshot } from "@/snapshot"
|
||||
import { Command } from "../command"
|
||||
import { Snapshot } from "@/snapshot"
|
||||
|
||||
export namespace Session {
|
||||
const log = Log.create({ service: "session" })
|
||||
@@ -42,7 +42,9 @@ export namespace Session {
|
||||
parentID: Identifier.schema("session").optional(),
|
||||
summary: z
|
||||
.object({
|
||||
diffs: Snapshot.FileDiff.array(),
|
||||
additions: z.number(),
|
||||
deletions: z.number(),
|
||||
diffs: Snapshot.FileDiff.array().optional(),
|
||||
})
|
||||
.optional(),
|
||||
share: z
|
||||
@@ -258,6 +260,11 @@ export namespace Session {
|
||||
return result
|
||||
}
|
||||
|
||||
export const diff = fn(Identifier.schema("session"), async (sessionID) => {
|
||||
const diffs = await Storage.read<Snapshot.FileDiff[]>(["session_diff", sessionID])
|
||||
return diffs ?? []
|
||||
})
|
||||
|
||||
export const messages = fn(Identifier.schema("session"), async (sessionID) => {
|
||||
const result = [] as MessageV2.WithParts[]
|
||||
for (const p of await Storage.list(["message", sessionID])) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import { SystemPrompt } from "./system"
|
||||
import { Log } from "@/util/log"
|
||||
import path from "path"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { Storage } from "@/storage/storage"
|
||||
|
||||
export namespace SessionSummary {
|
||||
const log = Log.create({ service: "session.summary" })
|
||||
@@ -44,9 +45,11 @@ export namespace SessionSummary {
|
||||
)
|
||||
await Session.update(input.sessionID, (draft) => {
|
||||
draft.summary = {
|
||||
diffs,
|
||||
additions: diffs.reduce((sum, x) => sum + x.additions, 0),
|
||||
deletions: diffs.reduce((sum, x) => sum + x.deletions, 0),
|
||||
}
|
||||
})
|
||||
await Storage.write(["session_diff", input.sessionID], diffs)
|
||||
}
|
||||
|
||||
async function summarizeMessage(input: { messageID: string; messages: MessageV2.WithParts[] }) {
|
||||
|
||||
@@ -85,7 +85,9 @@ export namespace Storage {
|
||||
const session = await Bun.file(sessionFile).json()
|
||||
await Bun.write(dest, JSON.stringify(session))
|
||||
log.info(`migrating messages for session ${session.id}`)
|
||||
for await (const msgFile of new Bun.Glob(`storage/session/message/${session.id}/*.json`).scan({
|
||||
for await (const msgFile of new Bun.Glob(
|
||||
`storage/session/message/${session.id}/*.json`,
|
||||
).scan({
|
||||
cwd: fullProjectDir,
|
||||
absolute: true,
|
||||
})) {
|
||||
@@ -98,12 +100,12 @@ export namespace Storage {
|
||||
await Bun.write(dest, JSON.stringify(message))
|
||||
|
||||
log.info(`migrating parts for message ${message.id}`)
|
||||
for await (const partFile of new Bun.Glob(`storage/session/part/${session.id}/${message.id}/*.json`).scan(
|
||||
{
|
||||
cwd: fullProjectDir,
|
||||
absolute: true,
|
||||
},
|
||||
)) {
|
||||
for await (const partFile of new Bun.Glob(
|
||||
`storage/session/part/${session.id}/${message.id}/*.json`,
|
||||
).scan({
|
||||
cwd: fullProjectDir,
|
||||
absolute: true,
|
||||
})) {
|
||||
const dest = path.join(dir, "part", message.id, path.basename(partFile))
|
||||
const part = await Bun.file(partFile).json()
|
||||
log.info("copying", {
|
||||
@@ -117,6 +119,29 @@ export namespace Storage {
|
||||
}
|
||||
}
|
||||
},
|
||||
async (dir) => {
|
||||
for await (const item of new Bun.Glob("session/*/*.json").scan({
|
||||
cwd: dir,
|
||||
absolute: true,
|
||||
})) {
|
||||
const session = await Bun.file(item).json()
|
||||
if (!session.projectID) continue
|
||||
if (!session.summary?.diffs) continue
|
||||
const { diffs } = session.summary
|
||||
await Bun.file(path.join(dir, "session_diff", session.id + ".json")).write(
|
||||
JSON.stringify(diffs),
|
||||
)
|
||||
await Bun.file(path.join(dir, "session", session.projectID, session.id + ".json")).write(
|
||||
JSON.stringify({
|
||||
...session,
|
||||
summary: {
|
||||
additions: diffs.reduce((sum: any, x: any) => sum + x.additions, 0),
|
||||
deletions: diffs.reduce((sum: any, x: any) => sum + x.deletions, 0),
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
const state = lazy(async () => {
|
||||
@@ -128,9 +153,7 @@ export namespace Storage {
|
||||
for (let index = migration; index < MIGRATIONS.length; index++) {
|
||||
log.info("running migration", { index })
|
||||
const migration = MIGRATIONS[index]
|
||||
await migration(dir).catch((e) => {
|
||||
log.error("failed to run migration", { error: e, index })
|
||||
})
|
||||
await migration(dir).catch(() => log.error("failed to run migration", { index }))
|
||||
await Bun.write(path.join(dir, "migration"), (index + 1).toString())
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -144,6 +144,9 @@ export const BashTool = Tool.define("bash", {
|
||||
const proc = spawn(params.command, {
|
||||
shell: true,
|
||||
cwd: Instance.directory,
|
||||
env: {
|
||||
...process.env,
|
||||
},
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
detached: process.platform !== "win32",
|
||||
})
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Instance } from "../../src/project/instance"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { pathToFileURL } from "url"
|
||||
|
||||
test("loads config with defaults when no files exist", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
@@ -350,3 +351,59 @@ test("gets config directories", async () => {
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("resolves scoped npm plugins in config", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
const pluginDir = path.join(dir, "node_modules", "@scope", "plugin")
|
||||
await fs.mkdir(pluginDir, { recursive: true })
|
||||
|
||||
await Bun.write(
|
||||
path.join(dir, "package.json"),
|
||||
JSON.stringify({ name: "config-fixture", version: "1.0.0", type: "module" }, null, 2),
|
||||
)
|
||||
|
||||
await Bun.write(
|
||||
path.join(pluginDir, "package.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
name: "@scope/plugin",
|
||||
version: "1.0.0",
|
||||
type: "module",
|
||||
main: "./index.js",
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
|
||||
await Bun.write(path.join(pluginDir, "index.js"), "export default {}\n")
|
||||
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify(
|
||||
{ $schema: "https://opencode.ai/config.json", plugin: ["@scope/plugin"] },
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await Config.get()
|
||||
const pluginEntries = config.plugin ?? []
|
||||
|
||||
const baseUrl = pathToFileURL(path.join(tmp.path, "opencode.json")).href
|
||||
const expected = import.meta.resolve("@scope/plugin", baseUrl)
|
||||
|
||||
expect(pluginEntries.includes(expected)).toBe(true)
|
||||
|
||||
const scopedEntry = pluginEntries.find((entry) => entry === expected)
|
||||
expect(scopedEntry).toBeDefined()
|
||||
expect(scopedEntry?.includes("/node_modules/@scope/plugin/")).toBe(true)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.0.19",
|
||||
"version": "1.0.22",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.0.19",
|
||||
"version": "1.0.22",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
|
||||
@@ -55,6 +55,7 @@ import type {
|
||||
SessionShareErrors,
|
||||
SessionDiffData,
|
||||
SessionDiffResponses,
|
||||
SessionDiffErrors,
|
||||
SessionSummarizeData,
|
||||
SessionSummarizeResponses,
|
||||
SessionSummarizeErrors,
|
||||
@@ -475,12 +476,16 @@ class Session extends _HeyApiClient {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the diff that resulted from this user message
|
||||
* Get the diff for this session
|
||||
*/
|
||||
public diff<ThrowOnError extends boolean = false>(
|
||||
options: Options<SessionDiffData, ThrowOnError>,
|
||||
) {
|
||||
return (options.client ?? this._client).get<SessionDiffResponses, unknown, ThrowOnError>({
|
||||
return (options.client ?? this._client).get<
|
||||
SessionDiffResponses,
|
||||
SessionDiffErrors,
|
||||
ThrowOnError
|
||||
>({
|
||||
url: "/session/{id}/diff",
|
||||
...options,
|
||||
})
|
||||
|
||||
@@ -163,7 +163,7 @@ export type KeybindsConfig = {
|
||||
*/
|
||||
history_previous?: string
|
||||
/**
|
||||
* Previous history item
|
||||
* Next history item
|
||||
*/
|
||||
history_next?: string
|
||||
/**
|
||||
@@ -527,7 +527,9 @@ export type Session = {
|
||||
directory: string
|
||||
parentID?: string
|
||||
summary?: {
|
||||
diffs: Array<FileDiff>
|
||||
additions: number
|
||||
deletions: number
|
||||
diffs?: Array<FileDiff>
|
||||
}
|
||||
share?: {
|
||||
url: string
|
||||
@@ -1882,6 +1884,9 @@ export type SessionShareResponse = SessionShareResponses[keyof SessionShareRespo
|
||||
export type SessionDiffData = {
|
||||
body?: never
|
||||
path: {
|
||||
/**
|
||||
* Session ID
|
||||
*/
|
||||
id: string
|
||||
}
|
||||
query?: {
|
||||
@@ -1891,9 +1896,22 @@ export type SessionDiffData = {
|
||||
url: "/session/{id}/diff"
|
||||
}
|
||||
|
||||
export type SessionDiffErrors = {
|
||||
/**
|
||||
* Bad request
|
||||
*/
|
||||
400: BadRequestError
|
||||
/**
|
||||
* Not found
|
||||
*/
|
||||
404: NotFoundError
|
||||
}
|
||||
|
||||
export type SessionDiffError = SessionDiffErrors[keyof SessionDiffErrors]
|
||||
|
||||
export type SessionDiffResponses = {
|
||||
/**
|
||||
* Successfully retrieved diff
|
||||
* List of diffs
|
||||
*/
|
||||
200: Array<FileDiff>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.0.19",
|
||||
"version": "1.0.22",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "bun run src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.0.19",
|
||||
"version": "1.0.22",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/components/index.ts",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/web",
|
||||
"type": "module",
|
||||
"version": "1.0.19",
|
||||
"version": "1.0.22",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
|
||||
|
||||
@@ -109,9 +109,9 @@ if (image) {
|
||||
</div>
|
||||
<div class="col3">
|
||||
<h3>Homebrew</h3>
|
||||
<button class="command" data-command="brew install sst/tap/opencode">
|
||||
<button class="command" data-command="brew install opencode">
|
||||
<code>
|
||||
<span>brew install</span> <span class="highlight">sst/tap/opencode</span>
|
||||
<span>brew install</span> <span class="highlight">opencode</span>
|
||||
</code>
|
||||
<span class="copy">
|
||||
<CopyIcon />
|
||||
|
||||
@@ -67,7 +67,7 @@ You can also install it with the following commands:
|
||||
- **Using Homebrew on macOS and Linux**
|
||||
|
||||
```bash
|
||||
brew install sst/tap/opencode
|
||||
brew install opencode
|
||||
```
|
||||
|
||||
- **Using Paru on Arch Linux**
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "opencode",
|
||||
"displayName": "opencode",
|
||||
"description": "opencode for VS Code",
|
||||
"version": "1.0.19",
|
||||
"version": "1.0.22",
|
||||
"publisher": "sst-dev",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
Reference in New Issue
Block a user