Skip to content

Commit 901f583

Browse files
committed
docs: add bidirectional-sync and three-state-layout recipes
- bidirectional-sync: share sessions between TUI/nvim via shared HTTP server - three-state-layout: toggle between code/split/dialog viewing modes - add recipe template for future contributions - include GIF demonstrations for both workflows
1 parent dffa3f3 commit 901f583

9 files changed

Lines changed: 537 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ Refer to the [Quick Chat](#-quick-chat) section for more details.
6262
- [Server-Sent Events (SSE) autocmds](#-server-sent-events-sse-autocmds)
6363
- [Quick Chat](#quick-chat)
6464
- [Setting up Opencode](#-setting-up-opencode)
65+
- [Recipes](./docs/recipes)
6566

6667
## ⚠️Caution
6768

docs/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Opencode.nvim Documentation
2+
3+
## Recipes
4+
5+
Community-contributed configurations and workflows for opencode.nvim.
6+
7+
### Available Recipes
8+
9+
- [Bidirectional TUI/nvim Sync](./recipes/bidirectional-sync/README.md) - Share sessions between TUI and nvim plugin seamlessly
10+
- [Three-State Layout Toggle](./recipes/three-state-layout/README.md) - Instantly switch between code/split/dialog viewing modes
11+
12+
### Contributing a Recipe
13+
14+
Want to share your setup? Use the [Recipe Template](./recipes/TEMPLATE.md) to ensure consistency:
15+
16+
- Start with the problem you're solving
17+
- Include a GIF demonstration
18+
- Provide step-by-step setup instructions
19+
- Cross-reference related recipes
20+
21+
Recipes should be self-contained and solve a specific workflow need.

docs/recipes/TEMPLATE.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Recipe Template
2+
3+
One-line description of what this recipe enables.
4+
5+
![Demo animation](./demo.gif)
6+
7+
## Problem
8+
9+
What workflow pain does this solve? 2-3 specific scenarios.
10+
11+
## Solution
12+
13+
Brief description of the approach.
14+
15+
## Quick Start
16+
17+
### Prerequisites
18+
19+
- Required tools/versions
20+
21+
### Setup
22+
23+
```bash
24+
# Installation steps
25+
```
26+
27+
### Usage
28+
29+
```lua
30+
-- Configuration snippet
31+
```
32+
33+
## How It Works
34+
35+
Key technical details. Link to relevant APIs if needed.
36+
37+
## Integration
38+
39+
- Combine with [other-recipe](../other-recipe/README.md) for more capabilities
40+
41+
---
42+
43+
Contributed by @[username](https://github.com/username)
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# Bidirectional TUI/nvim Sync
2+
3+
Switch seamlessly between opencode TUI and nvim plugin without losing context.
4+
5+
![Bidirectional sync demo](./bidirectional-sync.gif)
6+
7+
## Problem
8+
9+
Switching between opencode TUI and nvim plugin feels like using two separate tools:
10+
11+
1. **Session isolation** - Start a conversation in TUI, switch to nvim, your context is lost
12+
2. **Double initialization** - Each interface spawns its own server, wasting 15-20s on MCP loading every time
13+
3. **Mental overhead** - You have to remember which interface you were using for what task
14+
15+
You want TUI for complex workflows and nvim for quick code edits, seamlessly.
16+
17+
## Solution
18+
19+
Use a single shared HTTP server that both TUI and nvim connect to:
20+
21+
- Start server once, use from any interface
22+
- Session state persists across TUI/nvim switches
23+
- Zero context loss when changing tools
24+
25+
## State Flow
26+
27+
```mermaid
28+
flowchart LR
29+
A[Terminal: oc-sync.sh] -->|starts| B[Shared Server :4096]
30+
C[nvim] -->|connects| B
31+
D[TUI] -->|connects| B
32+
B -->|shares session| C
33+
B -->|shares session| D
34+
```
35+
36+
## Quick Start
37+
38+
### 1. Install Wrapper
39+
40+
```bash
41+
chmod +x oc-sync.sh
42+
cp oc-sync.sh ~/.local/bin/
43+
```
44+
45+
### 2. Configure Nvim
46+
47+
Add to your opencode.nvim setup:
48+
49+
```lua
50+
server = {
51+
url = "localhost",
52+
port = 4096,
53+
timeout = 30, -- First boot can be slow (MCP initialization)
54+
auto_kill = false, -- Keep server alive when TUI is active
55+
spawn_command = function(port, url)
56+
local script = vim.fn.expand("~/.local/bin/oc-sync.sh")
57+
vim.fn.system(script .. " --sync-ensure")
58+
return nil -- Server lifecycle managed externally
59+
end,
60+
}
61+
```
62+
63+
### 3. Use It
64+
65+
Terminal 1 - Start TUI:
66+
```bash
67+
oc-sync.sh /path/to/project
68+
```
69+
70+
Terminal 2 - Open nvim in same directory:
71+
```bash
72+
cd /path/to/project && nvim
73+
```
74+
75+
Both will share the same session state.
76+
77+
## Implementation Notes
78+
79+
- `oc-sync.sh --sync-ensure` starts shared HTTP server (port 4096)
80+
- TUI runs `opencode attach <endpoint>` to connect
81+
- Nvim plugin connects to same endpoint
82+
- Server stays alive until manually killed
83+
84+
## Customization
85+
86+
Environment variables:
87+
88+
| Variable | Default | Description |
89+
|----------|---------|-------------|
90+
| `OPENCODE_SYNC_PORT` | 4096 | HTTP server port |
91+
| `OPENCODE_SYNC_HOST` | 127.0.0.1 | Server bind address |
92+
| `OPENCODE_SYNC_WAIT_TIMEOUT_SEC` | 20 | Startup timeout |
93+
94+
## Troubleshooting
95+
96+
**Port already in use?**
97+
```bash
98+
# Check what's using it
99+
lsof -i :4096
100+
101+
# Kill the process
102+
kill $(lsof -t -i :4096)
103+
```
104+
105+
**MCP plugins taking too long?**
106+
```bash
107+
# Increase timeout
108+
export OPENCODE_SYNC_WAIT_TIMEOUT_SEC=60
109+
```
110+
111+
**Server not responding?**
112+
```bash
113+
# Check health
114+
curl http://localhost:4096/global/health
115+
```
116+
117+
## Integration Ideas
118+
119+
- Combine with [three-state-layout](../three-state-layout/README.md) to also control how you view opencode within nvim
120+
- Use terminal multiplexers (tmux/zellij) to manage both TUI and nvim in one window
121+
- Add shell aliases for common project paths
7.43 MB
Loading
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
#!/bin/bash
2+
# oc-sync.sh: low-complexity opencode sync wrapper
3+
# - default/path argument: ensure shared server, then attach
4+
# - other commands: pass through to opencode found in PATH
5+
# - fail fast when no executable opencode can be resolved
6+
7+
set -euo pipefail
8+
9+
DEFAULT_PORT="${OPENCODE_SYNC_PORT:-4096}"
10+
DEFAULT_HOST="${OPENCODE_SYNC_HOST:-127.0.0.1}"
11+
SERVER_READY_TIMEOUT_SEC="${OPENCODE_SYNC_WAIT_TIMEOUT_SEC:-20}"
12+
13+
log_info() { echo "[oc-sync] $*" >&2; }
14+
log_error() { echo "[oc-sync] ERROR: $*" >&2; }
15+
16+
build_endpoint() { echo "http://${1}:${2}"; }
17+
18+
check_health() {
19+
curl -sf "${1}/global/health" >/dev/null 2>&1
20+
}
21+
22+
port_in_use() {
23+
lsof -i ":${1}" -sTCP:LISTEN >/dev/null 2>&1
24+
}
25+
26+
port_owner_pid() {
27+
lsof -i ":${1}" -sTCP:LISTEN -t 2>/dev/null | head -1
28+
}
29+
30+
_norm_path() {
31+
local p="$1"
32+
local d
33+
d="$(cd "$(dirname "$p")" 2>/dev/null && pwd -P)" || return 1
34+
printf "%s/%s" "$d" "$(basename "$p")"
35+
}
36+
37+
get_opencode_bin() {
38+
local script_path
39+
local candidate
40+
local norm_script
41+
local norm_candidate
42+
43+
script_path="$(_norm_path "${BASH_SOURCE[0]}" 2>/dev/null || printf "%s" "${BASH_SOURCE[0]}")"
44+
norm_script="${script_path}"
45+
46+
candidate="$(command -v opencode 2>/dev/null || true)"
47+
if [ -z "${candidate}" ] || [ ! -x "${candidate}" ]; then
48+
log_error "opencode not found in PATH"
49+
return 1
50+
fi
51+
52+
norm_candidate="$(_norm_path "${candidate}" 2>/dev/null || printf "%s" "${candidate}")"
53+
if [ "${norm_candidate}" = "${norm_script}" ]; then
54+
log_error "resolved opencode points to wrapper itself: ${candidate}"
55+
log_error "fix PATH to point to the real opencode binary"
56+
return 1
57+
fi
58+
59+
echo "${candidate}"
60+
}
61+
62+
wait_for_server() {
63+
local endpoint="$1"
64+
local start_ts
65+
local now_ts
66+
start_ts="$(date +%s)"
67+
while true; do
68+
if check_health "${endpoint}"; then
69+
return 0
70+
fi
71+
now_ts="$(date +%s)"
72+
if [ $((now_ts - start_ts)) -ge "${SERVER_READY_TIMEOUT_SEC}" ]; then
73+
return 1
74+
fi
75+
sleep 0.5
76+
done
77+
}
78+
79+
start_server() {
80+
local host="$1"
81+
local port="$2"
82+
local endpoint
83+
local opencode_bin
84+
endpoint="$(build_endpoint "${host}" "${port}")"
85+
86+
if port_in_use "${port}"; then
87+
local pid
88+
pid="$(port_owner_pid "${port}")"
89+
log_error "Port ${port} is in use (PID: ${pid:-unknown})"
90+
return 1
91+
fi
92+
93+
opencode_bin="$(get_opencode_bin)" || return 1
94+
95+
log_info "Starting server on ${host}:${port}..."
96+
nohup "${opencode_bin}" serve --port "${port}" --hostname "${host}" \
97+
</dev/null >/dev/null 2>&1 &
98+
99+
if wait_for_server "${endpoint}"; then
100+
log_info "Server started"
101+
return 0
102+
fi
103+
104+
log_error "Server failed to start within timeout"
105+
return 1
106+
}
107+
108+
# Ensure the server is running and print endpoint to stdout.
109+
ensure_server() {
110+
local port="${1:-$DEFAULT_PORT}"
111+
local host="${2:-$DEFAULT_HOST}"
112+
local endpoint
113+
endpoint="$(build_endpoint "${host}" "${port}")"
114+
115+
if check_health "${endpoint}"; then
116+
echo "${endpoint}"
117+
return 0
118+
fi
119+
120+
if port_in_use "${port}"; then
121+
local pid
122+
pid="$(port_owner_pid "${port}")"
123+
log_error "Port ${port} occupied by PID ${pid:-unknown} but not healthy"
124+
return 1
125+
fi
126+
127+
start_server "${host}" "${port}" || return 1
128+
echo "${endpoint}"
129+
}
130+
131+
handler_passthrough() {
132+
local opencode_bin
133+
opencode_bin="$(get_opencode_bin)" || exit 1
134+
exec "${opencode_bin}" "$@"
135+
}
136+
137+
handler_wrap_tui() {
138+
local endpoint
139+
local opencode_bin
140+
local work_dir
141+
endpoint="$(ensure_server)" || {
142+
log_error "Failed to ensure shared server"
143+
exit 1
144+
}
145+
opencode_bin="$(get_opencode_bin)" || exit 1
146+
work_dir="${PWD}"
147+
if [ "$#" -gt 0 ] && [ -d "$1" ]; then
148+
work_dir="$1"
149+
shift
150+
fi
151+
exec "${opencode_bin}" attach "${endpoint}" --dir "${work_dir}" "$@"
152+
}
153+
154+
route_command() {
155+
local cmd="${1:-}"
156+
157+
if [ "${cmd}" = "--sync-ensure" ]; then
158+
shift
159+
ensure_server "$@"
160+
return
161+
fi
162+
163+
if [ -z "${cmd}" ] || [ -d "${cmd}" ]; then
164+
handler_wrap_tui "$@"
165+
return
166+
fi
167+
168+
handler_passthrough "$@"
169+
}
170+
171+
main() {
172+
route_command "$@"
173+
}
174+
175+
main "$@"

0 commit comments

Comments
 (0)