Skip to content

Commit 9c74f95

Browse files
author
Snir Turgeman
committed
fix: preserve terminal window size across session operations
Add dedicated window_manager module that owns the single terminal window. This separates window lifecycle from buffer lifecycle, fixing issues where: - Creating new sessions would reset window to default size - Switching between tabs could cause window duplication - Closing tabs could leave windows in wrong positions Changes: - Add window_manager.lua: singleton that manages THE terminal window - Refactor snacks.lua: create buffers only, delegate window to manager - Refactor native.lua: simplified buffer-only management - Update terminal.lua: initialize window_manager, improve tab navigation - New tab now selects the created session - Closing tab selects previous tab (or next if first)
1 parent ccde261 commit 9c74f95

5 files changed

Lines changed: 907 additions & 1236 deletions

File tree

ARCHITECTURE.md

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -129,22 +129,36 @@ vim.api.nvim_create_autocmd("CursorMoved", {
129129
-- Preserves selection context when switching to terminal
130130
```
131131

132-
### 6. Terminal Integration (`terminal.lua`)
132+
### 6. Terminal Integration (`terminal/`)
133133

134-
Flexible terminal management with provider pattern:
134+
Flexible terminal management with provider pattern and centralized window management:
135135

136136
```lua
137-
-- Snacks.nvim provider (preferred)
138-
if has_snacks then
139-
Snacks.terminal.open(cmd, {
140-
win = { position = "right", width = 0.3 }
141-
})
142-
else
143-
-- Native fallback
144-
vim.cmd("vsplit | terminal " .. cmd)
145-
end
137+
-- Window manager singleton owns THE terminal window
138+
-- Providers only create buffers, window_manager displays them
139+
local window_manager = require("claudecode.terminal.window_manager")
140+
141+
-- Display a buffer in the managed window (preserves user resizing)
142+
window_manager.display_buffer(bufnr, focus)
143+
144+
-- Snacks.nvim provider creates buffer, delegates window to manager
145+
local term = Snacks.terminal.open(cmd, opts)
146+
vim.api.nvim_win_close(term.win, false) -- Close snacks' window
147+
window_manager.display_buffer(term.buf, true) -- Use our window
148+
149+
-- Native provider creates buffer without window
150+
local bufnr = vim.api.nvim_create_buf(false, true)
151+
vim.fn.termopen(cmd, { env = env })
152+
window_manager.display_buffer(bufnr, true)
146153
```
147154

155+
Key features:
156+
157+
- **Single window**: All sessions share one terminal window
158+
- **Buffer switching**: `nvim_win_set_buf()` preserves window size
159+
- **Session management**: Multiple Claude sessions with tab-like switching
160+
- **Window preservation**: User resizing persists across session switches
161+
148162
## Key Implementation Patterns
149163

150164
### Thread Safety
@@ -199,7 +213,15 @@ lua/claudecode/
199213
├── tools/init.lua # MCP tool registry
200214
├── diff.lua # Native diff support
201215
├── selection.lua # Selection tracking
202-
├── terminal.lua # Terminal management
216+
├── session.lua # Multi-session state management
217+
├── terminal.lua # Terminal orchestration
218+
├── terminal/ # Terminal providers
219+
│ ├── window_manager.lua # Singleton window management
220+
│ ├── snacks.lua # Snacks.nvim provider
221+
│ ├── native.lua # Native Neovim terminal
222+
│ ├── external.lua # External terminal apps
223+
│ ├── tabbar.lua # Session tab bar UI
224+
│ └── osc_handler.lua # Terminal title detection
203225
└── lockfile.lua # Discovery files
204226
```
205227

lua/claudecode/terminal.lua

Lines changed: 91 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -777,6 +777,13 @@ function M.setup(user_term_config, p_terminal_cmd, p_env)
777777
end
778778
end
779779

780+
-- Setup window manager with config
781+
local window_manager = require("claudecode.terminal.window_manager")
782+
window_manager.setup({
783+
split_side = defaults.split_side,
784+
split_width_percentage = defaults.split_width_percentage,
785+
})
786+
780787
-- Setup providers with config
781788
get_provider().setup(defaults)
782789

@@ -831,6 +838,7 @@ end
831838
---Closes the managed Claude terminal if it's open and valid.
832839
function M.close()
833840
detach_tabbar()
841+
-- Call provider's close for backwards compatibility
834842
get_provider().close()
835843
end
836844

@@ -998,14 +1006,17 @@ function M.open_new_session(opts_override, cmd_args)
9981006
local effective_config = build_config(opts_override)
9991007
local cmd_string, claude_env_table = get_claude_command_and_env(cmd_args)
10001008

1009+
-- Make the new session active immediately
1010+
session_manager.set_active_session(session_id)
1011+
10011012
local provider = get_provider()
10021013

10031014
-- For multi-session, we need to pass session_id to providers
10041015
if provider.open_session then
1005-
provider.open_session(session_id, cmd_string, claude_env_table, effective_config)
1016+
provider.open_session(session_id, cmd_string, claude_env_table, effective_config, true) -- true = focus
10061017
else
10071018
-- Fallback: use regular open (single terminal mode)
1008-
provider.open(cmd_string, claude_env_table, effective_config)
1019+
provider.open(cmd_string, claude_env_table, effective_config, true) -- true = focus
10091020
end
10101021

10111022
return session_id
@@ -1020,41 +1031,94 @@ function M.close_session(session_id)
10201031
end
10211032

10221033
local provider = get_provider()
1034+
local effective_config = build_config(nil)
1035+
1036+
-- Check if there are other sessions to switch to
1037+
local session_count = session_manager.get_session_count()
1038+
1039+
if session_count > 1 then
1040+
-- There are other sessions - keep the window and switch to another session
1041+
-- Figure out which session to switch to: prefer previous tab, fallback to next
1042+
local sessions = session_manager.list_sessions()
1043+
local new_active_id = nil
1044+
local current_index = nil
1045+
1046+
-- Find the index of the session being closed
1047+
for i, s in ipairs(sessions) do
1048+
if s.id == session_id then
1049+
current_index = i
1050+
break
1051+
end
1052+
end
10231053

1024-
-- Detach tabbar before closing the terminal window
1025-
detach_tabbar()
1054+
if current_index then
1055+
-- Prefer previous tab (index - 1), fallback to next tab (index + 1)
1056+
if current_index > 1 then
1057+
new_active_id = sessions[current_index - 1].id
1058+
elseif current_index < #sessions then
1059+
new_active_id = sessions[current_index + 1].id
1060+
end
1061+
end
10261062

1027-
if provider.close_session then
1028-
provider.close_session(session_id)
1029-
else
1030-
-- Fallback: use regular close
1031-
provider.close()
1032-
end
1063+
-- Fallback: just pick any other session
1064+
if not new_active_id then
1065+
for _, s in ipairs(sessions) do
1066+
if s.id ~= session_id then
1067+
new_active_id = s.id
1068+
break
1069+
end
1070+
end
1071+
end
1072+
1073+
if new_active_id and provider.close_session_keep_window then
1074+
-- Use close_session_keep_window to keep window open and switch buffer
1075+
-- This function handles cleanup of the old session internally
1076+
provider.close_session_keep_window(session_id, new_active_id, effective_config)
1077+
session_manager.destroy_session(session_id)
1078+
session_manager.set_active_session(new_active_id)
1079+
else
1080+
-- Fallback: close and reopen
1081+
session_manager.destroy_session(session_id)
1082+
new_active_id = session_manager.get_active_session_id()
10331083

1034-
session_manager.destroy_session(session_id)
1084+
if provider.close_session then
1085+
provider.close_session(session_id)
1086+
else
1087+
provider.close()
1088+
end
10351089

1036-
-- If there are remaining sessions, switch to the new active session
1037-
local new_active_id = session_manager.get_active_session_id()
1038-
if new_active_id then
1039-
local effective_config = build_config(nil)
1040-
if provider.focus_session then
1041-
provider.focus_session(new_active_id, effective_config)
1090+
if new_active_id and provider.focus_session then
1091+
provider.focus_session(new_active_id, effective_config)
1092+
end
10421093
end
10431094

10441095
-- Re-attach tabbar to the new session's terminal
1045-
local new_bufnr
1046-
if provider.get_session_bufnr then
1047-
new_bufnr = provider.get_session_bufnr(new_active_id)
1048-
else
1049-
new_bufnr = provider.get_active_bufnr()
1050-
end
1096+
if new_active_id then
1097+
local new_bufnr
1098+
if provider.get_session_bufnr then
1099+
new_bufnr = provider.get_session_bufnr(new_active_id)
1100+
else
1101+
new_bufnr = provider.get_active_bufnr()
1102+
end
10511103

1052-
if new_bufnr and vim.fn.getbufinfo then
1053-
local ok, bufinfo = pcall(vim.fn.getbufinfo, new_bufnr)
1054-
if ok and bufinfo and #bufinfo > 0 and #bufinfo[1].windows > 0 then
1055-
attach_tabbar(bufinfo[1].windows[1], new_bufnr)
1104+
if new_bufnr and vim.fn.getbufinfo then
1105+
local ok, bufinfo = pcall(vim.fn.getbufinfo, new_bufnr)
1106+
if ok and bufinfo and #bufinfo > 0 and #bufinfo[1].windows > 0 then
1107+
attach_tabbar(bufinfo[1].windows[1], new_bufnr)
1108+
end
10561109
end
10571110
end
1111+
else
1112+
-- This is the last session - close everything
1113+
detach_tabbar()
1114+
1115+
if provider.close_session then
1116+
provider.close_session(session_id)
1117+
else
1118+
provider.close()
1119+
end
1120+
1121+
session_manager.destroy_session(session_id)
10581122
end
10591123
end
10601124

0 commit comments

Comments
 (0)