Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
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"

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))

Expand Down Expand Up @@ -46,6 +49,11 @@ export function DialogStatus() {
<text fg={theme.textMuted}>esc</text>
</box>
<text fg={theme.textMuted}>OpenCode v{Installation.VERSION}</text>
<Show when={isAttached()}>
<text fg={theme.textMuted}>
Attached to <span style={{ fg: theme.success }}>{sdk.url}</span>
</text>
</Show>
<Show when={Object.keys(sync.data.mcp).length > 0} fallback={<text fg={theme.text}>No MCP Servers</text>}>
<box>
<text fg={theme.text}>{Object.keys(sync.data.mcp).length} MCP Servers</text>
Expand Down
78 changes: 72 additions & 6 deletions packages/opencode/src/cli/cmd/tui/thread.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -40,6 +42,38 @@ function createEventSource(client: RpcClient): EventSource {
}
}

async function tryAttachToServer(url: string): Promise<boolean> {
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<string | undefined> {
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",
Expand Down Expand Up @@ -71,24 +105,56 @@ 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 () => {
if (typeof OPENCODE_WORKER_PATH !== "undefined") return OPENCODE_WORKER_PATH
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(
Expand Down
5 changes: 5 additions & 0 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/content/docs/config.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down
26 changes: 26 additions & 0 deletions packages/web/src/content/docs/server.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
32 changes: 32 additions & 0 deletions packages/web/src/content/docs/tui.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---

Expand Down
Loading