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
96 changes: 55 additions & 41 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ Supports [Claude Code](https://docs.anthropic.com/en/docs/claude-code) and [Open
{
"Cannon07/claude-preview.nvim",
config = function()
require("claude-preview").setup()
require("code-preview").setup()
end,
}
```
Expand All @@ -74,7 +74,7 @@ Supports [Claude Code](https://docs.anthropic.com/en/docs/claude-code) and [Open

```lua
vim.opt.rtp:prepend("/path/to/claude-preview.nvim")
require("claude-preview").setup()
require("code-preview").setup()
```

---
Expand All @@ -85,7 +85,7 @@ require("claude-preview").setup()

1. Install the plugin and call `setup()`
2. Open a project in Neovim
3. Run `:ClaudePreviewInstallHooks` — writes hooks to `.claude/settings.local.json`
3. Run `:CodePreviewInstallClaudeCodeHooks` — writes hooks to `.claude/settings.local.json`
4. Restart Claude Code CLI in the project directory
5. Ask Claude to edit a file — a diff opens automatically in Neovim
6. Accept/reject in the CLI; the diff closes automatically on accept
Expand Down Expand Up @@ -141,7 +141,7 @@ Both backends communicate with Neovim via RPC (`nvim --server <socket> --remote-
All options with defaults:

```lua
require("claude-preview").setup({
require("code-preview").setup({
diff = {
layout = "tab", -- "tab" (new tab) | "vsplit" (current tab) | "inline" (GitHub-style)
labels = { current = "CURRENT", proposed = "PROPOSED" },
Expand Down Expand Up @@ -178,25 +178,28 @@ require("claude-preview").setup({

| Command | Description |
|---------|-------------|
| `:ClaudePreviewInstallHooks` | Install Claude Code hooks to `.claude/settings.local.json` |
| `:ClaudePreviewUninstallHooks` | Remove Claude Code hooks (leaves other hooks intact) |
| `:CodePreviewInstallClaudeCodeHooks` | Install Claude Code hooks to `.claude/settings.local.json` |
| `:CodePreviewUninstallClaudeCodeHooks` | Remove Claude Code hooks (leaves other hooks intact) |
| `:CodePreviewInstallOpenCodeHooks` | Install OpenCode plugin to `.opencode/plugins/` |
| `:CodePreviewUninstallOpenCodeHooks` | Remove OpenCode plugin |
| `:ClaudePreviewCloseDiff` | Manually close the diff (use after rejecting a change) |
| `:ClaudePreviewStatus` | Show socket path, hook status, and dependency check |
| `:checkhealth claude-preview` | Full health check (both backends) |
| `:CodePreviewCloseDiff` | Manually close the diff (use after rejecting a change) |
| `:CodePreviewStatus` | Show socket path, hook status, and dependency check |
| `:CodePreviewToggleVisibleOnly` | Toggle visible_only — show diffs only for open buffers |
| `:checkhealth code-preview` | Full health check (both backends) |

> **Migrating?** The old `:ClaudePreview*` commands still work but show a deprecation warning. They will be removed in a future release.

## Keymaps

| Key | Description |
|-----|-------------|
| `<leader>dq` | Close the diff (same as `:ClaudePreviewCloseDiff`) |
| `<leader>dq` | Close the diff (same as `:CodePreviewCloseDiff`) |

---

## Diff Layouts

claude-preview supports three diff layouts, configured via `diff.layout`:
code-preview supports three diff layouts, configured via `diff.layout`:

| Layout | Description |
|--------|-------------|
Expand All @@ -214,7 +217,7 @@ claude-preview supports three diff layouts, configured via `diff.layout`:
To use inline diff:

```lua
require("claude-preview").setup({
require("code-preview").setup({
diff = { layout = "inline" },
})
```
Expand All @@ -223,7 +226,7 @@ require("claude-preview").setup({

## Neo-tree Integration (Optional)

If you use [neo-tree.nvim](https://github.com/nvim-neo-tree/neo-tree.nvim), claude-preview will automatically decorate your file tree with visual indicators when changes are proposed. No extra configuration is required — it works out of the box.
If you use [neo-tree.nvim](https://github.com/nvim-neo-tree/neo-tree.nvim), code-preview will automatically decorate your file tree with visual indicators when changes are proposed. No extra configuration is required — it works out of the box.

![neo-tree integration demo](docs/claude-preview-neotree-integration.gif)

Expand All @@ -246,7 +249,7 @@ Additional behaviors:
All neo-tree options with defaults:

```lua
require("claude-preview").setup({
require("code-preview").setup({
neo_tree = {
enabled = true, -- set false to disable neo-tree integration
position = "right", -- neo-tree window position: "left", "right", "float"
Expand All @@ -272,24 +275,30 @@ require("claude-preview").setup({

```
claude-preview.nvim/
├── lua/claude-preview/
│ ├── init.lua setup(), config, commands
│ ├── diff.lua show_diff(), close_diff()
│ ├── hooks.lua install/uninstall for both backends
│ ├── changes.lua change status registry (modified/created/deleted)
│ ├── neo_tree.lua neo-tree integration (icons, virtual nodes, reveal)
│ └── health.lua :checkhealth (both backends)
├── bin/ Claude Code hook scripts
│ ├── claude-preview-diff.sh PreToolUse hook entry point
│ ├── claude-close-diff.sh PostToolUse hook entry point
│ ├── nvim-socket.sh Neovim socket discovery
│ ├── nvim-send.sh RPC send helper
│ ├── apply-edit.lua Single Edit transformer
│ └── apply-multi-edit.lua MultiEdit transformer
└── opencode-plugin/ OpenCode plugin
├── index.ts tool.execute.before/after hooks
├── nvim.ts Neovim socket discovery + RPC
└── edits.ts Edit computation helpers
├── lua/code-preview/
│ ├── init.lua setup(), config, commands
│ ├── diff.lua show_diff(), close_diff()
│ ├── changes.lua change status registry (modified/created/deleted)
│ ├── neo_tree.lua neo-tree integration (icons, virtual nodes, reveal)
│ ├── health.lua :checkhealth (both backends)
│ └── backends/
│ ├── claudecode.lua Claude Code hook install/uninstall
│ └── opencode.lua OpenCode plugin install/uninstall
├── bin/ Shared core scripts
│ ├── core-pre-tool.sh Unified PreToolUse logic
│ ├── core-post-tool.sh Unified PostToolUse logic
│ ├── nvim-socket.sh Neovim socket discovery
│ ├── nvim-send.sh RPC send helper
│ ├── apply-edit.lua Single Edit transformer
│ └── apply-multi-edit.lua MultiEdit transformer
├── backends/
│ ├── claudecode/ Claude Code adapter
│ │ ├── code-preview-diff.sh PreToolUse hook entry point
│ │ └── code-close-diff.sh PostToolUse hook entry point
│ └── opencode/ OpenCode adapter
│ ├── index.ts tool.execute.before/after hooks
│ ├── package.json
│ └── tsconfig.json
```

---
Expand All @@ -299,11 +308,11 @@ claude-preview.nvim/
The test suite uses [plenary.nvim](https://github.com/nvim-lua/plenary.nvim) for core plugin tests and shell scripts for backend integration tests. CI runs on both Ubuntu and macOS.

```bash
./tests/run.sh # all tests (plugin + backends)
./tests/run.sh plugin # core plugin tests only (plenary busted)
./tests/run.sh backends # all backend integration tests
./tests/run.sh backends/claude # Claude Code backend only
./tests/run.sh backends/opencode # OpenCode backend only
./tests/run.sh # all tests (plugin + backends)
./tests/run.sh plugin # core plugin tests only (plenary busted)
./tests/run.sh backends # all backend integration tests
./tests/run.sh backends/claudecode # Claude Code backend only
./tests/run.sh backends/opencode # OpenCode backend only
```

**Dependencies:** Neovim >= 0.10, jq, bun (for OpenCode tests). Plenary is auto-installed to `deps/` on first run.
Expand All @@ -325,12 +334,12 @@ vim.api.nvim_create_autocmd({ "FocusGained", "BufEnter", "CursorHold" }, {
## Troubleshooting

**Diff doesn't open**
- Run `:ClaudePreviewStatus` — check that `Neovim socket` is found
- Run `:checkhealth claude-preview` — check for missing dependencies
- Run `:CodePreviewStatus` — check that `Neovim socket` is found
- Run `:checkhealth code-preview` — check for missing dependencies
- Restart the CLI agent after installing hooks (hooks are read at startup)

**Claude Code hooks not firing**
- Run `:ClaudePreviewInstallHooks` in the project root
- Run `:CodePreviewInstallClaudeCodeHooks` in the project root
- Verify `.claude/settings.local.json` contains the hook entries
- Ensure `jq` is in PATH
- Restart Claude Code CLI
Expand All @@ -342,7 +351,12 @@ vim.api.nvim_create_autocmd({ "FocusGained", "BufEnter", "CursorHold" }, {
- Restart OpenCode

**Diff doesn't close after rejecting**
- Press `<leader>dq` or run `:ClaudePreviewCloseDiff` — the post hook only fires on accept
- Press `<leader>dq` or run `:CodePreviewCloseDiff` — the post hook only fires on accept

**Migrating from older versions**
- Update `require("claude-preview")` to `require("code-preview")` in your Neovim config
- Re-run `:CodePreviewInstallClaudeCodeHooks` to update hook paths
- The old `:ClaudePreview*` commands still work but show deprecation warnings

---

Expand Down
8 changes: 8 additions & 0 deletions backends/claudecode/code-close-diff.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
# code-close-diff.sh — PostToolUse hook adapter for Claude Code
# Delegates to core-post-tool.sh with the Claude Code backend flag.

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
BIN_DIR="$SCRIPT_DIR/../../bin"
export CODE_PREVIEW_BACKEND="claudecode"
exec "$BIN_DIR/core-post-tool.sh"
8 changes: 8 additions & 0 deletions backends/claudecode/code-preview-diff.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
# code-preview-diff.sh — PreToolUse hook adapter for Claude Code
# Delegates to core-pre-tool.sh with the Claude Code backend flag.

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
BIN_DIR="$SCRIPT_DIR/../../bin"
export CODE_PREVIEW_BACKEND="claudecode"
exec "$BIN_DIR/core-pre-tool.sh"
4 changes: 2 additions & 2 deletions opencode-plugin/index.ts → backends/opencode/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ function binDir(): string {
return readFileSync(resolve(__dirname, "bin-path.txt"), "utf-8").trim()
} catch {
// Fallback for development: resolve relative to plugin source
return resolve(__dirname, "../bin")
return resolve(__dirname, "../../bin")
}
}

Expand Down Expand Up @@ -85,7 +85,7 @@ function runCoreScript(script: string, json: string): void {
try {
execSync(`"${bin}/${script}"`, {
input: json,
env: { ...process.env, CLAUDE_PREVIEW_BACKEND: "opencode" },
env: { ...process.env, CODE_PREVIEW_BACKEND: "opencode" },
timeout: 15000,
stdio: ["pipe", "pipe", "pipe"],
})
Expand Down
6 changes: 6 additions & 0 deletions backends/opencode/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "code-preview-opencode",
"version": "1.0.0",
"description": "OpenCode plugin for code-preview.nvim — sends diff previews to Neovim",
"type": "module"
}
File renamed without changes.
7 changes: 0 additions & 7 deletions bin/claude-close-diff.sh

This file was deleted.

7 changes: 0 additions & 7 deletions bin/claude-preview-diff.sh

This file was deleted.

16 changes: 8 additions & 8 deletions bin/core-post-tool.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
# "tool_input": { "file_path": "...", ... } }
#
# Environment:
# CLAUDE_PREVIEW_BACKEND — "claude" or "opencode" (currently unused, reserved)
# CODE_PREVIEW_BACKEND — "claudecode" or "opencode" (currently unused, reserved)

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"

Expand All @@ -24,8 +24,8 @@ source "$SCRIPT_DIR/nvim-send.sh"

# For Bash tool (rm detection), only clear deletion markers — don't touch edit markers or diff tab
if [[ "$TOOL_NAME" == "Bash" ]]; then
nvim_send "require('claude-preview.changes').clear_by_status('deleted')" || true
nvim_send "vim.defer_fn(function() pcall(function() require('claude-preview.neo_tree').refresh() end) end, 200)" || true
nvim_send "require('code-preview.changes').clear_by_status('deleted')" || true
nvim_send "vim.defer_fn(function() pcall(function() require('code-preview.neo_tree').refresh() end) end, 200)" || true
exit 0
fi

Expand All @@ -36,15 +36,15 @@ FILE_PATH_ESC="$(escape_lua "${FILE_PATH:-}")"
# Only clean up if a diff for THIS file is actually open.
# OpenCode fires all before-hooks before any after-hooks, so the open diff
# may belong to a different file — closing it would kill the wrong preview.
DIFF_OPEN=$(nvim --server "$NVIM_SOCKET" --remote-expr "luaeval(\"require('claude-preview.diff').is_open('${FILE_PATH_ESC}')\")" 2>/dev/null || echo "false")
DIFF_OPEN=$(nvim --server "$NVIM_SOCKET" --remote-expr "luaeval(\"require('code-preview.diff').is_open('${FILE_PATH_ESC}')\")" 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 "require('code-preview.changes').clear_all()" || true
nvim_send "require('code-preview.diff').close_diff()" || true
if [[ -n "$FILE_PATH" ]]; then
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
nvim_send "vim.defer_fn(function() pcall(function() require('code-preview.neo_tree').refresh() end) vim.defer_fn(function() pcall(function() require('code-preview.neo_tree').reveal('$FILE_PATH_ESC') end) end, 200) end, 200)" || true
else
nvim_send "vim.defer_fn(function() pcall(function() require('claude-preview.neo_tree').refresh() end) end, 200)" || true
nvim_send "vim.defer_fn(function() pcall(function() require('code-preview.neo_tree').refresh() end) end, 200)" || true
fi
fi

Expand Down
22 changes: 11 additions & 11 deletions bin/core-pre-tool.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
# "tool_input": { "file_path": "...", ... } }
#
# Environment:
# CLAUDE_PREVIEW_BACKEND — "claude" or "opencode" (gates output format)
# CODE_PREVIEW_BACKEND — "claudecode" or "opencode" (gates output format)

set -euo pipefail

Expand Down Expand Up @@ -121,13 +121,13 @@ case "$TOOL_NAME" in
if [[ "$HAS_NVIM" == "true" ]]; then
for path in $RM_PATHS; do
PATH_ESC="$(escape_lua "$path")"
nvim_send "require('claude-preview.changes').set('$PATH_ESC', 'deleted')" || true
nvim_send "require('code-preview.changes').set('$PATH_ESC', 'deleted')" || true
done
nvim_send "pcall(function() require('claude-preview.neo_tree').refresh() end)" || true
nvim_send "pcall(function() require('code-preview.neo_tree').refresh() end)" || 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('claude-preview.neo_tree').reveal('$FIRST_ESC') end) end, 300)" || true
nvim_send "vim.defer_fn(function() pcall(function() require('code-preview.neo_tree').reveal('$FIRST_ESC') end) end, 300)" || true
fi
exit 0
;;
Expand All @@ -153,7 +153,7 @@ if [[ "$HAS_NVIM" == "true" ]]; then
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 '{}')
HOOK_CTX=$(nvim --server "$NVIM_SOCKET" --remote-expr "luaeval(\"require('code-preview').hook_context('${FILE_PATH_ESC}')\")" 2>/dev/null || echo '{}')
# Use explicit conditional: jq's `//` operator treats boolean false like null,
# which would silently convert `reveal = false` into `true`.
NEO_TREE_REVEAL=$(echo "$HOOK_CTX" | jq -r 'if .neo_tree_reveal == false then "false" else "true" end')
Expand All @@ -177,7 +177,7 @@ if [[ "$HAS_NVIM" == "true" ]]; then
fi

if [[ "$SHOULD_SHOW" == "1" ]]; then
nvim_send "require('claude-preview.changes').set('$FILE_PATH_ESC', '$CHANGE_STATUS')" || true
nvim_send "require('code-preview.changes').set('$FILE_PATH_ESC', '$CHANGE_STATUS')" || true

# Neo-tree integration (gated by config)
if [[ "$NEO_TREE_REVEAL" == "true" ]]; then
Expand All @@ -204,17 +204,17 @@ if [[ "$HAS_NVIM" == "true" ]]; then
fi
REVEAL_TARGET_ESC="$(escape_lua "$REVEAL_TARGET")"

nvim_send "pcall(function() require('claude-preview.neo_tree').refresh() end)" || true
nvim_send "pcall(function() require('code-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('$REVEAL_TARGET_ESC', '$REVEAL_DIR_ESC') end) end, 300)" || true
nvim_send "vim.defer_fn(function() pcall(function() require('code-preview.neo_tree').reveal('$REVEAL_TARGET_ESC', '$REVEAL_DIR_ESC') end) end, 300)" || true
else
nvim_send "vim.defer_fn(function() pcall(function() require('claude-preview.neo_tree').reveal('$REVEAL_TARGET_ESC') end) end, 300)" || true
nvim_send "vim.defer_fn(function() pcall(function() require('code-preview.neo_tree').reveal('$REVEAL_TARGET_ESC') end) end, 300)" || true
fi
fi

nvim_send "require('claude-preview.diff').show_diff('$ORIG_ESC', '$PROP_ESC', '$DISPLAY_ESC', '$FILE_PATH_ESC')" || true
nvim_send "require('code-preview.diff').show_diff('$ORIG_ESC', '$PROP_ESC', '$DISPLAY_ESC', '$FILE_PATH_ESC')" || true
fi
fi

Expand All @@ -224,7 +224,7 @@ fi
# unreachable), produce no output and let Claude Code's own permission
# settings (bypass, ask, allowlist) decide. Otherwise return "ask" to
# prompt the user for every edit, preserving the default review workflow.
if [[ "${CLAUDE_PREVIEW_BACKEND:-}" == "claude" && "$HAS_NVIM" == "true" && "$DEFER_PERMISSIONS" != "true" ]]; then
if [[ "${CODE_PREVIEW_BACKEND:-}" == "claudecode" && "$HAS_NVIM" == "true" && "$DEFER_PERMISSIONS" != "true" ]]; then
REASON="Diff preview sent to Neovim. Review before accepting."
printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask","permissionDecisionReason":"%s"}}\n' "$REASON"
fi
4 changes: 2 additions & 2 deletions bin/nvim-send.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
#
# Usage:
# source bin/nvim-send.sh
# nvim_send "require('claude-preview.diff').show_diff('a', 'b', 'c')"
# nvim_send "require('code-preview.diff').show_diff('a', 'b', 'c')"
#
# Depends on nvim-socket.sh being sourced first (NVIM_SOCKET must be set).

Expand All @@ -24,7 +24,7 @@ nvim_send() {
return 1
fi
local tmp_lua
tmp_lua="$(mktemp /tmp/claude-preview-nvim-cmd.XXXXXX)"
tmp_lua="$(mktemp /tmp/code-preview-nvim-cmd.XXXXXX)"
printf '%s' "$lua_cmd" > "$tmp_lua"
nvim --server "$NVIM_SOCKET" --remote-expr "execute('luafile $tmp_lua')" >/dev/null 2>&1
local rc=$?
Expand Down
Loading
Loading