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:
@@ -1303,6 +1303,7 @@ async def start_instance(
|
|||||||
home_dir = "/root"
|
home_dir = "/root"
|
||||||
if tool_type and tool_type.definition_type == "manifest" and tool_type.manifest_id:
|
if tool_type and tool_type.definition_type == "manifest" and tool_type.manifest_id:
|
||||||
from src.models.tool_definition_manifest import ToolDefinitionManifest
|
from src.models.tool_definition_manifest import ToolDefinitionManifest
|
||||||
|
|
||||||
manifest_def = await session.get(ToolDefinitionManifest, tool_type.manifest_id)
|
manifest_def = await session.get(ToolDefinitionManifest, tool_type.manifest_id)
|
||||||
if manifest_def:
|
if manifest_def:
|
||||||
home_dir = get_manifest_home_dir(dict(manifest_def.manifest))
|
home_dir = get_manifest_home_dir(dict(manifest_def.manifest))
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ class TestExpandContainerPath:
|
|||||||
|
|
||||||
def test_tilde_slash_expands(self) -> None:
|
def test_tilde_slash_expands(self) -> None:
|
||||||
"""~/foo should expand to home_dir/foo."""
|
"""~/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:
|
def test_tilde_alone_expands(self) -> None:
|
||||||
"""~ should expand to home_dir."""
|
"""~ should expand to home_dir."""
|
||||||
@@ -20,7 +22,10 @@ class TestExpandContainerPath:
|
|||||||
|
|
||||||
def test_dollar_home_slash_expands(self) -> None:
|
def test_dollar_home_slash_expands(self) -> None:
|
||||||
"""$HOME/foo should expand to home_dir/foo."""
|
"""$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:
|
def test_dollar_home_alone_expands(self) -> None:
|
||||||
"""$HOME should expand to home_dir."""
|
"""$HOME should expand to home_dir."""
|
||||||
@@ -78,7 +83,9 @@ class TestResolveGitMountMappingsExpansion:
|
|||||||
"""Mapping with ~/repo target expands to home dir."""
|
"""Mapping with ~/repo target expands to home dir."""
|
||||||
(tmp_path / "src").mkdir()
|
(tmp_path / "src").mkdir()
|
||||||
mappings = [{"source_path": "src", "target_path": "~/repo"}]
|
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 len(result) == 1
|
||||||
assert result[0]["target"] == "/home/user/repo"
|
assert result[0]["target"] == "/home/user/repo"
|
||||||
|
|
||||||
@@ -86,7 +93,9 @@ class TestResolveGitMountMappingsExpansion:
|
|||||||
"""Mapping with $HOME/repo target expands to home dir."""
|
"""Mapping with $HOME/repo target expands to home dir."""
|
||||||
(tmp_path / "src").mkdir()
|
(tmp_path / "src").mkdir()
|
||||||
mappings = [{"source_path": "src", "target_path": "$HOME/repo"}]
|
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 len(result) == 1
|
||||||
assert result[0]["target"] == "/home/user/repo"
|
assert result[0]["target"] == "/home/user/repo"
|
||||||
|
|
||||||
@@ -94,6 +103,8 @@ class TestResolveGitMountMappingsExpansion:
|
|||||||
"""Absolute target paths are not modified."""
|
"""Absolute target paths are not modified."""
|
||||||
(tmp_path / "src").mkdir()
|
(tmp_path / "src").mkdir()
|
||||||
mappings = [{"source_path": "src", "target_path": "/app/src"}]
|
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 len(result) == 1
|
||||||
assert result[0]["target"] == "/app/src"
|
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.get.side_effect = _get
|
||||||
session.add = MagicMock(side_effect=_add)
|
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
|
return session
|
||||||
|
|
||||||
|
|
||||||
@@ -440,7 +442,11 @@ class TestStartInstanceLegacyFallback:
|
|||||||
mock_get_container_id.return_value = "abc123"
|
mock_get_container_id.return_value = "abc123"
|
||||||
mock_get_container_name.return_value = "test-container"
|
mock_get_container_name.return_value = "test-container"
|
||||||
mock_connect_network.return_value = True
|
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(
|
instance = ToolInstance(
|
||||||
id=fake_instance_id,
|
id=fake_instance_id,
|
||||||
@@ -534,7 +540,11 @@ class TestStartInstanceLegacyFallback:
|
|||||||
mock_get_container_id.return_value = "abc123"
|
mock_get_container_id.return_value = "abc123"
|
||||||
mock_get_container_name.return_value = "test-container"
|
mock_get_container_name.return_value = "test-container"
|
||||||
mock_connect_network.return_value = True
|
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(
|
instance = ToolInstance(
|
||||||
id=fake_instance_id,
|
id=fake_instance_id,
|
||||||
@@ -627,7 +637,11 @@ class TestStartInstanceLegacyFallback:
|
|||||||
mock_get_container_id.return_value = "abc123"
|
mock_get_container_id.return_value = "abc123"
|
||||||
mock_get_container_name.return_value = "test-container"
|
mock_get_container_name.return_value = "test-container"
|
||||||
mock_connect_network.return_value = True
|
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(
|
instance = ToolInstance(
|
||||||
id=fake_instance_id,
|
id=fake_instance_id,
|
||||||
@@ -730,7 +744,11 @@ class TestStartInstanceManifestBranch:
|
|||||||
mock_get_container_id.return_value = "abc123"
|
mock_get_container_id.return_value = "abc123"
|
||||||
mock_get_container_name.return_value = "test-container"
|
mock_get_container_name.return_value = "test-container"
|
||||||
mock_connect_network.return_value = True
|
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 = (
|
mock_prepare_manifest.return_value = (
|
||||||
"headquarter/test:latest",
|
"headquarter/test:latest",
|
||||||
"services:\n app:\n image: test",
|
"services:\n app:\n image: test",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export interface TerminalProps {
|
|||||||
sessionId?: string;
|
sessionId?: string;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
isMobile?: boolean;
|
isMobile?: boolean;
|
||||||
|
showControls?: boolean;
|
||||||
activeModifier?: ModifierKey | null;
|
activeModifier?: ModifierKey | null;
|
||||||
onModifierChange?: (modifier: ModifierKey | null) => void;
|
onModifierChange?: (modifier: ModifierKey | null) => void;
|
||||||
onTerminalReady?: (
|
onTerminalReady?: (
|
||||||
@@ -38,6 +39,7 @@ export interface TerminalProps {
|
|||||||
export interface TerminalRef {
|
export interface TerminalRef {
|
||||||
fit: () => void;
|
fit: () => void;
|
||||||
focus: () => void;
|
focus: () => void;
|
||||||
|
reset: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FONT_SIZE_KEY = "terminal-font-size";
|
const FONT_SIZE_KEY = "terminal-font-size";
|
||||||
@@ -53,6 +55,7 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
|||||||
sessionId,
|
sessionId,
|
||||||
onClose,
|
onClose,
|
||||||
isMobile = false,
|
isMobile = false,
|
||||||
|
showControls = true,
|
||||||
activeModifier,
|
activeModifier,
|
||||||
onModifierChange,
|
onModifierChange,
|
||||||
onTerminalReady,
|
onTerminalReady,
|
||||||
@@ -471,6 +474,11 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
|||||||
focus: () => {
|
focus: () => {
|
||||||
termRef.current?.focus();
|
termRef.current?.focus();
|
||||||
},
|
},
|
||||||
|
reset: () => {
|
||||||
|
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
||||||
|
wsRef.current.send(JSON.stringify({ type: "reset" }));
|
||||||
|
}
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Update parent about status changes
|
// Update parent about status changes
|
||||||
@@ -561,78 +569,80 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`terminal-wrapper ${isMobile ? "mobile" : ""}`}>
|
<div className={`terminal-wrapper ${isMobile ? "mobile" : ""}`}>
|
||||||
<div className="terminal-header">
|
{showControls && (
|
||||||
<div className="terminal-header-left">
|
<div className="terminal-header">
|
||||||
<div className="terminal-status">
|
<div className="terminal-header-left">
|
||||||
<span
|
<div className="terminal-status">
|
||||||
className={`status-dot ${status}`}
|
<span
|
||||||
aria-label={`Terminal status: ${status}`}
|
className={`status-dot ${status}`}
|
||||||
/>
|
aria-label={`Terminal status: ${status}`}
|
||||||
<span className="status-text">
|
/>
|
||||||
{status === "resetting"
|
<span className="status-text">
|
||||||
? "Resetting..."
|
{status === "resetting"
|
||||||
: reconnectAttemptsRef.current > 0 && status !== "connected"
|
? "Resetting..."
|
||||||
? `Reconnecting (${reconnectAttemptsRef.current}/${RECONNECT_ATTEMPTS})...`
|
: reconnectAttemptsRef.current > 0 && status !== "connected"
|
||||||
: status}
|
? `Reconnecting (${reconnectAttemptsRef.current}/${RECONNECT_ATTEMPTS})...`
|
||||||
</span>
|
: status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{isMobile && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
className="terminal-header-button"
|
||||||
|
onClick={handleCopy}
|
||||||
|
type="button"
|
||||||
|
aria-label="Copy selection"
|
||||||
|
>
|
||||||
|
Copy
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="terminal-header-button"
|
||||||
|
onClick={handlePaste}
|
||||||
|
type="button"
|
||||||
|
aria-label="Paste from clipboard"
|
||||||
|
>
|
||||||
|
Paste
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{isMobile && (
|
<div className="terminal-header-right">
|
||||||
<>
|
|
||||||
<button
|
|
||||||
className="terminal-header-button"
|
|
||||||
onClick={handleCopy}
|
|
||||||
type="button"
|
|
||||||
aria-label="Copy selection"
|
|
||||||
>
|
|
||||||
Copy
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="terminal-header-button"
|
|
||||||
onClick={handlePaste}
|
|
||||||
type="button"
|
|
||||||
aria-label="Paste from clipboard"
|
|
||||||
>
|
|
||||||
Paste
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="terminal-header-right">
|
|
||||||
<button
|
|
||||||
className="terminal-header-button"
|
|
||||||
onClick={() => handleFontSizeChange(-1)}
|
|
||||||
type="button"
|
|
||||||
aria-label="Decrease font size"
|
|
||||||
>
|
|
||||||
A-
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="terminal-header-button"
|
|
||||||
onClick={() => handleFontSizeChange(1)}
|
|
||||||
type="button"
|
|
||||||
aria-label="Increase font size"
|
|
||||||
>
|
|
||||||
A+
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="terminal-header-button"
|
|
||||||
onClick={() => setShowResetConfirm(true)}
|
|
||||||
type="button"
|
|
||||||
aria-label="Reset terminal"
|
|
||||||
>
|
|
||||||
Reset
|
|
||||||
</button>
|
|
||||||
{onClose && (
|
|
||||||
<button
|
<button
|
||||||
className="terminal-close"
|
className="terminal-header-button"
|
||||||
onClick={onClose}
|
onClick={() => handleFontSizeChange(-1)}
|
||||||
type="button"
|
type="button"
|
||||||
|
aria-label="Decrease font size"
|
||||||
>
|
>
|
||||||
Close
|
A-
|
||||||
</button>
|
</button>
|
||||||
)}
|
<button
|
||||||
|
className="terminal-header-button"
|
||||||
|
onClick={() => handleFontSizeChange(1)}
|
||||||
|
type="button"
|
||||||
|
aria-label="Increase font size"
|
||||||
|
>
|
||||||
|
A+
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="terminal-header-button"
|
||||||
|
onClick={() => setShowResetConfirm(true)}
|
||||||
|
type="button"
|
||||||
|
aria-label="Reset terminal"
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
{onClose && (
|
||||||
|
<button
|
||||||
|
className="terminal-close"
|
||||||
|
onClick={onClose}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
{showResetConfirm && (
|
{showResetConfirm && (
|
||||||
<div className="terminal-reset-confirm">
|
<div className="terminal-reset-confirm">
|
||||||
<div className="terminal-reset-confirm-content">
|
<div className="terminal-reset-confirm-content">
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ const SESSIONS_TO_INFO = (sessions: TerminalSession[]): TerminalSessionInfo[] =>
|
|||||||
status: s.status as TerminalSessionInfo["status"],
|
status: s.status as TerminalSessionInfo["status"],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
type TerminalStatus = "connecting" | "connected" | "disconnected" | "error" | "resetting";
|
||||||
|
|
||||||
export const TerminalPage: React.FC = () => {
|
export const TerminalPage: React.FC = () => {
|
||||||
const { instanceId } = useParams<{
|
const { instanceId } = useParams<{
|
||||||
instanceId: string;
|
instanceId: string;
|
||||||
@@ -27,6 +29,11 @@ export const TerminalPage: React.FC = () => {
|
|||||||
const terminalRefs = useRef<Record<string, React.RefObject<TerminalRef>>>({});
|
const terminalRefs = useRef<Record<string, React.RefObject<TerminalRef>>>({});
|
||||||
const headerAutoHide = useAutoHide({ timeout: 3000, enabled: isMobile });
|
const headerAutoHide = useAutoHide({ timeout: 3000, enabled: isMobile });
|
||||||
|
|
||||||
|
// Track terminal status and callbacks for unified fullscreen header
|
||||||
|
const [terminalStatuses, setTerminalStatuses] = useState<Record<string, TerminalStatus>>({});
|
||||||
|
const changeFontSizeRef = useRef<((delta: number) => void) | null>(null);
|
||||||
|
const [showResetConfirm, setShowResetConfirm] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
sessions,
|
sessions,
|
||||||
activeSessionId,
|
activeSessionId,
|
||||||
@@ -178,6 +185,29 @@ export const TerminalPage: React.FC = () => {
|
|||||||
[renameSession],
|
[renameSession],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleTerminalReady = useCallback(
|
||||||
|
(
|
||||||
|
_sendData: (data: string) => void,
|
||||||
|
status: TerminalStatus,
|
||||||
|
_focusInput: () => void,
|
||||||
|
changeFontSize: (delta: number) => void,
|
||||||
|
) => {
|
||||||
|
setTerminalStatuses((prev) => ({ ...prev, [activeSessionId ?? "default"]: status }));
|
||||||
|
changeFontSizeRef.current = changeFontSize;
|
||||||
|
},
|
||||||
|
[activeSessionId],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFontSizeChange = useCallback((delta: number) => {
|
||||||
|
changeFontSizeRef.current?.(delta);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleReset = useCallback(() => {
|
||||||
|
if (activeSessionId && terminalRefs.current[activeSessionId]) {
|
||||||
|
terminalRefs.current[activeSessionId].current?.reset();
|
||||||
|
}
|
||||||
|
}, [activeSessionId]);
|
||||||
|
|
||||||
if (!instanceId) {
|
if (!instanceId) {
|
||||||
return (
|
return (
|
||||||
<section className="stack">
|
<section className="stack">
|
||||||
@@ -240,6 +270,7 @@ export const TerminalPage: React.FC = () => {
|
|||||||
sessionId={session.id}
|
sessionId={session.id}
|
||||||
onClose={() => handleClose(session.id)}
|
onClose={() => handleClose(session.id)}
|
||||||
isMobile={true}
|
isMobile={true}
|
||||||
|
onTerminalReady={handleTerminalReady}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -275,15 +306,98 @@ export const TerminalPage: React.FC = () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<TerminalSessionTabs
|
{isFullscreen ? (
|
||||||
sessions={sessionInfos}
|
<div className="terminal-fullscreen-header">
|
||||||
activeSessionId={activeSessionId ?? ""}
|
<div className="terminal-fullscreen-header-tabs">
|
||||||
onSelect={handleSelect}
|
<TerminalSessionTabs
|
||||||
onClose={handleClose}
|
sessions={sessionInfos}
|
||||||
onCreate={handleCreate}
|
activeSessionId={activeSessionId ?? ""}
|
||||||
onRename={handleRename}
|
onSelect={handleSelect}
|
||||||
isMobile={false}
|
onClose={handleClose}
|
||||||
/>
|
onCreate={handleCreate}
|
||||||
|
onRename={handleRename}
|
||||||
|
isMobile={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="terminal-fullscreen-header-controls">
|
||||||
|
<span
|
||||||
|
className={`terminal-fullscreen-status status-dot ${terminalStatuses[activeSessionId ?? "default"] ?? "connecting"}`}
|
||||||
|
aria-label={`Terminal status: ${terminalStatuses[activeSessionId ?? "default"] ?? "connecting"}`}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="terminal-header-button"
|
||||||
|
onClick={() => handleFontSizeChange(-1)}
|
||||||
|
type="button"
|
||||||
|
aria-label="Decrease font size"
|
||||||
|
>
|
||||||
|
A-
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="terminal-header-button"
|
||||||
|
onClick={() => handleFontSizeChange(1)}
|
||||||
|
type="button"
|
||||||
|
aria-label="Increase font size"
|
||||||
|
>
|
||||||
|
A+
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="terminal-header-button"
|
||||||
|
onClick={() => setShowResetConfirm(true)}
|
||||||
|
type="button"
|
||||||
|
aria-label="Reset terminal"
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="terminal-close"
|
||||||
|
onClick={() => setIsFullscreen(false)}
|
||||||
|
type="button"
|
||||||
|
title="Exit fullscreen (Esc)"
|
||||||
|
>
|
||||||
|
Exit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{showResetConfirm && (
|
||||||
|
<div className="terminal-reset-confirm">
|
||||||
|
<div className="terminal-reset-confirm-content">
|
||||||
|
<p>
|
||||||
|
Reset terminal? This will kill the current shell session and
|
||||||
|
start fresh.
|
||||||
|
</p>
|
||||||
|
<div className="terminal-reset-confirm-buttons">
|
||||||
|
<button
|
||||||
|
className="terminal-reset-confirm-button cancel"
|
||||||
|
onClick={() => setShowResetConfirm(false)}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="terminal-reset-confirm-button confirm"
|
||||||
|
onClick={() => {
|
||||||
|
setShowResetConfirm(false);
|
||||||
|
handleReset();
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<TerminalSessionTabs
|
||||||
|
sessions={sessionInfos}
|
||||||
|
activeSessionId={activeSessionId ?? ""}
|
||||||
|
onSelect={handleSelect}
|
||||||
|
onClose={handleClose}
|
||||||
|
onCreate={handleCreate}
|
||||||
|
onRename={handleRename}
|
||||||
|
isMobile={false}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div className="terminal-page-content">
|
<div className="terminal-page-content">
|
||||||
{error && <div className="terminal-error-banner">{error}</div>}
|
{error && <div className="terminal-error-banner">{error}</div>}
|
||||||
{sessions
|
{sessions
|
||||||
@@ -296,6 +410,8 @@ export const TerminalPage: React.FC = () => {
|
|||||||
sessionId={session.id}
|
sessionId={session.id}
|
||||||
onClose={() => handleClose(session.id)}
|
onClose={() => handleClose(session.id)}
|
||||||
isMobile={false}
|
isMobile={false}
|
||||||
|
showControls={!isFullscreen}
|
||||||
|
onTerminalReady={handleTerminalReady}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
+49
-10
@@ -2890,18 +2890,57 @@ a.nav-item,
|
|||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal-page.fullscreen .terminal-session-tabs {
|
/* Unified fullscreen header: session tabs + terminal controls */
|
||||||
position: absolute;
|
.terminal-fullscreen-header {
|
||||||
top: 0;
|
display: flex;
|
||||||
left: 0;
|
align-items: center;
|
||||||
right: 0;
|
justify-content: space-between;
|
||||||
z-index: 10;
|
background: #2d2d2d;
|
||||||
opacity: 0;
|
border-bottom: 1px solid #3e3e3e;
|
||||||
transition: opacity 0.3s;
|
flex-shrink: 0;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal-page.fullscreen .terminal-session-tabs:hover {
|
.terminal-fullscreen-header-tabs {
|
||||||
opacity: 1;
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-fullscreen-header-tabs .terminal-session-tabs {
|
||||||
|
background: transparent;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-fullscreen-header-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
padding: 0 var(--space-3);
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-left: 1px solid #3e3e3e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-fullscreen-status {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #666;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-fullscreen-status.connecting {
|
||||||
|
background: #f5f543;
|
||||||
|
animation: pulse 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-fullscreen-status.connected {
|
||||||
|
background: #0dbc79;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-fullscreen-status.disconnected,
|
||||||
|
.terminal-fullscreen-status.error {
|
||||||
|
background: #cd3131;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mobile auto-hide header and tabs */
|
/* Mobile auto-hide header and tabs */
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
name: terminal-fullscreen-unified-header
|
||||||
|
status: in-progress
|
||||||
|
phase: apply
|
||||||
|
type: bugfix
|
||||||
|
description: Fix terminal fullscreen mode where session switcher overlays terminal controls. Combine both elements into a single unified header visible at all times.
|
||||||
|
created_at: 2026-05-28
|
||||||
|
updated_at: 2026-05-28
|
||||||
Reference in New Issue
Block a user