Skip to content

feat: support OpenCode v2 - #330

Closed
metal3d wants to merge 9 commits into
nickjvandyke:mainfrom
metal3d:feat/opencode-v2
Closed

metal3d wants to merge 9 commits into
nickjvandyke:mainfrom
metal3d:feat/opencode-v2

Conversation

@metal3d

@metal3d metal3d commented Sep 18, 2026

Copy link
Copy Markdown

Summary

Add support for OpenCode v2 (opencode serve / background service). Closes #322.

OpenCode v2 changed the server model substantially:

  • the HTTP API moved under /api/* (published as an OpenAPI spec at /openapi.json),
  • servers always require HTTP basic auth (a generated password),
  • it runs a background service that clients discover from a registration file,
  • the /tui/* control endpoints were removed in v2.0.x (re-added later on dev).

What changed

Discovery & auth (lua/opencode/server/discovery/init.lua)

  • Discover the background service from OpenCode's state directory (service.json{ url, password }), also accepting the older server.json name.
  • Send basic auth: password from the registration, vim.g.opencode_opts.server.password, or $OPENCODE_SERVER_PASSWORD (same env vars as OpenCode).
  • Drop the v1 process scan (pgrep + lsof).

API (lua/opencode/server/init.lua)

  • Target the /api/* surface: /api/info, /api/location, /api/session, /api/session/active, /api/agent, /api/event (SSE).
  • Prompts go through POST /api/session/{id}/prompt — fire-and-forget, since the endpoint admits and schedules the agent loop.
  • Commands map onto POST /api/session/{id}/interrupt, …/compact, POST /api/session (new) and POST /api/session/{id}/agent (cycle).
  • Permission replies go through POST /api/session/{id}/permission/{requestID}/reply.
  • Session targeting: the session the TUI is viewing (from its tabs.json), then the most recently viewed/updated session.

Events

  • Adopt v2's { id, type, data } shape (v1 used { type, properties }).
  • permission.asked now carries { action, resources, metadata }; edit diffs come from metadata.files[].patch.
  • file.editedfilesystem.changed; server.instance.disposedglobal.disposed / location.shutdown.

TUI

  • /tui/* availability is detected from the OpenAPI spec at connect time (server.tui). Commands needing them (session.half.page.*, session.first/last, prompt.clear/submit, session.select, session.share, undo/redo) work on servers that expose them and are no-ops otherwise.

Compatibility

This branch targets OpenCode v2; v1 is no longer supported. If you'd rather keep both, the /api/info version field (already fetched) makes runtime auto-detection straightforward — happy to add that instead.

Also included (separable)

  • require("opencode").open() / toggle() to show or hide an OpenCode TUI panel in a right-hand split, targeting its own session (per working directory). The upstream removed the terminal manager, so this restores a minimal version. Say the word and I'll drop it from this PR.

Testing

  • :checkhealth opencode against OpenCode v2.0.7.
  • Manual: discovery, sessions/agents, prompt admission (~10 ms), session create/delete, SSE events, connect() + heartbeat + statusline, agent.cycle, permission/edit flows.
  • stylua --check and the LuaLS type-check (same config as CI, .luarc.ci.json) are clean.

Related: #239 (x-opencode-directory), since v2 routes instance requests by directory.

OpenCode v2 replaced its server model: the HTTP API moved under `/api/*`,
servers always require HTTP basic auth, the background service is discovered
via its registration file, and the `/tui/*` control endpoints were removed.

- Discover the background service from `service.json` (URL + password)
- Authenticate all requests with basic auth
- Target the `/api/*` surface
- Send prompts and commands through session endpoints (fire-and-forget)
- Update event handling to v2's `{ id, type, data }` shape
- Detect legacy `/tui/*` endpoints and use them when available
- Drop v1 process discovery and the server/session pickers
Copilot AI lite review requested due to automatic review settings September 18, 2026 10:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved critical issues affect discovery, project routing, authentication, session targeting, and permission handling.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Ports the plugin to OpenCode v2’s authenticated background service, /api endpoints, new event model, and optional TUI panel.

Changes:

  • Replaces process scanning with registration-based discovery and Basic authentication.
  • Migrates sessions, prompts, commands, permissions, and events to v2.
  • Adds TUI panel support, health checks, configuration, and documentation updates.
File summaries
File Summary
README.md Documents v2 setup, authentication, commands, and events.
plugin/events/status.lua Updates status event subscriptions.
plugin/events/reload.lua Handles filesystem change events.
plugin/events/permissions/init.lua Updates permission replies.
plugin/events/permissions/edits.lua Updates edit permission replies.
lua/opencode/ui/select.lua Simplifies server and command selection.
lua/opencode/ui/select_session.lua Removes the obsolete session picker.
lua/opencode/ui/select_server.lua Removes the obsolete server picker.
lua/opencode/server/init.lua Implements the v2 API, authentication, SSE, and sessions.
lua/opencode/server/discovery/process/windows.lua Removes Windows process discovery.
lua/opencode/server/discovery/process/unix.lua Removes Unix process discovery.
lua/opencode/server/discovery/process/init.lua Removes the process discovery abstraction.
lua/opencode/server/discovery/init.lua Adds registration-based service discovery.
lua/opencode/health.lua Updates v2 health checks.
lua/opencode/events/status.lua Handles v2 status events.
lua/opencode/events/permissions/init.lua Formats v2 permission prompts.
lua/opencode/events/permissions/edits.lua Handles v2 edit patches.
lua/opencode/config.lua Starts the v2 service and TUI.
lua/opencode/api/prompt.lua Sends prompts through v2 sessions.
lua/opencode/api/command.lua Maps commands to v2 APIs.
lua/opencode.lua Adds TUI panel management and session caching.
AGENTS.md Updates architecture and dependency guidance.
Review details

Suppressed comments (4)

lua/opencode.lua:235

  • The per-directory cache is bypassed once a terminal is alive: after changing Neovim's cwd, this branch reuses the old TUI process and never updates current_session_id. Prompts from the new directory can consequently continue targeting the previous directory's panel session, contrary to the per-working-directory behavior documented above.
  if tui_alive() and tui_buf then
    vim.api.nvim_win_set_buf(0, tui_buf)
  else
    local session_id = panel_session()

lua/opencode/events/permissions/edits.lua:23

  • metadata.files is an array, but the preview always selects only files[1]. Approving or rejecting the permission applies to the whole request, so any additional file patches are silently omitted from the review and can be accepted without being shown. Render every file in the request or avoid treating a multi-file request as a single-file diff.
  local file = files and files[1]

lua/opencode/server/init.lua:510

  • The v2 route for clearing a staged revert is POST /api/session/{id}/revert/clear; this method currently calls DELETE /api/session/{id}/revert, which is not the v2 endpoint and will fail whenever the method is used.
function Server:revert_clear(session_id)
  return self:request("/api/session/" .. session_id .. "/revert", "DELETE")

lua/opencode/server/init.lua:309

  • The new empty-body success callback also runs for the persistent SSE job. If the server closes /api/event cleanly without a final event, connect() receives nil and immediately evaluates response.type, causing the subscription callback to error instead of disconnecting cleanly. Only resolve an empty response for non-persistent requests.
        elseif on_success then
          -- Empty success body (e.g. 204 No Content from DELETE): resolve anyway.
          vim.schedule(function()
            on_success(nil)
          end)
  • Files reviewed: 22/22 changed files
  • Comments generated: 11
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread lua/opencode.lua
Comment thread lua/opencode.lua Outdated
Comment thread lua/opencode/server/discovery/init.lua Outdated
Comment thread lua/opencode/server/init.lua
Comment thread lua/opencode/server/init.lua Outdated
Comment thread plugin/events/permissions/init.lua Outdated
Comment thread lua/opencode.lua Outdated
Comment thread lua/opencode.lua Outdated
Comment thread lua/opencode/api/command.lua Outdated
Comment thread lua/opencode/api/command.lua
- Send `x-opencode-directory` on every request: v2 routes instance calls by
  directory, so a shared service would otherwise answer for its own cwd.
- Return a rejected Promise from discovery when nothing is found, so the
  auto-start/poll path is reached instead of `nil:catch`.
- Key panel session targets by server URL, avoiding a stale target across
  services; make `session.new` update the target.
- Degrade TUI-only commands to a silent no-op on servers without `/tui/*`.
- Fall back to the generic permission prompt when an edit has no diff preview.
- Panel: honor configured credentials, bound the sync request with timeouts,
  and skip opening a local TUI when `server.url` is configured.
@metal3d

metal3d commented Sep 18, 2026

Copy link
Copy Markdown
Author

Known limitation: session tabs can redirect prompts

Worth flagging (not a blocker): the TUI panel this PR opens can end up showing — and switching between — sessions you didn't intend.

What happens

open() runs opencode --session <id> so the panel gets its own session. But OpenCode v2's session tabs feature (tabs.enabled, default true, tabs.scope default cwd) restores every session that was open for the working directory from ~/.local/state/opencode/latest/tui/tabs.json. So the panel comes up with the panel session plus whatever else was open for that directory (e.g. an unrelated long-running session). One <tab>-switch later, the active session is no longer the panel's.

Why it matters here

The plugin targets the session the TUI is showing (resolve_session_id() reads tabs.json, with Server.panel_targets[url] as a race-free head start). That is the right behavior, but it means a stray tab switch silently redirects subsequent prompts/replies to another session.

Workaround today

Disable the tab strip in the TUI: /settings (or the command palette → Open settings) → Tabs → Enabled → off. It's persisted by OpenCode. Downside: it's global, so it also affects manually launched TUIs.

A panel-scoped config would be ideal, but OPENCODE_TUI_CONFIG is dev-only — it isn't in v2.0.7/v2.0.8.

Possible directions (future)

  • OpenCode-side: honor OPENCODE_TUI_CONFIG (or an equivalent flag) in v2.0.x so the plugin can start a tab-less TUI just for the panel, without touching the user's global settings.
  • Or an option to fix tabs.scope/initial tabs for an invocation started with --session.
  • Plugin-side (last resort): have open() trim the directory's entry in tabs.json to just the panel session. This works but mutates OpenCode's TUI state and fights the TUI rewriting it, so it doesn't feel like the right layer.

Happy to adjust scope if you'd rather the plugin not open a panel at all, or to drop this from the PR and track it separately.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Critical API, authentication, permission, and session-targeting defects remain unresolved.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (12)

Previously missed (1) — in code that hasn't changed since the last review.

lua/opencode/ui/select.lua:155

  • When server.connect = false (the documented manual-connection mode), discovery.get() returns the server without calling connect() because M.get() honors that option. Consequently, selecting “Connect to a server” still leaves the server disconnected; this branch should explicitly connect the returned server.

lua/opencode.lua:196

  • M.open() is called synchronously at the start of ask(), select(), prompt(), and command(), so this vim.fn.system() runs on Neovim's main loop before asynchronous discovery can proceed. A stale or unreachable registration can block the editor for up to the three-second curl timeout (and potentially once per panel request); use an async job/Promise or defer panel creation until discovery completes.
  local out = vim.fn.system(cmd)

lua/opencode.lua:259

  • These fallback branches start opencode directly, but M.open() is now invoked by every public operation before discovery. That bypasses opts.server.start (including false and custom starters such as the documented Snacks setup), so a prompt can unexpectedly launch a second TUI/server instead of honoring the configured startup policy.
      vim.cmd("terminal opencode --session " .. session_id)
    else
      vim.cmd("terminal opencode")

lua/opencode.lua:222

  • The panel's POST /api/session response is the newly created session object, so extracting created.data.id leaves id nil. The TUI then starts without --session, defeating the dedicated per-directory panel target and preventing it from being persisted.
  local id = created and created.data and created.data.id

lua/opencode.lua:216

  • GET /api/session/{id} returns the session object directly, so a valid cached panel session has existing.id, not existing.data. This condition treats every cached session as missing and creates a new panel session after each Neovim restart.
    if existing and existing.data then

lua/opencode/api/command.lua:77

  • The session-create response is a Session object (as reflected by create_session()'s return type), so its ID is at created.id. Checking created.data.id never finds an ID; session.new consequently creates a session without making it the target for subsequent prompts.
      local id = created and created.data and created.data.id

lua/opencode/config.lua:31

  • Every public entrypoint now calls M.open() before discovery. When no registration exists, that call already runs terminal opencode (the fallback at lua/opencode.lua:259), and the default start then opens another TUI here after starting the service. First use therefore creates two panels, with the first one not associated with the dedicated panel session; the startup path needs one coordinated terminal launch.
      vim.fn.system({ "opencode", "service", "start" })
      vim.cmd("vsplit term://opencode | wincmd p")

lua/opencode/server/init.lua:234

  • These requests must send the current Neovim working directory verbatim. self.cwd is the server's /api/location value and can become stale after :cd, while vim.uri_encode turns a path such as /home/me/project into %2Fhome%2Fme%2Fproject; this header is not a URI path, so a shared service will route requests to the wrong instance. Use vim.fn.getcwd() without URI encoding.
  local directory = self.cwd or vim.fn.getcwd()
  if directory and directory ~= "" then
    table.insert(cmd, "-H")
    table.insert(cmd, "x-opencode-directory: " .. vim.uri_encode(directory))

lua/opencode/server/init.lua:523

  • The v2 permission reply schema names this field reply, not decision. Every permission response sent through this method will therefore fail request validation and leave the OpenCode permission pending.
    decision = reply,

lua/opencode/server/init.lua:514

  • revert_clear is mapped to the wrong v2 route and method. The API exposes POST /api/session/{id}/revert/clear; this implementation calls DELETE /revert, so callers cannot clear a staged revert.
  return self:request("/api/session/" .. session_id .. "/revert", "DELETE")

lua/opencode/server/init.lua:377

  • /api/agent likewise returns the agent array directly. Reading response.data makes get_agents() always return an empty table, which disables subagent discovery and leaves agent.cycle with no agents to switch to.
    return Promise.resolve(response.data or {})

lua/opencode/server/init.lua:308

  • A persistent SSE job can exit cleanly without a final event. Calling on_success(nil) in that case invokes connect()'s callback with response == nil, which then indexes response.type and leaves the connection handling broken. Empty-body success should only be synthesized for non-persistent requests.
        elseif on_success then
  • Files reviewed: 22/22 changed files
  • Comments generated: 5
  • Review effort level: Lite

Comment on lines +22 to +26
local files = event.data.metadata and event.data.metadata.files
local file = files and files[1]
if not file or not file.patch then
return nil
end

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in 2875ae6. edit_preview() now returns nil unless there is exactly one file, so multi-file edits fall back to the generic permission prompt instead of approving unseen changes.

Comment thread lua/opencode/server/init.lua Outdated
Comment on lines +114 to +117
local creds = credentials or Server.credentials or {}
self.url = url:gsub("/$", "")
self.username = creds.username or require("opencode.config").opts.server.username or "opencode"
self.password = creds.password or require("opencode.config").opts.server.password

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2875ae6. Server.credentials is now a table keyed by the normalized URL, and explicit credentials still win, so a registered service's password is no longer reused for a configured/remote URL.

function Server:get_sessions()
local Promise = require("opencode.promise")
return self:request("/api/session", "GET"):next(function(response)
return Promise.resolve(response.data or {})

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — but I don't think this one holds against v2.0.7. GET /api/session does return an object with a top-level data array. Repro against a running server:

$ curl -s -u opencode:$PW -H 'Accept: application/json' $URL/api/session | head -c 80
{"data":[{"id":"ses_f4c4d6b2dffet6D1F291pEEhCT","projectID":"8a0d6c809141b113acf56615702db32cb97f4238"

So the response.data unwrap is correct, and resolve_session_id() works (sessions are listed successfully).

self:curl("/tui/select-session", "POST", { sessionID = session_id }, resolve, reject)
end)
function Server:send_prompt(session_id, text)
return self:request("/api/session/" .. session_id .. "/prompt", "POST", { text = text })

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this matches v2.0.7. The live OpenAPI document (GET /openapi.json) for POST /api/session/{sessionID}/prompt declares text at the top level with required: ["text"]:

{ "type": "object", "properties": { "id": {...}, "text": { "type": "string" }, "files": [...], "agents": [...], "skills": [...], "metadata": {...}, "delivery": {...}, "resume": {...} }, "required": ["text"] }

Sending { "text": "..." } is admitted (observed ~10 ms, returns the inbox entry). There is no top-level prompt object in this schema.

Comment thread lua/opencode.lua Outdated
"-H",
"Accept: application/json",
"-H",
"x-opencode-directory: " .. vim.uri_encode(vim.fn.getcwd()),

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a false positive: vim.uri_encode does not escape /, so a normal path is sent verbatim.

:lua print(vim.uri_encode('/home/patrice'))
/home/patrice

And the directory header demonstrably routes correctly (checked GET /api/location with x-opencode-directory):

$ curl -s -u opencode:$PW -H 'Accept: application/json' -H 'x-opencode-directory: %2Ftmp%2Fopencode' $URL/api/location
{"directory":"/tmp/opencode","project":{"id":"3c45a4f7...","canonical":"/tmp/opencode"}}

(The v2 SDK's own rewrite uses encodeURIComponent for this header, so an encoded value is fine too.)

- Defer multi-file edit permissions to the generic prompt: only files[1] was
  previewed while the reply covered the whole request.
- Key the discovered-credentials cache by normalized URL so one service's
  password is never sent to another host.
@nickjvandyke

nickjvandyke commented Sep 18, 2026

Copy link
Copy Markdown
Owner

the /tui/* control endpoints were removed in v2.0.x (re-added later on dev)

In OpenCode you mean right? If so, maybe we wait for that to merge and release to reduce our workload. OpenCode seems to release frequently.

We'd still consider other improvements from leveraging the new API.

@metal3d

metal3d commented Sep 18, 2026

Copy link
Copy Markdown
Author

Thanks so much for the review and the pointers — I really appreciate the direction.

Just to confirm: yes, that's on the OpenCode side. /tui/* was never the core API — it was the TUI control surface, used only to drive the on-screen TUI: scrolling and navigation (session.first/last, session.half.page.*, session.page.*), the prompt box (prompt.clear / prompt.submit), and a few others (session.select, session.share, session.undo/redo). Everything else — prompting, sessions, events, permissions, agent cycling, interrupt, compact — all goes through the new /api/* surface, which is what the PR targets.

On "waiting for the release": I can see the appeal of reducing workload, but I'm a little worried about the experience for people moving to v2. The PR detects whether /tui/* is available at connect time (from the published OpenAPI spec) and degrades those 12 TUI-driving commands to a silent no-op when it's absent. So on v2.0.x the plugin is fully functional for the core workflow — only the TUI scroll / prompt-box commands are inert.

My concern is that people who update OpenCode to v2 will land in the same spot I was in: commands that silently do nothing, with no obvious reason why. If we wait for the OpenCode release that re-adds /tui/*, users on v2.0.x in the meantime have no working path — they'd be stuck on v1 or without a functioning plugin. Merging now gives them a working baseline on v2, and the TUI commands light up automatically once a release exposes /tui/* again (no plugin change needed, since availability is detected at connect time).

I'm happy to keep the PR scoped to the v2 API migration and track the TUI-command restoration separately. And I'm very open to the "other improvements from leveraging the new API" — happy to look at what the new surface offers (e.g. GET /api/command for a dynamic command list) once the core migration is settled.

The x-opencode-directory header was using the server's own directory
(from /api/location) instead of Neovim's working directory, causing
requests to be routed to the service's ambient cwd (often HOME)
instead of the project the user is working in.
@nickjvandyke

nickjvandyke commented Sep 18, 2026

Copy link
Copy Markdown
Owner

If we wait for the OpenCode release that re-adds /tui/*, users on v2.0.x in the meantime have no working path — they'd be stuck on v1 or without a functioning plugin. Merging now gives them a working baseline on v2

How much of this PR is still necessary after OpenCode v2 exposes /tui/*? This is a lot of code to review and maintain for a temporary fix. I'd rather let OpenCode close the gap as much as possible. They make money from this; I don't 😀

e.g. GET /api/command for a dynamic command list

I already tried this a while ago but the OpenCode server no-op'd when calling custom commands, so it was pointless (and confusing) unfortunately. Maybe v2 fixed that.

@metal3d

metal3d commented Sep 19, 2026

Copy link
Copy Markdown
Author

I think the framing is worth clarifying: this PR isn't really a "temporary fix" for /tui/*. The /tui/* endpoints were never the core API — they were the TUI control surface (scroll, navigation, prompt box). The plugin's core functionality goes through the HTTP API, which moved from the root (/session, /agent, /event, /permission/*) to /api/* in v2. So the plugin has to migrate to /api/* regardless of whether /tui/* comes back.

What's actually /tui/*-specific is small — roughly ~75 lines out of ~815 insertions:

  • probe_tui() (detects /tui/* from the OpenAPI spec at connect time)
  • the TUI_ONLY table + no-op degradation in command.lua
  • one if server.tui branch in prompt.lua
  • tui_execute_command / tui_append_prompt

Everything else — basic auth, background-service discovery, the /api/* surface, the v2 event shape, session targeting — is the permanent v2 migration, necessary whether or not /tui/* is re-exposed.

And that small part isn't even "temporary." It's a graceful degradation path that works for both v2.0.x (no /tui/*) and a future v2 (with /tui/*). When OpenCode re-exposes /tui/*, server.tui flips to true and those commands light up with zero plugin changes. So it's not a stopgap that gets replaced — it's a compatibility shim that stays.

The practical reality: v2.0.x is released now (v2.0.9 shipped today). Users who upgrade to v2 have a broken plugin without this PR — every v1 endpoint 404s on a v2 server. Concretely, launching the plugin on v2 errors out with --port option doesn't exist, because the default server.start runs opencode --port, which isn't a valid flag in v2 (the server is now a background service). Waiting for OpenCode to re-expose /tui/* means the plugin is non-functional on v2 in the meantime.

To reduce your review burden, I'm happy to split it:

  1. Core v2 migration (auth, discovery, /api/*, event shape, session targeting) — necessary regardless
  2. TUI shim (probe_tui + TUI_ONLY + prompt branch) — small, self-contained
  3. Panel (open()/toggle()) — already separable

Or, if you'd rather, I can drop the TUI shim entirely and let the TUI-driven commands hard-error on v2.0.x. To be clear, this isn't about scrolling the view — scrolling in the TUI is native. The commands affected are the TUI-driven ones the default config exposes: prompt.clear, prompt.submit, session.select, session.undo, session.redo. Of those, undo/redo are a gap I plan to close: v2 has native API equivalents (POST /api/session/{id}/revert/commit and DELETE /api/session/{id}/revert), so they don't actually need /tui/* once wired to the API.

On GET /api/command: agreed that custom commands no-op'd in v1. v2 does have POST /api/session/{id}/command (the PR's run_command uses it), but that's for running user-defined commands within a session, not for listing built-ins. Happy to re-test whether v2 fixed the no-op behavior before building on it.

OpenCode v2 (2.0.x) ignores the directory header/query on /api/session and
always creates a new session in the shared service's ambient cwd (often the
home directory). Pre-creating a panel session through that API was therefore
the cause of prompts running in ~ instead of the project.

- open(): run the TUI in the Neovim working directory instead of attaching
  to an API-created session, so opencode manages the project's own sessions.
- resolve_session_id(): scope the resolved session to the current directory,
  preferring the TUI's per-directory tab, then the most recent session whose
  location matches the cwd.
- Remove the now-unused panel-session cache and panel_targets tracking.
- config(): server.start no longer opens a second TUI panel on startup.
When opencode has already recorded open session tabs for the working
directory (tabs.json), launching the bare TUI presented a fresh session next
to them. Open the TUI on the last open tab so tabs restore without creating
a new session.
- Add lua/opencode/util/state.lua exposing the opencode state dir and the last
  open session tab for a directory, removing duplicated parsing of tabs.json in
  open() and Server:tui_current_session().
- Drop the unused self.session_id short-circuit in resolve_session_id() so a
  non-project active session can never bypass the directory-scoped resolution.
@nickjvandyke

Copy link
Copy Markdown
Owner

I explored this myself and ended up with +402 / -527 for non-TUI v2 migrations. Thanks for your effort! However I feel more confident in my own changes because they are smaller and I have the most context. I'd love to review your PR and iterate instead, but unfortunately I don't have time for that.

@metal3d

metal3d commented Sep 21, 2026

Copy link
Copy Markdown
Author

Thanks for the reply, and for the work you put into opencode.nvim — it's a genuinely nice plugin, and the reason this fork exists.

You did add a v2 branch, and that's the right instinct. But as it stands it still doesn't let you use OpenCode from Neovim on the released v2.0.x: the select menu opens, but every prompt or command rejects with the TUI_UNAVAILABLE message, because the branch depends on /tui/*. So the core use case — prompting from the editor — isn't available.

I'm also not convinced /tui/* is coming back. I read the v2 source (anomalyco/opencode, branch v2, 2.0.12): the generated spec (packages/protocol/openapi.json) has 113 paths and none under /tui, packages/protocol/src/groups/ has no tui group, and the tui.* events that remain (packages/schema/src/tui-event.ts) are described as "events the TUI consumes from the public stream" — with only GET /api/event exposed and no endpoint to publish them. In v2 the TUI is a client that listens; the server no longer drives it. Given where the project is heading (a proper /api/* instance API, the TUI as one client among several, ACP for editor integration), there's a real risk /tui/* is never reintroduced.

That's also why my fork is necessarily larger: OpenCode's API was entirely redesigned in v2, so supporting it means rebuilding on /api/* (send a prompt to a session, own that session, follow events) rather than just adapting the old calls. It isn't meant to replace your direction — only to keep a working baseline for v2.0.x users in the meantime.

Thanks again for your time and for the plugin.

Repository owner locked and limited conversation to collaborators Sep 21, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feature: OpenCode v2 support

3 participants