Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
100 commits
Select commit Hold shift + click to select a range
c666167
feat(protocol): dual-protocol client for OpenCode V1 and V2 wire APIs
jensenojs Sep 15, 2026
5a020ab
fix(renderer): reach older history through the Observation contract
jensenojs Sep 16, 2026
bbe8d3c
fix: perpetual loading state for v1 server
sudo-tee Sep 16, 2026
8bfd1d4
refactor(formatter): pass the tool registry into task formatting
jensenojs Sep 16, 2026
02c54f5
refactor(server): move kill_pid into util
jensenojs Sep 16, 2026
7390619
fix(util): kill the whole process tree recursively
jensenojs Sep 16, 2026
d48d397
feat(topology): rule-level allowed exceptions
jensenojs Sep 16, 2026
4321eff
fix(tests): repair broken port_mapping spec and catch syntax errors
jensenojs Sep 16, 2026
6260ba6
fix: 400 error when body is empty in http protocol
sudo-tee Sep 16, 2026
927ad58
fix: new session tab not receiving location
sudo-tee Sep 16, 2026
9bc4f3c
fix: broken session tabs tests
sudo-tee Sep 16, 2026
c282dcb
feat: support async prompt submission and V1 reply in quick chat
sudo-tee Sep 16, 2026
c1929ec
refactor(server): centralize server connection via server_job.ensure_…
sudo-tee Sep 16, 2026
7e266aa
perf(server): cache connection health checks
sudo-tee Sep 16, 2026
2739f98
perf(renderer): reconcile incremental output updates
sudo-tee Sep 16, 2026
9ff60fe
perf(renderer): cache reference facts incrementally
sudo-tee Sep 16, 2026
f21d345
fix(session): restore last used model
sudo-tee Sep 16, 2026
bdf21f2
feat(ui): add streaming event throttle and collapse for message renders
sudo-tee Sep 16, 2026
f1ab7ed
feat(messaging): apply model selection for V2 sessions
sudo-tee Sep 16, 2026
0318f95
refactor(ui): coalesce topbar renders and extend stats handling
sudo-tee Sep 16, 2026
4317bdd
fix(protocol): use ascending message IDs for proper server completion…
sudo-tee Sep 16, 2026
5b711a9
feat: unify quick chat reply handling via protocol-independent reques…
sudo-tee Sep 16, 2026
788ea5b
refactor(protocol): share HTTP and action lifecycle mechanics
sudo-tee Sep 17, 2026
4543f34
refactor(protocol): separate native fact conversion from observation …
sudo-tee Sep 17, 2026
2f0a947
fix(protocol): give each submission its own completion and cancellation
sudo-tee Sep 17, 2026
58a7eb1
fix(session): define cancel target session
sudo-tee Sep 17, 2026
b82a914
fix(protocol): carry editor context as attachments, not visible text
jensenojs Sep 17, 2026
95a4556
fix(ui): preserve usage stats across updates and focus changes
sudo-tee Sep 17, 2026
6c29d37
fix(ui): prevent cursor jumps during history loads and auto-scroll
sudo-tee Sep 17, 2026
13749d4
refactor(ui): hoist hide_rendered_message before its call site
sudo-tee Sep 17, 2026
1b4748c
refactor(protocols): renderer session batch
sudo-tee Sep 17, 2026
6b020e2
fix(scroll): account for folds when bottom-aligning scroll
sudo-tee Sep 17, 2026
bdfb7cb
refactor(messaging): consume attachments against per-tab context
sudo-tee Sep 17, 2026
0fe2822
fix(ui): re-render footer on loading animation start/stop
sudo-tee Sep 17, 2026
65a2af1
fix: restore top viewport without moving cursor
sudo-tee Sep 17, 2026
34c8677
feat(session): move active session observation out of renderer
sudo-tee Sep 17, 2026
e334be9
refactor(renderer): own renderer context per session tab
sudo-tee Sep 18, 2026
e0bb84f
refactor(agent): move picker flows into command handler
sudo-tee Sep 18, 2026
86e8018
refactor(ui): own session switch presentation
sudo-tee Sep 18, 2026
8e1b3ac
refactor(session): centralize session ops in session_runtime
sudo-tee Sep 18, 2026
7cae861
refactor(ui): decouple loading animation from footer via callback
sudo-tee Sep 18, 2026
083efd9
refactor(image_handler): extract clipboard save from paste flow
sudo-tee Sep 18, 2026
afcd1af
refactor(ui): extract panel window preparation into ui.prepare_windows
sudo-tee Sep 18, 2026
666ed22
refactor(keymap): centralize panel keymap bindings in state subscription
sudo-tee Sep 18, 2026
af554eb
refactor(ui): decouple footer render from output_window module
sudo-tee Sep 18, 2026
03c879b
refactor(ui): subscribe contextual actions to window store
sudo-tee Sep 18, 2026
d56ac1c
refactor(ui): subscribe window autocmds to lifecycle store
sudo-tee Sep 18, 2026
cbadb2b
refactor(input): move shell and slash dispatch out of input_window
sudo-tee Sep 18, 2026
ea8726e
refactor(autocmds): consolidate panel autocmd setup into single module
sudo-tee Sep 18, 2026
ce8e5c9
feat(v2): handle session.retry.scheduled execution event
sudo-tee Sep 18, 2026
6f5f7ab
refactor(slash-commands): extract builtin definitions into dedicated …
sudo-tee Sep 18, 2026
cfde190
fix(server): retry health probe while native service starts
sudo-tee Sep 18, 2026
6d2910a
fix(session-picker): render messages before releasing observation state
sudo-tee Sep 18, 2026
e6ed354
fix: retry fetching opencode agents while server initializes
sudo-tee Sep 18, 2026
1a4a5a0
refactor(ui): route output window gg through keymap config
sudo-tee Sep 18, 2026
11b416e
feat: select session tab under cursor or mouse
sudo-tee Sep 18, 2026
c5c72e3
refactor(session): replace ui.switch_session with session_runtime.sel…
sudo-tee Sep 18, 2026
43f7efa
feat(commands): route timeline picker actions through command dispatch
sudo-tee Sep 18, 2026
598df85
refactor(quick_chat): delegate session startup to create_detached_ses…
sudo-tee Sep 18, 2026
32d49d9
fix: normalize model variants by id in get_model_info
sudo-tee Sep 18, 2026
9f0447f
fix(protocol): align V2 endpoints with opencode 2.0.8
jensenojs Sep 19, 2026
6fbcaed
fix(test): give inline_input resize polling a window that tolerates s…
jensenojs Sep 19, 2026
63b51a9
fix(protocol): drop stale prompt mentions instead of failing the page
jensenojs Sep 21, 2026
ac7022f
fix(protocol): clear the files field after all attachments are collected
jensenojs Sep 21, 2026
0db3c2e
refactor(server): decouple protocol connections and reuse legacy servers
sudo-tee Sep 21, 2026
41c54b4
fix(v2): prefer id over modelID in model catalog
sudo-tee Sep 21, 2026
66d2b5d
feat(protocol): render native V2 patch and shell tools
sudo-tee Sep 21, 2026
ea2a203
feat(protocols/v2): project native tool inputs into formatter fields
sudo-tee Sep 21, 2026
83fd776
fix(ui): fall back to new session title when active session title is …
sudo-tee Sep 21, 2026
801a201
fix(ui): close respects persist_state when tearing down windows
sudo-tee Sep 21, 2026
28c57c3
fix(http): encode nested query params with bracket notation
sudo-tee Sep 21, 2026
085ee9b
fix(protocols/v2): include decision and message in permission reply
sudo-tee Sep 21, 2026
220401e
fix(context): mention pasted images by basename
sudo-tee Sep 21, 2026
68ae98b
fix: detect pasted images from filename instead of temp path
sudo-tee Sep 21, 2026
95216cb
refactor: route undo/redo through Observation for consistent state
sudo-tee Sep 21, 2026
4ed3b6f
refactor(v2): split observation into specific submodules
sudo-tee Sep 21, 2026
fcb1418
fix(renderer): use latest assistant usage over cumulative session usage
sudo-tee Sep 21, 2026
ccaab11
refactor(protocol): add runtime shape validator and typed protocol co…
sudo-tee Sep 22, 2026
b50c163
chore: add emmylua static type checking tooling
sudo-tee Sep 22, 2026
07da385
docs: reference check_types.sh wrapper in type validation step
sudo-tee Sep 22, 2026
a584324
feat: normalize V2 tool metadata into structured changes
sudo-tee Sep 22, 2026
d651564
fix(ui): auto-scroll to bottom on local user message submission
sudo-tee Sep 22, 2026
393f8c5
feat(ui): format V2 websearch and execute tools
sudo-tee Sep 22, 2026
259e324
fix(ui): preserve viewport position when loading older messages
sudo-tee Sep 22, 2026
751d785
fix(context): skip context reload when focus returns to panel
sudo-tee Sep 22, 2026
6675a81
fix(session): clear stale attachments before loading context on new s…
sudo-tee Sep 22, 2026
ed65a86
ci: add typecheck job to GitHub Actions workflow
sudo-tee Sep 22, 2026
52a8122
fix(protocol): rename sessions through the PATCH session endpoint
jensenojs Sep 23, 2026
ea83ac0
feat(protocol): check the V2 contract against the server at startup
jensenojs Sep 23, 2026
9ec4f8d
feat(protocol): surface contract drift through vim.notify with an upd…
jensenojs Sep 23, 2026
5b2e7dc
docs: remove the migration draft
jensenojs Sep 23, 2026
1507691
test(input): follow submit flow moved to workflow and autocmds
jensenojs Sep 23, 2026
5d5bd0f
Refactor message submission through protocol observations
sudo-tee Sep 23, 2026
f329d44
refactor: fix Lua type annotations across UI modules
sudo-tee Sep 23, 2026
b583906
refactor(messaging): extract message send pipeline into helpers
sudo-tee Sep 23, 2026
98556db
fix(architecture): remove protocol config dependency
sudo-tee Sep 23, 2026
5f0a9e4
feat(context): deduplicate automatic payloads per session
sudo-tee Sep 23, 2026
7bd6d36
fix(session-tabs): resolve send promises only after request completion
sudo-tee Sep 23, 2026
abee5e4
refactor: consolidate session and location type annotations
sudo-tee Sep 23, 2026
c9c84fd
fix(services): await sent message count updates
sudo-tee Sep 23, 2026
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 .emmyrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
"version": "LuaJIT"
},
"diagnostics": {
"globals": [
"vim"
],
"enables": [
"missing-global-doc"
]
Expand All @@ -15,6 +18,11 @@
],
"ignoreGlobs": [
"**/*_spec.lua"
],
"ignoreDir": [
"tests/manual/deps/",
"tests/data/deps/",
"deps"
]
}
}
25 changes: 25 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,31 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
args: --check . -g '*.lua' -g '!deps/'

typecheck:
runs-on: ubuntu-latest
name: typecheck
steps:
- uses: actions/checkout@v4

- name: setup neovim
id: setup_nvim
uses: rhysd/action-setup-vim@v1
with:
neovim: true
version: v0.11.4

- name: Add nvim to PATH
run: echo "${{ steps.setup_nvim.outputs.executable }}" >> $GITHUB_PATH

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Install emmylua_check
run: cargo install emmylua_check --version 0.25.1 --locked

- name: Run type checks
run: ./check_types.sh -f github

test:
timeout-minutes: 4
strategy:
Expand Down
17 changes: 14 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,22 @@
- **Run a single test:** Replace the directory in the above command with the test file path, e.g.:
- `./run_tests.sh -t tests/unit/test_example.lua`

## Code Style Guidelines
# Developer Environment: EmmyLua Analyzer Rust (emmylua_ls)

- **Static Type Enforcement:** We use `emmylua-analyzer-rust` for strict type checking. Assume the LSP handles all type diagnostics.
- **Zero Defensive Over-Engineering:** Do not write manual runtime code (`type()`, `if nil`, or fallback operators) to catch typing errors. The Rust-backed LSP will catch them at compile-time.
- **Annotation-Only Contracts:** Document complex shapes using `---@class`, `---@alias`, and inline shapes. If a property or parameter is optional, strictly use the `?` marker (e.g., `---@param options? Table`).
- **Idiomatic Lua Flow:** Write clean, raw, performant Lua code. Let the application "fail fast" if code contract invariants are broken at runtime.
- **Comments:** Avoid obvious comments that merely restate what the code does. Only add comments when necessary to explain _why_ something is done, not _what_ is being done. Prefer self-explanatory code.
- **Config:** Centralize in `config.lua`. Use deep merge for user overrides.
- **Types:** Use Lua annotations (`---@class`, `---@field`, etc.) for public APIs/config.

# Code Validation Step (Mandatory)

Before you mark a Lua code generation task as complete, you must validate your types against the project's static analysis rules:

1. Run the `./check_types.sh` CLI tool over the generated workspace to execute `emmylua_check`.
2. Review the output for any static analysis diagnostics (e.g., syntax errors, type mismatches, missing fields).
3. If `emmylua_check` flags any type mismatches, you must fix the code's annotations or types—**do not write manual runtime boilerplate checking (`type()`) to quiet the linter**.
4. Iterate until `emmylua_check` passes with zero errors.

## Dependency Topology Tool

Expand Down
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ require('opencode').setup({
path_map = nil, -- Map host paths to server paths: string ('/app') or function(path) -> string
username = nil, -- Username for Basic auth. Falls back to OPENCODE_SERVER_USERNAME env var, then "opencode"
password = nil, -- Password for Basic auth. Falls back to OPENCODE_SERVER_PASSWORD env var
password_file = nil, -- Shared V1 password file; fixed ports default to an owner-only per-port state file
},

keymap = {
Expand Down Expand Up @@ -319,7 +320,7 @@ require('opencode').setup({
info = false, -- Include diagnostics info in the context (default to false
warning = true, -- Include diagnostics warnings in the context
error = true, -- Include diagnostics errors in the context
only_closest = false, -- If true, only diagnostics for cursor/selection
only_closest = true, -- Only diagnostics for cursor/selection; disable to include the whole buffer
},
current_file = {
enabled = true, -- Include current file path and content in the context
Expand Down Expand Up @@ -532,15 +533,15 @@ Available icon keys (see implementation at lua/opencode/ui/icons.lua lines 7-29)

### Window Persistence Behavior

`ui.persist_state` controls how `toggle` behaves:
`ui.persist_state` controls how `toggle` and `close` behave:

- `persist_state = true` (default): `toggle()` hides/restores the UI and keeps buffers/session view in memory for fast restore.
- `persist_state = false`: `toggle()` fully tears down UI buffers and recreates them on next open.

Related APIs:

- `require('opencode.api').toggle()` follows the `persist_state` behavior above.
- `require('opencode.api').close()` always fully closes and clears hidden snapshot state.
- `require('opencode.api').close()` preserves buffers when `persist_state = true`; otherwise it fully closes.
- `require('opencode.api').hide()` preserves buffers only when `persist_state = true`; otherwise it behaves like close.

### Picker Layout
Expand Down Expand Up @@ -798,6 +799,10 @@ Opencode can issue permission requests for potentially destructive operations (f

The following editor context is automatically captured and included in your conversations.

Unchanged automatic payloads (diagnostics, buffer, cursor data, and staged diff) are sent once per session, then sent again only after their content changes. Explicit file mentions and selections are always sent.

When a selection targets the current file, the automatic current-file attachment is skipped. The selected lines provide focused context; the agent can read more of the file when needed. An explicit file mention is still honored.

| Context Type | Description |
| --------------- | ---------------------------------------------------- |
| Current file | Path to the focused file before entering opencode |
Expand Down
29 changes: 29 additions & 0 deletions check_types.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"

if ! command -v nvim >/dev/null 2>&1; then
echo 'error: nvim is required to resolve VIMRUNTIME' >&2
exit 127
fi

if ! command -v emmylua_check >/dev/null 2>&1; then
echo 'error: emmylua_check is required' >&2
exit 127
fi

if [[ ! -d "${VIMRUNTIME:-}" ]]; then
VIMRUNTIME="$(nvim --headless -u NONE -i NONE --noplugin \
--cmd 'lua io.write(vim.env.VIMRUNTIME or "")' \
--cmd 'qa!' 2>/dev/null)"
fi

if [[ ! -d "${VIMRUNTIME:-}" ]]; then
echo 'error: unable to resolve a valid VIMRUNTIME' >&2
exit 1
fi

export VIMRUNTIME
exec emmylua_check . "$@"
107 changes: 56 additions & 51 deletions docs/recipes/bidirectional-sync/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Use a single shared HTTP server that both TUI and nvim connect to:

```mermaid
flowchart LR
A[Terminal: oc-sync.sh] -->|starts| B[Shared Server :4096]
A[Terminal: native opencode --server] -->|connects| B[Shared Server]
C[nvim] -->|connects| B
D[TUI] -->|connects| B
B -->|shares session| C
Expand All @@ -35,53 +35,40 @@ flowchart LR

## Quick Start

### 1. Install Wrapper
### V2 native service

V2 2.0.x 的 TUI 默认连接 OpenCode 自己管理的后台 service。Neovim 默认也使用这个 service,无需 wrapper、固定端口、额外 password_file 或用户填写 ownership。下面的 V2 路径已在 2.0.3 实测。

```lua
require("opencode").setup({
server = { timeout = 30 },
})
```

```bash
chmod +x oc-sync.sh
cp oc-sync.sh ~/.local/bin/
# 普通 TUI 使用原生后台 service
opencode /path/to/project
# 继续 Neovim 正在显示的同一 session
opencode --session ses_... /path/to/project
```

### 2. Configure Nvim
插件用 CLI 的 `service status` 获取地址、`service get password` 获取凭据;仅当状态明确为 `stopped` 时调用 `service start`。HTTP health 决定 V1/V2 协议。Neovim 退出不关闭原生 service。CLI 能力检查只选择启动入口,不代替 server health 的协议判定。

Add to your opencode.nvim setup:
同一目录不代表两端自动选中同一 session;在另一端显式 resume 同一 session。两端共享消息、工具、question 与 permission 状态,各自保留窗口、光标和未提交输入。

```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,
}
```
### V1 and explicit servers

### 3. Use It
旧 V1 CLI 没有 service 命令,插件保留原有本地 `serve` 路径。已有 `server.url`、`port`、`spawn_command` 的配置继续按显式连接处理。

Terminal 1 - Start TUI:
```bash
oc-sync.sh /path/to/project
```
V1 的共享服务需要两端使用同一 endpoint 和凭据,TUI 原生命令是:

Terminal 2 - Open nvim in same directory:
```bash
cd /path/to/project && nvim
opencode attach http://127.0.0.1:4096 --dir /path/to/project --session ses_...
```

Both will share the same session state.
V2 的显式远端连接可使用 `opencode --server <endpoint> --session ses_... <directory>`,并按服务要求提供 `OPENCODE_PASSWORD`。只有该显式场景需要双方约定地址。以下 legacy helper 配置仅适用于 V1;V2 无需安装或调用 `oc-sync.sh`。

## 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
## V1 legacy helper configuration

Environment variables:

Expand All @@ -90,29 +77,47 @@ Environment variables:
| `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 |
| `OPENCODE_SYNC_PASSWORD_FILE` | `$XDG_STATE_HOME/nvim/opencode/server-password` or `~/.local/state/nvim/opencode/server-password` | Shared credential file |

## Troubleshooting

**Port already in use?**
```bash
# Check what's using it
lsof -i :4096
V2 先用原生命令检查服务状态和真实 health:

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

**MCP plugins taking too long?**
```bash
# Increase timeout
export OPENCODE_SYNC_WAIT_TIMEOUT_SEC=60
opencode service status
opencode api GET /api/health
```

**Server not responding?**
```bash
# Check health
curl http://localhost:4096/global/health
```
插件错误与 CLI 错误应分别检查。401/403 不会触发私有 server 启动或 V1 回退。无需查找并杀掉某个约定端口的进程。

The nvim client and TUI share the HTTP server and session data. Selecting a
session in one frontend does not select it in the other frontend. Pass
`--session ses_...` when both clients must display the same conversation. Each
frontend still owns its windows, cursor, input draft, and current selection.
Native V2 service lifecycle belongs to OpenCode. For an explicitly managed shared server, do not use
`--shutdown-after-last-client` when starting it.

Server ownership controls shutdown and port cleanup only. Prompt completion uses
the admission ID returned to nvim and the matching inbox events from the shared
server. The server runs one serial execution horizon per session, so messages
delivered by another client during that horizon are included in the same next
terminal event. The plugin keeps one local prompt in flight per session. A lost
event stream, or evidence that the server started overlapping execution horizons,
resolves that local completion as `unknown`; messages already stored by the server
remain visible to both frontends after a snapshot refresh.

For a V1 explicit launcher shared with the TUI, set `server.password_file` to a state-directory path. Fixed-port
V1 servers otherwise use an owner-only per-port file under Neovim's state directory so later nvim clients reuse
the same credential. On a launcher path, the plugin persists the selected password before
starting its local server, so a later nvim process and, when explicitly configured, the TUI read the same value.
Plugin credential selection is deterministic: `server.password`, then the
configured password file, then `OPENCODE_PASSWORD`, then
`OPENCODE_SERVER_PASSWORD`. This recipe leaves `server.password` unset and uses
the password file as the shared source. When the file is absent, the V1 helper
persists the environment password or generates one; an existing invalid file
fails immediately instead of being replaced.

The legacy helper rejects a CLI with the native service command before creating credentials or starting a process. Its health endpoint is `/global/health`, where a healthy V1 JSON response is required; the liveness check does not depend on the server version. HTML 200 and authentication errors are failures. V2 never enters this script's launcher path.

## Integration Ideas

Expand Down
Loading
Loading