fix: unified fullscreen terminal header

- Add showControls prop to TerminalComponent to optionally hide internal header
- Add reset() method to TerminalRef for external reset control
- TerminalPage now renders a unified fullscreen header bar combining:
  - Session tabs (TerminalSessionTabs)
  - Terminal controls (status dot, A-, A+, Reset, Exit Fullscreen)
- Unified header is always visible in fullscreen (no hover-to-reveal)
- TerminalComponent internal header hidden when in fullscreen mode
- Remove old CSS that hid session tabs with opacity:0 until hover

Quality gates: pytest 188 passed, frontend typecheck clean

Fixes: terminal-fullscreen-unified-header
This commit is contained in:
Alex Blank
2026-05-29 10:34:31 +02:00
parent 0fdbef578f
commit 8926152fca
7 changed files with 297 additions and 95 deletions
@@ -12,7 +12,9 @@ class TestExpandContainerPath:
def test_tilde_slash_expands(self) -> None:
"""~/foo should expand to home_dir/foo."""
assert expand_container_path("~/workspace", "/home/user") == "/home/user/workspace"
assert (
expand_container_path("~/workspace", "/home/user") == "/home/user/workspace"
)
def test_tilde_alone_expands(self) -> None:
"""~ should expand to home_dir."""
@@ -20,7 +22,10 @@ class TestExpandContainerPath:
def test_dollar_home_slash_expands(self) -> None:
"""$HOME/foo should expand to home_dir/foo."""
assert expand_container_path("$HOME/workspace", "/home/user") == "/home/user/workspace"
assert (
expand_container_path("$HOME/workspace", "/home/user")
== "/home/user/workspace"
)
def test_dollar_home_alone_expands(self) -> None:
"""$HOME should expand to home_dir."""
@@ -78,7 +83,9 @@ class TestResolveGitMountMappingsExpansion:
"""Mapping with ~/repo target expands to home dir."""
(tmp_path / "src").mkdir()
mappings = [{"source_path": "src", "target_path": "~/repo"}]
result = _resolve_git_mount_mappings(str(tmp_path), mappings, None, "/home/user")
result = _resolve_git_mount_mappings(
str(tmp_path), mappings, None, "/home/user"
)
assert len(result) == 1
assert result[0]["target"] == "/home/user/repo"
@@ -86,7 +93,9 @@ class TestResolveGitMountMappingsExpansion:
"""Mapping with $HOME/repo target expands to home dir."""
(tmp_path / "src").mkdir()
mappings = [{"source_path": "src", "target_path": "$HOME/repo"}]
result = _resolve_git_mount_mappings(str(tmp_path), mappings, None, "/home/user")
result = _resolve_git_mount_mappings(
str(tmp_path), mappings, None, "/home/user"
)
assert len(result) == 1
assert result[0]["target"] == "/home/user/repo"
@@ -94,6 +103,8 @@ class TestResolveGitMountMappingsExpansion:
"""Absolute target paths are not modified."""
(tmp_path / "src").mkdir()
mappings = [{"source_path": "src", "target_path": "/app/src"}]
result = _resolve_git_mount_mappings(str(tmp_path), mappings, None, "/home/user")
result = _resolve_git_mount_mappings(
str(tmp_path), mappings, None, "/home/user"
)
assert len(result) == 1
assert result[0]["target"] == "/app/src"
@@ -77,7 +77,9 @@ def mock_session(fake_user_id, fake_project_id, fake_repo_id, fake_tool_type_id)
session.get.side_effect = _get
session.add = MagicMock(side_effect=_add)
session.execute.return_value = MagicMock(scalars=MagicMock(return_value=MagicMock(all=MagicMock(return_value=[]))))
session.execute.return_value = MagicMock(
scalars=MagicMock(return_value=MagicMock(all=MagicMock(return_value=[])))
)
return session
@@ -440,7 +442,11 @@ class TestStartInstanceLegacyFallback:
mock_get_container_id.return_value = "abc123"
mock_get_container_name.return_value = "test-container"
mock_connect_network.return_value = True
mock_wait_container.return_value = {"success": True, "status": "running", "waited_seconds": 0.5}
mock_wait_container.return_value = {
"success": True,
"status": "running",
"waited_seconds": 0.5,
}
instance = ToolInstance(
id=fake_instance_id,
@@ -534,7 +540,11 @@ class TestStartInstanceLegacyFallback:
mock_get_container_id.return_value = "abc123"
mock_get_container_name.return_value = "test-container"
mock_connect_network.return_value = True
mock_wait_container.return_value = {"success": True, "status": "running", "waited_seconds": 0.5}
mock_wait_container.return_value = {
"success": True,
"status": "running",
"waited_seconds": 0.5,
}
instance = ToolInstance(
id=fake_instance_id,
@@ -627,7 +637,11 @@ class TestStartInstanceLegacyFallback:
mock_get_container_id.return_value = "abc123"
mock_get_container_name.return_value = "test-container"
mock_connect_network.return_value = True
mock_wait_container.return_value = {"success": True, "status": "running", "waited_seconds": 0.5}
mock_wait_container.return_value = {
"success": True,
"status": "running",
"waited_seconds": 0.5,
}
instance = ToolInstance(
id=fake_instance_id,
@@ -730,7 +744,11 @@ class TestStartInstanceManifestBranch:
mock_get_container_id.return_value = "abc123"
mock_get_container_name.return_value = "test-container"
mock_connect_network.return_value = True
mock_wait_container.return_value = {"success": True, "status": "running", "waited_seconds": 0.5}
mock_wait_container.return_value = {
"success": True,
"status": "running",
"waited_seconds": 0.5,
}
mock_prepare_manifest.return_value = (
"headquarter/test:latest",
"services:\n app:\n image: test",