fix: route terminal callbacks and status per session
The recent redraw fix keeps all xterm instances mounted (display:none) when switching sessions. However, sendData/focus/font-size refs and the header connection status were still stored globally, so the last-mounted hidden session could own keyboard input, font-size buttons, and the status dot for the active session. - Pass sessionId to onTerminalReady from TerminalComponent. - Store terminal callbacks and status keyed by sessionId in use-terminal-page. - Use activeSessionId to route special-key input, font-size changes, and header status. - Clean up per-session refs and status when sessions are closed. - Update MobileTerminalWrapper signature for the new callback shape. Quality gates: npm run typecheck, npm run lint, npm test (87 passed)
This commit is contained in:
@@ -27,10 +27,17 @@ export const MobileTerminalWrapper: React.FC<MobileTerminalWrapperProps> = ({
|
||||
const { isOpen: isKeyboardOpen, height: keyboardHeight } =
|
||||
useVirtualKeyboard();
|
||||
const [showPanel, setShowPanel] = useState(false);
|
||||
const [activeModifier, setActiveModifier] = useState<ModifierKey | null>(null);
|
||||
const [activeModifier, setActiveModifier] = useState<ModifierKey | null>(
|
||||
null,
|
||||
);
|
||||
const [terminalRef, setTerminalRef] = useState<{
|
||||
sendData: (data: string) => void;
|
||||
connectionStatus: "connecting" | "connected" | "disconnected" | "error" | "resetting";
|
||||
connectionStatus:
|
||||
| "connecting"
|
||||
| "connected"
|
||||
| "disconnected"
|
||||
| "error"
|
||||
| "resetting";
|
||||
focusInput: () => void;
|
||||
changeFontSize: (delta: number) => void;
|
||||
} | null>(null);
|
||||
@@ -42,17 +49,33 @@ export const MobileTerminalWrapper: React.FC<MobileTerminalWrapperProps> = ({
|
||||
}, [headerAutoHide]);
|
||||
|
||||
const handleTerminalReady = useCallback(
|
||||
(sendData: (data: string) => void, connectionStatus: "connecting" | "connected" | "disconnected" | "error" | "resetting", focusInput: () => void, changeFontSize: (delta: number) => void) => {
|
||||
setTerminalRef({ sendData, connectionStatus, focusInput, changeFontSize });
|
||||
(
|
||||
_sessionId: string | undefined,
|
||||
sendData: (data: string) => void,
|
||||
connectionStatus:
|
||||
| "connecting"
|
||||
| "connected"
|
||||
| "disconnected"
|
||||
| "error"
|
||||
| "resetting",
|
||||
focusInput: () => void,
|
||||
changeFontSize: (delta: number) => void,
|
||||
) => {
|
||||
setTerminalRef({
|
||||
sendData,
|
||||
connectionStatus,
|
||||
focusInput,
|
||||
changeFontSize,
|
||||
});
|
||||
},
|
||||
[]
|
||||
[],
|
||||
);
|
||||
|
||||
const handleSendKey = useCallback(
|
||||
(data: string) => {
|
||||
terminalRef?.sendData(data);
|
||||
},
|
||||
[terminalRef]
|
||||
[terminalRef],
|
||||
);
|
||||
|
||||
if (!isMobile) {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import React from "react";
|
||||
import { TerminalComponent, type TerminalRef } from "./terminal";
|
||||
import { TerminalSessionTabs, type TerminalSessionInfo } from "./terminal-session-tabs";
|
||||
import {
|
||||
TerminalSessionTabs,
|
||||
type TerminalSessionInfo,
|
||||
} from "./terminal-session-tabs";
|
||||
import type { TerminalSession } from "../../../api/terminal";
|
||||
|
||||
interface Props {
|
||||
@@ -8,7 +11,9 @@ interface Props {
|
||||
sessions: TerminalSession[];
|
||||
sessionInfos: TerminalSessionInfo[];
|
||||
activeSessionId: string;
|
||||
terminalRefs: React.MutableRefObject<Record<string, React.RefObject<TerminalRef>>>;
|
||||
terminalRefs: React.MutableRefObject<
|
||||
Record<string, React.RefObject<TerminalRef>>
|
||||
>;
|
||||
isFullscreen: boolean;
|
||||
status: string;
|
||||
error: string | null;
|
||||
@@ -26,6 +31,7 @@ interface Props {
|
||||
onHideResetConfirm: () => void;
|
||||
onReset: () => void;
|
||||
onTerminalReady: (
|
||||
sessionId: string | undefined,
|
||||
sendData: (data: string) => void,
|
||||
status: "connecting" | "connected" | "disconnected" | "error" | "resetting",
|
||||
focusInput: () => void,
|
||||
@@ -64,7 +70,11 @@ export const DesktopTerminalView: React.FC<Props> = ({
|
||||
>
|
||||
{!isFullscreen && (
|
||||
<div className="terminal-page-header">
|
||||
<button className="secondary-button" onClick={onNavigateBack} type="button">
|
||||
<button
|
||||
className="secondary-button"
|
||||
onClick={onNavigateBack}
|
||||
type="button"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
<h1>Terminal</h1>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import React from "react";
|
||||
import { TerminalComponent, type TerminalRef } from "./terminal";
|
||||
import { TerminalSessionTabs, type TerminalSessionInfo } from "./terminal-session-tabs";
|
||||
import {
|
||||
TerminalSessionTabs,
|
||||
type TerminalSessionInfo,
|
||||
} from "./terminal-session-tabs";
|
||||
import { Icon } from "../../icon";
|
||||
import { SpecialKeysStrip } from "./special-keys-strip";
|
||||
import { SpecialKeysPanel } from "./special-keys-panel";
|
||||
@@ -12,7 +15,9 @@ interface Props {
|
||||
sessions: TerminalSession[];
|
||||
sessionInfos: TerminalSessionInfo[];
|
||||
activeSessionId: string;
|
||||
terminalRefs: React.MutableRefObject<Record<string, React.RefObject<TerminalRef>>>;
|
||||
terminalRefs: React.MutableRefObject<
|
||||
Record<string, React.RefObject<TerminalRef>>
|
||||
>;
|
||||
status: string;
|
||||
error: string | null;
|
||||
loading: boolean;
|
||||
@@ -29,6 +34,7 @@ interface Props {
|
||||
onCreate: () => void;
|
||||
onRename: (id: string, name: string) => void;
|
||||
onTerminalReady: (
|
||||
sessionId: string | undefined,
|
||||
sendData: (data: string) => void,
|
||||
status: "connecting" | "connected" | "disconnected" | "error" | "resetting",
|
||||
focusInput: () => void,
|
||||
@@ -73,25 +79,53 @@ export const MobileTerminalView: React.FC<Props> = ({
|
||||
|
||||
return (
|
||||
<section className="terminal-page mobile">
|
||||
<div className={`mobile-terminal-overlay ${isVisible ? "visible" : "hidden"}`} onClick={(e) => e.stopPropagation()}>
|
||||
<div
|
||||
className={`mobile-terminal-overlay ${isVisible ? "visible" : "hidden"}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="mobile-terminal-toolbar">
|
||||
<div className="mobile-terminal-toolbar-left">
|
||||
<button className="mobile-terminal-toolbtn" onClick={onNavigateBack} type="button" aria-label="Back">
|
||||
<button
|
||||
className="mobile-terminal-toolbtn"
|
||||
onClick={onNavigateBack}
|
||||
type="button"
|
||||
aria-label="Back"
|
||||
>
|
||||
<Icon name="arrow-left" size="sm" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="mobile-terminal-toolbar-center">
|
||||
<span className="mobile-terminal-title">{activeSession?.name || "Terminal"}</span>
|
||||
<span className={`mobile-terminal-status status-dot ${status}`} aria-label={`Connection status: ${status}`} />
|
||||
<span className="mobile-terminal-title">
|
||||
{activeSession?.name || "Terminal"}
|
||||
</span>
|
||||
<span
|
||||
className={`mobile-terminal-status status-dot ${status}`}
|
||||
aria-label={`Connection status: ${status}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="mobile-terminal-toolbar-right">
|
||||
<button className="mobile-terminal-toolbtn" onClick={() => onFontSizeChange(-1)} type="button" aria-label="Decrease font size">
|
||||
<button
|
||||
className="mobile-terminal-toolbtn"
|
||||
onClick={() => onFontSizeChange(-1)}
|
||||
type="button"
|
||||
aria-label="Decrease font size"
|
||||
>
|
||||
<span style={{ fontSize: "0.75rem" }}>A-</span>
|
||||
</button>
|
||||
<button className="mobile-terminal-toolbtn" onClick={() => onFontSizeChange(1)} type="button" aria-label="Increase font size">
|
||||
<button
|
||||
className="mobile-terminal-toolbtn"
|
||||
onClick={() => onFontSizeChange(1)}
|
||||
type="button"
|
||||
aria-label="Increase font size"
|
||||
>
|
||||
<span style={{ fontSize: "1rem" }}>A+</span>
|
||||
</button>
|
||||
<button className="mobile-terminal-toolbtn" onClick={onNavigateBack} type="button" aria-label="Exit terminal">
|
||||
<button
|
||||
className="mobile-terminal-toolbtn"
|
||||
onClick={onNavigateBack}
|
||||
type="button"
|
||||
aria-label="Exit terminal"
|
||||
>
|
||||
<Icon name="close" size="sm" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user