diff --git a/fixtures/issue-218/README.md b/fixtures/issue-218/README.md new file mode 100644 index 00000000..a6707340 --- /dev/null +++ b/fixtures/issue-218/README.md @@ -0,0 +1,84 @@ +# Fixture: issue #218 — Neovim crashes accepting a new-file markdown diff with render-markdown.nvim + +> [BUG] Neovim crashes when accepting new file diff with render-markdown.nvim installed +> https://github.com/coder/claudecode.nvim/issues/218 + +Accepting (`:w`) a **new-file** diff whose proposed buffer is **markdown**, when the +diff was opened in a **new tab** (`diff_opts.open_in_new_tab = true`) and +**render-markdown.nvim** is installed, abnormally terminates Neovim. Existing-file +diffs are fine — only new files (the original side is an empty buffer, so every +proposed line is a diff "add"). + +This fixture mirrors the reporter's minimal `repro.lua` (snacks.nvim + +claudecode.nvim + render-markdown.nvim) but loads the **local** claudecode.nvim +checkout (resolved in `lua/config/lazy.lua`), so it exercises this repo's code. + +## Reproduce (no real Claude needed) + +```sh +source fixtures/nvim-aliases.sh +vv issue-218 +``` + +Then in Neovim: + +1. `:Repro218` — opens a harmless Claude terminal split and a **new-file markdown + diff in a new tab**, leaving the cursor in the proposed (right) pane. It drives + the same `openDiff` coroutine flow the MCP server uses and wires the post-accept + `close_tab` exactly as the Claude CLI would. +2. Press `:w` to accept. +3. **Neovim disappears.** Depending on the Neovim build the symptom is either a + `SIGSEGV` (raw exit `139`, what the reporter saw) or an abnormal exit `0` with + no `VimLeave` — both are the same memory-unsafety bug. + +`:Repro218Reset` restores a single clean tab if you ran the setup but did not press +`:w`. + +### Scripted / CI-style verification + +```sh +# Expect: "#218 REPRODUCED — Neovim SIGSEGV (139) on diff accept." +NVIM_BIN=/path/to/nvim-0.12.x scripts/repro_issue_218.sh + +# Control — expect: "without render-markdown the diff accepts cleanly (no crash)." +scripts/repro_issue_218.sh --no-render-markdown +``` + +The crash lives in the redraw/teardown path, which does **not** run under +`--headless`, so the driver uses the `agent-tty` CLI to get a real terminal UI. + +## Reproduce with the real Claude CLI (faithful to the report) + +1. `vv issue-218`, then open the Claude terminal (`` / `:ClaudeCode`). +2. **Turn off auto-accept** (`shift+tab` until the "auto mode" indicator is gone) — + in auto mode Claude writes files directly and never sends `openDiff`. +3. Ask Claude to create a new `.md` file (e.g. _"create demo.md with a heading, a + list, a code block and a table using the Write tool"_). +4. When the diff opens, with the cursor in the proposed pane, press `:w`. + +## Root cause (verified) + +On accept, claudecode resolves the diff and the Claude CLI sends `close_tab`, which +runs `diff.close_diff_by_tab_name` → `_cleanup_diff_state`. For the +`open_in_new_tab` path this executes `vim.cmd("tabclose")` on the tab whose windows +are **still in diff mode**, while render-markdown.nvim is attached to the markdown +proposed buffer and the Claude terminal is open in the other tab. That `:tabclose` +is where Neovim dies (the surrounding `pcall` cannot catch it — it is a C-level +abnormal termination, not a Lua error, and `VimLeave` never fires). + +Confirmed by isolation: + +| render-markdown | Claude terminal | outcome on `:w` accept | +| ---------------------------------------------------- | --------------- | ---------------------- | +| installed | open | **crash** (tabclose) | +| **removed** | open | clean teardown | +| installed | not open | clean teardown | +| installed (diff mode turned **off** before teardown) | open | clean teardown | + +The reporter's workaround — `pcall(vim.cmd, "diffoff")` at the end of the +`BufWriteCmd` callback — works because turning diff mode off before the tab is +closed removes the trigger. A more targeted plugin-side fix is to turn diff mode +off on the diff windows inside `_cleanup_diff_state` **before** `:tabclose`. + +Reproduced on: Neovim 0.11.0 (abnormal exit 0) and Neovim 0.12.3 (SIGSEGV 139), +render-markdown.nvim 8.12.0 and 8.13.0, claudecode.nvim current `main`. diff --git a/fixtures/issue-218/init.lua b/fixtures/issue-218/init.lua new file mode 100644 index 00000000..4c915a9f --- /dev/null +++ b/fixtures/issue-218/init.lua @@ -0,0 +1,27 @@ +-- Fixture for issue #218: +-- "[BUG] Neovim crashes when accepting new file diff with render-markdown.nvim +-- installed" +-- https://github.com/coder/claudecode.nvim/issues/218 +-- +-- Symptom: Neovim abnormally terminates (SIGSEGV / exit 139 for the reporter) +-- when a NEW-file diff opened in a NEW TAB (open_in_new_tab = true) and whose +-- proposed buffer is markdown is accepted with `:w`, but only when +-- render-markdown.nvim is installed (attached to that markdown buffer) and the +-- Claude terminal is open. Existing-file diffs are fine; the crash is specific +-- to new files (the original side is an empty buffer, so every proposed line is +-- a diff "add"). See lua/plugins/dev-claudecode.lua and the README for the +-- verified root cause (the teardown's `:tabclose` over still-in-diff windows). +-- +-- This fixture mirrors the reporter's minimal repro.lua (snacks.nvim + +-- claudecode.nvim + render-markdown.nvim) but loads the LOCAL claudecode.nvim +-- checkout (via `dir`, resolved in lua/config/lazy.lua) so we test this repo's +-- code, including git worktrees. +-- +-- Usage (from repo root): +-- source fixtures/nvim-aliases.sh && vv issue-218 +-- then run `:Repro218` and, once it reports "Repro218 ready", type `:w` with the +-- cursor in the proposed (right) pane. Neovim disappears on accept. +-- +-- A faithful, scripted driver lives in scripts/repro_issue_218.sh (agent-tty). + +require("config.lazy") diff --git a/fixtures/issue-218/lazy-lock.json b/fixtures/issue-218/lazy-lock.json new file mode 100644 index 00000000..baf542d2 --- /dev/null +++ b/fixtures/issue-218/lazy-lock.json @@ -0,0 +1,7 @@ +{ + "lazy.nvim": { "branch": "main", "commit": "306a05526ada86a7b30af95c5cc81ffba93fef97" }, + "nvim-treesitter": { "branch": "main", "commit": "4916d6592ede8c07973490d9322f187e07dfefac" }, + "nvim-web-devicons": { "branch": "master", "commit": "dfbfaa967a6f7ec50789bead7ef87e336c1fa63c" }, + "render-markdown.nvim": { "branch": "main", "commit": "f422cb5c6855f150e2ddcfaf44e7157b98b34f6a" }, + "snacks.nvim": { "branch": "main", "commit": "882c996cf28183f4d63640de0b4c02ec886d01f2" } +} diff --git a/fixtures/issue-218/lua/config/lazy.lua b/fixtures/issue-218/lua/config/lazy.lua new file mode 100644 index 00000000..a932814d --- /dev/null +++ b/fixtures/issue-218/lua/config/lazy.lua @@ -0,0 +1,37 @@ +-- Bootstrap lazy.nvim +local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim" +if not (vim.uv or vim.loop).fs_stat(lazypath) then + local lazyrepo = "https://github.com/folke/lazy.nvim.git" + local out = vim.fn.system({ "git", "clone", "--filter=blob:none", "--branch=stable", lazyrepo, lazypath }) + if vim.v.shell_error ~= 0 then + vim.api.nvim_echo({ + { "Failed to clone lazy.nvim:\n", "ErrorMsg" }, + { out, "WarningMsg" }, + { "\nPress any key to exit..." }, + }, true, {}) + vim.fn.getchar() + os.exit(1) + end +end +vim.opt.rtp:prepend(lazypath) + +vim.g.mapleader = " " +vim.g.maplocalleader = "\\" + +-- Resolve the claudecode.nvim checkout that owns this fixture. +-- XDG_CONFIG_HOME is set to the `fixtures/` dir by the `vv` launcher, so the +-- repository root is its parent. This makes the fixture load the local plugin +-- copy (including git worktrees) without relying on lazy's default dev path. +local repo_root = vim.fn.fnamemodify(vim.env.XDG_CONFIG_HOME or vim.fn.getcwd(), ":h") +vim.g.claudecode_dev_dir = repo_root + +require("lazy").setup({ + spec = { + { import = "plugins" }, + }, + install = { colorscheme = { "habamax" } }, + checker = { enabled = false }, +}) + +vim.keymap.set("n", "l", "Lazy", { desc = "Lazy Plugin Manager" }) +vim.keymap.set("t", "", "", { desc = "Exit terminal mode (double esc)" }) diff --git a/fixtures/issue-218/lua/plugins/dev-claudecode.lua b/fixtures/issue-218/lua/plugins/dev-claudecode.lua new file mode 100644 index 00000000..68e6493d --- /dev/null +++ b/fixtures/issue-218/lua/plugins/dev-claudecode.lua @@ -0,0 +1,150 @@ +-- claudecode.nvim (local checkout) configured exactly like the reporter's +-- repro.lua for issue #218, plus a deterministic, Claude-free trigger +-- (:Repro218) that recreates the precise state that crashes Neovim. +-- +-- Root cause (verified): accepting (:w) a NEW-file *markdown* diff that was +-- opened in a NEW TAB (diff_opts.open_in_new_tab = true) tears the diff down via +-- diff.close_diff_by_tab_name -> _cleanup_diff_state, which runs `:tabclose` on +-- the tab whose windows are STILL in diff mode. When render-markdown.nvim is +-- attached to that markdown buffer and the Claude terminal is open in the other +-- tab, that `:tabclose` abnormally terminates Neovim (SIGSEGV / exit 139 for the +-- reporter). Removing render-markdown, or turning diff mode off before the +-- teardown (the reporter's `diffoff` workaround), avoids it. +-- +-- :Repro218 reproduces this with NO real Claude needed: it opens a harmless +-- Claude *terminal* (a sleeping process) so the layout matches real usage, then +-- opens the new-tab markdown diff through the SAME coroutine machinery the +-- openDiff MCP tool uses (diff.open_diff_blocking), wiring the deferred response +-- so that accepting the diff simulates Claude writing the file and sending +-- close_tab. Focus is left in the proposed pane: just press `:w` to crash. +-- +-- We load eagerly (lazy = false) so the diff module is configured at startup. +return { + "coder/claudecode.nvim", + dir = vim.g.claudecode_dev_dir, + dependencies = { "folke/snacks.nvim" }, + lazy = false, + keys = { + { "", "ClaudeCode", desc = "Toggle Claude" }, + }, + ---@type PartialClaudeCodeConfig + opts = { + terminal = { split_side = "left", split_width_percentage = 0.5 }, + diff_opts = { open_in_new_tab = true, hide_terminal_in_new_tab = true }, + }, + config = function(_, opts) + require("claudecode").setup(opts) + + -- Markdown payload with the structures render-markdown attaches to (headings, + -- fenced code, lists, blockquotes, tables). + local function payload() + return table.concat({ + "# Issue 218 Repro", + "", + "This is a **new** markdown file proposed by Claude.", + "", + "## Section heading", + "", + "- item one", + "- item two", + " - nested", + "", + "```lua", + "local x = 1", + "print(x)", + "```", + "", + "> A blockquote for render-markdown to decorate.", + "", + "| col a | col b |", + "| ----- | ----- |", + "| 1 | 2 |", + "", + "1. first", + "2. second", + "", + }, "\n") .. "\n" + end + + -- Open a harmless Claude *terminal* (a sleeping process) without stealing + -- focus, so the window layout matches real usage. The crash only reproduces + -- when the Claude terminal is present in the original tab. + local function ensure_dummy_terminal() + local term = require("claudecode.terminal") + if term.get_active_terminal_bufnr and term.get_active_terminal_bufnr() then + return -- a terminal is already open (e.g. real :ClaudeCode); leave it + end + -- Override the launched command so no real Claude/API is needed. + term.defaults.terminal_cmd = "sh -c 'while :; do sleep 3600; done'" + pcall(vim.cmd, "ClaudeCodeStart") + pcall(function() + term.toggle_open_no_focus() + end) + end + + -- Recreate the exact openDiff flow the server runs: open_diff_blocking inside + -- a coroutine, with _G.claude_deferred_responses wired so the :w resolution + -- resumes the coroutine and then simulates Claude (write the file to disk, + -- then send close_tab via close_diff_by_tab_name on a later tick). + local function repro_218() + ensure_dummy_terminal() + local diff = require("claudecode.diff") + local new_file = vim.fn.tempname() .. "_issue218.md" + pcall(os.remove, new_file) -- ensure it does not exist => is_new_file = true + local contents = payload() + local tab_name = "✻ [Claude Code] issue218.md ⧉" + + _G.claude_deferred_responses = _G.claude_deferred_responses or {} + local co = coroutine.create(function() + return diff.open_diff_blocking(new_file, new_file, contents, tab_name, nil) + end) + _G.claude_deferred_responses[tostring(co)] = function() + -- Claude received FILE_SAVED: write the file, then send close_tab. + vim.schedule(function() + local fh = io.open(new_file, "w") + if fh then + fh:write(contents) + fh:close() + end + vim.schedule(function() + pcall(diff.close_diff_by_tab_name, tab_name) + end) + end) + end + + -- Defer the open so the terminal split settles first, then resume the + -- coroutine (which sets up the diff) and leave focus in the proposed pane. + vim.schedule(function() + coroutine.resume(co) + vim.schedule(function() + for _, b in ipairs(vim.api.nvim_list_bufs()) do + local name = vim.api.nvim_buf_get_name(b) + if name:match("proposed") and vim.bo[b].buftype == "acwrite" then + local win = vim.fn.win_findbuf(b)[1] + if win then + vim.api.nvim_set_current_win(win) + end + end + end + vim.api.nvim_echo({ + { + "Repro218 ready. Press :w in the proposed pane to accept (Neovim crashes — issue #218).", + "WarningMsg", + }, + }, false, {}) + end) + end) + end + + vim.api.nvim_create_user_command("Repro218", repro_218, { + desc = "Set up the issue #218 crash: open a NEW-file markdown diff in a new tab; then :w", + }) + + vim.api.nvim_create_user_command("Repro218Reset", function() + require("claudecode.diff")._cleanup_all_active_diffs("repro reset") + vim.cmd("silent! tabonly!") + vim.cmd("silent! only!") + vim.cmd("silent! enew!") + end, { desc = "Reset the #218 repro layout" }) + end, +} diff --git a/fixtures/issue-218/lua/plugins/render-markdown.lua b/fixtures/issue-218/lua/plugins/render-markdown.lua new file mode 100644 index 00000000..4f037c97 --- /dev/null +++ b/fixtures/issue-218/lua/plugins/render-markdown.lua @@ -0,0 +1,20 @@ +-- render-markdown.nvim — the plugin the reporter fingered. Removing it makes the +-- crash disappear (verified). Note: it does NOT render diff-mode windows by +-- default; merely being *attached* to the markdown proposed buffer (its +-- buffer-local autocmds + treesitter) is enough to make the teardown's +-- `:tabclose` crash Neovim. +-- +-- Mirrors the reporter's spec (deps on nvim-treesitter + web-devicons, default +-- opts). Neovim 0.11+ bundles the markdown/markdown_inline treesitter parsers, so +-- render-markdown attaches even without nvim-treesitter installing anything. +-- Loaded eagerly so it is attached before any diff opens. +return { + "MeanderingProgrammer/render-markdown.nvim", + dependencies = { + "nvim-treesitter/nvim-treesitter", + "nvim-tree/nvim-web-devicons", + }, + lazy = false, + ---@type render.md.UserConfig + opts = {}, +} diff --git a/fixtures/issue-218/lua/plugins/snacks.lua b/fixtures/issue-218/lua/plugins/snacks.lua new file mode 100644 index 00000000..2f153640 --- /dev/null +++ b/fixtures/issue-218/lua/plugins/snacks.lua @@ -0,0 +1,10 @@ +-- snacks.nvim, present in the reporter's repro. claudecode's terminal provider +-- auto-selects snacks when available; including it keeps the environment faithful +-- even though the crash itself is in the diff/redraw path, not the terminal. +return { + "folke/snacks.nvim", + priority = 1000, + lazy = false, + ---@type snacks.Config + opts = {}, +} diff --git a/lua/claudecode/diff.lua b/lua/claudecode/diff.lua index 79f8bb9d..e88d5692 100644 --- a/lua/claudecode/diff.lua +++ b/lua/claudecode/diff.lua @@ -858,6 +858,11 @@ local function register_diff_autocmds(tab_name, new_buffer) buffer = new_buffer, callback = function() M._resolve_diff_as_saved(tab_name, new_buffer) + -- Explicitly turn off diff mode before Neovim does its post-write redraw. + -- This prevents a crash (exit code 139) when render-markdown.nvim is installed + -- and the diff involves a new file. Without this, the post-callback redraw + -- triggers render-markdown on a buffer still in diff mode, causing a segfault. + pcall(vim.cmd, "diffoff") -- Prevent actual file write since we're handling it through MCP return true end, diff --git a/scripts/repro_issue_218.sh b/scripts/repro_issue_218.sh new file mode 100755 index 00000000..2e4a24a7 --- /dev/null +++ b/scripts/repro_issue_218.sh @@ -0,0 +1,150 @@ +#!/usr/bin/env bash +# +# Reproduction / verification for issue #218: +# "[BUG] Neovim crashes when accepting new file diff with render-markdown.nvim +# installed" +# https://github.com/coder/claudecode.nvim/issues/218 +# +# Root cause (verified): accepting (:w) a NEW-file *markdown* diff that claudecode +# opened in a NEW TAB (diff_opts.open_in_new_tab = true) tears the diff down via +# diff.close_diff_by_tab_name -> _cleanup_diff_state, which runs `:tabclose` on +# the tab whose windows are STILL in diff mode. When render-markdown.nvim is +# attached to that markdown buffer AND the Claude terminal is open in the other +# tab, that `:tabclose` abnormally terminates Neovim. Removing render-markdown, +# or turning diff mode off before the teardown (the reporter's `diffoff` +# workaround), avoids it. +# +# The exact symptom is build-dependent (same memory-unsafety bug): +# * Neovim 0.12.3 -> SIGSEGV, raw exit 139 (matches the reporter) +# * some 0.11.0 release builds -> abnormal exit 0, no VimLeave, dies mid-tabclose +# Either way Neovim disappears the instant the diff is accepted. +# +# This driver needs a REAL terminal UI (the crash is in the redraw/teardown path, +# which does not run under --headless), so it uses the `agent-tty` CLI. No real +# Claude/API is required: the fixture's :Repro218 opens a harmless Claude terminal +# (a sleeping process) and drives the openDiff coroutine flow directly. +# +# Usage: +# scripts/repro_issue_218.sh # repro (expects crash) +# NVIM_BIN=/path/to/nvim scripts/repro_issue_218.sh +# scripts/repro_issue_218.sh --no-render-markdown # control (expects survival) +# +# Exit code: 0 if the expected outcome was observed, 1 otherwise. + +# Note: no `set -e` — agent-tty subcommands can return non-zero on benign +# conditions (e.g. a `wait` that times out), and we handle outcomes explicitly. +set -uo pipefail + +REPO_ROOT="$(cd "$(dirname "$(dirname "$(realpath "$0")")")" && pwd)" +NVIM_BIN="${NVIM_BIN:-nvim}" +WANT_RM=1 +[[ "${1:-}" == "--no-render-markdown" ]] && WANT_RM=0 + +command -v agent-tty >/dev/null 2>&1 || { + echo "ERROR: agent-tty not found on PATH (required: the crash needs a real TTY/UI)." >&2 + exit 1 +} +command -v "$NVIM_BIN" >/dev/null 2>&1 || { + echo "ERROR: nvim binary '$NVIM_BIN' not found (set NVIM_BIN)." >&2 + exit 1 +} + +WORK="$(mktemp -d)" +AGENT_HOME="$WORK/atty" +mkdir -p "$AGENT_HOME" # agent-tty requires its --home dir to exist before `create` +EXITF="$WORK/nvim_exit.txt" +: >"$EXITF" + +if [[ "$WANT_RM" -eq 1 ]]; then + APPNAME="issue-218" + CONFHOME="$REPO_ROOT/fixtures" +else + # Build a sibling fixture without render-markdown.lua (the control). + APPNAME="issue-218norm" + CONFHOME="$WORK/fixtures-norm" + mkdir -p "$CONFHOME/$APPNAME/lua/plugins" + cp -R "$REPO_ROOT/fixtures/issue-218/lua/config" "$CONFHOME/$APPNAME/lua/config" + cp "$REPO_ROOT/fixtures/issue-218/lua/plugins/dev-claudecode.lua" "$CONFHOME/$APPNAME/lua/plugins/" + cp "$REPO_ROOT/fixtures/issue-218/lua/plugins/snacks.lua" "$CONFHOME/$APPNAME/lua/plugins/" + printf 'require("config.lazy")\n' >"$CONFHOME/$APPNAME/init.lua" +fi + +echo "Repo: $REPO_ROOT" +echo "Neovim: $($NVIM_BIN --version | head -1) ($NVIM_BIN)" +echo "render-md: $([[ $WANT_RM -eq 1 ]] && echo INSTALLED || echo REMOVED)" +echo "Workdir: $WORK" +echo + +echo "==> Installing fixture plugins (lazy sync)…" +NVIM_APPNAME="$APPNAME" XDG_CONFIG_HOME="$CONFHOME" "$NVIM_BIN" --headless "+Lazy! sync" "+qa" >/dev/null 2>&1 || true + +# Wrapper launcher that captures Neovim's RAW exit status (139 = SIGSEGV). +LAUNCH="$WORK/launch.sh" +cat >"$LAUNCH" < "$EXITF" +exit \$? +EOF +chmod +x "$LAUNCH" + +ATTY=(agent-tty --home "$AGENT_HOME") +echo "==> Launching Neovim under agent-tty…" +SID="$("${ATTY[@]}" create --json --cols 150 --rows 44 -- "$LAUNCH" | jq -r '.result.sessionId')" +if [[ -z "$SID" || "$SID" == "null" ]]; then + echo "ERROR: failed to create agent-tty session." >&2 + exit 1 +fi +# shellcheck disable=SC2329 # invoked indirectly via the EXIT trap below +cleanup() { "${ATTY[@]}" destroy "$SID" --json >/dev/null 2>&1 || true; } +trap cleanup EXIT + +"${ATTY[@]}" wait "$SID" --screen-stable-ms 1800 --json >/dev/null 2>&1 || true + +echo "==> :Repro218 (open new-file markdown diff in a new tab)…" +"${ATTY[@]}" type "$SID" ":Repro218" --json >/dev/null 2>&1 +"${ATTY[@]}" send-keys "$SID" "Enter" --json >/dev/null 2>&1 +"${ATTY[@]}" wait "$SID" --text "Repro218 ready" --timeout 10000 --json >/dev/null 2>&1 || true +"${ATTY[@]}" wait "$SID" --screen-stable-ms 1200 --json >/dev/null 2>&1 || true + +echo "==> :w (accept the diff)…" +"${ATTY[@]}" send-keys "$SID" "Escape" --json >/dev/null 2>&1 +"${ATTY[@]}" type "$SID" ":w" --json >/dev/null 2>&1 +"${ATTY[@]}" send-keys "$SID" "Enter" --json >/dev/null 2>&1 + +# Give Neovim a moment to crash (or not). +"${ATTY[@]}" wait "$SID" --exit --timeout 8000 --json >/dev/null 2>&1 || true + +STATUS="$("${ATTY[@]}" inspect "$SID" --json 2>&1 | jq -r '.result.session.status')" +RAW="$(sed -n 's/^RAW=//p' "$EXITF" 2>/dev/null || true)" + +echo +echo "------------------------------------------------------------" +echo "agent-tty session status : $STATUS" +echo "raw nvim exit code : ${RAW:-}" +echo "------------------------------------------------------------" + +if [[ "$WANT_RM" -eq 1 ]]; then + if [[ "$STATUS" == "exited" ]]; then + if [[ "$RAW" == "139" ]]; then + echo "RESULT: #218 REPRODUCED — Neovim SIGSEGV (139) on diff accept." + else + echo "RESULT: #218 REPRODUCED — Neovim abnormally terminated (raw=$RAW) on diff accept." + echo " (exit 0 with no VimLeave is the same memory bug surfacing differently;" + echo " try NVIM_BIN pointing at a 0.12.x build to see the canonical SIGSEGV 139.)" + fi + exit 0 + fi + echo "RESULT: NOT reproduced on this build — Neovim is still running after accept." + exit 1 +else + if [[ "$STATUS" != "exited" ]]; then + echo "RESULT: control OK — without render-markdown the diff accepts cleanly (no crash)." + exit 0 + fi + echo "RESULT: unexpected — Neovim terminated (raw=$RAW) even without render-markdown." + exit 1 +fi