refactor terminal page into renderer, catalog, and session hooks
release / build (push) Successful in 4m16s

also bump beta cuz why not
This commit is contained in:
lda committed 2026-10-01 14:26:50 +07:00 Verified
1 parent 37656c88ca
commit 5d77b9aeb2
7 files changed
+1070 -819

No files matched your search

+434
View File
@@ -0,0 +1,434 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { toast } from "sonner";
import {
APIError,
attachTerminal,
closeTerminal,
createTerminal,
type TerminalSession,
} from "@/api";
import {
NO_TERMINAL_MODIFIERS,
applyTerminalModifiers,
orderTerminalSessions,
restoreCandidates,
type TerminalConnectionState,
type TerminalModifiers,
} from "@/pages/terminal/sessionUtils";
import type { TerminalCatalog } from "@/pages/terminal/useTerminalSessions";
import type { TerminalPort } from "@/pages/terminal/useXterm";
const REMEMBERED_TERMINAL_KEY = "wakey.active-terminal-id";
const TERMINAL_OPERATOR_KEY = "wakey.terminal-operator-id";
// Fallback for browsers that do not implement crypto.randomUUID().
const randomId = () =>
(String(1e7) + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c: string) => {
const num = Number(c);
return (
num ^
(window.crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (num / 4)))
).toString(16);
});
function terminalOperatorId(): string {
const remembered = window.sessionStorage.getItem(TERMINAL_OPERATOR_KEY);
if (remembered) return remembered;
const created = window.crypto.randomUUID?.() ?? randomId();
window.sessionStorage.setItem(TERMINAL_OPERATOR_KEY, created);
return created;
}
function websocketUrl(path: string): string {
const scheme = window.location.protocol === "https:" ? "wss:" : "ws:";
return `${scheme}//${window.location.host}${path}`;
}
type Options = {
terminal: TerminalPort;
catalog: TerminalCatalog;
selectedAgentId: string;
};
export function useTerminalSession({
terminal,
catalog,
selectedAgentId,
}: Options) {
const socketRef = useRef<WebSocket | null>(null);
const activeSessionRef = useRef<TerminalSession | null>(null);
const mountedRef = useRef(false);
const selectedAgentIdRef = useRef(selectedAgentId);
const catalogRef = useRef(catalog);
const [session, setSession] = useState<TerminalSession | null>(null);
const [connection, setConnection] = useState<TerminalConnectionState>("idle");
const [modifiers, setModifiers] = useState<TerminalModifiers>(
NO_TERMINAL_MODIFIERS,
);
const [operatorId] = useState(terminalOperatorId);
selectedAgentIdRef.current = selectedAgentId;
catalogRef.current = catalog;
activeSessionRef.current = session;
const detachTransport = useCallback(() => {
const socket = socketRef.current;
socketRef.current = null;
socket?.close();
}, []);
const writeTerminalData = useCallback(
(data: string, consumeModifiers = false) => {
const socket = socketRef.current;
if (socket?.readyState !== WebSocket.OPEN) return;
const output = consumeModifiers
? applyTerminalModifiers(data, modifiersRef.current)
: data;
socket.send(new TextEncoder().encode(output));
if (
consumeModifiers &&
(modifiersRef.current.ctrl || modifiersRef.current.meta)
) {
modifiersRef.current = NO_TERMINAL_MODIFIERS;
setModifiers(NO_TERMINAL_MODIFIERS);
}
},
[],
);
const modifiersRef = useRef(modifiers);
modifiersRef.current = modifiers;
const connect = useCallback(
(nextSession: TerminalSession) => {
if (!nextSession.attachment_token) {
throw new Error(
"Control plane did not issue a terminal attachment token",
);
}
detachTransport();
setSession(nextSession);
activeSessionRef.current = nextSession;
terminal.setActiveTerminalId(nextSession.terminal_id);
catalogRef.current.merge({ ...nextSession, operator_attached: true });
window.sessionStorage.setItem(
REMEMBERED_TERMINAL_KEY,
nextSession.terminal_id,
);
setConnection("connecting");
const socket = new WebSocket(websocketUrl(nextSession.websocket_url));
socket.binaryType = "arraybuffer";
socketRef.current = socket;
socket.onopen = () => {
if (socketRef.current !== socket) return;
socket.send(
JSON.stringify({
type: "attach",
attachment_token: nextSession.attachment_token,
operator_id: operatorId,
}),
);
terminal.fit();
terminal.sendResize((message) => socket.send(message));
};
socket.onmessage = (event) => {
if (socketRef.current !== socket) return;
if (typeof event.data !== "string") {
terminal.write(new Uint8Array(event.data as ArrayBuffer));
return;
}
try {
const control = JSON.parse(event.data) as {
type: string;
exit_code?: number | null;
message?: string;
};
if (control.type === "ready") {
setConnection("ready");
void catalogRef.current.refresh().catch(() => undefined);
window.requestAnimationFrame(() => {
terminal.fit();
terminal.sendResize((message) => socket.send(message), true);
terminal.focus();
});
} else if (control.type === "exited") {
setConnection("exited");
terminal.writeLine(
`\r\n[process exited${control.exit_code == null ? "" : ` ${control.exit_code}`}]`,
);
} else if (control.type === "error") {
setConnection("exited");
terminal.writeLine(
`\r\n[terminal error: ${control.message ?? "unknown error"}]`,
);
}
} catch {
terminal.writeLine("\r\n[invalid terminal control frame]");
}
};
socket.onerror = () => {
if (socketRef.current !== socket) return;
terminal.writeLine("\r\n[terminal transport error]");
};
socket.onclose = () => {
if (socketRef.current !== socket) return;
socketRef.current = null;
catalogRef.current.markDetached(nextSession.terminal_id);
setConnection((current) =>
current === "exited" ? current : "disconnected",
);
void catalogRef.current.refresh().catch(() => undefined);
};
},
[detachTransport, operatorId, terminal],
);
const activate = useCallback(
async (nextSession: TerminalSession) => {
if (nextSession.terminal_id === activeSessionRef.current?.terminal_id)
return;
if (nextSession.operator_attached) return;
const previousSession = activeSessionRef.current;
detachTransport();
if (previousSession)
catalogRef.current.markDetached(previousSession.terminal_id);
setSession(null);
activeSessionRef.current = null;
terminal.setActiveTerminalId(undefined);
setConnection("connecting");
terminal.reset();
window.sessionStorage.setItem(
REMEMBERED_TERMINAL_KEY,
nextSession.terminal_id,
);
try {
const attached = await attachTerminal(
nextSession.terminal_id,
operatorId,
);
if (!mountedRef.current) return;
connect(attached);
} catch (error) {
if (!mountedRef.current) return;
setConnection("idle");
toast.error("Could not attach terminal session", {
description: String(error),
});
void catalogRef.current.refresh().catch(() => undefined);
}
},
[connect, detachTransport, operatorId, terminal],
);
const start = useCallback(async () => {
const dimensions = terminal.dimensions();
if (!selectedAgentIdRef.current || !dimensions) return;
const previousSession = activeSessionRef.current;
detachTransport();
if (previousSession)
catalogRef.current.markDetached(previousSession.terminal_id);
setConnection("connecting");
terminal.reset();
try {
terminal.fit();
const fittedDimensions = terminal.dimensions() ?? dimensions;
const created = await createTerminal(
selectedAgentIdRef.current,
fittedDimensions.rows,
fittedDimensions.cols,
);
if (!mountedRef.current) return;
connect(created);
} catch (error) {
if (!mountedRef.current) return;
setSession(null);
activeSessionRef.current = null;
terminal.setActiveTerminalId(undefined);
setConnection("idle");
toast.error("Could not start terminal", { description: String(error) });
}
}, [connect, detachTransport, terminal]);
const reconnect = useCallback(async () => {
const currentSession = activeSessionRef.current;
if (!currentSession) return;
detachTransport();
try {
terminal.reset();
setConnection("connecting");
const attached = await attachTerminal(
currentSession.terminal_id,
operatorId,
);
if (!mountedRef.current) return;
connect(attached);
} catch (error) {
if (!mountedRef.current) return;
setConnection("disconnected");
toast.error("Terminal session is no longer available", {
description: String(error),
});
}
}, [connect, detachTransport, operatorId, terminal]);
const close = useCallback(
async (closingSession: TerminalSession) => {
const closingId = closingSession.terminal_id;
const closesActiveSession =
closingId === activeSessionRef.current?.terminal_id;
const remaining = catalogRef.current
.getSessions()
.filter((item) => item.terminal_id !== closingId);
const fallback = remaining.find((item) => !item.operator_attached);
if (closesActiveSession) {
if (socketRef.current?.readyState === WebSocket.OPEN) {
socketRef.current.send(JSON.stringify({ type: "close" }));
}
detachTransport();
}
try {
await closeTerminal(closingId);
} catch (error) {
if (!mountedRef.current) return;
toast.error("Terminal cleanup failed", { description: String(error) });
void catalogRef.current.refresh().catch(() => undefined);
return;
}
if (!mountedRef.current) return;
catalogRef.current.remove(closingId);
terminal.forgetTitle(closingId);
if (
window.sessionStorage.getItem(REMEMBERED_TERMINAL_KEY) === closingId
) {
window.sessionStorage.removeItem(REMEMBERED_TERMINAL_KEY);
}
if (closesActiveSession) {
setSession(null);
activeSessionRef.current = null;
terminal.setActiveTerminalId(undefined);
setConnection("idle");
// reset discards the complete screen and terminal modes; clear() does not.
terminal.reset();
if (fallback) void activate(fallback);
}
},
[activate, detachTransport, terminal],
);
const toggleModifier = useCallback(
(modifier: keyof TerminalModifiers) => {
setModifiers((current) => {
const next = { ...current, [modifier]: !current[modifier] };
modifiersRef.current = next;
return next;
});
window.requestAnimationFrame(() => terminal.focus());
},
[terminal],
);
const writeAccessoryKey = useCallback(
(data: string) => {
writeTerminalData(data);
window.requestAnimationFrame(() => terminal.focus());
},
[terminal, writeTerminalData],
);
// Transport input/resize subscriptions and restoration begin only once xterm is usable.
useEffect(() => {
if (!terminal.ready) return;
mountedRef.current = true;
const input = terminal.onInput((data) => writeTerminalData(data, true));
const resize = terminal.onResize(() => {
const socket = socketRef.current;
if (socket?.readyState === WebSocket.OPEN) {
terminal.sendResize((message) => socket.send(message));
}
});
let cancelled = false;
async function restoreTerminalSession() {
try {
const listed = await catalogRef.current.refresh();
if (cancelled) return;
const rememberedId = window.sessionStorage.getItem(
REMEMBERED_TERMINAL_KEY,
);
const candidates = restoreCandidates(
orderTerminalSessions(listed),
rememberedId,
selectedAgentIdRef.current,
);
for (const candidate of candidates) {
if (cancelled) return;
setConnection("connecting");
try {
const attached = await attachTerminal(
candidate.terminal_id,
operatorId,
);
if (cancelled) return;
terminal.reset();
connect(attached);
return;
} catch (error) {
if (
!(error instanceof APIError) ||
error.code !== "terminal_operator_already_attached"
) {
throw error;
}
}
}
if (!cancelled) setConnection("idle");
} catch (error) {
if (cancelled) return;
setConnection("idle");
toast.error("Could not restore terminal session", {
description: String(error),
});
}
}
void restoreTerminalSession();
return () => {
cancelled = true;
mountedRef.current = false;
input.dispose();
resize.dispose();
detachTransport();
};
}, [
terminal.ready,
terminal.onInput,
terminal.onResize,
terminal.reset,
terminal.sendResize,
terminal,
connect,
detachTransport,
operatorId,
writeTerminalData,
]);
return {
session,
connection,
modifiers,
start,
activate,
reconnect,
close,
writeTerminalData,
writeAccessoryKey,
toggleModifier,
};
}