No description
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-28 12:29:42 +02:00
doc Initial commit 2026-09-28 12:29:42 +02:00
lua/opencode-pair Initial commit 2026-09-28 12:29:42 +02:00
plugin Initial commit 2026-09-28 12:29:42 +02:00
tests Initial commit 2026-09-28 12:29:42 +02:00
LICENSE Initial commit 2026-09-28 12:29:42 +02:00
README.md Initial commit 2026-09-28 12:29:42 +02:00

opencode-pair.nvim

A dependency-free Neovim 0.10+ client for sending discussed changes from an OpenCode session to Neovim as reviewable ghost text.

The plugin starts no process during setup. :OpenCodePairAttach finds the workspace, launches the bridge through code-box, and lets you choose an OpenCode session. After discussing a change, request it at the cursor, review the ghost text, and explicitly accept or reject it.

Installation

With lazy.nvim:

{
  "your-name/opencode-pair.nvim",
  opts = {
    keymaps = {
      attach = "<leader>oa",
      complete = { n = "<leader>oc", x = "<leader>oc" },
      accept = { n = "<leader>oy", i = "<Tab>", s = "<Tab>" },
      reject = { n = "<leader>on", i = "<Left>" },
      status = "<leader>os",
      detach = "<leader>od",
    },
  },
}

No keymaps are installed unless they are present in keymaps. Each action may use one string for every supported mode or a table of mode-specific mappings. A successful normal-mode request enters Insert mode automatically. In the example, Tab accepts and Left rejects only while ghost text is visible. Tab otherwise jumps through an active native snippet or inserts a normal Tab; Left retains native cursor movement.

Configuration

require("opencode-pair").setup({
  command = "code-box",
  args = { "opencode-bridge" },
  root_dir = nil,
  context = {
    before_lines = 40,
    after_lines = 20,
  },
  timeout_ms = 120000,
  keymaps = {},
})

Mode tables use n, i, s, and x, restricted to supported modes for each action. Arrow and Tab fallback does not invoke mappings from other plugins.

When Blink owns <Tab>, let Blink consume the key and schedule acceptance after its mapping callback completes:

["<Tab>"] = {
  function(cmp)
    local pair = require("opencode-pair")
    if not pair.has_suggestion() then
      return false
    end
    vim.schedule(function()
      if pair.accept({ silent = true }) then
        return
      end
      if cmp.select_and_accept() then
        return
      end
      if cmp.snippet_forward() then
        return
      end
      local tab = vim.api.nvim_replace_termcodes(
        "<Tab>", true, false, true)
      vim.api.nvim_feedkeys(tab, "ni", false)
    end)
    return true
  end,
  "select_and_accept",
  "snippet_forward",
  "fallback",
},

root_dir may be a path or a callback with the signature function(bufnr, absolute_path). A callback may return a path or nil to use root discovery. Discovery uses the nearest .git directory and then falls back to Neovim's current working directory.

The current file must be inside the selected root. Context comes from the current buffer, including unsaved changes, byte-oriented cursor position, diagnostics in the included line range, and the current visual selection. Visual mappings preserve characterwise, linewise, and blockwise selections; an explicit Ex command range supplies complete lines.

Requests are always explicit. This keeps discussion-driven suggestions separate from speculative completion plugins. A newer request supersedes an older one locally; the bridge protocol does not abort work already started.

Commands

Command Action
:OpenCodePairAttach Start the bridge and select a session
:OpenCodePairComplete Request a completion immediately
:OpenCodePairAccept Insert the visible suggestion
:OpenCodePairReject Clear the visible suggestion
:OpenCodePairStatus Show process, session, and backend state
:OpenCodePairDetach Detach from the current session

A suggestion is invalidated by edits, cursor movement, or leaving its buffer. Suggestions may contain up to 100 lines and 64 KiB. Acceptance inserts the complete response exactly at the captured byte position. Multiline suggestions require the cursor at the end of a line so the ghost-text preview matches the accepted edit.

Testing

nvim --headless \
  -u tests/minimal_init.lua \
  -l tests/run.lua

The test suite uses only Neovim's built-in Lua runtime.

Protocol

The bridge is launched as:

code-box opencode-bridge <canonical-project-root>

Requests and responses use newline-delimited JSON on stdin and stdout. Diagnostics on stderr are retained for status reporting. The successful hello response must explicitly confirm protocol = 1; extra response fields are ignored.

License

MIT