import { combineTerminalSessionState, EMPTY_TERMINAL_BUFFER_STATE, EMPTY_TERMINAL_SESSION_STATE, selectRunningSubprocessTerminalIds, type KnownTerminalSession, type TerminalSessionState, } from "@t3tools/client-runtime/state/terminal"; import { ThreadId, type EnvironmentId, type TerminalAttachInput, type TerminalSummary, } from "@t3tools/contracts"; import { useMemo } from "react"; import { useEnvironmentQuery } from "./query"; import { terminalEnvironment } from "./terminal"; const EMPTY_KNOWN_TERMINAL_SESSIONS = Object.freeze>([]); interface TerminalMetadataIndex { readonly all: ReadonlyArray; readonly byThreadId: ReadonlyMap>; readonly views: Map>>; } const metadataIndexes = new WeakMap, TerminalMetadataIndex>(); const sessionsBySummary = new WeakMap>(); // Reuse groups a consumer still holds without keeping old group members alive // merely because their first summary remains in a newer metadata snapshot. const groupsByAnchor = new WeakMap< TerminalSummary, Map< EnvironmentId, { readonly all?: WeakRef>; readonly thread?: WeakRef>; } > >(); function knownSession( summary: TerminalSummary, environmentId: EnvironmentId, ): KnownTerminalSession { let byEnvironment = sessionsBySummary.get(summary); const previous = byEnvironment?.get(environmentId); if (previous) return previous; const session = { target: { environmentId, threadId: ThreadId.make(summary.threadId), terminalId: summary.terminalId, }, state: combineTerminalSessionState(summary, EMPTY_TERMINAL_BUFFER_STATE), }; if (!byEnvironment) { byEnvironment = new Map(); sessionsBySummary.set(summary, byEnvironment); } byEnvironment.set(environmentId, session); return session; } function terminalMetadataIndex(metadata: ReadonlyArray): TerminalMetadataIndex { let index = metadataIndexes.get(metadata); if (!index) { const compare = new Intl.Collator(undefined, { numeric: true }).compare; const all = metadata.toSorted((left, right) => compare(left.terminalId, right.terminalId)); const byThreadId = new Map(); for (const summary of all) { const group = byThreadId.get(summary.threadId); if (group) group.push(summary); else byThreadId.set(summary.threadId, [summary]); } index = { all, byThreadId, views: new Map() }; metadataIndexes.set(metadata, index); } return index; } /** Share one ordered index per immutable snapshot without changing metadata subscriptions. */ export function selectKnownTerminalSessions( metadata: ReadonlyArray | null, environmentId: EnvironmentId | null, threadId: ThreadId | null, ): ReadonlyArray { if (environmentId === null || metadata === null || metadata.length === 0) { return EMPTY_KNOWN_TERMINAL_SESSIONS; } const index = terminalMetadataIndex(metadata); let views = index.views.get(environmentId); const cached = views?.get(threadId); if (cached) return cached; const summaries = threadId === null ? index.all : index.byThreadId.get(threadId); if (!summaries || summaries.length === 0) return EMPTY_KNOWN_TERMINAL_SESSIONS; const anchor = summaries[0]!; const kind = threadId === null ? "all" : "thread"; let groups = groupsByAnchor.get(anchor); const previousGroups = groups?.get(environmentId); const previous = previousGroups?.[kind]?.deref(); const sessions = previous?.length === summaries.length && previous.every((session, index) => session.state.summary === summaries[index]) ? previous : summaries.map((summary) => knownSession(summary, environmentId)); if (!groups) { groups = new Map(); groupsByAnchor.set(anchor, groups); } if (sessions !== previous) { groups.set(environmentId, { ...previousGroups, [kind]: new WeakRef(sessions) }); } if (!views) { views = new Map(); index.views.set(environmentId, views); } views.set(threadId, sessions); return sessions; } export function useAttachedTerminalSession(input: { readonly environmentId: EnvironmentId | null; readonly terminal: TerminalAttachInput | null; }): TerminalSessionState { const attach = useEnvironmentQuery( input.environmentId !== null && input.terminal !== null ? terminalEnvironment.attach({ environmentId: input.environmentId, input: input.terminal, }) : null, ); const metadata = useEnvironmentQuery( input.environmentId === null ? null : terminalEnvironment.metadata({ environmentId: input.environmentId, input: null, }), ); return useMemo(() => { if (input.environmentId === null || input.terminal === null) { return EMPTY_TERMINAL_SESSION_STATE; } const summary = (metadata.data === null ? null : terminalMetadataIndex(metadata.data) .byThreadId.get(input.terminal.threadId) ?.find((terminal) => terminal.terminalId === input.terminal?.terminalId)) ?? null; const state = combineTerminalSessionState(summary, attach.data ?? EMPTY_TERMINAL_BUFFER_STATE); return attach.error === null ? state : { ...state, error: attach.error, status: "error" }; }, [attach.data, attach.error, input.environmentId, input.terminal, metadata.data]); } export function useKnownTerminalSessions(input: { readonly environmentId: EnvironmentId | null; readonly threadId: ThreadId | null; }): ReadonlyArray { const metadata = useEnvironmentQuery( input.environmentId === null ? null : terminalEnvironment.metadata({ environmentId: input.environmentId, input: null, }), ); return useMemo( () => selectKnownTerminalSessions(metadata.data, input.environmentId, input.threadId), [input.environmentId, input.threadId, metadata.data], ); } export function useThreadRunningTerminalIds(input: { readonly environmentId: EnvironmentId | null; readonly threadId: ThreadId | null; }): ReadonlyArray { const sessions = useKnownTerminalSessions(input); return useMemo(() => selectRunningSubprocessTerminalIds(sessions), [sessions]); }