Skip to content
Merged
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ Refer to the [Quick Chat](#-quick-chat) section for more details.
- [Server-Sent Events (SSE) autocmds](#-server-sent-events-sse-autocmds)
- [Quick Chat](#quick-chat)
- [Setting up Opencode](#-setting-up-opencode)
- [Recipes](./docs/recipes)

## ⚠️Caution

Expand Down
21 changes: 21 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Opencode.nvim Documentation

## Recipes

Community-contributed configurations and workflows for opencode.nvim.

### Available Recipes

- [Bidirectional TUI/nvim Sync](./recipes/bidirectional-sync/README.md) - Share sessions between TUI and nvim plugin seamlessly
- [Three-State Layout Toggle](./recipes/three-state-layout/README.md) - Instantly switch between code/split/dialog viewing modes

### Contributing a Recipe

Want to share your setup? Use the [Recipe Template](./recipes/TEMPLATE.md) to ensure consistency:

- Start with the problem you're solving
- Include a GIF demonstration
- Provide step-by-step setup instructions
- Cross-reference related recipes

Recipes should be self-contained and solve a specific workflow need.
43 changes: 43 additions & 0 deletions docs/recipes/TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Recipe Template

One-line description of what this recipe enables.

![Demo animation](./demo.gif)

## Problem

What workflow pain does this solve? 2-3 specific scenarios.

## Solution

Brief description of the approach.

## Quick Start

### Prerequisites

- Required tools/versions

### Setup

```bash
# Installation steps
```

### Usage

```lua
-- Configuration snippet
```

## How It Works

Key technical details. Link to relevant APIs if needed.

## Integration

- Combine with [other-recipe](../other-recipe/README.md) for more capabilities

---

Contributed by @[username](https://github.com/username)
121 changes: 121 additions & 0 deletions docs/recipes/bidirectional-sync/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# Bidirectional TUI/nvim Sync

Switch seamlessly between opencode TUI and nvim plugin without losing context.

![Bidirectional sync demo](./bidirectional-sync.gif)

## Problem

Switching between opencode TUI and nvim plugin feels like using two separate tools:

1. **Session isolation** - Start a conversation in TUI, switch to nvim, your context is lost
2. **Double initialization** - Each interface spawns its own server, wasting 15-20s on MCP loading every time
3. **Mental overhead** - You have to remember which interface you were using for what task

You want TUI for complex workflows and nvim for quick code edits, seamlessly.

## Solution

Use a single shared HTTP server that both TUI and nvim connect to:

- Start server once, use from any interface
- Session state persists across TUI/nvim switches
- Zero context loss when changing tools

## State Flow

```mermaid
flowchart LR
A[Terminal: oc-sync.sh] -->|starts| B[Shared Server :4096]
C[nvim] -->|connects| B
D[TUI] -->|connects| B
B -->|shares session| C
B -->|shares session| D
```

## Quick Start

### 1. Install Wrapper

```bash
chmod +x oc-sync.sh
cp oc-sync.sh ~/.local/bin/
```

### 2. Configure Nvim

Add to your opencode.nvim setup:

```lua
server = {
url = "localhost",
port = 4096,
timeout = 30, -- First boot can be slow (MCP initialization)
auto_kill = false, -- Keep server alive when TUI is active
spawn_command = function(port, url)
local script = vim.fn.expand("~/.local/bin/oc-sync.sh")
vim.fn.system(script .. " --sync-ensure")
return nil -- Server lifecycle managed externally
end,
}
```

### 3. Use It

Terminal 1 - Start TUI:
```bash
oc-sync.sh /path/to/project
```

Terminal 2 - Open nvim in same directory:
```bash
cd /path/to/project && nvim
```

Both will share the same session state.

## Implementation Notes

- `oc-sync.sh --sync-ensure` starts shared HTTP server (port 4096)
- TUI runs `opencode attach <endpoint>` to connect
- Nvim plugin connects to same endpoint
- Server stays alive until manually killed

## Customization

Environment variables:

| Variable | Default | Description |
|----------|---------|-------------|
| `OPENCODE_SYNC_PORT` | 4096 | HTTP server port |
| `OPENCODE_SYNC_HOST` | 127.0.0.1 | Server bind address |
| `OPENCODE_SYNC_WAIT_TIMEOUT_SEC` | 20 | Startup timeout |

## Troubleshooting

**Port already in use?**
```bash
# Check what's using it
lsof -i :4096

# Kill the process
kill $(lsof -t -i :4096)
```

**MCP plugins taking too long?**
```bash
# Increase timeout
export OPENCODE_SYNC_WAIT_TIMEOUT_SEC=60
```

**Server not responding?**
```bash
# Check health
curl http://localhost:4096/global/health
```

## Integration Ideas

- Combine with [three-state-layout](../three-state-layout/README.md) to also control how you view opencode within nvim
- Use terminal multiplexers (tmux/zellij) to manage both TUI and nvim in one window
- Add shell aliases for common project paths
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
175 changes: 175 additions & 0 deletions docs/recipes/bidirectional-sync/oc-sync.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
#!/bin/bash
# oc-sync.sh: low-complexity opencode sync wrapper
# - default/path argument: ensure shared server, then attach
# - other commands: pass through to opencode found in PATH
# - fail fast when no executable opencode can be resolved

set -euo pipefail

DEFAULT_PORT="${OPENCODE_SYNC_PORT:-4096}"
DEFAULT_HOST="${OPENCODE_SYNC_HOST:-127.0.0.1}"
SERVER_READY_TIMEOUT_SEC="${OPENCODE_SYNC_WAIT_TIMEOUT_SEC:-20}"

log_info() { echo "[oc-sync] $*" >&2; }
log_error() { echo "[oc-sync] ERROR: $*" >&2; }

build_endpoint() { echo "http://${1}:${2}"; }

check_health() {
curl -sf "${1}/global/health" >/dev/null 2>&1
}

port_in_use() {
lsof -i ":${1}" -sTCP:LISTEN >/dev/null 2>&1
}

port_owner_pid() {
lsof -i ":${1}" -sTCP:LISTEN -t 2>/dev/null | head -1
}

_norm_path() {
local p="$1"
local d
d="$(cd "$(dirname "$p")" 2>/dev/null && pwd -P)" || return 1
printf "%s/%s" "$d" "$(basename "$p")"
}

get_opencode_bin() {
local script_path
local candidate
local norm_script
local norm_candidate

script_path="$(_norm_path "${BASH_SOURCE[0]}" 2>/dev/null || printf "%s" "${BASH_SOURCE[0]}")"
norm_script="${script_path}"

candidate="$(command -v opencode 2>/dev/null || true)"
if [ -z "${candidate}" ] || [ ! -x "${candidate}" ]; then
log_error "opencode not found in PATH"
return 1
fi

norm_candidate="$(_norm_path "${candidate}" 2>/dev/null || printf "%s" "${candidate}")"
if [ "${norm_candidate}" = "${norm_script}" ]; then
log_error "resolved opencode points to wrapper itself: ${candidate}"
log_error "fix PATH to point to the real opencode binary"
return 1
fi

echo "${candidate}"
}

wait_for_server() {
local endpoint="$1"
local start_ts
local now_ts
start_ts="$(date +%s)"
while true; do
if check_health "${endpoint}"; then
return 0
fi
now_ts="$(date +%s)"
if [ $((now_ts - start_ts)) -ge "${SERVER_READY_TIMEOUT_SEC}" ]; then
return 1
fi
sleep 0.5
done
}

start_server() {
local host="$1"
local port="$2"
local endpoint
local opencode_bin
endpoint="$(build_endpoint "${host}" "${port}")"

if port_in_use "${port}"; then
local pid
pid="$(port_owner_pid "${port}")"
log_error "Port ${port} is in use (PID: ${pid:-unknown})"
return 1
fi

opencode_bin="$(get_opencode_bin)" || return 1

log_info "Starting server on ${host}:${port}..."
nohup "${opencode_bin}" serve --port "${port}" --hostname "${host}" \
</dev/null >/dev/null 2>&1 &

if wait_for_server "${endpoint}"; then
log_info "Server started"
return 0
fi

log_error "Server failed to start within timeout"
return 1
}

# Ensure the server is running and print endpoint to stdout.
ensure_server() {
local port="${1:-$DEFAULT_PORT}"
local host="${2:-$DEFAULT_HOST}"
local endpoint
endpoint="$(build_endpoint "${host}" "${port}")"

if check_health "${endpoint}"; then
echo "${endpoint}"
return 0
fi

if port_in_use "${port}"; then
local pid
pid="$(port_owner_pid "${port}")"
log_error "Port ${port} occupied by PID ${pid:-unknown} but not healthy"
return 1
fi

start_server "${host}" "${port}" || return 1
echo "${endpoint}"
}

handler_passthrough() {
local opencode_bin
opencode_bin="$(get_opencode_bin)" || exit 1
exec "${opencode_bin}" "$@"
}

handler_wrap_tui() {
local endpoint
local opencode_bin
local work_dir
endpoint="$(ensure_server)" || {
log_error "Failed to ensure shared server"
exit 1
}
opencode_bin="$(get_opencode_bin)" || exit 1
work_dir="${PWD}"
if [ "$#" -gt 0 ] && [ -d "$1" ]; then
work_dir="$1"
shift
fi
exec "${opencode_bin}" attach "${endpoint}" --dir "${work_dir}" "$@"
}

route_command() {
local cmd="${1:-}"

if [ "${cmd}" = "--sync-ensure" ]; then
shift
ensure_server "$@"
return
fi

if [ -z "${cmd}" ] || [ -d "${cmd}" ]; then
handler_wrap_tui "$@"
return
fi

handler_passthrough "$@"
}

main() {
route_command "$@"
}

main "$@"
Loading
Loading