From 9789922bcc8683a4c970927223db708de415e485 Mon Sep 17 00:00:00 2001 From: Friendliness Date: Tue, 9 Jun 2026 19:55:05 +0200 Subject: [PATCH 1/3] Added support for backend-specific layouts --- README.md | 15 +++++++++ lua/code-preview/diff.lua | 23 ++++++++++--- lua/code-preview/init.lua | 1 + lua/code-preview/pre_tool/init.lua | 7 ++-- tests/plugin/diff_lifecycle_spec.lua | 48 ++++++++++++++++++++++++++-- 5 files changed, 85 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 1b7ef0e..8d54e70 100644 --- a/README.md +++ b/README.md @@ -199,6 +199,7 @@ require("code-preview").setup({ debug = false, -- enable debug logging to stdpath("log")/code-preview.log diff = { layout = "tab", -- "tab" (new tab) | "vsplit" (current tab) | "inline" (GitHub-style) + layouts = {}, -- add layouts overrides for specific backends: { opencode = "tab", codex = "vsplit" } labels = { current = "CURRENT", proposed = "PROPOSED" }, equalize = true, -- 50/50 split widths (tab/vsplit only) full_file = true, -- show full file, not just diff hunks (tab/vsplit only) @@ -300,6 +301,20 @@ require("code-preview").setup({ }) ``` +You can also override the layout per backend. In this case, the default will still be "tab" unless a backend specifies otherwise: + +```lua +require("code-preview").setup({ + diff = { + layout = "tab", + layouts = { + opencode = "tab", + codex = "vsplit", + }, + }, +}) +``` + --- ## Neo-tree Integration (Optional) diff --git a/lua/code-preview/diff.lua b/lua/code-preview/diff.lua index db14932..8d19964 100644 --- a/lua/code-preview/diff.lua +++ b/lua/code-preview/diff.lua @@ -127,6 +127,17 @@ local function active_count() return n end +local function layout_for_backend(cfg, backend) + local diff_cfg = (cfg and cfg.diff) or {} + local layouts = diff_cfg.layouts or {} + + if backend and layouts[backend] then + return layouts[backend] + end + + return diff_cfg.layout or "tab" +end + function M.is_open(file_path) if file_path and file_path ~= "" then local entry = active_diffs[file_path] @@ -416,12 +427,14 @@ local function show_inline_diff(original_path, proposed_path, real_file_path, cf return { tab = tab, bufs = { buf }, inline_win = win } end -function M.show_diff(original_path, proposed_path, real_file_path, abs_file_path, action) +function M.show_diff(original_path, proposed_path, real_file_path, abs_file_path, action, backend) local file_key = abs_file_path or real_file_path local cfg = require("code-preview").config - log.info(log.fmt("show_diff: file=%s layout=%s active=%d", + local layout = layout_for_backend(cfg, backend) + log.info(log.fmt("show_diff: file=%s layout=%s backend=%s active=%d", file_key or "nil", - (cfg.diff and cfg.diff.layout) or "tab", + layout, + backend or "nil", active_count())) -- If a diff for this SAME file is already open, close it first (re-edit) @@ -434,7 +447,7 @@ function M.show_diff(original_path, proposed_path, real_file_path, abs_file_path mark_change_and_reveal(abs_file_path, action) -- Inline layout - if cfg.diff.layout == "inline" then + if layout == "inline" then local result = show_inline_diff(original_path, proposed_path, real_file_path, cfg) active_diffs[file_key] = result -- Force terminal redraw so RPC-triggered tab creation is visible (see force_redraw). @@ -449,7 +462,7 @@ function M.show_diff(original_path, proposed_path, real_file_path, abs_file_path local labels = cfg.diff.labels or { current = "CURRENT", proposed = "PROPOSED" } local ft = vim.filetype.match({ filename = real_file_path }) or "" - if cfg.diff.layout == "vsplit" then + if layout == "vsplit" then vim.cmd("vsplit") else vim.cmd("tabnew") diff --git a/lua/code-preview/init.lua b/lua/code-preview/init.lua index fdbcc6f..7e34378 100644 --- a/lua/code-preview/init.lua +++ b/lua/code-preview/init.lua @@ -7,6 +7,7 @@ local default_config = { debug = false, -- enable debug logging to stdpath("log")/code-preview.log diff = { layout = "tab", -- "tab", "vsplit", or "inline" + layouts = {}, -- add layouts overrides for specific backends: { opencode = "tab", codex = "vsplit" } labels = { current = "CURRENT", proposed = "PROPOSED" }, equalize = true, full_file = true, diff --git a/lua/code-preview/pre_tool/init.lua b/lua/code-preview/pre_tool/init.lua index b6b7eab..e764ba5 100644 --- a/lua/code-preview/pre_tool/init.lua +++ b/lua/code-preview/pre_tool/init.lua @@ -159,7 +159,7 @@ local function present_single_file(file_path, proposed_content, input, cfg) return end - diff.show_diff(orig, prop, display_path(file_path, input.cwd), file_path, nil) + diff.show_diff(orig, prop, display_path(file_path, input.cwd), file_path, nil, input.backend) end local function handle_edit(input, cfg) @@ -250,7 +250,7 @@ local function handle_apply_patch(input, cfg) -- whatever the model wrote in the `*** Update File:` directive, and some -- codex models (e.g. GPT 5.3) write an absolute path there, which would -- render the tab as `D:\...` instead of a cwd-relative label. - diff.show_diff(orig, prop, display_path(file.path, input.cwd), file.path, file.action) + diff.show_diff(orig, prop, display_path(file.path, input.cwd), file.path, file.action, input.backend) else log.info(log.fmt("pre_tool: ApplyPatch skip %s (visible_only)", file.rel_path)) end @@ -274,6 +274,9 @@ local dispatchers = { function M.handle(raw, backend) local cfg = require("code-preview").config or {} local input = normalisers.normalise(raw, backend) + if input then + input.backend = backend + end local tool_name = input and input.tool_name log.info(log.fmt("pre_tool: tool=%s backend=%s", tostring(tool_name), tostring(backend))) diff --git a/tests/plugin/diff_lifecycle_spec.lua b/tests/plugin/diff_lifecycle_spec.lua index e50af6c..de805a0 100644 --- a/tests/plugin/diff_lifecycle_spec.lua +++ b/tests/plugin/diff_lifecycle_spec.lua @@ -213,12 +213,15 @@ describe("diff lifecycle", function() end) describe("diff layouts", function() - -- Temporarily override the diff layout for one test, restoring it afterwards. - local function with_layout(layout, fn) + -- Temporarily override diff layout config for one test, restoring it afterwards. + local function with_layout(layout, fn, layouts) local saved = require("code-preview").config.diff.layout + local saved_layouts = vim.deepcopy(require("code-preview").config.diff.layouts or {}) require("code-preview").config.diff.layout = layout + require("code-preview").config.diff.layouts = layouts or {} local ok, err = pcall(fn) require("code-preview").config.diff.layout = saved + require("code-preview").config.diff.layouts = saved_layouts if not ok then error(err, 2) end end @@ -289,4 +292,45 @@ describe("diff layouts", function() os.remove(orig) os.remove(prop) end) + + it("backend layout override wins over the default layout", function() + local orig = tmp_file("backend_vs_orig.txt", "alpha\nbeta") + local prop = tmp_file("backend_vs_prop.txt", "alpha\ngamma") + + local tabs_before = #vim.api.nvim_list_tabpages() + local tab = vim.api.nvim_get_current_tabpage() + local wins_before = #vim.api.nvim_tabpage_list_wins(tab) + + with_layout("tab", function() + diff.show_diff(orig, prop, "layout_backend_vsplit.txt", nil, nil, "codex") + end, { codex = "vsplit" }) + + assert.is_true(diff.is_open()) + assert.equals(tabs_before, #vim.api.nvim_list_tabpages()) + assert.equals(wins_before + 2, #vim.api.nvim_tabpage_list_wins(tab)) + + diff.close_diff() + os.remove(orig) + os.remove(prop) + end) + + it("missing backend falls back to the default layout", function() + local orig = tmp_file("no_backend_orig.txt", "alpha\nbeta") + local prop = tmp_file("no_backend_prop.txt", "alpha\ngamma") + + local tabs_before = #vim.api.nvim_list_tabpages() + + with_layout("tab", function() + diff.show_diff(orig, prop, "layout_no_backend.txt") + end, { codex = "vsplit" }) + + assert.is_true(diff.is_open()) + assert.equals(tabs_before + 1, #vim.api.nvim_list_tabpages()) + local diff_tabpage = vim.api.nvim_get_current_tabpage() + assert.equals(2, #vim.api.nvim_tabpage_list_wins(diff_tabpage)) + + diff.close_diff() + os.remove(orig) + os.remove(prop) + end) end) From 20e1173a115bab87f6f005e82d7215cd165a2e04 Mon Sep 17 00:00:00 2001 From: Friendliness Date: Tue, 9 Jun 2026 20:29:39 +0200 Subject: [PATCH 2/3] Changed the wording of a comment --- README.md | 2 +- lua/code-preview/init.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8d54e70..bf8ca24 100644 --- a/README.md +++ b/README.md @@ -199,7 +199,7 @@ require("code-preview").setup({ debug = false, -- enable debug logging to stdpath("log")/code-preview.log diff = { layout = "tab", -- "tab" (new tab) | "vsplit" (current tab) | "inline" (GitHub-style) - layouts = {}, -- add layouts overrides for specific backends: { opencode = "tab", codex = "vsplit" } + layouts = {}, -- override layout per backend: { opencode = "tab", codex = "vsplit" } labels = { current = "CURRENT", proposed = "PROPOSED" }, equalize = true, -- 50/50 split widths (tab/vsplit only) full_file = true, -- show full file, not just diff hunks (tab/vsplit only) diff --git a/lua/code-preview/init.lua b/lua/code-preview/init.lua index 7e34378..b586625 100644 --- a/lua/code-preview/init.lua +++ b/lua/code-preview/init.lua @@ -7,7 +7,7 @@ local default_config = { debug = false, -- enable debug logging to stdpath("log")/code-preview.log diff = { layout = "tab", -- "tab", "vsplit", or "inline" - layouts = {}, -- add layouts overrides for specific backends: { opencode = "tab", codex = "vsplit" } + layouts = {}, -- override layout per backend: { opencode = "tab", codex = "vsplit" } labels = { current = "CURRENT", proposed = "PROPOSED" }, equalize = true, full_file = true, From 5942103334eef88091347c7e301523127dfb6993 Mon Sep 17 00:00:00 2001 From: Friendliness Date: Wed, 10 Jun 2026 19:48:06 +0200 Subject: [PATCH 3/3] Reverted README.md and changed input.backend to just pass backend through the function calls --- README.md | 18 ++---------------- lua/code-preview/pre_tool/init.lua | 27 ++++++++++++--------------- 2 files changed, 14 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index bf8ca24..3e5c806 100644 --- a/README.md +++ b/README.md @@ -199,7 +199,6 @@ require("code-preview").setup({ debug = false, -- enable debug logging to stdpath("log")/code-preview.log diff = { layout = "tab", -- "tab" (new tab) | "vsplit" (current tab) | "inline" (GitHub-style) - layouts = {}, -- override layout per backend: { opencode = "tab", codex = "vsplit" } labels = { current = "CURRENT", proposed = "PROPOSED" }, equalize = true, -- 50/50 split widths (tab/vsplit only) full_file = true, -- show full file, not just diff hunks (tab/vsplit only) @@ -301,20 +300,6 @@ require("code-preview").setup({ }) ``` -You can also override the layout per backend. In this case, the default will still be "tab" unless a backend specifies otherwise: - -```lua -require("code-preview").setup({ - diff = { - layout = "tab", - layouts = { - opencode = "tab", - codex = "vsplit", - }, - }, -}) -``` - --- ## Neo-tree Integration (Optional) @@ -467,7 +452,8 @@ vim.api.nvim_create_autocmd({ "FocusGained", "BufEnter", "CursorHold" }, { **Copilot CLI hooks not firing** - Run `:CodePreviewInstallCopilotCliHooks` in the project root - Verify `.github/hooks/code-preview.json` exists -- Ensure `jq` is in PATH +- Ensure `jq` is in PATH (macOS/Linux only; the Windows hook uses PowerShell's native JSON parsing) +- On Windows, Copilot runs the hook under **pwsh 7+** (its own requirement). If hooks silently don't fire, check your PowerShell execution policy with `Get-ExecutionPolicy` — pwsh's default `RemoteSigned` runs the local hook script, but `Restricted`/`AllSigned` blocks it (e.g. `Set-ExecutionPolicy -Scope CurrentUser RemoteSigned`) - Restart Copilot CLI (hooks are loaded at session start) **Diff doesn't close after rejecting** diff --git a/lua/code-preview/pre_tool/init.lua b/lua/code-preview/pre_tool/init.lua index e764ba5..ad7a130 100644 --- a/lua/code-preview/pre_tool/init.lua +++ b/lua/code-preview/pre_tool/init.lua @@ -146,7 +146,7 @@ M.display_path = display_path -- exposed for testing -- to diff.show_diff (or skip when visible_only excludes the file). The only -- per-tool variation is how `proposed_content` is computed, so each handler -- below is a one-liner around this helper. -local function present_single_file(file_path, proposed_content, input, cfg) +local function present_single_file(file_path, proposed_content, input, cfg, backend) local id = next_id() local orig = tmpdir() .. "/code-preview-diff-original-" .. id local prop = tmpdir() .. "/code-preview-diff-proposed-" .. id @@ -159,10 +159,10 @@ local function present_single_file(file_path, proposed_content, input, cfg) return end - diff.show_diff(orig, prop, display_path(file_path, input.cwd), file_path, nil, input.backend) + diff.show_diff(orig, prop, display_path(file_path, input.cwd), file_path, nil, backend) end -local function handle_edit(input, cfg) +local function handle_edit(input, cfg, backend) local fp = input.tool_input.file_path local content = apply_edit.apply( fp, @@ -170,18 +170,18 @@ local function handle_edit(input, cfg) input.tool_input.new_string or "", input.tool_input.replace_all == true ) - present_single_file(fp, content, input, cfg) + present_single_file(fp, content, input, cfg, backend) end -local function handle_write(input, cfg) +local function handle_write(input, cfg, backend) local fp = input.tool_input.file_path - present_single_file(fp, input.tool_input.content or "", input, cfg) + present_single_file(fp, input.tool_input.content or "", input, cfg, backend) end -local function handle_multi_edit(input, cfg) +local function handle_multi_edit(input, cfg, backend) local fp = input.tool_input.file_path local content = apply_multi_edit.apply(fp, input.tool_input.edits or {}) - present_single_file(fp, content, input, cfg) + present_single_file(fp, content, input, cfg, backend) end local function handle_bash(input) @@ -214,7 +214,7 @@ local function handle_bash(input) end end -local function handle_apply_patch(input, cfg) +local function handle_apply_patch(input, cfg, backend) local patch_text = input.tool_input and input.tool_input.patch_text if not patch_text or patch_text == "" then log.info("pre_tool: ApplyPatch with empty patch_text") @@ -250,7 +250,7 @@ local function handle_apply_patch(input, cfg) -- whatever the model wrote in the `*** Update File:` directive, and some -- codex models (e.g. GPT 5.3) write an absolute path there, which would -- render the tab as `D:\...` instead of a cwd-relative label. - diff.show_diff(orig, prop, display_path(file.path, input.cwd), file.path, file.action, input.backend) + diff.show_diff(orig, prop, display_path(file.path, input.cwd), file.path, file.action, backend) else log.info(log.fmt("pre_tool: ApplyPatch skip %s (visible_only)", file.rel_path)) end @@ -261,7 +261,7 @@ local dispatchers = { Edit = handle_edit, Write = handle_write, MultiEdit = handle_multi_edit, - Bash = function(input, _cfg) handle_bash(input) end, + Bash = function(input, _cfg, _backend) handle_bash(input) end, ApplyPatch = handle_apply_patch, } @@ -274,16 +274,13 @@ local dispatchers = { function M.handle(raw, backend) local cfg = require("code-preview").config or {} local input = normalisers.normalise(raw, backend) - if input then - input.backend = backend - end local tool_name = input and input.tool_name log.info(log.fmt("pre_tool: tool=%s backend=%s", tostring(tool_name), tostring(backend))) local fn = dispatchers[tool_name] if fn then - local ok, err = pcall(fn, input, cfg) + local ok, err = pcall(fn, input, cfg, backend) if not ok then log.error("pre_tool: dispatch failed: " .. tostring(err)) end