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
4 changes: 2 additions & 2 deletions backends/codex/code-close-diff.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ log() { :; }
# shellcheck source=/dev/null
source "$BIN_DIR/nvim-socket.sh" "$CWD" 2>/dev/null || true
# shellcheck source=/dev/null
source "$BIN_DIR/nvim-send.sh" 2>/dev/null || true
source "$BIN_DIR/nvim-call.sh" 2>/dev/null || true
if [[ -n "${NVIM_SOCKET:-}" ]]; then
_CTX=$(nvim --server "$NVIM_SOCKET" --remote-expr "luaeval(\"vim.json.encode({debug=require('code-preview.log').is_enabled(),log_file=require('code-preview.log').get_log_path() or ''})\")" 2>/dev/null || echo '{}')
_CTX="$(nvim_call code-preview.log state '[]' || echo '{}')"
_DBG=$(echo "$_CTX" | jq -r '.debug // false' 2>/dev/null)
_LOG=$(echo "$_CTX" | jq -r '.log_file // ""' 2>/dev/null)
if [[ "$_DBG" == "true" && -n "$_LOG" ]]; then
Expand Down
4 changes: 2 additions & 2 deletions backends/codex/code-preview-diff.sh
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ log() { :; }
# shellcheck source=/dev/null
source "$BIN_DIR/nvim-socket.sh" "$CWD" 2>/dev/null || true
# shellcheck source=/dev/null
source "$BIN_DIR/nvim-send.sh" 2>/dev/null || true
source "$BIN_DIR/nvim-call.sh" 2>/dev/null || true
if [[ -n "${NVIM_SOCKET:-}" ]]; then
_CTX=$(nvim --server "$NVIM_SOCKET" --remote-expr "luaeval(\"vim.json.encode({debug=require('code-preview.log').is_enabled(),log_file=require('code-preview.log').get_log_path() or ''})\")" 2>/dev/null || echo '{}')
_CTX="$(nvim_call code-preview.log state '[]' || echo '{}')"
_DBG=$(echo "$_CTX" | jq -r '.debug // false' 2>/dev/null)
_LOG=$(echo "$_CTX" | jq -r '.log_file // ""' 2>/dev/null)
if [[ "$_DBG" == "true" && -n "$_LOG" ]]; then
Expand Down
4 changes: 2 additions & 2 deletions backends/copilot/code-close-diff.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ log() { :; }
# shellcheck source=/dev/null
source "$BIN_DIR/nvim-socket.sh" "$CWD" 2>/dev/null || true
# shellcheck source=/dev/null
source "$BIN_DIR/nvim-send.sh" 2>/dev/null || true
source "$BIN_DIR/nvim-call.sh" 2>/dev/null || true
if [[ -n "${NVIM_SOCKET:-}" ]]; then
_CTX=$(nvim --server "$NVIM_SOCKET" --remote-expr "luaeval(\"vim.json.encode({debug=require('code-preview.log').is_enabled(),log_file=require('code-preview.log').get_log_path() or ''})\")" 2>/dev/null || echo '{}')
_CTX="$(nvim_call code-preview.log state '[]' || echo '{}')"
_DBG=$(echo "$_CTX" | jq -r '.debug // false' 2>/dev/null)
_LOG=$(echo "$_CTX" | jq -r '.log_file // ""' 2>/dev/null)
if [[ "$_DBG" == "true" && -n "$_LOG" ]]; then
Expand Down
4 changes: 2 additions & 2 deletions backends/copilot/code-preview-diff.sh
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@ log() { :; }
# shellcheck source=/dev/null
source "$BIN_DIR/nvim-socket.sh" "$CWD" 2>/dev/null || true
# shellcheck source=/dev/null
source "$BIN_DIR/nvim-send.sh" 2>/dev/null || true
source "$BIN_DIR/nvim-call.sh" 2>/dev/null || true
_NVIM_SERVERNAME=""
_NVIM_CWD=""
if [[ -n "${NVIM_SOCKET:-}" ]]; then
_CTX=$(nvim --server "$NVIM_SOCKET" --remote-expr "luaeval(\"vim.json.encode({debug=require('code-preview.log').is_enabled(),log_file=require('code-preview.log').get_log_path() or '',servername=vim.v.servername,cwd=vim.fn.getcwd()})\")" 2>/dev/null || echo '{}')
_CTX="$(nvim_call code-preview.log state '[]' || echo '{}')"
_DBG=$(echo "$_CTX" | jq -r '.debug // false' 2>/dev/null)
_LOG=$(echo "$_CTX" | jq -r '.log_file // ""' 2>/dev/null)
_NVIM_SERVERNAME=$(echo "$_CTX" | jq -r '.servername // ""' 2>/dev/null)
Expand Down
17 changes: 9 additions & 8 deletions bin/core-post-tool.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ TOOL_NAME="$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null || true)"

# Discover Neovim socket (prefer instance whose cwd matches project) and load RPC helpers
source "$SCRIPT_DIR/nvim-socket.sh" "$CWD" 2>/dev/null
source "$SCRIPT_DIR/nvim-send.sh"
source "$SCRIPT_DIR/nvim-call.sh"

# Set up logging — query debug config from nvim
log_post() { :; }
if [[ -n "${NVIM_SOCKET:-}" ]]; then
_POST_CTX=$(nvim --server "$NVIM_SOCKET" --remote-expr "luaeval(\"vim.json.encode({debug=require('code-preview.log').is_enabled(),log_file=require('code-preview.log').get_log_path() or ''})\")" 2>/dev/null || echo '{}')
_POST_CTX="$(nvim_call code-preview.log state '[]' || echo '{}')"
_POST_DEBUG=$(echo "$_POST_CTX" | jq -r '.debug // false')
_POST_LOG_FILE=$(echo "$_POST_CTX" | jq -r '.log_file // ""')
if [[ "$_POST_DEBUG" == "true" && -n "$_POST_LOG_FILE" ]]; then
Expand All @@ -42,8 +42,9 @@ log_post "tool=$TOOL_NAME"
# doesn't clobber `modified` markers from concurrent Edit/Write/ApplyPatch
# operations whose post-hook hasn't fired yet.
if [[ "$TOOL_NAME" == "Bash" ]]; then
nvim_send "require('code-preview.changes').clear_by_statuses({'deleted','bash_modified','bash_created'})" || true
nvim_send "vim.defer_fn(function() pcall(function() require('code-preview.neo_tree').refresh() end) end, 200)" || true
nvim_call code-preview.changes clear_by_statuses \
'[["deleted","bash_modified","bash_created"]]' >/dev/null || true
nvim_call code-preview.neo_tree refresh_deferred '[200]' >/dev/null || true
exit 0
fi

Expand Down Expand Up @@ -71,9 +72,9 @@ if [[ "$TOOL_NAME" == "ApplyPatch" ]]; then
if [[ "$fpath" != /* && -n "$CWD_POST" ]]; then
fpath="$CWD_POST/$fpath"
fi
fpath_esc="$(escape_lua "$fpath")"
log_post "closing diff for patch file=$fpath"
nvim_send "require('code-preview.diff').close_for_file('$fpath_esc')" || true
nvim_call code-preview.diff close_for_file \
"$(jq -nc --arg f "$fpath" '[$f]')" >/dev/null || true
done < <(extract_patch_paths "$PATCH_TEXT")
fi
rm -f "${TMPDIR:-/tmp}"/claude-diff-original* "${TMPDIR:-/tmp}"/claude-diff-proposed* "${TMPDIR:-/tmp}"/claude-patch-*
Expand All @@ -82,13 +83,13 @@ fi

# Extract file path early — needed for tagged is_open() check
FILE_PATH="$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null || true)"
FILE_PATH_ESC="$(escape_lua "${FILE_PATH:-}")"

# Tell Lua to handle this file's close — tolerates out-of-order post-hooks
# (OpenCode may fire them in a different order than pre-hooks).
if [[ -n "$FILE_PATH" ]]; then
log_post "closing diff for file=$FILE_PATH"
nvim_send "require('code-preview.diff').close_for_file('$FILE_PATH_ESC')" || true
nvim_call code-preview.diff close_for_file \
"$(jq -nc --arg f "$FILE_PATH" '[$f]')" >/dev/null || true
# neo_tree.refresh() is handled inside close_for_file() via vim.schedule()
fi

Expand Down
84 changes: 49 additions & 35 deletions bin/core-pre-tool.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ CWD="$(echo "$INPUT" | jq -r '.cwd')"

# Discover Neovim socket (prefer instance whose cwd matches project) and load RPC helpers
source "$SCRIPT_DIR/nvim-socket.sh" "$CWD" 2>/dev/null || true
source "$SCRIPT_DIR/nvim-send.sh"
source "$SCRIPT_DIR/nvim-call.sh"

HAS_NVIM=true
if [[ -z "${NVIM_SOCKET:-}" ]]; then
Expand All @@ -36,7 +36,7 @@ fi
# Set up logging early so all code paths can use it
log_pre() { :; }
if [[ "$HAS_NVIM" == "true" ]]; then
_PRE_CTX=$(nvim --server "$NVIM_SOCKET" --remote-expr "luaeval(\"vim.json.encode({debug=require('code-preview.log').is_enabled(),log_file=require('code-preview.log').get_log_path() or ''})\")" 2>/dev/null || echo '{}')
_PRE_CTX="$(nvim_call code-preview.log state '[]' || echo '{}')"
_PRE_DEBUG=$(echo "$_PRE_CTX" | jq -r '.debug // false')
_PRE_LOG_FILE=$(echo "$_PRE_CTX" | jq -r '.log_file // ""')
if [[ "$_PRE_DEBUG" == "true" && -n "$_PRE_LOG_FILE" ]]; then
Expand Down Expand Up @@ -113,12 +113,24 @@ case "$TOOL_NAME" in
| grep -vE '^-' \
| while read -r p; do
if [[ -z "$p" ]]; then continue; fi
# Resolve relative paths against CWD
if [[ "$p" != /* ]]; then
echo "$CWD/$p"
else
echo "$p"
fi
# Strip outer single/double quotes — agents wrap
# paths with shell-special chars (apostrophes,
# spaces) in quotes, and that quoting survives
# into tool_input.command literally.
p="${p#\"}"; p="${p%\"}"
p="${p#\'}"; p="${p%\'}"
# Strip trailing CR (Windows-style payloads).
p="${p%$'\r'}"
if [[ -z "$p" ]]; then continue; fi
# Resolve relative paths against CWD; absolute
# paths and `~/`-prefixed paths pass through.
# `'~/'*` is quoted so bash doesn't tilde-expand
# the pattern at match time.
case "$p" in
/*) echo "$p" ;;
'~/'*) echo "${HOME}/${p#'~/'}" ;;
*) echo "$CWD/$p" ;;
esac
done
fi
}
Expand All @@ -131,19 +143,24 @@ case "$TOOL_NAME" in
done < <(detect_rm_paths "$subcmd")
done < <(echo "$COMMAND" | sed 's/[;&|]\{1,2\}/\n/g')

RM_PATHS="$(echo "$RM_PATHS" | xargs)"
# Trim leading/trailing whitespace without invoking xargs — xargs does
# shell-like quote processing on its input and would discard everything
# if any path contained an unbalanced quote (e.g. an apostrophe in
# `it's-mine.txt`).
RM_PATHS="${RM_PATHS#"${RM_PATHS%%[![:space:]]*}"}"
RM_PATHS="${RM_PATHS%"${RM_PATHS##*[![:space:]]}"}"

# Mark each rm-detected path as deleted in neo-tree
if [[ -n "$RM_PATHS" && "$HAS_NVIM" == "true" ]]; then
for path in $RM_PATHS; do
PATH_ESC="$(escape_lua "$path")"
nvim_send "require('code-preview.changes').set('$PATH_ESC', 'deleted')" || true
nvim_call code-preview.changes set \
"$(jq -nc --arg p "$path" '[$p, "deleted"]')" >/dev/null || true
done
nvim_send "pcall(function() require('code-preview.neo_tree').refresh() end)" || true
nvim_call code-preview.neo_tree refresh '[]' >/dev/null || true
# Reveal the first deleted file in the tree
FIRST_PATH="$(echo "$RM_PATHS" | awk '{print $1}')"
FIRST_ESC="$(escape_lua "$FIRST_PATH")"
nvim_send "vim.defer_fn(function() pcall(function() require('code-preview.neo_tree').reveal('$FIRST_ESC') end) end, 300)" || true
nvim_call code-preview.neo_tree reveal_deferred \
"$(jq -nc --arg p "$FIRST_PATH" --argjson d 300 '[$p, $d]')" >/dev/null || true
fi

# ── Tier 1 shell-write detection ────────────────────────────────
Expand Down Expand Up @@ -244,7 +261,9 @@ case "$TOOL_NAME" in
*) WRITE_PATHS="$WRITE_PATHS $raw" ;;
esac
done < <(detect_write_paths "$COMMAND")
WRITE_PATHS="$(echo "$WRITE_PATHS" | xargs)"
# Trim without xargs (see RM_PATHS comment above re: apostrophes).
WRITE_PATHS="${WRITE_PATHS#"${WRITE_PATHS%%[![:space:]]*}"}"
WRITE_PATHS="${WRITE_PATHS%"${WRITE_PATHS##*[![:space:]]}"}"

# Note: this branch always runs for Bash (no early-exit on read-only
# commands). The detector forks several subshells per invocation; if
Expand All @@ -260,17 +279,17 @@ case "$TOOL_NAME" in
else
STATUS="bash_created"
fi
PATH_ESC="$(escape_lua "$path")"
nvim_send "require('code-preview.changes').set('$PATH_ESC', '$STATUS')" || true
nvim_call code-preview.changes set \
"$(jq -nc --arg p "$path" --arg s "$STATUS" '[$p, $s]')" >/dev/null || true
done
nvim_send "pcall(function() require('code-preview.neo_tree').refresh() end)" || true
nvim_call code-preview.neo_tree refresh '[]' >/dev/null || true
# Reveal precedence: rm wins. If the rm branch already queued a
# reveal, skip ours so we don't double-fire two defer_fn reveals on
# a command that both rm's and writes (e.g. `rm a && echo x > b`).
if [[ -z "$RM_PATHS" ]]; then
FIRST_PATH="$(echo "$WRITE_PATHS" | awk '{print $1}')"
FIRST_ESC="$(escape_lua "$FIRST_PATH")"
nvim_send "vim.defer_fn(function() pcall(function() require('code-preview.neo_tree').reveal('$FIRST_ESC') end) end, 300)" || true
nvim_call code-preview.neo_tree reveal_deferred \
"$(jq -nc --arg p "$FIRST_PATH" --argjson d 300 '[$p, $d]')" >/dev/null || true
fi
fi

Expand Down Expand Up @@ -318,12 +337,8 @@ case "$TOOL_NAME" in
log_pre "ApplyPatch: file=$REL_PATH action=$ACTION"

if [[ "$HAS_NVIM" == "true" ]]; then
display_esc="$(escape_lua "$REL_PATH")"
orig_esc="$(escape_lua "$PATCH_ORIG")"
prop_esc="$(escape_lua "$PATCH_PROP")"
fpath_esc="$(escape_lua "$PATCH_FILE_PATH")"

HOOK_CTX=$(nvim --server "$NVIM_SOCKET" --remote-expr "luaeval(\"require('code-preview').hook_context('${fpath_esc}')\")" 2>/dev/null || echo '{}')
HOOK_CTX="$(nvim_call code-preview hook_context \
"$(jq -nc --arg fp "$PATCH_FILE_PATH" '[$fp]')" || echo '{}')"
VISIBLE_ONLY=$(echo "$HOOK_CTX" | jq -r '.visible_only // false')
FILE_VISIBLE=$(echo "$HOOK_CTX" | jq -r '.file_visible // false')

Expand All @@ -335,8 +350,9 @@ case "$TOOL_NAME" in

if [[ "$SHOULD_SHOW" == "1" ]]; then
log_pre "ApplyPatch: sending diff for $REL_PATH to nvim (action=$ACTION)"
action_esc="$(escape_lua "$ACTION")"
nvim_send "require('code-preview.diff').show_diff('$orig_esc', '$prop_esc', '$display_esc', '$fpath_esc', '$action_esc')" || true
nvim_call code-preview.diff show_diff \
"$(jq -nc --arg o "$PATCH_ORIG" --arg p "$PATCH_PROP" --arg d "$REL_PATH" --arg f "$PATCH_FILE_PATH" --arg a "$ACTION" \
'[$o, $p, $d, $f, $a]')" >/dev/null || true
fi
else
log_pre "ApplyPatch: no nvim connection, skipping diff for $REL_PATH"
Expand All @@ -357,15 +373,11 @@ esac
DISPLAY_NAME="${FILE_PATH#"$CWD/"}"

if [[ "$HAS_NVIM" == "true" ]]; then
ORIG_ESC="$(escape_lua "$ORIG_FILE")"
PROP_ESC="$(escape_lua "$PROP_FILE")"
DISPLAY_ESC="$(escape_lua "$DISPLAY_NAME")"
FILE_PATH_ESC="$(escape_lua "$FILE_PATH")"

# Query config + file visibility from nvim in a single RPC call.
# Neo-tree indicator/reveal is now driven from lua/code-preview/diff.lua
# (inside show_diff), so we only need visibility + permission fields here.
HOOK_CTX=$(nvim --server "$NVIM_SOCKET" --remote-expr "luaeval(\"require('code-preview').hook_context('${FILE_PATH_ESC}')\")" 2>/dev/null || echo '{}')
HOOK_CTX="$(nvim_call code-preview hook_context \
"$(jq -nc --arg fp "$FILE_PATH" '[$fp]')" || echo '{}')"
VISIBLE_ONLY=$(echo "$HOOK_CTX" | jq -r '.visible_only // false')
FILE_VISIBLE=$(echo "$HOOK_CTX" | jq -r '.file_visible // false')
DEFER_PERMISSIONS=$(echo "$HOOK_CTX" | jq -r 'if .defer_claude_permissions == true then "true" else "false" end')
Expand All @@ -382,7 +394,9 @@ if [[ "$HAS_NVIM" == "true" ]]; then

if [[ "$SHOULD_SHOW" == "1" ]]; then
log_pre "sending diff to nvim (layout via config)"
nvim_send "require('code-preview.diff').show_diff('$ORIG_ESC', '$PROP_ESC', '$DISPLAY_ESC', '$FILE_PATH_ESC')" || true
nvim_call code-preview.diff show_diff \
"$(jq -nc --arg o "$ORIG_FILE" --arg p "$PROP_FILE" --arg d "$DISPLAY_NAME" --arg f "$FILE_PATH" \
'[$o, $p, $d, $f]')" >/dev/null || true
fi
fi

Expand Down
48 changes: 48 additions & 0 deletions bin/nvim-call.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
# nvim-call.sh — Structured RPC into the running Neovim.
#
# Replaces nvim-send.sh's string-interpolated "build Lua source in bash"
# pattern. Args travel as a JSON array via a temp file; the receiving Lua
# decodes them with vim.json.decode and calls the target function. No user
# data ever enters a Lua source string, so escape_lua and its quoting
# footguns are gone.
#
# Usage:
# source bin/nvim-call.sh
# ARGS=$(jq -nc --arg p "$path" --arg s "deleted" '[$p, $s]')
# nvim_call code-preview.changes set "$ARGS"
#
# # Capturing a return value (function returns a string or table):
# CTX=$(nvim_call code-preview.log state '[]')
# echo "$CTX" | jq -r '.debug'
#
# Depends on nvim-socket.sh being sourced first (NVIM_SOCKET must be set).

# nvim_call MOD FN JSON_ARGS
# Returns:
# 0 on success
# 1 if no nvim socket is available
# 2 if the dispatch itself failed (bad JSON, missing module/function, etc.)
# Stdout is the function's return value: strings pass through, tables are
# JSON-encoded, nil becomes empty string. Dispatch failures are also logged
# inside nvim via log.error (vim.notify), so a silent rc=2 here is visible
# to the developer through the editor.
nvim_call() {
local mod="$1" fn="$2" args="${3:-[]}"
if [[ -z "${NVIM_SOCKET:-}" ]]; then
return 1
fi
local tmp
tmp="$(mktemp "${TMPDIR:-/tmp}/code-preview-args.XXXXXX")"
printf '%s' "$args" > "$tmp"
# Only mod/fn/tmp — all controlled by us — get interpolated into Lua source.
# User data flows through $tmp as JSON.
nvim --server "$NVIM_SOCKET" --remote-expr \
"luaeval(\"require('code-preview.rpc').dispatch('$mod', '$fn', '$tmp')\")" 2>/dev/null
local rc=$?
rm -f "$tmp"
if [[ $rc -ne 0 ]]; then
return 2
fi
return 0
}
33 changes: 0 additions & 33 deletions bin/nvim-send.sh

This file was deleted.

10 changes: 9 additions & 1 deletion lua/code-preview/health.lua
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ function M.check()
end
end

-- RPC dispatcher (loaded by every hook RPC; readability check only since
-- it's a Lua module on the rtp, not a standalone script).
if pcall(require, "code-preview.rpc") then
ok("code-preview.rpc dispatcher loadable")
else
error("code-preview.rpc dispatcher failed to load — hook RPC will not work")
end

-- ── Claude Code backend ───────────────────────────────────────

start("Claude Code backend")
Expand Down Expand Up @@ -79,7 +87,7 @@ function M.check()
-- Shared scripts
for _, script in ipairs({
"nvim-socket.sh",
"nvim-send.sh",
"nvim-call.sh",
"apply-edit.lua",
"apply-multi-edit.lua",
}) do
Expand Down
Loading
Loading