Skip to content
Closed
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
14 changes: 6 additions & 8 deletions bin/claude-close-diff.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
64 changes: 40 additions & 24 deletions bin/claude-preview-diff.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
49 changes: 49 additions & 0 deletions lua/claude-preview/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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)
Expand All @@ -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) }

Expand Down
10 changes: 7 additions & 3 deletions lua/claude-preview/neo_tree.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down