Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@

### Features

- `User ClaudeCodeSendComplete` autocmd, fired once per file when a send is accepted while Claude is connected, with `data = { file_path, start_line, end_line, context }` (lines 0-indexed). Lets you run arbitrary post-send logic — in particular, focus a Claude session running outside Neovim (`provider = "none"`/`"external"`), e.g. via `tmux select-pane`, which `focus_after_send` cannot do. ([#228](https://github.com/coder/claudecode.nvim/issues/228))
- `:ClaudeCodeCloseAllDiffs` command to close pending Claude diffs at once (e.g. proposals orphaned by resolving them via Claude remote control). Diffs you have already accepted but whose file has not been written yet are left intact so saved edits are never discarded. ([#248](https://github.com/coder/claudecode.nvim/issues/248))

### Bug Fixes

- `focus_after_send = true` no longer fails silently with `terminal.provider = "none"`/`"external"`: those providers run Claude outside Neovim, so focus cannot move there. A one-time warning is now emitted at setup pointing to the new `User ClaudeCodeSendComplete` autocmd, which you can hook to focus your own terminal. (`focus_after_send` still only auto-focuses the in-editor providers.) ([#228](https://github.com/coder/claudecode.nvim/issues/228))
- Diffs opened via `openDiff` no longer linger forever when they are resolved outside this Neovim or their Claude session goes away. Pending diffs are now automatically closed when the client that opened them disconnects or the integration is stopped, and `closeAllDiffTabs` now also resolves/cleans the diff module's tracked state instead of only closing windows. ([#248](https://github.com/coder/claudecode.nvim/issues/248))
- Show diffs when the Claude Code terminal is the only window (no other splits). Previously `openDiff` failed with "No suitable editor window found"; now a split is created to host the diff, matching the behavior of the `openFile` tool. ([#231](https://github.com/coder/claudecode.nvim/issues/231))
- Work around a Neovim core bug (< 0.12.2) that fragmented large bracketed pastes into the terminal across `vim.paste` phases, making Cmd+V appear to truncate content. Added a scoped, version-gated `vim.paste` shim controlled by `terminal.fix_streamed_paste` (`"auto"` by default; no-op on Neovim >= 0.12.2). ([#161](https://github.com/coder/claudecode.nvim/issues/161))
Expand Down
6 changes: 6 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,12 @@ opts = {
- `plugin/claudecode.lua` - Plugin loader with version checks
- `tests/` - Comprehensive test suite with unit, component, and integration tests

### Autocmd Events

The plugin emits `User` autocmds (not config fields) that integrations can hook:

- **`ClaudeCodeSendComplete`** - Fired in `M.send_at_mention` (init.lua) once per file, synchronously, when a send is accepted on the connected branch (acceptance-time, not delivery; not fired on the queued/disconnected path). `data = { file_path, start_line, end_line, context }` — `file_path` is the formatted path Claude received, lines are 0-indexed and may be nil. Primary use: focus an external Claude session (`provider = "none"`/`"external"`) where `focus_after_send` is inert. Emitted via the guarded, pcall-wrapped `fire_send_complete` helper (no-op when `vim.api.nvim_exec_autocmds` is absent, e.g. minimal test stubs). See `lua/claudecode/types.lua` `ClaudeCodeSendCompleteData` and README "Events".

## MCP Protocol Compliance

### Protocol Implementation Status
Expand Down
43 changes: 42 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,43 @@ You can edit Claude's suggestions before accepting them.

If a diff is resolved outside this Neovim (for example via Claude remote control on another device) the diff windows would otherwise stay open. They are now closed automatically when the Claude session that opened them disconnects. If you resolve diffs remotely while the session is still connected, run `:ClaudeCodeCloseAllDiffs` to clear the leftover pending proposals — it leaves any diff you have already accepted (`:w`) but whose file has not been written yet untouched, so your saved edits are never discarded.

## Events

The plugin fires `User` autocmds you can hook with `nvim_create_autocmd`.

### `ClaudeCodeSendComplete`

Fired once per file, synchronously, when a send (`:ClaudeCodeSend`, `:ClaudeCodeAdd`, tree add, etc.) is **accepted while a Claude client is connected**. This is the recommended way to focus a Claude session that runs **outside** Neovim (`provider = "none"`/`"external"`), where `focus_after_send` cannot help.

The autocmd `data` carries:

| field | type | notes |
| ------------ | -------------- | ------------------------------------------------------------------------------------ |
| `file_path` | `string` | The formatted/cwd-relative path Claude received |
| `start_line` | `integer\|nil` | **0-indexed** (Claude convention, not 1-indexed editor lines); `nil` for whole files |
| `end_line` | `integer\|nil` | 0-indexed; `nil` for whole-file/directory sends |
| `context` | `string\|nil` | Internal trigger tag (e.g. `"ClaudeCodeSend"`); best-effort, may change |

Notes and caveats:

- Fires at **acceptance** time, not delivery — sending is debounced, so a later transport failure is logged, not reported here.
- Fires **per file**: sending a multi-file selection from a file-explorer buffer (`:ClaudeCodeSend` / `:ClaudeCodeTreeAdd`) fires it once per file. `:ClaudeCodeAdd` sends a single path and fires once. Keep handlers idempotent.
- Fires only when Claude is **already connected** at send time; a send that queues while Claude is launching is delivered later without firing this event.

Example — focus a tmux pane after sending (supply your own pane target):

```lua
vim.api.nvim_create_autocmd("User", {
pattern = "ClaudeCodeSendComplete",
callback = function(ev)
-- ev.data.file_path / ev.data.start_line / ev.data.end_line / ev.data.context
if vim.env.TMUX then
vim.fn.system({ "tmux", "select-pane", "-t", "{last}" }) -- replace target as needed
end
end,
})
```

## How It Works

This plugin creates a WebSocket server that Claude Code CLI connects to, implementing the same protocol as the official VS Code extension. When you launch Claude, it automatically detects Neovim and gains full access to your editor.
Expand Down Expand Up @@ -253,7 +290,10 @@ For deep technical details, see [ARCHITECTURE.md](./ARCHITECTURE.md).
-- For native binary: use output from 'which claude'

-- Send/Focus Behavior
-- When true, successful sends will focus the Claude terminal if already connected
-- When true, successful sends focus the in-editor Claude terminal if already
-- connected. NOTE: this only works for in-editor providers (snacks/native);
-- it has no effect with provider = "none"/"external" (Claude runs outside
-- Neovim). For those, hook the `User ClaudeCodeSendComplete` event (see Events).
focus_after_send = false,

-- Selection Tracking
Expand Down Expand Up @@ -521,6 +561,7 @@ Notes:

- No windows/buffers are created. `:ClaudeCode` and related commands will not open anything.
- The WebSocket server still starts and broadcasts work as usual. Launch the Claude CLI externally when desired.
- `focus_after_send` has no effect here (there is no in-editor terminal to focus); enabling it logs a one-time warning at startup. To focus your external session after a send, hook the [`User ClaudeCodeSendComplete`](#claudecodesendcomplete) event.

### External Terminal Provider

Expand Down
76 changes: 76 additions & 0 deletions fixtures/issue-228/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Issue #228 — `focus_after_send` with `provider = "none"` / `"external"`

> Source: https://github.com/coder/claudecode.nvim/issues/228

## Background

`focus_after_send = true` focuses the **in-editor** Claude terminal after a send. It
routes through `terminal.open()`, which dispatches to the configured provider. The
`none` provider (`lua/claudecode/terminal/none.lua`) is a no-op by design, and the
`external` provider cannot move focus to an already-running external terminal. So for
those providers `focus_after_send` could never do anything — and it did so **silently**,
with no warning and no documentation of the limitation.

## The fix (this change set)

`focus_after_send` still cannot focus a Claude session running outside Neovim (it
can't — Claude isn't in a Neovim window). Instead:

- **(c)** A one-time warning is emitted at `setup()` when `focus_after_send = true` and
`terminal.provider` is `"none"` or `"external"`, pointing users at the hook below.
- **(b)** A `User ClaudeCodeSendComplete` autocmd fires on every connected send,
carrying `data = { file_path, start_line, end_line, context }`, so external-terminal
users can run their own focus logic:

```lua
vim.api.nvim_create_autocmd("User", {
pattern = "ClaudeCodeSendComplete",
callback = function()
if vim.env.TMUX then
vim.fn.system({ "tmux", "select-pane", "-t", "{last}" }) -- use your own target
end
end,
})
```

## Gate (deterministic, headless, no Claude CLI required)

```sh
# from the repo root
bash fixtures/issue-228/run.sh
# or: nvim -u NONE -l fixtures/issue-228/repro.lua
```

Expected output ends with:

```
PASS issue #228 fix verified: warning fires for none/external, ClaudeCodeSendComplete fires on send.
```

The harness uses the **real** plugin and the **real** `none` provider, runs four
provider/flag scenarios plus a real `User ClaudeCodeSendComplete` autocmd, and asserts:

| provider | focus_after_send | #228 warning | focus effect | event fires |
| -------------- | ---------------- | ------------ | --------------- | ----------- |
| `none` | `true` | **1** | none (no-op) | yes |
| `none` | `false` | 0 | none (no-op) | yes |
| custom (table) | `true` | 0 | **focus fires** | yes |
| custom (table) | `false` | 0 | show, no focus | yes |

The `none` rows confirm the limitation is unchanged (no terminal is ever created), but
it is no longer silent (a warning fires) and the `ClaudeCodeSendComplete` event lets the
user focus their own terminal. The custom-provider rows confirm focusable providers are
unaffected and never warn.

## Live confirmation (agent-tty / TUI)

`live.lua` is a minimal fixture (provider `none`, connection stubbed) that exercises the
real `:ClaudeCodeSend` path in a running Neovim and hooks the new event:

```sh
nvim -u fixtures/issue-228/live.lua fixtures/issue-228/sample.txt
# then visually select lines and run :'<,'>ClaudeCodeSend (the real path), or
# run :Issue228Probe (before/after report)
# -> a "ClaudeCodeSendComplete fired" message appears; :messages also shows the
# one-time setup warning. focus_after_send itself stays inert for "none".
```
104 changes: 104 additions & 0 deletions fixtures/issue-228/live.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
-- Live (TUI) fixture for issue #228 — run a real Neovim and watch the FIX:
-- * provider="none" + focus_after_send=true emits a one-time warning at setup, and
-- * a `User ClaudeCodeSendComplete` autocmd fires on every connected :ClaudeCodeSend,
-- which this fixture hooks to prove the event (the focus_after_send option itself
-- still cannot focus a Claude session running outside Neovim — that's expected).
-- The websocket connection is STUBBED so the real :ClaudeCodeSend path runs with no CLI.
--
-- Usage (from repo root):
-- nvim -u fixtures/issue-228/live.lua fixtures/issue-228/sample.txt
--
-- Then either:
-- * visually select a few lines and run :'<,'>ClaudeCodeSend (the real path), or
-- * run :Issue228Probe (deterministic before/after report)
-- Expect: focus stays on the sample buffer (focus_after_send is inert for "none"), and
-- a "ClaudeCodeSendComplete fired" message appears (the new hook). :messages also shows
-- the one-time setup warning.

local script_path = debug.getinfo(1, "S").source:sub(2)
local repo_root = vim.fn.fnamemodify(script_path, ":h:h:h")
vim.opt.runtimepath:prepend(repo_root)

vim.g.mapleader = " "
vim.o.number = true
vim.o.laststatus = 2

local claudecode = require("claudecode")
claudecode.setup({
auto_start = false,
log_level = "warn", -- the #228 setup warning is WARN level
track_selection = true, -- required for the visual :ClaudeCodeSend path
focus_after_send = true, -- the option under test (inert for provider="none")
terminal = { provider = "none" }, -- triggers the #228 warning + makes focus a no-op
})

-- ---- Stub a connected Claude so send_at_mention takes the "connected" branch ----
local server_init = require("claudecode.server.init")
server_init.get_status = function()
return { running = true, client_count = 1 }
end
claudecode.state.server = {
_fake = true,
stop = function()
return true
end,
broadcast = function()
return true
end,
}
claudecode._broadcast_at_mention = function(file_path, s, e)
vim.notify(("(stub) broadcast @%s:%s-%s"):format(file_path, tostring(s), tostring(e)), vim.log.levels.INFO)
return true, nil, { file_path = file_path, start_line = s, end_line = e }
end

-- start() normally enables selection tracking; we skipped it, so enable directly
-- so the real visual :ClaudeCodeSend path works against the stubbed server.
require("claudecode.selection").enable(claudecode.state.server, 50)

-- ---- The #228 (b) hook: prove the event fires (this is what an external-terminal
-- ---- user would use to run e.g. `tmux select-pane`). ----
vim.api.nvim_create_autocmd("User", {
group = vim.api.nvim_create_augroup("Issue228Demo", { clear = true }),
pattern = "ClaudeCodeSendComplete",
callback = function(ev)
local d = ev.data or {}
vim.notify(
("ClaudeCodeSendComplete fired: file=%s lines=%s-%s"):format(
tostring(d.file_path),
tostring(d.start_line),
tostring(d.end_line)
),
vim.log.levels.INFO
)
end,
})

-- ---- Deterministic probe: report focus/window state before and after a send ----
local function snapshot()
return {
win = vim.api.nvim_get_current_win(),
buf = vim.fn.fnamemodify(vim.api.nvim_buf_get_name(0), ":t"),
wins = vim.fn.winnr("$"),
}
end

vim.api.nvim_create_user_command("Issue228Probe", function()
local before = snapshot()
local file = vim.api.nvim_buf_get_name(0)
claudecode.send_at_mention(file, 0, 2, "Issue228Probe")
local after = snapshot()
local moved = (before.win ~= after.win) or (before.wins ~= after.wins)
local lines = {
"issue #228 probe (provider=none, focus_after_send=true)",
(" before: win=%d buf=%s wins=%d"):format(before.win, before.buf, before.wins),
(" after : win=%d buf=%s wins=%d"):format(after.win, after.buf, after.wins),
(" focus moved by focus_after_send? %s (expected: NO)"):format(tostring(moved)),
" (a ClaudeCodeSendComplete message above proves the new hook fired)",
}
vim.notify(table.concat(lines, "\n"), moved and vim.log.levels.WARN or vim.log.levels.INFO)
end, { desc = "Issue #228: send and report focus + that the event fired" })

-- short, single-line banner (a long one trips the hit-enter prompt under automation)
vim.schedule(function()
vim.notify("issue228 fixture ready — run :Issue228Probe or :'<,'>ClaudeCodeSend", vim.log.levels.INFO)
end)
Loading
Loading