feat: in-process Lua core handler for claudecode (#47, phase 3)#63
Merged
Conversation
Replaces bin/core-pre-tool.sh's role for Claude Code with an in-process Lua orchestrator (lua/code-preview/pre_tool/), invoked through a single RPC call from the per-OS hook entry. Eliminates the per-proposal nvim cold-start (~50-100ms) and collapses the 5+ RPC round-trips per proposal into one. apply-edit / apply-multi-edit / apply-patch move to lua/code-preview/apply/ and are called in-process; bin/ retains thin shims for external callers. The bash core handler stays in place for opencode / codex / copilot; they flip in follow-up PRs. See docs/adr/0005 for the design rationale, including why we run in-process rather than as a headless worker. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three CI failures, all rooted in subtle parity gaps between the new in-process emitter and the bash core handler: - vim.json.encode does not preserve table key order; Ubuntu's Lua hash seed produced a key order that broke the shell tests' byte-exact JSON comparison. Switched the claudecode emitter to a hand-built string matching the bash printf format exactly. - Bash core exited 0 for Bash / ApplyPatch / unknown tools BEFORE reaching the permissionDecision printf. The new emitter was firing unconditionally. Restricted emission to Edit / Write / MultiEdit and threaded tool_name into the emitter context. - test_stale_socket.sh asserted that the proposed temp file was written even when no Neovim was reachable — a side effect of the bash core always spawning apply-edit.lua. ADR-0005 deliberately removes that side effect (the plugin abstains entirely). Test updated to match the new contract. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Six small fixes surfaced in independent code review:
- pre_tool: extract present_single_file() helper from handle_edit /
handle_write / handle_multi_edit (removed ~30 lines of duplication).
- pre_tool: add sweep_leftover_tempfiles(); call from setup() to clear
stale /tmp/claude-diff-* across sessions (the bash post-tool did this
via a global wildcard rm; per-proposal tracking deferred to follow-up).
- emitters: drop dead has_nvim branch (handle() always sets it true).
- claudecode shims: guard against malformed jq output, add comment
explaining why `set -e` is intentionally omitted.
- post_tool: add post_tool_handle_spec.lua covering Bash status clear,
concurrent-edit marker preservation, return-value contract, and
robustness against malformed inputs.
Two review items deferred to a follow-up issue (per discussion):
* Apply looks_like_path to rm tokens (behavioural change beyond bash
parity).
* Per-proposal tempfile tracking (the startup sweep covers the
cross-session leak; within-session refinement comes later).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 3 of #47 — ports the bash core handler to in-process Lua and flips the Claude Code backend to use it. Backends still on bash (opencode/codex/copilot/gemini) continue working unchanged; they flip in follow-up PRs.
Architectural choice (see ADR-0005): the new handler runs in-process inside the user's Neovim via a single RPC call, not as a headless worker. This eliminates the per-proposal cold-start (~50–100ms × every Edit / Write / MultiEdit / ApplyPatch) and collapses 5+ RPC round-trips per proposal into one. Issue #47 originally framed phase 3 as a like-for-like swap to a headless worker; the ADR records why we deviated.
What changed
lua/code-preview/pre_tool/{init,bash_detect,normalisers,emitters}.lua— orchestration, Tier 1 Bash detection, per-backend dispatch tables.lua/code-preview/post_tool.lua— post-tool cleanup.lua/code-preview/apply/{edit,multi_edit,patch}.lua— extracted frombin/apply-*.luaas pure Lua modules;bin/apply-*.luanow thin shims for external callers.backends/claudecode/code-{preview,close}-diff.sh— onenvim_callinto the new in-process handler, abstain (exit 0) when nvim is unreachable.tests/plugin/pre_tool_{bash_detect,normaliser,handle}_spec.lua, including regressions for theit\'s-mine.txtapostrophe-escape case (a fix that goes beyond pure bash parity).Core handler,Headless worker,Hook context query, ADR-0004 xref.Behavioural contract
bin/core-{pre,post}-tool.sh.changes/neo_tree/diffmodules and serialise on the main loop — no concurrency hazard during the per-backend rollout.Out of scope
.cmd/.ps1shims — tracked in Request Windows 11 support #46, will consume the in-process Lua landed here.bin/core-{pre,post}-tool.shandjq/lsofhealthcheck cleanup — happens in a separate cleanup PR after the last backend flips.diff.luarefactor — separate.Test plan
./tests/run_lua.sh): 63 success / 0 fail / 0 errorBashwithrm <file>marks the file deleted in neo-tree (no diff tab)Bashwithrm it\'s-mine.txtcorrectly highlights (new beyond bash parity)Bashwithecho x > newfile.txtmarksbash_createddiff.defer_claude_permissions = truesuppresses theaskpromptdiff.visible_only = trueskips diffs for non-visible files🤖 Generated with Claude Code