Merge branch 'fix/terminal-session-callbacks' into dev

This commit is contained in:
Developer
2026-06-15 13:53:15 +00:00
5 changed files with 1216 additions and 1118 deletions
@@ -27,10 +27,17 @@ export const MobileTerminalWrapper: React.FC<MobileTerminalWrapperProps> = ({
const { isOpen: isKeyboardOpen, height: keyboardHeight } = const { isOpen: isKeyboardOpen, height: keyboardHeight } =
useVirtualKeyboard(); useVirtualKeyboard();
const [showPanel, setShowPanel] = useState(false); const [showPanel, setShowPanel] = useState(false);
const [activeModifier, setActiveModifier] = useState<ModifierKey | null>(null); const [activeModifier, setActiveModifier] = useState<ModifierKey | null>(
null,
);
const [terminalRef, setTerminalRef] = useState<{ const [terminalRef, setTerminalRef] = useState<{
sendData: (data: string) => void; sendData: (data: string) => void;
connectionStatus: "connecting" | "connected" | "disconnected" | "error" | "resetting"; connectionStatus:
| "connecting"
| "connected"
| "disconnected"
| "error"
| "resetting";
focusInput: () => void; focusInput: () => void;
changeFontSize: (delta: number) => void; changeFontSize: (delta: number) => void;
} | null>(null); } | null>(null);
@@ -42,17 +49,33 @@ export const MobileTerminalWrapper: React.FC<MobileTerminalWrapperProps> = ({
}, [headerAutoHide]); }, [headerAutoHide]);
const handleTerminalReady = useCallback( 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( const handleSendKey = useCallback(
(data: string) => { (data: string) => {
terminalRef?.sendData(data); terminalRef?.sendData(data);
}, },
[terminalRef] [terminalRef],
); );
if (!isMobile) { if (!isMobile) {
@@ -1,6 +1,9 @@
import React from "react"; import React from "react";
import { TerminalComponent, type TerminalRef } from "./terminal"; 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"; import type { TerminalSession } from "../../../api/terminal";
interface Props { interface Props {
@@ -8,7 +11,9 @@ interface Props {
sessions: TerminalSession[]; sessions: TerminalSession[];
sessionInfos: TerminalSessionInfo[]; sessionInfos: TerminalSessionInfo[];
activeSessionId: string; activeSessionId: string;
terminalRefs: React.MutableRefObject<Record<string, React.RefObject<TerminalRef>>>; terminalRefs: React.MutableRefObject<
Record<string, React.RefObject<TerminalRef>>
>;
isFullscreen: boolean; isFullscreen: boolean;
status: string; status: string;
error: string | null; error: string | null;
@@ -26,6 +31,7 @@ interface Props {
onHideResetConfirm: () => void; onHideResetConfirm: () => void;
onReset: () => void; onReset: () => void;
onTerminalReady: ( onTerminalReady: (
sessionId: string | undefined,
sendData: (data: string) => void, sendData: (data: string) => void,
status: "connecting" | "connected" | "disconnected" | "error" | "resetting", status: "connecting" | "connected" | "disconnected" | "error" | "resetting",
focusInput: () => void, focusInput: () => void,
@@ -64,7 +70,11 @@ export const DesktopTerminalView: React.FC<Props> = ({
> >
{!isFullscreen && ( {!isFullscreen && (
<div className="terminal-page-header"> <div className="terminal-page-header">
<button className="secondary-button" onClick={onNavigateBack} type="button"> <button
className="secondary-button"
onClick={onNavigateBack}
type="button"
>
Back Back
</button> </button>
<h1>Terminal</h1> <h1>Terminal</h1>
@@ -1,6 +1,9 @@
import React from "react"; import React from "react";
import { TerminalComponent, type TerminalRef } from "./terminal"; 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 { Icon } from "../../icon";
import { SpecialKeysStrip } from "./special-keys-strip"; import { SpecialKeysStrip } from "./special-keys-strip";
import { SpecialKeysPanel } from "./special-keys-panel"; import { SpecialKeysPanel } from "./special-keys-panel";
@@ -12,7 +15,9 @@ interface Props {
sessions: TerminalSession[]; sessions: TerminalSession[];
sessionInfos: TerminalSessionInfo[]; sessionInfos: TerminalSessionInfo[];
activeSessionId: string; activeSessionId: string;
terminalRefs: React.MutableRefObject<Record<string, React.RefObject<TerminalRef>>>; terminalRefs: React.MutableRefObject<
Record<string, React.RefObject<TerminalRef>>
>;
status: string; status: string;
error: string | null; error: string | null;
loading: boolean; loading: boolean;
@@ -29,6 +34,7 @@ interface Props {
onCreate: () => void; onCreate: () => void;
onRename: (id: string, name: string) => void; onRename: (id: string, name: string) => void;
onTerminalReady: ( onTerminalReady: (
sessionId: string | undefined,
sendData: (data: string) => void, sendData: (data: string) => void,
status: "connecting" | "connected" | "disconnected" | "error" | "resetting", status: "connecting" | "connected" | "disconnected" | "error" | "resetting",
focusInput: () => void, focusInput: () => void,
@@ -73,25 +79,53 @@ export const MobileTerminalView: React.FC<Props> = ({
return ( return (
<section className="terminal-page mobile"> <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">
<div className="mobile-terminal-toolbar-left"> <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" /> <Icon name="arrow-left" size="sm" />
</button> </button>
</div> </div>
<div className="mobile-terminal-toolbar-center"> <div className="mobile-terminal-toolbar-center">
<span className="mobile-terminal-title">{activeSession?.name || "Terminal"}</span> <span className="mobile-terminal-title">
<span className={`mobile-terminal-status status-dot ${status}`} aria-label={`Connection status: ${status}`} /> {activeSession?.name || "Terminal"}
</span>
<span
className={`mobile-terminal-status status-dot ${status}`}
aria-label={`Connection status: ${status}`}
/>
</div> </div>
<div className="mobile-terminal-toolbar-right"> <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> <span style={{ fontSize: "0.75rem" }}>A-</span>
</button> </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> <span style={{ fontSize: "1rem" }}>A+</span>
</button> </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" /> <Icon name="close" size="sm" />
</button> </button>
</div> </div>
@@ -24,6 +24,7 @@ export interface TerminalProps {
activeModifier?: ModifierKey | null; activeModifier?: ModifierKey | null;
onModifierChange?: (modifier: ModifierKey | null) => void; onModifierChange?: (modifier: ModifierKey | null) => void;
onTerminalReady?: ( onTerminalReady?: (
sessionId: string | undefined,
sendData: (data: string) => void, sendData: (data: string) => void,
connectionStatus: connectionStatus:
| "connecting" | "connecting"
@@ -563,6 +564,7 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
handleFontSizeChangeRef.current(delta); handleFontSizeChangeRef.current(delta);
}; };
onTerminalReadyRef.current( onTerminalReadyRef.current(
sessionId,
sendData, sendData,
status, status,
focusInput, focusInput,
@@ -657,9 +659,15 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
const changeFontSize = (delta: number) => { const changeFontSize = (delta: number) => {
handleFontSizeChangeRef.current(delta); handleFontSizeChangeRef.current(delta);
}; };
onTerminalReady(sendData, status, focusInput, changeFontSize); onTerminalReady(
sessionId,
sendData,
status,
focusInput,
changeFontSize,
);
} }
}, [status, onTerminalReady]); }, [sessionId, status, onTerminalReady]);
const handleFontSizeChange = (delta: number) => { const handleFontSizeChange = (delta: number) => {
const newSize = Math.max( const newSize = Math.max(
+40 -17
View File
@@ -34,9 +34,13 @@ export const useTerminalPage = () => {
const [terminalStatuses, setTerminalStatuses] = useState< const [terminalStatuses, setTerminalStatuses] = useState<
Record<string, TerminalStatus> Record<string, TerminalStatus>
>({}); >({});
const changeFontSizeRef = useRef<((delta: number) => void) | null>(null); const changeFontSizeRefs = useRef<
const sendDataRef = useRef<((data: string) => void) | null>(null); Record<string, ((delta: number) => void) | null>
const focusInputRef = useRef<(() => void) | null>(null); >({});
const sendDataRefs = useRef<Record<string, ((data: string) => void) | null>>(
{},
);
const focusInputRefs = useRef<Record<string, (() => void) | null>>({});
const [showResetConfirm, setShowResetConfirm] = useState(false); const [showResetConfirm, setShowResetConfirm] = useState(false);
const [showSpecialKeysPanel, setShowSpecialKeysPanel] = useState(false); const [showSpecialKeysPanel, setShowSpecialKeysPanel] = useState(false);
const [activeModifier, setActiveModifier] = useState<ModifierKey | null>( const [activeModifier, setActiveModifier] = useState<ModifierKey | null>(
@@ -124,8 +128,20 @@ export const useTerminalPage = () => {
for (const id of Object.keys(terminalRefs.current)) { for (const id of Object.keys(terminalRefs.current)) {
if (!currentIds.has(id)) { if (!currentIds.has(id)) {
delete terminalRefs.current[id]; delete terminalRefs.current[id];
delete sendDataRefs.current[id];
delete focusInputRefs.current[id];
delete changeFontSizeRefs.current[id];
} }
} }
setTerminalStatuses((prev) => {
const next: Record<string, TerminalStatus> = {};
for (const id of currentIds) {
if (prev[id]) {
next[id] = prev[id];
}
}
return next;
});
}, [sessions]); }, [sessions]);
// Fit and focus active terminal // Fit and focus active terminal
@@ -276,29 +292,39 @@ export const useTerminalPage = () => {
const handleTerminalReady = useCallback( const handleTerminalReady = useCallback(
( (
sessionId: string | undefined,
sendData: (data: string) => void, sendData: (data: string) => void,
status: TerminalStatus, status: TerminalStatus,
focusInput: () => void, focusInput: () => void,
changeFontSize: (delta: number) => void, changeFontSize: (delta: number) => void,
) => { ) => {
const key = sessionId ?? "default";
setTerminalStatuses((prev) => ({ setTerminalStatuses((prev) => ({
...prev, ...prev,
[activeSessionId ?? "default"]: status, [key]: status,
})); }));
sendDataRef.current = sendData; sendDataRefs.current[key] = sendData;
focusInputRef.current = focusInput; focusInputRefs.current[key] = focusInput;
changeFontSizeRef.current = changeFontSize; changeFontSizeRefs.current[key] = changeFontSize;
},
[],
);
const handleFontSizeChange = useCallback(
(delta: number) => {
const key = activeSessionId ?? "default";
changeFontSizeRefs.current[key]?.(delta);
}, },
[activeSessionId], [activeSessionId],
); );
const handleFontSizeChange = useCallback((delta: number) => { const handleSendKey = useCallback(
changeFontSizeRef.current?.(delta); (data: string) => {
}, []); const key = activeSessionId ?? "default";
sendDataRefs.current[key]?.(data);
const handleSendKey = useCallback((data: string) => { },
sendDataRef.current?.(data); [activeSessionId],
}, []); );
const handleReset = useCallback(() => { const handleReset = useCallback(() => {
if (activeSessionId && terminalRefs.current[activeSessionId]) { if (activeSessionId && terminalRefs.current[activeSessionId]) {
@@ -315,9 +341,6 @@ export const useTerminalPage = () => {
terminalRefs, terminalRefs,
headerAutoHide, headerAutoHide,
terminalStatuses, terminalStatuses,
sendDataRef,
focusInputRef,
changeFontSizeRef,
showResetConfirm, showResetConfirm,
setShowResetConfirm, setShowResetConfirm,
showSpecialKeysPanel, showSpecialKeysPanel,