fix(terminal): reconcile tabs with server sessions and keep shown terminals alive
The tab list lived only in per-tab sessionStorage, so a new browser tab, another device, or cleared storage showed an empty terminal sidebar while PTYs kept running server-side, and orphans leaked until the idle sweep. Add GET /api/terminal/sessions and adopt unknown server sessions into the local tab projection (additive only; a failed listing changes nothing). The idle sweep also reaped terminals in background tabs because only the active tab holds a WebSocket attachment. Add POST /api/terminal/touch and have open clients periodically refresh activity for every session their tabs reference.
This commit is contained in:
@@ -58,6 +58,7 @@ export const TerminalView: React.FC<TerminalViewProps> = ({ visible }) => {
|
||||
const setActiveTab = useTerminalStore((s) => s.setActiveTab);
|
||||
const closeTab = useTerminalStore((s) => s.closeTab);
|
||||
const setTabSessionId = useTerminalStore((s) => s.setTabSessionId);
|
||||
const adoptServerSessions = useTerminalStore((s) => s.adoptServerSessions);
|
||||
const setTabLifecycle = useTerminalStore((s) => s.setTabLifecycle);
|
||||
const setConnecting = useTerminalStore((s) => s.setConnecting);
|
||||
const appendToBuffer = useTerminalStore((s) => s.appendToBuffer);
|
||||
@@ -176,6 +177,50 @@ export const TerminalView: React.FC<TerminalViewProps> = ({ visible }) => {
|
||||
directoryRef.current = effectiveDirectory;
|
||||
}, [effectiveDirectory]);
|
||||
|
||||
// The tab list is a per-client projection, so ask the server what actually
|
||||
// exists for this directory and adopt sessions no local tab references
|
||||
// (another device, a fresh browser tab, or a reload with cleared storage).
|
||||
// A failed listing changes nothing: adoption is additive only.
|
||||
React.useEffect(() => {
|
||||
if (!terminalHydrated || !effectiveDirectory || !terminal.listSessions) {
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
const directory = effectiveDirectory;
|
||||
void terminal.listSessions(directory)
|
||||
.then((serverSessions) => {
|
||||
if (cancelled || directoryRef.current !== directory) return;
|
||||
adoptServerSessions(directory, serverSessions);
|
||||
})
|
||||
.catch(() => { /* keep local tabs; the next mount or directory switch retries */ });
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [terminalHydrated, effectiveDirectory, terminal, adoptServerSessions]);
|
||||
|
||||
// The server reaps terminals with no attached socket after an idle timeout,
|
||||
// but only the active tab holds an attachment. While this client is open,
|
||||
// periodically mark every session its tabs reference as active so
|
||||
// background tabs (and other directories' terminals) are not reaped.
|
||||
React.useEffect(() => {
|
||||
if (!terminal.touchSessions) {
|
||||
return;
|
||||
}
|
||||
const touch = () => {
|
||||
if (typeof navigator !== 'undefined' && !navigator.onLine) return;
|
||||
const ids: string[] = [];
|
||||
for (const dirState of useTerminalStore.getState().sessions.values()) {
|
||||
for (const tab of dirState.tabs) {
|
||||
if (tab.terminalSessionId) ids.push(tab.terminalSessionId);
|
||||
}
|
||||
}
|
||||
if (ids.length > 0) void terminal.touchSessions?.(ids).catch(() => {});
|
||||
};
|
||||
touch();
|
||||
const interval = setInterval(touch, 10 * 60 * 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [terminal]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!showQuickKeys && activeModifier !== null) {
|
||||
setActiveModifier(null);
|
||||
|
||||
Reference in New Issue
Block a user