Merge branch 'fix/terminal-session-callbacks' into dev
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
@@ -10,336 +10,359 @@ import type { TerminalSession } from "../api/terminal";
|
||||
import type { ModifierKey } from "./use-special-keys";
|
||||
|
||||
const SESSIONS_TO_INFO = (sessions: TerminalSession[]): TerminalSessionInfo[] =>
|
||||
sessions.map((s) => ({
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
status: s.status as TerminalSessionInfo["status"],
|
||||
}));
|
||||
sessions.map((s) => ({
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
status: s.status as TerminalSessionInfo["status"],
|
||||
}));
|
||||
|
||||
type TerminalStatus =
|
||||
| "connecting"
|
||||
| "connected"
|
||||
| "disconnected"
|
||||
| "error"
|
||||
| "resetting";
|
||||
| "connecting"
|
||||
| "connected"
|
||||
| "disconnected"
|
||||
| "error"
|
||||
| "resetting";
|
||||
|
||||
export const useTerminalPage = () => {
|
||||
const { instanceId } = useParams<{ instanceId: string }>();
|
||||
const navigate = useNavigate();
|
||||
const isMobile = useMobileViewport();
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const terminalRefs = useRef<Record<string, React.RefObject<TerminalRef>>>({});
|
||||
const headerAutoHide = useAutoHide({ timeout: 3000, enabled: isMobile });
|
||||
const { instanceId } = useParams<{ instanceId: string }>();
|
||||
const navigate = useNavigate();
|
||||
const isMobile = useMobileViewport();
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const terminalRefs = useRef<Record<string, React.RefObject<TerminalRef>>>({});
|
||||
const headerAutoHide = useAutoHide({ timeout: 3000, enabled: isMobile });
|
||||
|
||||
const [terminalStatuses, setTerminalStatuses] = useState<
|
||||
Record<string, TerminalStatus>
|
||||
>({});
|
||||
const changeFontSizeRef = useRef<((delta: number) => void) | null>(null);
|
||||
const sendDataRef = useRef<((data: string) => void) | null>(null);
|
||||
const focusInputRef = useRef<(() => void) | null>(null);
|
||||
const [showResetConfirm, setShowResetConfirm] = useState(false);
|
||||
const [showSpecialKeysPanel, setShowSpecialKeysPanel] = useState(false);
|
||||
const [activeModifier, setActiveModifier] = useState<ModifierKey | null>(
|
||||
null,
|
||||
);
|
||||
const { isOpen: isKeyboardOpen, height: keyboardHeight } =
|
||||
useVirtualKeyboard();
|
||||
const [terminalStatuses, setTerminalStatuses] = useState<
|
||||
Record<string, TerminalStatus>
|
||||
>({});
|
||||
const changeFontSizeRefs = useRef<
|
||||
Record<string, ((delta: number) => void) | null>
|
||||
>({});
|
||||
const sendDataRefs = useRef<Record<string, ((data: string) => void) | null>>(
|
||||
{},
|
||||
);
|
||||
const focusInputRefs = useRef<Record<string, (() => void) | null>>({});
|
||||
const [showResetConfirm, setShowResetConfirm] = useState(false);
|
||||
const [showSpecialKeysPanel, setShowSpecialKeysPanel] = useState(false);
|
||||
const [activeModifier, setActiveModifier] = useState<ModifierKey | null>(
|
||||
null,
|
||||
);
|
||||
const { isOpen: isKeyboardOpen, height: keyboardHeight } =
|
||||
useVirtualKeyboard();
|
||||
|
||||
const [instanceInfo, setInstanceInfo] = useState<{
|
||||
display_name: string;
|
||||
workspace_name?: string | null;
|
||||
tool_type_name: string;
|
||||
} | null>(null);
|
||||
const [instanceInfo, setInstanceInfo] = useState<{
|
||||
display_name: string;
|
||||
workspace_name?: string | null;
|
||||
tool_type_name: string;
|
||||
} | null>(null);
|
||||
|
||||
const {
|
||||
sessions,
|
||||
activeSessionId,
|
||||
setActiveSessionId,
|
||||
createSession,
|
||||
closeSession,
|
||||
renameSession,
|
||||
resetSession,
|
||||
loading,
|
||||
error,
|
||||
} = useTerminalSessions(instanceId ?? "");
|
||||
const {
|
||||
sessions,
|
||||
activeSessionId,
|
||||
setActiveSessionId,
|
||||
createSession,
|
||||
closeSession,
|
||||
renameSession,
|
||||
resetSession,
|
||||
loading,
|
||||
error,
|
||||
} = useTerminalSessions(instanceId ?? "");
|
||||
|
||||
// Fetch instance details for tab title
|
||||
useEffect(() => {
|
||||
if (!instanceId) return;
|
||||
const load = async () => {
|
||||
try {
|
||||
const { getUserSessions } = await import("../api/sessions");
|
||||
const allSessions = await getUserSessions();
|
||||
const match = allSessions.find((s) => s.id === instanceId);
|
||||
if (match) {
|
||||
setInstanceInfo({
|
||||
display_name: match.display_name,
|
||||
workspace_name: match.workspace_name,
|
||||
tool_type_name: match.tool_type_name,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
void load();
|
||||
}, [instanceId]);
|
||||
// Fetch instance details for tab title
|
||||
useEffect(() => {
|
||||
if (!instanceId) return;
|
||||
const load = async () => {
|
||||
try {
|
||||
const { getUserSessions } = await import("../api/sessions");
|
||||
const allSessions = await getUserSessions();
|
||||
const match = allSessions.find((s) => s.id === instanceId);
|
||||
if (match) {
|
||||
setInstanceInfo({
|
||||
display_name: match.display_name,
|
||||
workspace_name: match.workspace_name,
|
||||
tool_type_name: match.tool_type_name,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
void load();
|
||||
}, [instanceId]);
|
||||
|
||||
// Auto-create default session
|
||||
useEffect(() => {
|
||||
if (!loading && sessions.length === 0 && !error && instanceId) {
|
||||
void createSession("Session 1");
|
||||
}
|
||||
}, [loading, sessions.length, error, instanceId, createSession]);
|
||||
// Auto-create default session
|
||||
useEffect(() => {
|
||||
if (!loading && sessions.length === 0 && !error && instanceId) {
|
||||
void createSession("Session 1");
|
||||
}
|
||||
}, [loading, sessions.length, error, instanceId, createSession]);
|
||||
|
||||
// Update document title based on active terminal session
|
||||
useEffect(() => {
|
||||
if (!instanceId) {
|
||||
document.title = "Terminal";
|
||||
return;
|
||||
}
|
||||
const active = sessions.find((s) => s.id === activeSessionId);
|
||||
const baseName = instanceInfo
|
||||
? `${instanceInfo.workspace_name ?? instanceInfo.display_name} · ${instanceInfo.tool_type_name}`
|
||||
: `Instance ${instanceId.slice(0, 8)}`;
|
||||
if (sessions.length <= 1) {
|
||||
document.title = baseName;
|
||||
} else {
|
||||
const sessionName = active?.name ?? "Session";
|
||||
document.title = `${baseName} ${sessionName}`;
|
||||
}
|
||||
return () => {
|
||||
document.title = "Headquarter";
|
||||
};
|
||||
}, [instanceId, activeSessionId, sessions, instanceInfo]);
|
||||
// Update document title based on active terminal session
|
||||
useEffect(() => {
|
||||
if (!instanceId) {
|
||||
document.title = "Terminal";
|
||||
return;
|
||||
}
|
||||
const active = sessions.find((s) => s.id === activeSessionId);
|
||||
const baseName = instanceInfo
|
||||
? `${instanceInfo.workspace_name ?? instanceInfo.display_name} · ${instanceInfo.tool_type_name}`
|
||||
: `Instance ${instanceId.slice(0, 8)}`;
|
||||
if (sessions.length <= 1) {
|
||||
document.title = baseName;
|
||||
} else {
|
||||
const sessionName = active?.name ?? "Session";
|
||||
document.title = `${baseName} ${sessionName}`;
|
||||
}
|
||||
return () => {
|
||||
document.title = "Headquarter";
|
||||
};
|
||||
}, [instanceId, activeSessionId, sessions, instanceInfo]);
|
||||
|
||||
// Sync refs with sessions
|
||||
useEffect(() => {
|
||||
for (const session of sessions) {
|
||||
if (!terminalRefs.current[session.id]) {
|
||||
terminalRefs.current[session.id] = React.createRef<TerminalRef>();
|
||||
}
|
||||
}
|
||||
const currentIds = new Set(sessions.map((s) => s.id));
|
||||
for (const id of Object.keys(terminalRefs.current)) {
|
||||
if (!currentIds.has(id)) {
|
||||
delete terminalRefs.current[id];
|
||||
}
|
||||
}
|
||||
}, [sessions]);
|
||||
// Sync refs with sessions
|
||||
useEffect(() => {
|
||||
for (const session of sessions) {
|
||||
if (!terminalRefs.current[session.id]) {
|
||||
terminalRefs.current[session.id] = React.createRef<TerminalRef>();
|
||||
}
|
||||
}
|
||||
const currentIds = new Set(sessions.map((s) => s.id));
|
||||
for (const id of Object.keys(terminalRefs.current)) {
|
||||
if (!currentIds.has(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]);
|
||||
|
||||
// Fit and focus active terminal
|
||||
useEffect(() => {
|
||||
if (activeSessionId && terminalRefs.current[activeSessionId]) {
|
||||
const ref = terminalRefs.current[activeSessionId];
|
||||
let raf1 = 0;
|
||||
let raf2 = 0;
|
||||
raf1 = requestAnimationFrame(() => {
|
||||
raf2 = requestAnimationFrame(() => {
|
||||
ref.current?.fit();
|
||||
ref.current?.focus();
|
||||
});
|
||||
});
|
||||
return () => {
|
||||
cancelAnimationFrame(raf1);
|
||||
cancelAnimationFrame(raf2);
|
||||
};
|
||||
}
|
||||
}, [activeSessionId]);
|
||||
// Fit and focus active terminal
|
||||
useEffect(() => {
|
||||
if (activeSessionId && terminalRefs.current[activeSessionId]) {
|
||||
const ref = terminalRefs.current[activeSessionId];
|
||||
let raf1 = 0;
|
||||
let raf2 = 0;
|
||||
raf1 = requestAnimationFrame(() => {
|
||||
raf2 = requestAnimationFrame(() => {
|
||||
ref.current?.fit();
|
||||
ref.current?.focus();
|
||||
});
|
||||
});
|
||||
return () => {
|
||||
cancelAnimationFrame(raf1);
|
||||
cancelAnimationFrame(raf2);
|
||||
};
|
||||
}
|
||||
}, [activeSessionId]);
|
||||
|
||||
// Keyboard shortcuts
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
const isAltShift = e.altKey && e.shiftKey && !e.ctrlKey && !e.metaKey;
|
||||
if (!isAltShift) return;
|
||||
// Keyboard shortcuts
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
const isAltShift = e.altKey && e.shiftKey && !e.ctrlKey && !e.metaKey;
|
||||
if (!isAltShift) return;
|
||||
|
||||
switch (e.key.toLowerCase()) {
|
||||
case "n":
|
||||
e.preventDefault();
|
||||
if (sessions.length < 5) {
|
||||
void createSession(`Session ${sessions.length + 1}`);
|
||||
}
|
||||
break;
|
||||
case "w":
|
||||
e.preventDefault();
|
||||
if (
|
||||
activeSessionId &&
|
||||
window.confirm("Close this terminal session?")
|
||||
) {
|
||||
void closeSession(activeSessionId);
|
||||
}
|
||||
break;
|
||||
case "arrowleft":
|
||||
e.preventDefault();
|
||||
if (activeSessionId) {
|
||||
const idx = sessions.findIndex((s) => s.id === activeSessionId);
|
||||
if (idx > 0) setActiveSessionId(sessions[idx - 1].id);
|
||||
}
|
||||
break;
|
||||
case "arrowright":
|
||||
e.preventDefault();
|
||||
if (activeSessionId) {
|
||||
const idx = sessions.findIndex((s) => s.id === activeSessionId);
|
||||
if (idx < sessions.length - 1)
|
||||
setActiveSessionId(sessions[idx + 1].id);
|
||||
}
|
||||
break;
|
||||
case "r":
|
||||
e.preventDefault();
|
||||
if (activeSessionId) void resetSession(activeSessionId);
|
||||
break;
|
||||
case "f":
|
||||
e.preventDefault();
|
||||
setIsFullscreen((prev) => !prev);
|
||||
break;
|
||||
}
|
||||
};
|
||||
switch (e.key.toLowerCase()) {
|
||||
case "n":
|
||||
e.preventDefault();
|
||||
if (sessions.length < 5) {
|
||||
void createSession(`Session ${sessions.length + 1}`);
|
||||
}
|
||||
break;
|
||||
case "w":
|
||||
e.preventDefault();
|
||||
if (
|
||||
activeSessionId &&
|
||||
window.confirm("Close this terminal session?")
|
||||
) {
|
||||
void closeSession(activeSessionId);
|
||||
}
|
||||
break;
|
||||
case "arrowleft":
|
||||
e.preventDefault();
|
||||
if (activeSessionId) {
|
||||
const idx = sessions.findIndex((s) => s.id === activeSessionId);
|
||||
if (idx > 0) setActiveSessionId(sessions[idx - 1].id);
|
||||
}
|
||||
break;
|
||||
case "arrowright":
|
||||
e.preventDefault();
|
||||
if (activeSessionId) {
|
||||
const idx = sessions.findIndex((s) => s.id === activeSessionId);
|
||||
if (idx < sessions.length - 1)
|
||||
setActiveSessionId(sessions[idx + 1].id);
|
||||
}
|
||||
break;
|
||||
case "r":
|
||||
e.preventDefault();
|
||||
if (activeSessionId) void resetSession(activeSessionId);
|
||||
break;
|
||||
case "f":
|
||||
e.preventDefault();
|
||||
setIsFullscreen((prev) => !prev);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [
|
||||
sessions,
|
||||
activeSessionId,
|
||||
createSession,
|
||||
closeSession,
|
||||
resetSession,
|
||||
setActiveSessionId,
|
||||
]);
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [
|
||||
sessions,
|
||||
activeSessionId,
|
||||
createSession,
|
||||
closeSession,
|
||||
resetSession,
|
||||
setActiveSessionId,
|
||||
]);
|
||||
|
||||
// Keep screen awake
|
||||
useEffect(() => {
|
||||
let wakeLock: WakeLockSentinel | null = null;
|
||||
const requestWakeLock = async () => {
|
||||
try {
|
||||
if ("wakeLock" in navigator) {
|
||||
wakeLock = await navigator.wakeLock.request("screen");
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
void requestWakeLock();
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.visibilityState === "visible") void requestWakeLock();
|
||||
};
|
||||
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||
return () => {
|
||||
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
||||
wakeLock?.release().catch(() => {});
|
||||
};
|
||||
}, []);
|
||||
// Keep screen awake
|
||||
useEffect(() => {
|
||||
let wakeLock: WakeLockSentinel | null = null;
|
||||
const requestWakeLock = async () => {
|
||||
try {
|
||||
if ("wakeLock" in navigator) {
|
||||
wakeLock = await navigator.wakeLock.request("screen");
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
void requestWakeLock();
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.visibilityState === "visible") void requestWakeLock();
|
||||
};
|
||||
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||
return () => {
|
||||
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
||||
wakeLock?.release().catch(() => {});
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Lock page scroll on mobile
|
||||
useEffect(() => {
|
||||
if (!isMobile) return;
|
||||
document.documentElement.classList.add("terminal-page-open");
|
||||
document.body.classList.add("terminal-page-open");
|
||||
return () => {
|
||||
document.documentElement.classList.remove("terminal-page-open");
|
||||
document.body.classList.remove("terminal-page-open");
|
||||
};
|
||||
}, [isMobile]);
|
||||
// Lock page scroll on mobile
|
||||
useEffect(() => {
|
||||
if (!isMobile) return;
|
||||
document.documentElement.classList.add("terminal-page-open");
|
||||
document.body.classList.add("terminal-page-open");
|
||||
return () => {
|
||||
document.documentElement.classList.remove("terminal-page-open");
|
||||
document.body.classList.remove("terminal-page-open");
|
||||
};
|
||||
}, [isMobile]);
|
||||
|
||||
const handleFullscreenClick = useCallback(
|
||||
(e: React.MouseEvent<HTMLElement>) => {
|
||||
if (!isFullscreen) return;
|
||||
const target = e.target as Node;
|
||||
const current = e.currentTarget as HTMLElement;
|
||||
const content = current.querySelector(".terminal-page-content");
|
||||
const header = current.querySelector(".terminal-fullscreen-header");
|
||||
if (content?.contains(target) || header?.contains(target)) return;
|
||||
setIsFullscreen(false);
|
||||
},
|
||||
[isFullscreen],
|
||||
);
|
||||
const handleFullscreenClick = useCallback(
|
||||
(e: React.MouseEvent<HTMLElement>) => {
|
||||
if (!isFullscreen) return;
|
||||
const target = e.target as Node;
|
||||
const current = e.currentTarget as HTMLElement;
|
||||
const content = current.querySelector(".terminal-page-content");
|
||||
const header = current.querySelector(".terminal-fullscreen-header");
|
||||
if (content?.contains(target) || header?.contains(target)) return;
|
||||
setIsFullscreen(false);
|
||||
},
|
||||
[isFullscreen],
|
||||
);
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(sessionId: string) => setActiveSessionId(sessionId),
|
||||
[setActiveSessionId],
|
||||
);
|
||||
const handleSelect = useCallback(
|
||||
(sessionId: string) => setActiveSessionId(sessionId),
|
||||
[setActiveSessionId],
|
||||
);
|
||||
|
||||
const handleClose = useCallback(
|
||||
async (sessionId: string) => closeSession(sessionId),
|
||||
[closeSession],
|
||||
);
|
||||
const handleClose = useCallback(
|
||||
async (sessionId: string) => closeSession(sessionId),
|
||||
[closeSession],
|
||||
);
|
||||
|
||||
const handleCreate = useCallback(() => {
|
||||
void createSession(`Session ${sessions.length + 1}`);
|
||||
}, [createSession, sessions.length]);
|
||||
const handleCreate = useCallback(() => {
|
||||
void createSession(`Session ${sessions.length + 1}`);
|
||||
}, [createSession, sessions.length]);
|
||||
|
||||
const handleRename = useCallback(
|
||||
(sessionId: string, newName: string) => {
|
||||
void renameSession(sessionId, newName);
|
||||
},
|
||||
[renameSession],
|
||||
);
|
||||
const handleRename = useCallback(
|
||||
(sessionId: string, newName: string) => {
|
||||
void renameSession(sessionId, newName);
|
||||
},
|
||||
[renameSession],
|
||||
);
|
||||
|
||||
const handleTerminalReady = useCallback(
|
||||
(
|
||||
sendData: (data: string) => void,
|
||||
status: TerminalStatus,
|
||||
focusInput: () => void,
|
||||
changeFontSize: (delta: number) => void,
|
||||
) => {
|
||||
setTerminalStatuses((prev) => ({
|
||||
...prev,
|
||||
[activeSessionId ?? "default"]: status,
|
||||
}));
|
||||
sendDataRef.current = sendData;
|
||||
focusInputRef.current = focusInput;
|
||||
changeFontSizeRef.current = changeFontSize;
|
||||
},
|
||||
[activeSessionId],
|
||||
);
|
||||
const handleTerminalReady = useCallback(
|
||||
(
|
||||
sessionId: string | undefined,
|
||||
sendData: (data: string) => void,
|
||||
status: TerminalStatus,
|
||||
focusInput: () => void,
|
||||
changeFontSize: (delta: number) => void,
|
||||
) => {
|
||||
const key = sessionId ?? "default";
|
||||
setTerminalStatuses((prev) => ({
|
||||
...prev,
|
||||
[key]: status,
|
||||
}));
|
||||
sendDataRefs.current[key] = sendData;
|
||||
focusInputRefs.current[key] = focusInput;
|
||||
changeFontSizeRefs.current[key] = changeFontSize;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleFontSizeChange = useCallback((delta: number) => {
|
||||
changeFontSizeRef.current?.(delta);
|
||||
}, []);
|
||||
const handleFontSizeChange = useCallback(
|
||||
(delta: number) => {
|
||||
const key = activeSessionId ?? "default";
|
||||
changeFontSizeRefs.current[key]?.(delta);
|
||||
},
|
||||
[activeSessionId],
|
||||
);
|
||||
|
||||
const handleSendKey = useCallback((data: string) => {
|
||||
sendDataRef.current?.(data);
|
||||
}, []);
|
||||
const handleSendKey = useCallback(
|
||||
(data: string) => {
|
||||
const key = activeSessionId ?? "default";
|
||||
sendDataRefs.current[key]?.(data);
|
||||
},
|
||||
[activeSessionId],
|
||||
);
|
||||
|
||||
const handleReset = useCallback(() => {
|
||||
if (activeSessionId && terminalRefs.current[activeSessionId]) {
|
||||
terminalRefs.current[activeSessionId].current?.reset();
|
||||
}
|
||||
}, [activeSessionId]);
|
||||
const handleReset = useCallback(() => {
|
||||
if (activeSessionId && terminalRefs.current[activeSessionId]) {
|
||||
terminalRefs.current[activeSessionId].current?.reset();
|
||||
}
|
||||
}, [activeSessionId]);
|
||||
|
||||
return {
|
||||
instanceId,
|
||||
navigate,
|
||||
isMobile,
|
||||
isFullscreen,
|
||||
setIsFullscreen,
|
||||
terminalRefs,
|
||||
headerAutoHide,
|
||||
terminalStatuses,
|
||||
sendDataRef,
|
||||
focusInputRef,
|
||||
changeFontSizeRef,
|
||||
showResetConfirm,
|
||||
setShowResetConfirm,
|
||||
showSpecialKeysPanel,
|
||||
setShowSpecialKeysPanel,
|
||||
activeModifier,
|
||||
setActiveModifier,
|
||||
isKeyboardOpen,
|
||||
keyboardHeight,
|
||||
sessions,
|
||||
activeSessionId,
|
||||
setActiveSessionId,
|
||||
loading,
|
||||
error,
|
||||
handleFullscreenClick,
|
||||
handleSelect,
|
||||
handleClose,
|
||||
handleCreate,
|
||||
handleRename,
|
||||
handleTerminalReady,
|
||||
handleFontSizeChange,
|
||||
handleSendKey,
|
||||
handleReset,
|
||||
sessionInfos: SESSIONS_TO_INFO(sessions),
|
||||
};
|
||||
return {
|
||||
instanceId,
|
||||
navigate,
|
||||
isMobile,
|
||||
isFullscreen,
|
||||
setIsFullscreen,
|
||||
terminalRefs,
|
||||
headerAutoHide,
|
||||
terminalStatuses,
|
||||
showResetConfirm,
|
||||
setShowResetConfirm,
|
||||
showSpecialKeysPanel,
|
||||
setShowSpecialKeysPanel,
|
||||
activeModifier,
|
||||
setActiveModifier,
|
||||
isKeyboardOpen,
|
||||
keyboardHeight,
|
||||
sessions,
|
||||
activeSessionId,
|
||||
setActiveSessionId,
|
||||
loading,
|
||||
error,
|
||||
handleFullscreenClick,
|
||||
handleSelect,
|
||||
handleClose,
|
||||
handleCreate,
|
||||
handleRename,
|
||||
handleTerminalReady,
|
||||
handleFontSizeChange,
|
||||
handleSendKey,
|
||||
handleReset,
|
||||
sessionInfos: SESSIONS_TO_INFO(sessions),
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user