feat: multi-session terminal frontend UI + tests (PR 3)

- Add TerminalSessionTabs component with status dots, rename, close, max-5 limit
- Add 7 component tests for tab rendering, selection, close, rename
- TerminalComponent: sessionId prop, forwardRef with fit() method
- TerminalPage: multi-session orchestration, tab switching, auto-create default
- Fullscreen mode: Alt+Shift+F toggle, auto-hide tabs, Esc exit
- Keyboard shortcuts: Alt+Shift+N/W/ArrowLeft/ArrowRight/R
- Add CSS for tabs, fullscreen, mobile responsive
- Update useTerminalSessions hook for session CRUD
- terminal_manager.py: lookup by internal session_id fallback

Quality gates: tsc --noEmit clean, vitest (7/7 new tests passed), pytest (182 passed)
This commit is contained in:
2026-05-28 13:35:45 +02:00
parent 0b35ae3bf0
commit 62d1bdc462
26 changed files with 4589 additions and 775 deletions
+28 -3
View File
@@ -248,8 +248,26 @@ class TerminalManager:
instance_id: str,
session_id: str,
) -> TerminalSession | None:
"""Lookup a session by composite key."""
return self._sessions.get((instance_id, session_id))
"""Lookup a session by composite key, or by internal session_id."""
session = self._sessions.get((instance_id, session_id))
if session is not None:
return session
# Fallback: search by internal TerminalSession.session_id
for (iid, _sid), sess in self._sessions.items():
if iid == instance_id and sess.session_id == session_id:
return sess
return None
def _find_key_by_internal_id(
self,
instance_id: str,
internal_session_id: str,
) -> tuple[str, str] | None:
"""Find the manager dict key for a session by its internal session_id."""
for (iid, sid), session in self._sessions.items():
if iid == instance_id and session.session_id == internal_session_id:
return (iid, sid)
return None
def get_sessions_for_instance(
self,
@@ -328,6 +346,7 @@ class TerminalManager:
container_id: str,
startup_command: str | None = None,
session_id: str | None = None,
name: str | None = None,
) -> TerminalSession:
"""Reset a session by killing it and creating a new one.
@@ -336,6 +355,7 @@ class TerminalManager:
container_id: Docker container ID.
startup_command: Optional startup command.
session_id: Specific session to reset. If None, resets the default session.
name: Optional name to preserve for the new session.
Returns:
The newly created TerminalSession.
@@ -344,6 +364,11 @@ class TerminalManager:
target_session_id = session_id or "default"
key = (instance_id_str, target_session_id)
# Preserve old name if not provided
old_name = name
if old_name is None and key in self._sessions:
old_name = self._sessions[key].name
# Close existing session if any
if key in self._sessions:
logger.debug(
@@ -363,7 +388,7 @@ class TerminalManager:
instance_id=instance_id,
container_id=container_id,
startup_command=startup_command,
name="Session 1" if target_session_id == "default" else None,
name=old_name or ("Session 1" if target_session_id == "default" else None),
)
await new_session.start(startup_command=startup_command)
self._sessions[key] = new_session