@@ -232,6 +232,69 @@ local function find_claudecode_terminal_window()
232232 return floating_fallback
233233end
234234
235+ --- Get or create a persistent editor window in tab 2 for Claude Code diffs.
236+ --- Finds an existing non-terminal, non-diff window in tab 2 (the Claude editor).
237+ --- If none exists, creates a new window to the left of any terminal in tab 2.
238+ local function get_or_create_editor_win_in_diff_tab ()
239+ local tabs = vim .api .nvim_list_tabpages ()
240+ if # tabs < 2 then
241+ return nil
242+ end
243+ local tab2 = tabs [2 ]
244+
245+ -- First, look for an existing Claude Code editor window in tab 2:
246+ -- a non-terminal, non-prompt, non-diff window showing a real file.
247+ for _ , win in ipairs (vim .api .nvim_tabpage_list_wins (tab2 )) do
248+ local b = vim .api .nvim_win_get_buf (win )
249+ local bt = vim .api .nvim_buf_get_option (b , " buftype" )
250+ local ft = vim .api .nvim_buf_get_option (b , " filetype" )
251+ if bt ~= " terminal" and bt ~= " prompt"
252+ and not vim .api .nvim_win_get_option (win , " diff" )
253+ and ft ~= " neo-tree" and ft ~= " NvimTree"
254+ and ft ~= " oil" and ft ~= " aerial"
255+ then
256+ return win
257+ end
258+ end
259+
260+ -- No editor window found; try to find any terminal in tab 2 to anchor a new split.
261+ local terminal_win = nil
262+ pcall (function ()
263+ local terminal_module = require (" claudecode.terminal" )
264+ local terminal_bufnr = terminal_module .get_active_terminal_bufnr ()
265+ if terminal_bufnr then
266+ for _ , win in ipairs (vim .api .nvim_tabpage_list_wins (tab2 )) do
267+ if vim .api .nvim_win_get_buf (win ) == terminal_bufnr then
268+ terminal_win = win
269+ break
270+ end
271+ end
272+ end
273+ end )
274+
275+ -- If no terminal in tab 2, fall back to any non-floating, non-special window in tab 2.
276+ if not terminal_win then
277+ for _ , win in ipairs (vim .api .nvim_tabpage_list_wins (tab2 )) do
278+ local cfg = vim .api .nvim_win_get_config (win )
279+ if not cfg .relative or cfg .relative == " " then
280+ local b = vim .api .nvim_win_get_buf (win )
281+ local bt = vim .api .nvim_buf_get_option (b , " buftype" )
282+ if bt ~= " terminal" and bt ~= " prompt" and not vim .api .nvim_win_get_option (win , " diff" ) then
283+ return win
284+ end
285+ end
286+ end
287+ return nil
288+ end
289+
290+ vim .api .nvim_set_current_tabpage (tab2 )
291+ vim .api .nvim_set_current_win (terminal_win )
292+ vim .cmd (" leftabove vsplit" )
293+ local new_editor = vim .api .nvim_get_current_win ()
294+ vim .cmd (" wincmd =" )
295+ return new_editor
296+ end
297+
235298--- Create a split based on configured layout
236299local function create_split ()
237300 if config and config .diff_opts and config .diff_opts .layout == " horizontal" then
@@ -333,20 +396,28 @@ end
333396local function display_terminal_in_new_tab ()
334397 local original_tab = vim .api .nvim_get_current_tabpage ()
335398
399+ local function get_or_create_diff_tab ()
400+ local tabs = vim .api .nvim_list_tabpages ()
401+ if # tabs >= 2 then
402+ return tabs [2 ]
403+ end
404+ vim .cmd (" tabnew" )
405+ mark_tabnew_buffer_ephemeral ()
406+ return vim .api .nvim_get_current_tabpage ()
407+ end
408+
336409 -- Get existing terminal buffer
337410 local terminal_ok , terminal_module = pcall (require , " claudecode.terminal" )
338411 if not terminal_ok then
339- vim .cmd (" tabnew" )
340- mark_tabnew_buffer_ephemeral ()
341- local new_tab = vim .api .nvim_get_current_tabpage ()
412+ local new_tab = get_or_create_diff_tab ()
413+ vim .api .nvim_set_current_tabpage (new_tab )
342414 return original_tab , nil , false , new_tab
343415 end
344416
345417 local terminal_bufnr = terminal_module .get_active_terminal_bufnr ()
346418 if not terminal_bufnr or not vim .api .nvim_buf_is_valid (terminal_bufnr ) then
347- vim .cmd (" tabnew" )
348- mark_tabnew_buffer_ephemeral ()
349- local new_tab = vim .api .nvim_get_current_tabpage ()
419+ local new_tab = get_or_create_diff_tab ()
420+ vim .api .nvim_set_current_tabpage (new_tab )
350421 return original_tab , nil , false , new_tab
351422 end
352423
@@ -359,9 +430,8 @@ local function display_terminal_in_new_tab()
359430 terminal_options = get_default_terminal_options ()
360431 end
361432
362- vim .cmd (" tabnew" )
363- mark_tabnew_buffer_ephemeral ()
364- local new_tab = vim .api .nvim_get_current_tabpage ()
433+ local new_tab = get_or_create_diff_tab ()
434+ vim .api .nvim_set_current_tabpage (new_tab )
365435
366436 local terminal_config = config .terminal or {}
367437 local split_side = terminal_config .split_side or " right"
@@ -377,16 +447,27 @@ local function display_terminal_in_new_tab()
377447 return original_tab , nil , had_terminal_in_original , new_tab
378448 end
379449
380- vim .cmd (" vsplit" )
381-
382- local terminal_win = vim .api .nvim_get_current_win ()
450+ -- Reuse an existing terminal window in the new tab if already present, instead of splitting again.
451+ local existing_terminal_in_new_tab
452+ for _ , win in ipairs (vim .api .nvim_tabpage_list_wins (new_tab )) do
453+ if vim .api .nvim_win_get_buf (win ) == terminal_bufnr then
454+ existing_terminal_in_new_tab = win
455+ break
456+ end
457+ end
383458
384- if split_side == " left" then
385- vim .cmd (" wincmd H" )
386- else
387- vim .cmd (" wincmd L" )
459+ if not existing_terminal_in_new_tab then
460+ vim .cmd (" vsplit" )
461+ existing_terminal_in_new_tab = vim .api .nvim_get_current_win ()
462+ if split_side == " left" then
463+ vim .cmd (" wincmd H" )
464+ else
465+ vim .cmd (" wincmd L" )
466+ end
388467 end
389468
469+ local terminal_win = existing_terminal_in_new_tab
470+
390471 vim .api .nvim_win_set_buf (terminal_win , terminal_bufnr )
391472
392473 apply_window_options (terminal_win , terminal_options )
@@ -1308,6 +1389,7 @@ function M._setup_blocking_diff(params, resolution_callback)
13081389 local setup_success , setup_error = pcall (function ()
13091390 local old_file_exists = vim .fn .filereadable (params .old_file_path ) == 1
13101391 local is_new_file = not old_file_exists
1392+ local original_tab_number = vim .api .nvim_get_current_tabpage ()
13111393
13121394 if old_file_exists then
13131395 local is_dirty = is_buffer_dirty (params .old_file_path )
@@ -1338,6 +1420,12 @@ function M._setup_blocking_diff(params, resolution_callback)
13381420 tab_number = state .created_new_tab and state .new_tab_number or nil ,
13391421 })
13401422 end
1423+
1424+ if original_tab_number and vim .api .nvim_tabpage_is_valid (original_tab_number ) then
1425+ vim .schedule (function ()
1426+ vim .api .nvim_set_current_tabpage (original_tab_number )
1427+ end )
1428+ end
13411429 return
13421430 end
13431431
@@ -1515,18 +1603,42 @@ function M._setup_blocking_diff(params, resolution_callback)
15151603 })
15161604 end ) -- End of pcall
15171605
1606+ --- Format an error value (string or structured {message,data} table) into a flat,
1607+ --- bounded string. Avoids `tostring(table)` blowups that spam :messages and the UI.
1608+ --- @param err any Error value (string or table with optional .message /.data )
1609+ --- @return string
1610+ local function format_error (err )
1611+ if type (err ) == " table" then
1612+ local msg = err .message or " unknown error"
1613+ local data = err .data
1614+ if data ~= nil and data ~= " " then
1615+ if type (data ) == " table" then
1616+ data = " table"
1617+ end
1618+ return msg .. " (" .. tostring (data ) .. " )"
1619+ end
1620+ return msg
1621+ end
1622+ return tostring (err )
1623+ end
1624+
1625+ -- Exposed for testing/debugging the error formatter.
1626+ M ._format_error = format_error
1627+
15181628 -- Handle setup errors
15191629 if not setup_success then
1520- local error_msg
1521- if type (setup_error ) == " table" and setup_error .message then
1522- -- Handle structured error objects
1523- error_msg = " Failed to setup diff operation: " .. setup_error .message
1524- if setup_error .data then
1525- error_msg = error_msg .. " (" .. setup_error .data .. " )"
1526- end
1630+ error_msg = " Failed to setup diff operation: " .. format_error (setup_error )
1631+
1632+ vim .schedule (function ()
1633+ vim .notify (" ClaudeCode diff setup failed: " .. format_error (setup_error ), vim .log .levels .ERROR )
1634+ end )
1635+
1636+ -- Log the structured error before we wrap it, so the original cause is visible in :messages.
1637+ local structured = setup_error
1638+ if type (structured ) == " table" and structured .message then
1639+ logger .error (" diff" , " Diff setup failed for" , tab_name , " -" , structured .message , structured .data or " " )
15271640 else
1528- -- Handle string errors or other types
1529- error_msg = " Failed to setup diff operation: " .. tostring (setup_error )
1641+ logger .error (" diff" , " Diff setup failed for" , tab_name , " -" , tostring (structured ))
15301642 end
15311643
15321644 -- Clean up any partial state that might have been created
@@ -1643,24 +1755,15 @@ function M.open_diff_blocking(old_file_path, new_file_path, new_file_contents, t
16431755 end )
16441756
16451757 if not success then
1646- local error_msg
1647- if type (err ) == " table" and err .message then
1648- error_msg = err .message
1649- if err .data then
1650- error_msg = error_msg .. " - " .. err .data
1651- end
1652- else
1653- error_msg = tostring (err )
1654- end
1655- logger .error (" diff" , " Diff setup failed for" , ' "' .. tab_name .. ' "' , " error:" , error_msg )
1758+ logger .error (" diff" , " Diff setup failed for" , ' "' .. tab_name .. ' "' , " error:" , format_error (err ))
16561759 -- If the error is already structured, propagate it directly
16571760 if type (err ) == " table" and err .code then
16581761 error (err )
16591762 else
16601763 error ({
16611764 code = - 32000 ,
16621765 message = " Error setting up diff" ,
1663- data = tostring (err ),
1766+ data = format_error (err ),
16641767 })
16651768 end
16661769 end
@@ -1813,6 +1916,7 @@ function M.accept_current_diff()
18131916 local tab_name = vim .b [current_buffer ].claudecode_diff_tab_name
18141917 if tab_name then
18151918 M ._resolve_diff_as_saved (tab_name , current_buffer )
1919+ M ._cleanup_diff_state (tab_name , " user accepted" )
18161920 else
18171921 vim .notify (" No active diff found in current buffer" , vim .log .levels .WARN )
18181922 end
@@ -1827,6 +1931,7 @@ function M.accept_current_diff()
18271931 end
18281932
18291933 M ._resolve_diff_as_saved (tab_name , current_buffer )
1934+ M ._cleanup_diff_state (tab_name , " user accepted" )
18301935end
18311936
18321937--- Deny/reject the current diff (user command version)
@@ -1839,6 +1944,7 @@ function M.deny_current_diff()
18391944 local tab_name = vim .b [current_buffer ].claudecode_diff_tab_name
18401945 if tab_name then
18411946 M ._resolve_diff_as_rejected (tab_name )
1947+ M ._cleanup_diff_state (tab_name , " user denied" )
18421948 else
18431949 vim .notify (" No active diff found in current buffer" , vim .log .levels .WARN )
18441950 end
@@ -1852,8 +1958,8 @@ function M.deny_current_diff()
18521958 return
18531959 end
18541960
1855- -- Do not close windows/tabs here; just mark as rejected.
18561961 M ._resolve_diff_as_rejected (tab_name )
1962+ M ._cleanup_diff_state (tab_name , " user denied" )
18571963end
18581964
18591965-- Expose internal utilities for use by diff_inline.lua
@@ -1863,6 +1969,7 @@ M._is_buffer_dirty = is_buffer_dirty
18631969M ._detect_filetype = detect_filetype
18641970M ._get_autocmd_group = get_autocmd_group
18651971M ._display_terminal_in_new_tab = display_terminal_in_new_tab
1972+ M .get_or_create_editor_win_in_diff_tab = get_or_create_editor_win_in_diff_tab
18661973
18671974return M
18681975--- @alias NvimWin integer
0 commit comments