From eefefdd6b0096deca511ba4f82dd34457b695eee Mon Sep 17 00:00:00 2001 From: kam-hak Date: Thu, 2 Apr 2026 17:26:07 -0400 Subject: [PATCH] feat: visible_only mode, toggle command, neo-tree reveal improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add diff.visible_only config (default false) — only show diff for files open in a visible nvim window; auto-approve others - Add :ClaudePreviewToggleVisibleOnly command for session toggle - Add neo_tree.reveal config (default true) — disable to skip reveal - Add neo_tree.reveal_root config ("cwd"|"git") — pass dir param to neo-tree reveal instead of changing nvim's cwd - Batch all config + visibility queries into single M.hook_context() RPC - Use vim.uv.fs_realpath() for canonical path resolution - Guard close-diff hook: only clean up when a diff is actually open - Remove post-close reveal to avoid stale CWD prompts --- bin/claude-close-diff.sh | 14 ++++---- bin/claude-preview-diff.sh | 64 ++++++++++++++++++++------------- lua/claude-preview/init.lua | 49 +++++++++++++++++++++++++ lua/claude-preview/neo_tree.lua | 10 ++++-- 4 files changed, 102 insertions(+), 35 deletions(-) diff --git a/bin/claude-close-diff.sh b/bin/claude-close-diff.sh index e1918df..8f230f7 100755 --- a/bin/claude-close-diff.sh +++ b/bin/claude-close-diff.sh @@ -23,14 +23,12 @@ fi # Extract file path for post-close reveal FILE_PATH="$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null || true)" -# Clear neo-tree change indicators and close the diff tab -nvim_send "require('claude-preview.changes').clear_all()" || true -nvim_send "require('claude-preview.diff').close_diff()" || true -# Deferred refresh + reveal so neo-tree picks up changes after Claude writes them to disk -if [[ -n "$FILE_PATH" ]]; then - FILE_PATH_ESC="$(escape_lua "$FILE_PATH")" - nvim_send "vim.defer_fn(function() pcall(function() require('claude-preview.neo_tree').refresh() end) vim.defer_fn(function() pcall(function() require('claude-preview.neo_tree').reveal('$FILE_PATH_ESC') end) end, 200) end, 200)" || true -else +# Only clean up if a diff was actually open +DIFF_OPEN=$(nvim --server "$NVIM_SOCKET" --remote-expr "luaeval(\"require('claude-preview.diff').is_open()\")" 2>/dev/null || echo "false") + +if [[ "$DIFF_OPEN" == "true" ]]; then + nvim_send "require('claude-preview.changes').clear_all()" || true + nvim_send "require('claude-preview.diff').close_diff()" || true nvim_send "vim.defer_fn(function() pcall(function() require('claude-preview.neo_tree').refresh() end) end, 200)" || true fi diff --git a/bin/claude-preview-diff.sh b/bin/claude-preview-diff.sh index 62b28a5..6409c19 100755 --- a/bin/claude-preview-diff.sh +++ b/bin/claude-preview-diff.sh @@ -138,6 +138,19 @@ if [[ "$HAS_NVIM" == "true" ]]; then DISPLAY_ESC="$(escape_lua "$DISPLAY_NAME")" FILE_PATH_ESC="$(escape_lua "$FILE_PATH")" + # Query config + file visibility from nvim in a single RPC call + HOOK_CTX=$(nvim --server "$NVIM_SOCKET" --remote-expr "luaeval(\"require('claude-preview').hook_context('${FILE_PATH_ESC}')\")" 2>/dev/null || echo '{}') + VISIBLE_ONLY=$(echo "$HOOK_CTX" | jq -r '.visible_only // false') + NEO_TREE_REVEAL=$(echo "$HOOK_CTX" | jq -r '.neo_tree_reveal // true') + NEO_TREE_REVEAL_ROOT=$(echo "$HOOK_CTX" | jq -r '.reveal_root // "cwd"') + FILE_VISIBLE=$(echo "$HOOK_CTX" | jq -r '.file_visible // false') + + # Decide whether to show the diff + SHOULD_SHOW="1" + if [[ "$VISIBLE_ONLY" == "true" && "$FILE_VISIBLE" != "true" ]]; then + SHOULD_SHOW="0" + fi + # Determine change status for neo-tree indicator # Check if the actual file exists on disk (not the temp copy, which is always created) if [[ -f "$FILE_PATH" ]]; then @@ -146,35 +159,38 @@ if [[ "$HAS_NVIM" == "true" ]]; then CHANGE_STATUS="created" fi - nvim_send "require('claude-preview.changes').set('$FILE_PATH_ESC', '$CHANGE_STATUS')" || true - nvim_send "pcall(function() require('claude-preview.neo_tree').refresh() end)" || true - # Reveal the file in neo-tree: for modified files reveal the file itself, - # for created files reveal the nearest existing parent directory - if [[ "$CHANGE_STATUS" == "modified" ]]; then - nvim_send "vim.defer_fn(function() pcall(function() require('claude-preview.neo_tree').reveal('$FILE_PATH_ESC') end) end, 300)" || true - else - # Walk up to find the nearest existing parent directory - REVEAL_DIR="$(dirname "$FILE_PATH")" - while [[ ! -d "$REVEAL_DIR" && "$REVEAL_DIR" != "/" ]]; do - REVEAL_DIR="$(dirname "$REVEAL_DIR")" - done - # Reveal a file inside the parent dir to force neo-tree to expand it - REVEAL_TARGET="$(find "$REVEAL_DIR" -maxdepth 1 -type f | head -1)" - if [[ -z "$REVEAL_TARGET" ]]; then - REVEAL_TARGET="$REVEAL_DIR" + if [[ "$SHOULD_SHOW" == "1" ]]; then + nvim_send "require('claude-preview.changes').set('$FILE_PATH_ESC', '$CHANGE_STATUS')" || true + + # Neo-tree integration (gated by config) + if [[ "$NEO_TREE_REVEAL" == "true" ]]; then + # Resolve the directory neo-tree should root from + REVEAL_DIR="" + if [[ "$NEO_TREE_REVEAL_ROOT" == "git" ]]; then + REVEAL_DIR=$(git -C "$(dirname "$FILE_PATH")" rev-parse --show-toplevel 2>/dev/null || echo "") + REVEAL_DIR="${REVEAL_DIR:-$(dirname "$FILE_PATH")}" + fi + + nvim_send "pcall(function() require('claude-preview.neo_tree').refresh() end)" || true + + if [[ -n "$REVEAL_DIR" ]]; then + REVEAL_DIR_ESC="$(escape_lua "$REVEAL_DIR")" + nvim_send "vim.defer_fn(function() pcall(function() require('claude-preview.neo_tree').reveal('$FILE_PATH_ESC', '$REVEAL_DIR_ESC') end) end, 300)" || true + else + nvim_send "vim.defer_fn(function() pcall(function() require('claude-preview.neo_tree').reveal('$FILE_PATH_ESC') end) end, 300)" || true + fi fi - REVEAL_TARGET_ESC="$(escape_lua "$REVEAL_TARGET")" - nvim_send "vim.defer_fn(function() pcall(function() require('claude-preview.neo_tree').reveal('$REVEAL_TARGET_ESC') end) end, 300)" || true + + nvim_send "require('claude-preview.diff').show_diff('$ORIG_ESC', '$PROP_ESC', '$DISPLAY_ESC')" || true fi - nvim_send "require('claude-preview.diff').show_diff('$ORIG_ESC', '$PROP_ESC', '$DISPLAY_ESC')" || true fi -# --- Always ask for user confirmation --- +# --- Permission decision --- -if [[ "$HAS_NVIM" == "true" ]]; then +if [[ "$HAS_NVIM" == "true" && "$SHOULD_SHOW" == "1" ]]; then REASON="Diff preview sent to Neovim. Review before accepting." + printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask","permissionDecisionReason":"%s"}}\n' "$REASON" else - REASON="Neovim not running. Review the diff in CLI before accepting." + # File not visible or no nvim — auto-approve + printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","permissionDecisionReason":"Auto-approved"}}\n' fi - -printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask","permissionDecisionReason":"%s"}}\n' "$REASON" diff --git a/lua/claude-preview/init.lua b/lua/claude-preview/init.lua index fc292e9..2948b9d 100644 --- a/lua/claude-preview/init.lua +++ b/lua/claude-preview/init.lua @@ -10,9 +10,12 @@ local default_config = { auto_close = true, equalize = true, full_file = true, + visible_only = false, -- only show diffs for files open in a visible nvim window }, neo_tree = { enabled = true, + reveal = true, -- reveal edited files in neo-tree + reveal_root = "cwd", -- "cwd" (default), "git" (nearest git root), or false (skip reveal) refresh_on_change = true, position = "right", symbols = { @@ -87,6 +90,15 @@ function M.setup(user_config) M.status() end, { desc = "Show claude-preview status" }) + vim.api.nvim_create_user_command("ClaudePreviewToggleVisibleOnly", function() + M.config.diff.visible_only = not M.config.diff.visible_only + vim.notify( + "claude-preview: visible_only = " .. tostring(M.config.diff.visible_only), + vim.log.levels.INFO, + { title = "claude-preview" } + ) + end, { desc = "Toggle visible_only — show diffs only for open buffers vs all files" }) + -- Neo-tree integration (soft dependency) if M.config.neo_tree.enabled then require("claude-preview.neo_tree").setup(M.config) @@ -97,6 +109,43 @@ function M.setup(user_config) end, { desc = "Close claude-preview diff" }) end +--- Query hook context for the PreToolUse shell script. +--- Returns a JSON string with config + file visibility in a single RPC call. +--- @param file_path string absolute path of the file being edited +--- @return string JSON: { visible_only, neo_tree_reveal, reveal_root, file_visible } +function M.hook_context(file_path) + local cfg = M.config + local visible_only = cfg.diff.visible_only and true or false + local neo_tree_reveal = (cfg.neo_tree.enabled and cfg.neo_tree.reveal) and true or false + local reveal_root = cfg.neo_tree.reveal_root or "cwd" + + local file_visible = false + if visible_only and file_path ~= "" then + -- Resolve to canonical path; use case-insensitive compare on macOS + local is_mac = vim.fn.has("mac") == 1 + local target = vim.uv.fs_realpath(file_path) or vim.fn.fnamemodify(file_path, ":p") + if is_mac then target = target:lower() end + + for _, w in ipairs(vim.api.nvim_list_wins()) do + local b = vim.api.nvim_win_get_buf(w) + local name = vim.uv.fs_realpath(vim.api.nvim_buf_get_name(b)) + or vim.fn.fnamemodify(vim.api.nvim_buf_get_name(b), ":p") + if is_mac then name = name:lower() end + if name == target then + file_visible = true + break + end + end + end + + return vim.json.encode({ + visible_only = visible_only, + neo_tree_reveal = neo_tree_reveal, + reveal_root = reveal_root, + file_visible = file_visible, + }) +end + function M.status() local lines = { "claude-preview.nvim status", string.rep("─", 40) } diff --git a/lua/claude-preview/neo_tree.lua b/lua/claude-preview/neo_tree.lua index 4dd6d68..41fe477 100644 --- a/lua/claude-preview/neo_tree.lua +++ b/lua/claude-preview/neo_tree.lua @@ -345,20 +345,24 @@ function M.refresh() end) end -function M.reveal(filepath) +function M.reveal(filepath, dir) if not has_neo_tree then return end pcall(function() local cfg = require("claude-preview").config local position = cfg.neo_tree.position or "right" - require("neo-tree.command").execute({ + local opts = { action = "show", source = "filesystem", reveal_file = filepath, position = position, toggle = false, - }) + } + if dir then + opts.dir = dir + end + require("neo-tree.command").execute(opts) end) end