Skip to content

Put OpenCode next to your editor

You’ve shipped a reviewed change to feedmill from a terminal, and it worked — but it still felt like a detour. Your real day happens in VS Code or Neovim, with an LSP underlining type errors in the gutter and your hands on muscle-memory shortcuts. A terminal agent that asks you to abandon all of that to live in a TUI is a tool you’ll reach for twice and then forget. The point of this lesson is the opposite: keep your editor exactly where it is, and slot OpenCode in beside it so the agent becomes part of the day instead of an exception to it.

There are two distinct ways to bridge the gap, and it’s worth keeping them straight from the start. The first keeps you in the TUI but borrows your editor for the parts a terminal is bad at — long prompts, file references, quick shell checks. The second runs OpenCode inside your IDE over a protocol called ACP. You’ll use both, for different moods.

The first friction you hit in any terminal agent is composing a real message. A one-liner is fine inline, but the moment you want to write three paragraphs describing a refactor across feedmill’s feed parsers, editing in a single-line prompt is misery. OpenCode’s answer is /editor: it opens a message composer in whatever editor your $EDITOR environment variable points at, you write the message there with all your normal editing power, save and close, and the composed text drops back into the conversation.

So set it once, the way you’d set it for git:

export EDITOR="code --wait"

The --wait matters: it tells VS Code to block until you close the file, which is exactly what /editor is waiting on. Neovim users just set export EDITOR="nvim"; emacs, emacsclient, whatever you already live in. Now in the TUI:

> /editor
(your editor opens an empty buffer; you write:)
the JSON-feed parser in internal/feed/json.go assumes every item has a
published date, but some sources only send updated. fall back to
updated when published is missing, and add a test for a feed that
only has updated.
(save, close — the message lands back in the prompt and sends)

That’s the same kind of outcome-first request you wrote by hand in the last lesson, except you composed it with real editing instead of fighting a prompt line.

Two smaller moves keep you on the keyboard the rest of the time. Typing @ in the prompt opens a fuzzy file picker scoped to the current working directory — start typing json and it surfaces internal/feed/json.go, you pick it, and the reference is attached to your message so the agent reads exactly the file you meant instead of guessing from a description:

> the dedupe logic in @internal/feed/dedupe.go is dropping items that
share a title but have different links — it should only dedupe on the
canonical URL. fix it.

And prefixing a message with ! runs it as a shell command straight from the TUI, dropping the output into the conversation as context the agent can then reason about:

> !go test ./internal/feed/...
--- FAIL: TestDedupeByURL (0.00s)
dedupe_test.go:41: expected 3 items, got 2
(output is now in the thread)
> that's the failure I meant — make the dedupe key the canonical URL

You ran the test without leaving the prompt, and the agent now sees the exact failure instead of your paraphrase of it. Between @ for files and ! for commands, you rarely have to break flow to give the agent what it needs.

/editor borrows your editor for one task at a time. ACP — the Agent Client Protocol — is the other direction entirely: it lets OpenCode run as the agent inside an ACP-capable IDE, so the conversation lives in a panel next to your code instead of in a separate terminal. ACP is an open, JSON-RPC protocol that standardizes how editors talk to coding agents, and OpenCode speaks it through a dedicated entry point:

opencode acp

You don’t usually type that yourself — you point your editor at it. Zed has built-in ACP support: add OpenCode to agent_servers in settings.json and pick it from agent: new thread. JetBrains is supported too, configured through an acp.json file (pointing at the OpenCode binary with the acp argument) rather than a packaged extension. Neovim users have ACP options as well — OpenCode’s docs list Avante.nvim and CodeCompanion.nvim as supported clients. VS Code isn’t covered in OpenCode’s own ACP docs; support there comes from community extensions (e.g. formulahendry/vscode-acp) rather than a first-party path. In every case the editor starts OpenCode as a subprocess and drives it over the protocol: sessions, tool calls, and the same approval prompts you’ve been answering in the TUI all flow through the IDE’s UI instead.

Keep the mental model clean: /editor is a TUI command that borrows your editor for composing one message; opencode acp is a separate runtime that hosts the whole agent inside your IDE. Same tool, two integration surfaces — and you’ll switch between them by habit, the TUI for a focused session and ACP when you want the agent in arm’s reach of the file you’re already editing.

Why this is more than convenience: shared diagnostics

Section titled “Why this is more than convenience: shared diagnostics”

Here’s the part that makes living next to your editor pay off beyond keystrokes. OpenCode can integrate with Language Server Protocol servers and feed their diagnostics back to the agent. LSP is disabled by default, so you enable it in config first — set "lsp": true to turn on all built-in servers (or "lsp": {} to keep the built-ins while adding your own); once enabled and pointed at feedmill’s typed Go codebase, OpenCode starts gopls. OpenCode auto-downloads missing language servers — gopls among them: if it isn’t on your PATH, OpenCode runs go install to fetch it on first use (provided the go toolchain is present and you haven’t set OPENCODE_DISABLE_LSP_DOWNLOAD=true to suppress downloads). The one thing it can’t supply is go itself — install the Go toolchain and OpenCode handles gopls from there — and reads what it reports. That’s the same language server your editor leans on to draw the red squiggles in your gutter.

The consequence is concrete. When the agent edits internal/feed/json.go and introduces a type mismatch or an unused import, it doesn’t have to wait for a test run or a fresh go build to find out — the LSP surfaces the diagnostic, and the agent sees the error you’d see, in the same place you’d see it:

⏵ Edit internal/feed/json.go
diagnostics: internal/feed/json.go:52:9 — cannot use item.Updated
(type *time.Time) as type time.Time in assignment
Caught a nil-deref risk from the LSP — item.Updated is a pointer.
I'll guard for nil before falling back to it.

When it works, there’s no gap between what your editor knows and what the agent knows, because they’re reading the same source of truth. That’s the real argument for putting OpenCode next to your editor rather than off in a corner: when the integration is healthy it isn’t a second, blinder pair of eyes on your code — it’s looking at the same diagnostics you are. (One honest caveat from the docs: language servers can drift out of sync, use real memory, and slow a workflow down — so for some loops you’ll still want the agent to run go vet or the test suite directly rather than trusting LSP feedback alone. Diagnostics are a fast first signal, not the whole verification story.)

You now have OpenCode where you actually work — composing in your real editor, picking files and running commands without leaving the keyboard, and reading the same diagnostics your IDE draws. That’s the end of getting started: installed, on your own model, one turn of the loop understood, a first change shipped, and the agent wired into your day. Next, chapter 2 turns OpenCode’s signature move — its indifference to which model you run — into real fluency: wiring up multiple providers and models, switching mid-session, and picking the right model for each kind of task.