144 lines
4.3 KiB
TypeScript
144 lines
4.3 KiB
TypeScript
import { useEffect, useRef } from "react";
|
|
import { getWebSocketClient } from "@/lib/ws/connection";
|
|
import { useAppStore, useAppStoreApi } from "@/components/state-provider";
|
|
import type { UserShellInfo } from "@/lib/state/slices";
|
|
|
|
interface UseUserShellsReturn {
|
|
shells: UserShellInfo[];
|
|
isLoading: boolean;
|
|
isLoaded: boolean;
|
|
addShell: (shell: UserShellInfo) => void;
|
|
removeShell: (terminalId: string) => void;
|
|
}
|
|
|
|
const EMPTY_SHELLS: UserShellInfo[] = [];
|
|
|
|
/**
|
|
* Hook to fetch and manage user shell terminals for a session.
|
|
*
|
|
* Follows the data fetching pattern:
|
|
* 1. Read from store first
|
|
* 2. Fetch from backend if not loaded
|
|
* 3. Track loading/loaded state
|
|
*/
|
|
export function useUserShells(sessionId: string | null): UseUserShellsReturn {
|
|
const store = useAppStoreApi();
|
|
|
|
// Read from store — resolve environmentId for shared workspace state
|
|
const shells = useAppStore((state) => {
|
|
if (!sessionId) return EMPTY_SHELLS;
|
|
const envKey = state.environmentIdBySessionId[sessionId] ?? sessionId;
|
|
return state.userShells.byEnvironmentId[envKey] ?? EMPTY_SHELLS;
|
|
});
|
|
const isLoading = useAppStore((state) => {
|
|
if (!sessionId) return false;
|
|
const envKey = state.environmentIdBySessionId[sessionId] ?? sessionId;
|
|
return state.userShells.loading[envKey] ?? false;
|
|
});
|
|
const isLoaded = useAppStore((state) => {
|
|
if (!sessionId) return false;
|
|
const envKey = state.environmentIdBySessionId[sessionId] ?? sessionId;
|
|
return state.userShells.loaded[envKey] ?? false;
|
|
});
|
|
const connectionStatus = useAppStore((state) => state.connection.status);
|
|
|
|
// Guard refs to prevent duplicate fetches
|
|
const lastFetchedSessionIdRef = useRef<string | null>(null);
|
|
const prevSessionIdRef = useRef<string | null>(null);
|
|
|
|
// Reset refs when session clears
|
|
useEffect(() => {
|
|
if (!sessionId) {
|
|
lastFetchedSessionIdRef.current = null;
|
|
}
|
|
}, [sessionId]);
|
|
|
|
// Fetch user shells from backend
|
|
useEffect(() => {
|
|
if (!sessionId) return;
|
|
if (connectionStatus !== "connected") return;
|
|
|
|
// Detect session change to force refetch
|
|
const sessionChanged =
|
|
prevSessionIdRef.current !== null && prevSessionIdRef.current !== sessionId;
|
|
prevSessionIdRef.current = sessionId;
|
|
|
|
// If session changed, reset fetch guard
|
|
if (sessionChanged) {
|
|
lastFetchedSessionIdRef.current = null;
|
|
}
|
|
|
|
// Check if already loaded (unless session just changed)
|
|
if (isLoaded && !sessionChanged) {
|
|
lastFetchedSessionIdRef.current = sessionId;
|
|
return;
|
|
}
|
|
|
|
// Don't fetch again if already fetched for this session
|
|
if (lastFetchedSessionIdRef.current === sessionId) {
|
|
return;
|
|
}
|
|
|
|
const fetchShells = async () => {
|
|
const client = getWebSocketClient();
|
|
if (!client) return;
|
|
|
|
store.getState().setUserShellsLoading(sessionId, true);
|
|
|
|
try {
|
|
const response = await client.request<{
|
|
shells?: Array<{
|
|
terminal_id: string;
|
|
process_id: string;
|
|
running: boolean;
|
|
label: string;
|
|
closable: boolean;
|
|
initial_command?: string;
|
|
}>;
|
|
}>("user_shell.list", { session_id: sessionId }, 10000);
|
|
|
|
// Transform backend format (snake_case) to frontend format (camelCase)
|
|
const shells: UserShellInfo[] = (response.shells ?? []).map((s) => ({
|
|
terminalId: s.terminal_id,
|
|
processId: s.process_id,
|
|
running: s.running,
|
|
label: s.label || "Terminal",
|
|
closable: s.closable,
|
|
initialCommand: s.initial_command,
|
|
}));
|
|
|
|
store.getState().setUserShells(sessionId, shells);
|
|
lastFetchedSessionIdRef.current = sessionId;
|
|
} catch (error) {
|
|
console.error("Failed to fetch user shells:", error);
|
|
// Set empty shells on error so we don't keep retrying
|
|
store.getState().setUserShells(sessionId, []);
|
|
lastFetchedSessionIdRef.current = sessionId;
|
|
}
|
|
};
|
|
|
|
fetchShells();
|
|
}, [sessionId, connectionStatus, isLoaded, store]);
|
|
|
|
// Actions
|
|
const addShell = (shell: UserShellInfo) => {
|
|
if (sessionId) {
|
|
store.getState().addUserShell(sessionId, shell);
|
|
}
|
|
};
|
|
|
|
const removeShell = (terminalId: string) => {
|
|
if (sessionId) {
|
|
store.getState().removeUserShell(sessionId, terminalId);
|
|
}
|
|
};
|
|
|
|
return {
|
|
shells,
|
|
isLoading,
|
|
isLoaded,
|
|
addShell,
|
|
removeShell,
|
|
};
|
|
}
|