From ad17cf00ad4136d8f37e80305e2137b30e0e87be Mon Sep 17 00:00:00 2001 From: R44VC0RP Date: Tue, 20 Jan 2026 15:05:37 -0500 Subject: [PATCH 1/2] feat(tui): add tui.attach config and --attach flag to connect to existing server --- .../cli/cmd/tui/component/dialog-status.tsx | 8 ++ packages/opencode/src/cli/cmd/tui/thread.ts | 78 +++++++++++++++++-- packages/opencode/src/config/config.ts | 5 ++ 3 files changed, 85 insertions(+), 6 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx index c08fc99b6e3..5ecd130eaed 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx @@ -1,6 +1,7 @@ import { TextAttributes } from "@opentui/core" import { useTheme } from "../context/theme" import { useSync } from "@tui/context/sync" +import { useSDK } from "@tui/context/sdk" import { For, Match, Switch, Show, createMemo } from "solid-js" import { Installation } from "@/installation" @@ -8,7 +9,9 @@ export type DialogStatusProps = {} export function DialogStatus() { const sync = useSync() + const sdk = useSDK() const { theme } = useTheme() + const isAttached = createMemo(() => !sdk.url.includes(".internal")) const enabledFormatters = createMemo(() => sync.data.formatter.filter((f) => f.enabled)) @@ -46,6 +49,11 @@ export function DialogStatus() { esc OpenCode v{Installation.VERSION} + + + Attached to {sdk.url} + + 0} fallback={No MCP Servers}> {Object.keys(sync.data.mcp).length} MCP Servers diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index 05714268545..c02712df20d 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -9,6 +9,8 @@ import { Log } from "@/util/log" import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network" import type { Event } from "@opencode-ai/sdk/v2" import type { EventSource } from "./context/sdk" +import { Global } from "@/global" +import { parse as parseJsonc } from "jsonc-parser" declare global { const OPENCODE_WORKER_PATH: string @@ -40,6 +42,38 @@ function createEventSource(client: RpcClient): EventSource { } } +async function tryAttachToServer(url: string): Promise { + try { + const response = await fetch(`${url}/health`, { + method: "GET", + signal: AbortSignal.timeout(2000), + }) + return response.ok + } catch { + return false + } +} + +async function getAttachUrl(cwd: string): Promise { + const candidates = [ + path.join(cwd, "opencode.jsonc"), + path.join(cwd, "opencode.json"), + path.join(Global.Path.config, "opencode.jsonc"), + path.join(Global.Path.config, "opencode.json"), + ] + + for (const filepath of candidates) { + try { + const text = await Bun.file(filepath).text() + const config = parseJsonc(text) as { tui?: { attach?: string } } | undefined + if (config?.tui?.attach) return config.tui.attach + } catch { + // File doesn't exist or invalid, try next + } + } + return undefined +} + export const TuiThreadCommand = cmd({ command: "$0 [project]", describe: "start opencode tui", @@ -71,11 +105,49 @@ export const TuiThreadCommand = cmd({ .option("agent", { type: "string", describe: "agent to use", + }) + .option("attach", { + type: "string", + describe: "attach to a running opencode server (e.g., http://localhost:4096)", }), handler: async (args) => { // 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() + + try { + process.chdir(cwd) + } catch (e) { + UI.error("Failed to change directory to " + cwd) + return + } + + // Check if --attach flag or tui.attach config is set - try to connect to existing server first + const attachUrl = args.attach ?? (await getAttachUrl(cwd)) + if (attachUrl) { + const serverAvailable = await tryAttachToServer(attachUrl) + if (serverAvailable) { + // Server is available, attach to it directly (no worker spawn) + const prompt = await iife(async () => { + const piped = !process.stdin.isTTY ? await Bun.stdin.text() : undefined + if (!args.prompt) return piped + return piped ? piped + "\n" + args.prompt : args.prompt + }) + await tui({ + url: attachUrl, + args: { + continue: args.continue, + sessionID: args.session, + agent: args.agent, + model: args.model, + prompt, + }, + }) + return + } + // Server not available, fall through to spawn a new worker + } + const localWorker = new URL("./worker.ts", import.meta.url) const distWorker = new URL("./cli/cmd/tui/worker.js", import.meta.url) const workerPath = await iife(async () => { @@ -83,12 +155,6 @@ export const TuiThreadCommand = cmd({ if (await Bun.file(distWorker).exists()) return distWorker return localWorker }) - try { - process.chdir(cwd) - } catch (e) { - UI.error("Failed to change directory to " + cwd) - return - } const worker = new Worker(workerPath, { env: Object.fromEntries( diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index b2142e29b94..6c59cb57653 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -804,6 +804,11 @@ export namespace Config { .enum(["auto", "stacked"]) .optional() .describe("Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column"), + attach: z + .string() + .url() + .optional() + .describe("URL to attach to existing server. Falls back to spawning new server if unavailable."), }) export const Server = z From 34d7ed57320e3674c90c373195619170ce3f5b48 Mon Sep 17 00:00:00 2001 From: R44VC0RP Date: Tue, 20 Jan 2026 15:17:14 -0500 Subject: [PATCH 2/2] docs: add tui.attach and --attach flag documentation --- packages/web/src/content/docs/config.mdx | 1 + packages/web/src/content/docs/server.mdx | 26 +++++++++++++++++++ packages/web/src/content/docs/tui.mdx | 32 ++++++++++++++++++++++++ 3 files changed, 59 insertions(+) diff --git a/packages/web/src/content/docs/config.mdx b/packages/web/src/content/docs/config.mdx index 1474cb91558..9a2da035820 100644 --- a/packages/web/src/content/docs/config.mdx +++ b/packages/web/src/content/docs/config.mdx @@ -174,6 +174,7 @@ Available options: - `scroll_acceleration.enabled` - Enable macOS-style scroll acceleration. **Takes precedence over `scroll_speed`.** - `scroll_speed` - Custom scroll speed multiplier (default: `3`, minimum: `1`). Ignored if `scroll_acceleration.enabled` is `true`. - `diff_style` - Control diff rendering. `"auto"` adapts to terminal width, `"stacked"` always shows single column. +- `attach` - URL of an existing OpenCode server to connect to. Falls back to spawning a new server if unavailable. [Learn more about using the TUI here](/docs/tui). diff --git a/packages/web/src/content/docs/server.mdx b/packages/web/src/content/docs/server.mdx index 7229e09b22f..f81c5141cd8 100644 --- a/packages/web/src/content/docs/server.mdx +++ b/packages/web/src/content/docs/server.mdx @@ -68,6 +68,32 @@ The [`/tui`](#tui) endpoint can be used to drive the TUI through the server. For --- +#### Attach TUI to a running server + +You can attach a new TUI instance to an existing server using the `--attach` flag or `tui.attach` config: + +```bash +# Start the server +opencode serve --port 4096 + +# In another terminal, attach the TUI +opencode --attach http://localhost:4096 +``` + +Or configure it in your config file to always attach: + +```json title="opencode.json" +{ + "tui": { + "attach": "http://localhost:4096" + } +} +``` + +[Learn more about attaching to servers](/docs/tui#attach-to-server). + +--- + ## Spec The server publishes an OpenAPI 3.1 spec that can be viewed at: diff --git a/packages/web/src/content/docs/tui.mdx b/packages/web/src/content/docs/tui.mdx index 085eb6169f8..2250e8de6ff 100644 --- a/packages/web/src/content/docs/tui.mdx +++ b/packages/web/src/content/docs/tui.mdx @@ -373,6 +373,38 @@ You can customize TUI behavior through your OpenCode config file. - `scroll_acceleration` - Enable macOS-style scroll acceleration for smooth, natural scrolling. When enabled, scroll speed increases with rapid scrolling gestures and stays precise for slower movements. **This setting takes precedence over `scroll_speed` and overrides it when enabled.** - `scroll_speed` - Controls how fast the TUI scrolls when using scroll commands (minimum: `1`). Defaults to `3`. **Note: This is ignored if `scroll_acceleration.enabled` is set to `true`.** +- `attach` - URL of an existing OpenCode server to connect to instead of spawning a new one. Falls back to spawning a new server if unavailable. [Learn more](#attach-to-server). + +--- + +## Attach to server + +You can attach the TUI to an existing OpenCode server instead of spawning a new one. This is useful when running OpenCode as a persistent background service. + +### Via CLI flag + +```bash +opencode --attach http://localhost:4096 +``` + +### Via config + +```json title="opencode.json" +{ + "$schema": "https://opencode.ai/config.json", + "tui": { + "attach": "http://opencode.local" + } +} +``` + +When `attach` is configured, the TUI will: + +1. Check if the server is available via a health check +2. If available, connect directly without spawning a new worker +3. If unavailable, fall back to spawning a new server + +The CLI flag `--attach` takes precedence over the config file setting. ---