v1.8.3: session pool + sidebar indicators

- SessionPool: LRU cache for SSH/SFTP sessions across server switches
- Sidebar: green dot indicators for servers with active sessions
- Sidebar: active sessions count label
- Terminal: buffer preservation on server switch via get_current_buffer()
- FilesTab/TerminalTab: pool integration for session reuse

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
chrome-storm-c442
2026-02-24 03:05:05 -05:00
parent 1a7b075cca
commit 8f55b210b3
9 changed files with 426 additions and 31 deletions

View File

@@ -11,9 +11,10 @@ from core.i18n import t
class TerminalTab(ctk.CTkFrame):
def __init__(self, master, store):
def __init__(self, master, store, session_pool=None):
super().__init__(master, fg_color="transparent")
self.store = store
self.session_pool = session_pool
self._current_alias: str | None = None
self._session: ShellSession | None = None
self._reconnect_count = 0
@@ -39,7 +40,16 @@ class TerminalTab(ctk.CTkFrame):
def set_server(self, alias: str | None):
if alias == self._current_alias:
return
# Store state of current session before switching
if self._current_alias and self._session and self.session_pool:
# Store terminal buffer from widget
buf = self._terminal.get_current_buffer()
self.session_pool.store_shell_state(self._current_alias, buf)
# Disconnect current session
self._disconnect()
self._current_alias = alias
if alias:
self._connect()
@@ -65,15 +75,32 @@ class TerminalTab(ctk.CTkFrame):
def _do_connect():
try:
key_path = self.store.get_ssh_key_path()
cols, rows = self._terminal.get_size()
session = ShellSession(server, key_path, cols=cols, rows=rows)
session.on_data = self._on_data_received
session.on_disconnect = self._on_disconnected
session.connect()
# Use session pool if available
if self.session_pool:
cols, rows = self._terminal.get_size()
session, is_new = self.session_pool.get_or_create_shell_session(alias, server, key_path)
if is_new:
# Configure the session with proper terminal dimensions
session.cols = cols
session.rows = rows
session.connect()
# Set up callbacks even if session already existed
session.on_data = self._on_data_received
session.on_disconnect = self._on_disconnected
self._session = session
else:
# Legacy behavior without session pool
cols, rows = self._terminal.get_size()
session = ShellSession(server, key_path, cols=cols, rows=rows)
session.on_data = self._on_data_received
session.on_disconnect = self._on_disconnected
session.connect()
self._session = session
# Set session on main thread to avoid races
def _set_session():
self._session = session
self._reconnect_count = 0
self._terminal.set_status(
t("term_connected").format(alias=alias), "#44cc44"
@@ -89,10 +116,16 @@ class TerminalTab(ctk.CTkFrame):
def _disconnect(self):
self._intentional_disconnect = True
session = self._session
self._session = None
if session:
session.disconnect()
# Only disconnect if we don't have a session pool (otherwise session stays alive)
if not self.session_pool and self._session:
self._session.disconnect()
self._session = None
# If using session pool, session remains active in the pool
elif self.session_pool and self._session:
# Remove callbacks to prevent processing data after switch
self._session.on_data = None
self._session.on_disconnect = None
self._session = None
def _on_data_received(self, data: bytes):
"""Called from SSH thread — put data in thread-safe queue."""
@@ -117,6 +150,10 @@ class TerminalTab(ctk.CTkFrame):
self._terminal.set_status(t("term_disconnected"), "#888888")
return
# Remove dead session from pool so it gets recreated on next connect
if self.session_pool and self._current_alias:
self.session_pool.disconnect_session(self._current_alias)
self._session = None
if self._reconnect_count < self._max_reconnect: