diff --git a/packages/opencode/script/build.ts b/packages/opencode/script/build.ts index 26c61382a..40f982ba7 100755 --- a/packages/opencode/script/build.ts +++ b/packages/opencode/script/build.ts @@ -50,7 +50,7 @@ for (const [os, arch] of targets) { execArgv: [`--user-agent=opencode/${version}`, `--env-file=""`, `--`], windows: {}, }, - entrypoints: ["./src/index.ts"], + entrypoints: ["./src/index.ts", "./src/cli/cmd/tui/worker.ts"], define: { OPENCODE_VERSION: `'${version}'`, OPENCODE_TUI_PATH: `'../../../dist/${name}/bin/tui'`, diff --git a/packages/opencode/src/cli/cmd/attach.ts b/packages/opencode/src/cli/cmd/attach.ts deleted file mode 100644 index 5a0c23ea2..000000000 --- a/packages/opencode/src/cli/cmd/attach.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Global } from "../../global" -import { cmd } from "./cmd" -import path from "path" -import fs from "fs/promises" -import { Log } from "../../util/log" - -import { $ } from "bun" - -export const AttachCommand = cmd({ - command: "attach ", - describe: "attach to a running opencode server", - builder: (yargs) => - yargs.positional("server", { - type: "string", - describe: "http://localhost:4096", - }), - handler: async (args) => { - 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, - stdout: "inherit", - stderr: "inherit", - stdin: "inherit", - env: { - ...process.env, - CGO_ENABLED: "0", - OPENCODE_SERVER: args.server, - }, - }) - - await proc.exited - }, -}) diff --git a/packages/opencode/src/cli/cmd/tui/attach.tsx b/packages/opencode/src/cli/cmd/tui/attach.tsx new file mode 100644 index 000000000..4a4ba0afa --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/attach.tsx @@ -0,0 +1,212 @@ +import { cmd } from "@/cli/cmd/cmd" +import { render, useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid" +import { TextAttributes } from "@opentui/core" +import { RouteProvider, useRoute } from "@tui/context/route" +import { Switch, Match, createEffect, untrack } from "solid-js" +import { Installation } from "@/installation" +import { Global } from "@/global" +import { DialogProvider, useDialog } from "@tui/ui/dialog" +import { SDKProvider } from "@tui/context/sdk" +import { SyncProvider } from "@tui/context/sync" +import { LocalProvider, useLocal } from "@tui/context/local" +import { DialogModel } from "@tui/component/dialog-model" +import { CommandProvider, useCommandDialog } from "@tui/component/dialog-command" +import { DialogAgent } from "@tui/component/dialog-agent" +import { DialogSessionList } from "@tui/component/dialog-session-list" +import { KeybindProvider, useKeybind } from "@tui/context/keybind" +import { Instance } from "@/project/instance" +import { Theme } from "@tui/context/theme" +import { Home } from "@tui/routes/home" +import { Session } from "@tui/routes/session" +import { PromptHistoryProvider } from "./component/prompt/history" +import { DialogAlert } from "./ui/dialog-alert" + +export const AttachCommand = cmd({ + command: "attach ", + describe: "attach to a running opencode server", + builder: (yargs) => + yargs + .positional("url", { + type: "string", + describe: "http://localhost:4096", + demandOption: true, + }) + .option("dir", { + type: "string", + description: "directory to run in", + }), + handler: async (args) => { + if (args.dir) process.chdir(args.dir) + await render( + () => { + return ( + + + + + + + + + { + process.exit(0) + }} + /> + + + + + + + + + ) + }, + { + targetFps: 60, + gatherStats: false, + exitOnCtrlC: false, + useKittyKeyboard: true, + }, + ) + }, +}) + +function App(props: { onExit: () => void }) { + const route = useRoute() + const dimensions = useTerminalDimensions() + const renderer = useRenderer() + const dialog = useDialog() + const local = useLocal() + const command = useCommandDialog() + const keybind = useKeybind() + + useKeyboard(async (evt) => { + if (keybind.match("agent_cycle", evt)) { + local.agent.move(1) + return + } + if (keybind.match("agent_cycle_reverse", evt)) { + local.agent.move(-1) + } + + if (evt.meta && evt.name === "t") { + renderer.toggleDebugOverlay() + return + } + + if (evt.meta && evt.name === "d") { + renderer.console.toggle() + return + } + if (keybind.match("app_exit", evt)) { + await Instance.disposeAll() + renderer.destroy() + props.onExit() + } + }) + + createEffect(() => { + console.log(JSON.stringify(route.data)) + }) + + command.register(() => [ + { + title: "Switch session", + value: "session.list", + keybind: "session_list", + category: "Session", + onSelect: () => { + dialog.replace(() => ) + }, + }, + { + title: "New session", + value: "session.new", + keybind: "session_new", + category: "Session", + onSelect: () => { + route.navigate({ + type: "home", + }) + dialog.clear() + }, + }, + { + title: "Switch model", + value: "model.list", + keybind: "model_list", + category: "Agent", + onSelect: () => { + dialog.replace(() => ) + }, + }, + { + title: "Switch agent", + value: "agent.list", + keybind: "agent_list", + category: "Agent", + onSelect: () => { + dialog.replace(() => ) + }, + }, + ]) + + createEffect(() => { + const providerID = local.model.current().providerID + if (providerID === "openrouter" && !local.kv.data.openrouter_warning) { + untrack(() => { + DialogAlert.show( + dialog, + "Warning", + "While openrouter is a convenient way to access LLMs your request will often be routed to subpar providers that do not work well in our testing.\n\nFor reliable access to models check out OpenCode Zen\nhttps://opencode.ai/zen", + ).then(() => local.kv.set("openrouter_warning", true)) + }) + } + }) + + return ( + + + + + + + + + + + + + + + open + code + v{Installation.VERSION} + + + {process.cwd().replace(Global.Path.home, "~")} + + + + + tab + + + + {" "} + {local.agent.current().name.toUpperCase()} + AGENT + + + + + ) +} diff --git a/packages/opencode/src/cli/cmd/tui/tui.tsx b/packages/opencode/src/cli/cmd/tui/tui.tsx index 291bcf3ea..bf1ba7ff9 100644 --- a/packages/opencode/src/cli/cmd/tui/tui.tsx +++ b/packages/opencode/src/cli/cmd/tui/tui.tsx @@ -1,27 +1,9 @@ import { cmd } from "@/cli/cmd/cmd" -import { render, useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid" -import { TextAttributes } from "@opentui/core" -import { RouteProvider, useRoute } from "@tui/context/route" -import { Switch, Match, createEffect, untrack } from "solid-js" -import { Installation } from "@/installation" -import { Global } from "@/global" -import { DialogProvider, useDialog } from "@tui/ui/dialog" -import { SDKProvider } from "@tui/context/sdk" -import { SyncProvider } from "@tui/context/sync" -import { LocalProvider, useLocal } from "@tui/context/local" -import { DialogModel } from "@tui/component/dialog-model" -import { CommandProvider, useCommandDialog } from "@tui/component/dialog-command" -import { DialogAgent } from "@tui/component/dialog-agent" -import { DialogSessionList } from "@tui/component/dialog-session-list" -import { KeybindProvider, useKeybind } from "@tui/context/keybind" -import { Config } from "@/config/config" import { Instance } from "@/project/instance" -import { Theme } from "@tui/context/theme" - -import { Home } from "@tui/routes/home" -import { Session } from "@tui/routes/session" -import { PromptHistoryProvider } from "./component/prompt/history" -import { DialogAlert } from "./ui/dialog-alert" +import path from "path" +import { Server } from "@/server/server" +import { Config } from "@/config/config" +import { InstanceBootstrap } from "@/project/bootstrap" export const TuiCommand = cmd({ command: "$0 [project]", @@ -32,30 +14,6 @@ export const TuiCommand = cmd({ 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", @@ -67,193 +25,38 @@ export const TuiCommand = cmd({ describe: "hostname to listen on", default: "127.0.0.1", }), - handler: async () => { - await Instance.provide({ - directory: process.cwd(), - fn: () => Config.get(), + handler: async (args) => { + const server = Server.listen({ + port: args.port, + hostname: "127.0.0.1", }) - - const worker = new Worker("./src/cli/cmd/tui/worker.ts") - worker.onerror = console.log - worker.onmessageerror = console.log - const url = await new Promise((resolve) => { - worker.onmessage = (event) => { - const data = JSON.parse(event.data) - if (data.type === "ready") { - resolve(data.url) - } - } - }) - await render( - () => { - return ( - - - - - - - - - { - worker.terminate() - }} - /> - - - - - - - - - ) - }, - { - targetFps: 60, - gatherStats: false, - exitOnCtrlC: false, - useKittyKeyboard: true, - }, - ) + const bin = process.execPath + const cmd = [] + let cwd = process.cwd() + if (bin.endsWith("bun")) { + cmd.push( + process.execPath, + "run", + "--conditions", + "browser", + new URL("../../../index.ts", import.meta.url).pathname, + ) + cwd = new URL("../../../../", import.meta.url).pathname + } else cmd.push(process.execPath) + cmd.push("attach", server.url.toString(), "--dir", args.project ? path.resolve(args.project) : process.cwd()) + while (true) { + const proc = Bun.spawn({ + cmd, + cwd, + stdout: "inherit", + stderr: "inherit", + stdin: "inherit", + }) + await proc.exited + const code = proc.exitCode + if (code === 0) break + } + await server.stop(true) + await Instance.disposeAll() }, }) - -function App(props: { onExit: () => void }) { - const route = useRoute() - const dimensions = useTerminalDimensions() - const renderer = useRenderer() - const dialog = useDialog() - const local = useLocal() - const command = useCommandDialog() - const keybind = useKeybind() - - useKeyboard(async (evt) => { - if (keybind.match("agent_cycle", evt)) { - local.agent.move(1) - return - } - if (keybind.match("agent_cycle_reverse", evt)) { - local.agent.move(-1) - } - - if (evt.meta && evt.name === "t") { - renderer.toggleDebugOverlay() - return - } - - if (evt.meta && evt.name === "d") { - renderer.console.toggle() - return - } - if (keybind.match("app_exit", evt)) { - await Instance.disposeAll() - renderer.destroy() - props.onExit() - } - }) - - createEffect(() => { - console.log(JSON.stringify(route.data)) - }) - - command.register(() => [ - { - title: "Switch session", - value: "session.list", - keybind: "session_list", - category: "Session", - onSelect: () => { - dialog.replace(() => ) - }, - }, - { - title: "New session", - value: "session.new", - keybind: "session_new", - category: "Session", - onSelect: () => { - route.navigate({ - type: "home", - }) - dialog.clear() - }, - }, - { - title: "Switch model", - value: "model.list", - keybind: "model_list", - category: "Agent", - onSelect: () => { - dialog.replace(() => ) - }, - }, - { - title: "Switch agent", - value: "agent.list", - keybind: "agent_list", - category: "Agent", - onSelect: () => { - dialog.replace(() => ) - }, - }, - ]) - - createEffect(() => { - const providerID = local.model.current().providerID - if (providerID === "openrouter" && !local.kv.data.openrouter_warning) { - untrack(() => { - DialogAlert.show( - dialog, - "Warning", - "While openrouter is a convenient way to access LLMs your request will often be routed to subpar providers that do not work well in our testing.\n\nFor reliable access to models check out OpenCode Zen\nhttps://opencode.ai/zen", - ).then(() => local.kv.set("openrouter_warning", true)) - }) - } - }) - - return ( - - - - - - - - - - - - - - - open - code - v{Installation.VERSION} - - - {process.cwd().replace(Global.Path.home, "~")} - - - - - tab - - - - {" "} - {local.agent.current().name.toUpperCase()} - AGENT - - - - - ) -} diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 47290f776..b52573137 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -13,6 +13,7 @@ import { NamedError } from "./util/error" import { FormatError } from "./cli/error" import { ServeCommand } from "./cli/cmd/serve" import { TuiCommand } from "./cli/cmd/tui/tui" +import { AttachCommand } from "./cli/cmd/tui/attach" import { DebugCommand } from "./cli/cmd/debug" import { StatsCommand } from "./cli/cmd/stats" import { McpCommand } from "./cli/cmd/mcp" @@ -68,6 +69,7 @@ const cli = yargs(hideBin(process.argv)) .usage("\n" + UI.logo()) .command(McpCommand) .command(TuiCommand) + .command(AttachCommand) .command(RunCommand) .command(GenerateCommand) .command(DebugCommand) diff --git a/packages/opencode/src/project/instance.ts b/packages/opencode/src/project/instance.ts index 5a3adbd4d..a8d42aa27 100644 --- a/packages/opencode/src/project/instance.ts +++ b/packages/opencode/src/project/instance.ts @@ -47,7 +47,6 @@ export const Instance = { async disposeAll() { for (const [key, value] of cache) { context.provide(value, async () => { - process.stdout.write(`disposing ${key}...`) await Instance.dispose() }) } diff --git a/packages/opencode/src/session/system.ts b/packages/opencode/src/session/system.ts index b7555a644..3173dcac5 100644 --- a/packages/opencode/src/session/system.ts +++ b/packages/opencode/src/session/system.ts @@ -108,7 +108,8 @@ export namespace SystemPrompt { const found = Array.from(paths).map((p) => Bun.file(p) .text() - .catch(() => ""), + .catch(() => "") + .then((x) => "Instructions from: " + p + "\n" + x), ) return Promise.all(found).then((result) => result.filter(Boolean)) }