import { type AuthSessionRole, type EnvironmentId, type OrchestrationEvent, type OrchestrationShellSnapshot, type OrchestrationShellStreamEvent, type ServerConfig, type TerminalEvent, ThreadId, } from "@t3tools/contracts"; import { type QueryClient } from "@tanstack/react-query"; import { Throttler } from "@tanstack/react-pacer"; import { createKnownEnvironment, getKnownEnvironmentWsBaseUrl, scopedThreadKey, scopeProjectRef, scopeThreadRef, } from "@t3tools/client-runtime"; import { markPromotedDraftThreadByRef, markPromotedDraftThreadsByRef, useComposerDraftStore, } from "~/composerDraftStore"; import { ensureLocalApi } from "~/localApi"; import { collectActiveTerminalThreadIds } from "~/lib/terminalStateCleanup"; import { deriveOrchestrationBatchEffects } from "~/orchestrationEventEffects"; import { projectQueryKeys } from "~/lib/projectReactQuery"; import { providerQueryKeys } from "~/lib/providerReactQuery"; import { getPrimaryKnownEnvironment } from "../primary"; import { bootstrapRemoteBearerSession, fetchRemoteEnvironmentDescriptor, fetchRemoteSessionState, resolveRemoteWebSocketConnectionUrl, } from "../remote/api"; import { resolveRemotePairingTarget } from "../remote/target"; import { getSavedEnvironmentRecord, hasSavedEnvironmentRegistryHydrated, listSavedEnvironmentRecords, persistSavedEnvironmentRecord, readSavedEnvironmentBearerToken, removeSavedEnvironmentBearerToken, type SavedEnvironmentRecord, useSavedEnvironmentRegistryStore, useSavedEnvironmentRuntimeStore, waitForSavedEnvironmentRegistryHydration, writeSavedEnvironmentBearerToken, } from "./catalog"; import { createEnvironmentConnection, type EnvironmentConnection } from "./connection"; import { useStore, selectProjectsAcrossEnvironments, selectSidebarThreadSummaryByRef, selectThreadByRef, selectThreadsAcrossEnvironments, } from "~/store"; import { useTerminalStateStore } from "~/terminalStateStore"; import { useUiStateStore } from "~/uiStateStore"; import { WsTransport } from "../../rpc/wsTransport"; import { createWsRpcClient, type WsRpcClient } from "../../rpc/wsRpcClient"; import { deriveLogicalProjectKeyFromSettings, derivePhysicalProjectKey, } from "../../logicalProject"; import { getClientSettings } from "~/hooks/useSettings"; type EnvironmentServiceState = { readonly queryClient: QueryClient; readonly queryInvalidationThrottler: Throttler<() => void>; refCount: number; stop: () => void; }; type ThreadDetailSubscriptionEntry = { readonly environmentId: EnvironmentId; readonly threadId: ThreadId; unsubscribe: () => void; unsubscribeConnectionListener: (() => void) | null; refCount: number; lastAccessedAt: number; evictionTimeoutId: ReturnType | null; }; const environmentConnections = new Map(); const environmentConnectionListeners = new Set<() => void>(); const threadDetailSubscriptions = new Map(); let activeService: EnvironmentServiceState | null = null; let needsProviderInvalidation = false; // Thread detail subscription cache policy: // - Active consumers keep a subscription retained via refCount. // - Released subscriptions stay warm for a longer idle TTL to avoid churn // while moving around the UI. // - Threads with active work or pending user action are sticky and are never // evicted while they remain non-idle. // - Capacity eviction only targets idle cached subscriptions. const THREAD_DETAIL_SUBSCRIPTION_IDLE_EVICTION_MS = 15 * 60 * 1000; const MAX_CACHED_THREAD_DETAIL_SUBSCRIPTIONS = 32; const NOOP = () => undefined; function getThreadDetailSubscriptionKey(environmentId: EnvironmentId, threadId: ThreadId): string { return scopedThreadKey(scopeThreadRef(environmentId, threadId)); } function clearThreadDetailSubscriptionEviction( entry: ThreadDetailSubscriptionEntry, ): ThreadDetailSubscriptionEntry { if (entry.evictionTimeoutId !== null) { clearTimeout(entry.evictionTimeoutId); entry.evictionTimeoutId = null; } return entry; } function isNonIdleThreadDetailSubscription(entry: ThreadDetailSubscriptionEntry): boolean { const threadRef = scopeThreadRef(entry.environmentId, entry.threadId); const state = useStore.getState(); const sidebarThread = selectSidebarThreadSummaryByRef(state, threadRef); // Prefer shell/sidebar state first because it carries the coarse thread // readiness flags used throughout the UI (pending approvals/input/plan). if (sidebarThread) { if ( sidebarThread.hasPendingApprovals || sidebarThread.hasPendingUserInput || sidebarThread.hasActionableProposedPlan ) { return true; } const orchestrationStatus = sidebarThread.session?.orchestrationStatus; if ( orchestrationStatus && orchestrationStatus !== "idle" && orchestrationStatus !== "stopped" ) { return true; } if (sidebarThread.latestTurn?.state === "running") { return true; } } const thread = selectThreadByRef(state, threadRef); if (!thread) { return false; } const orchestrationStatus = thread.session?.orchestrationStatus; return ( Boolean( orchestrationStatus && orchestrationStatus !== "idle" && orchestrationStatus !== "stopped", ) || thread.latestTurn?.state === "running" || thread.pendingSourceProposedPlan !== undefined ); } function shouldEvictThreadDetailSubscription(entry: ThreadDetailSubscriptionEntry): boolean { return entry.refCount === 0 && !isNonIdleThreadDetailSubscription(entry); } function attachThreadDetailSubscription(entry: ThreadDetailSubscriptionEntry): boolean { if (entry.unsubscribeConnectionListener !== null) { entry.unsubscribeConnectionListener(); entry.unsubscribeConnectionListener = null; } if (entry.unsubscribe !== NOOP) { return true; } const connection = readEnvironmentConnection(entry.environmentId); if (!connection) { return false; } entry.unsubscribe = connection.client.orchestration.subscribeThread( { threadId: entry.threadId }, (item) => { if (item.kind === "snapshot") { useStore.getState().syncServerThreadDetail(item.snapshot.thread, entry.environmentId); return; } applyEnvironmentThreadDetailEvent(item.event, entry.environmentId); }, ); return true; } function watchThreadDetailSubscriptionConnection(entry: ThreadDetailSubscriptionEntry): void { if (entry.unsubscribeConnectionListener !== null) { return; } entry.unsubscribeConnectionListener = subscribeEnvironmentConnections(() => { if (attachThreadDetailSubscription(entry)) { entry.lastAccessedAt = Date.now(); } }); attachThreadDetailSubscription(entry); } function disposeThreadDetailSubscriptionByKey(key: string): boolean { const entry = threadDetailSubscriptions.get(key); if (!entry) { return false; } clearThreadDetailSubscriptionEviction(entry); entry.unsubscribeConnectionListener?.(); entry.unsubscribeConnectionListener = null; threadDetailSubscriptions.delete(key); entry.unsubscribe(); entry.unsubscribe = NOOP; return true; } function disposeThreadDetailSubscriptionsForEnvironment(environmentId: EnvironmentId): void { for (const [key, entry] of threadDetailSubscriptions) { if (entry.environmentId === environmentId) { disposeThreadDetailSubscriptionByKey(key); } } } function reconcileThreadDetailSubscriptionsForEnvironment( environmentId: EnvironmentId, threadIds: ReadonlyArray, ): void { const activeThreadIds = new Set(threadIds); for (const [key, entry] of threadDetailSubscriptions) { if (entry.environmentId === environmentId && !activeThreadIds.has(entry.threadId)) { disposeThreadDetailSubscriptionByKey(key); } } } function scheduleThreadDetailSubscriptionEviction(entry: ThreadDetailSubscriptionEntry): void { clearThreadDetailSubscriptionEviction(entry); if (!shouldEvictThreadDetailSubscription(entry)) { return; } entry.evictionTimeoutId = setTimeout(() => { const currentEntry = threadDetailSubscriptions.get( getThreadDetailSubscriptionKey(entry.environmentId, entry.threadId), ); if (!currentEntry) { return; } currentEntry.evictionTimeoutId = null; if (!shouldEvictThreadDetailSubscription(currentEntry)) { return; } disposeThreadDetailSubscriptionByKey( getThreadDetailSubscriptionKey(entry.environmentId, entry.threadId), ); }, THREAD_DETAIL_SUBSCRIPTION_IDLE_EVICTION_MS); } function evictIdleThreadDetailSubscriptionsToCapacity(): void { if (threadDetailSubscriptions.size <= MAX_CACHED_THREAD_DETAIL_SUBSCRIPTIONS) { return; } const idleEntries = [...threadDetailSubscriptions.entries()] .filter(([, entry]) => shouldEvictThreadDetailSubscription(entry)) .toSorted(([, left], [, right]) => left.lastAccessedAt - right.lastAccessedAt); for (const [key] of idleEntries) { if (threadDetailSubscriptions.size <= MAX_CACHED_THREAD_DETAIL_SUBSCRIPTIONS) { return; } disposeThreadDetailSubscriptionByKey(key); } } function reconcileThreadDetailSubscriptionEvictionState( entry: ThreadDetailSubscriptionEntry, ): void { clearThreadDetailSubscriptionEviction(entry); if (!shouldEvictThreadDetailSubscription(entry)) { return; } scheduleThreadDetailSubscriptionEviction(entry); } function reconcileThreadDetailSubscriptionEvictionForThread( environmentId: EnvironmentId, threadId: ThreadId, ): void { const entry = threadDetailSubscriptions.get( getThreadDetailSubscriptionKey(environmentId, threadId), ); if (!entry) { return; } reconcileThreadDetailSubscriptionEvictionState(entry); } function reconcileThreadDetailSubscriptionEvictionForEnvironment( environmentId: EnvironmentId, ): void { for (const entry of threadDetailSubscriptions.values()) { if (entry.environmentId === environmentId) { reconcileThreadDetailSubscriptionEvictionState(entry); } } evictIdleThreadDetailSubscriptionsToCapacity(); } export function retainThreadDetailSubscription( environmentId: EnvironmentId, threadId: ThreadId, ): () => void { const key = getThreadDetailSubscriptionKey(environmentId, threadId); const existing = threadDetailSubscriptions.get(key); if (existing) { clearThreadDetailSubscriptionEviction(existing); existing.refCount += 1; existing.lastAccessedAt = Date.now(); if (!attachThreadDetailSubscription(existing)) { watchThreadDetailSubscriptionConnection(existing); } let released = false; return () => { if (released) { return; } released = true; existing.refCount = Math.max(0, existing.refCount - 1); existing.lastAccessedAt = Date.now(); if (existing.refCount === 0) { reconcileThreadDetailSubscriptionEvictionState(existing); evictIdleThreadDetailSubscriptionsToCapacity(); } }; } const entry: ThreadDetailSubscriptionEntry = { environmentId, threadId, unsubscribe: NOOP, unsubscribeConnectionListener: null, refCount: 1, lastAccessedAt: Date.now(), evictionTimeoutId: null, }; threadDetailSubscriptions.set(key, entry); if (!attachThreadDetailSubscription(entry)) { watchThreadDetailSubscriptionConnection(entry); } evictIdleThreadDetailSubscriptionsToCapacity(); let released = false; return () => { if (released) { return; } released = true; entry.refCount = Math.max(0, entry.refCount - 1); entry.lastAccessedAt = Date.now(); if (entry.refCount === 0) { reconcileThreadDetailSubscriptionEvictionState(entry); evictIdleThreadDetailSubscriptionsToCapacity(); } }; } function emitEnvironmentConnectionRegistryChange() { for (const listener of environmentConnectionListeners) { listener(); } } function getRuntimeErrorFields(error: unknown) { return { lastError: error instanceof Error ? error.message : String(error), lastErrorAt: new Date().toISOString(), } as const; } function isoNow(): string { return new Date().toISOString(); } function setRuntimeConnecting(environmentId: EnvironmentId) { useSavedEnvironmentRuntimeStore.getState().patch(environmentId, { connectionState: "connecting", lastError: null, lastErrorAt: null, }); } function setRuntimeConnected(environmentId: EnvironmentId) { const connectedAt = isoNow(); useSavedEnvironmentRuntimeStore.getState().patch(environmentId, { connectionState: "connected", authState: "authenticated", connectedAt, disconnectedAt: null, lastError: null, lastErrorAt: null, }); useSavedEnvironmentRegistryStore.getState().markConnected(environmentId, connectedAt); } function setRuntimeDisconnected(environmentId: EnvironmentId, reason?: string | null) { useSavedEnvironmentRuntimeStore.getState().patch(environmentId, { connectionState: "disconnected", disconnectedAt: isoNow(), ...(reason && reason.trim().length > 0 ? { lastError: reason, lastErrorAt: isoNow(), } : {}), }); } function setRuntimeError(environmentId: EnvironmentId, error: unknown) { useSavedEnvironmentRuntimeStore.getState().patch(environmentId, { connectionState: "error", ...getRuntimeErrorFields(error), }); } function coalesceOrchestrationUiEvents( events: ReadonlyArray, ): OrchestrationEvent[] { if (events.length < 2) { return [...events]; } const coalesced: OrchestrationEvent[] = []; for (const event of events) { const previous = coalesced.at(-1); if ( previous?.type === "thread.message-sent" && event.type === "thread.message-sent" && previous.payload.threadId === event.payload.threadId && previous.payload.messageId === event.payload.messageId ) { coalesced[coalesced.length - 1] = { ...event, payload: { ...event.payload, attachments: event.payload.attachments ?? previous.payload.attachments, createdAt: previous.payload.createdAt, text: !event.payload.streaming && event.payload.text.length > 0 ? event.payload.text : previous.payload.text + event.payload.text, }, }; continue; } coalesced.push(event); } return coalesced; } function syncProjectUiFromStore() { const projects = selectProjectsAcrossEnvironments(useStore.getState()); const clientSettings = getClientSettings(); useUiStateStore.getState().syncProjects( projects.map((project) => ({ key: derivePhysicalProjectKey(project), logicalKey: deriveLogicalProjectKeyFromSettings(project, clientSettings), cwd: project.cwd, })), ); } function syncThreadUiFromStore() { const threads = selectThreadsAcrossEnvironments(useStore.getState()); useUiStateStore.getState().syncThreads( threads.map((thread) => ({ key: scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id)), seedVisitedAt: thread.updatedAt ?? thread.createdAt, })), ); markPromotedDraftThreadsByRef( threads.map((thread) => scopeThreadRef(thread.environmentId, thread.id)), ); } function reconcileSnapshotDerivedState() { syncProjectUiFromStore(); syncThreadUiFromStore(); const threads = selectThreadsAcrossEnvironments(useStore.getState()); const activeThreadKeys = collectActiveTerminalThreadIds({ snapshotThreads: threads.map((thread) => ({ key: scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id)), deletedAt: null, archivedAt: thread.archivedAt, })), draftThreadKeys: useComposerDraftStore.getState().listDraftThreadKeys(), }); useTerminalStateStore.getState().removeOrphanedTerminalStates(activeThreadKeys); } export function shouldApplyTerminalEvent(input: { serverThreadArchivedAt: string | null | undefined; hasDraftThread: boolean; }): boolean { if (input.serverThreadArchivedAt !== undefined) { return input.serverThreadArchivedAt === null; } return input.hasDraftThread; } function applyRecoveredEventBatch( events: ReadonlyArray, environmentId: EnvironmentId, ) { if (events.length === 0) { return; } const batchEffects = deriveOrchestrationBatchEffects(events); const uiEvents = coalesceOrchestrationUiEvents(events); const needsProjectUiSync = events.some( (event) => event.type === "project.created" || event.type === "project.meta-updated" || event.type === "project.deleted", ); if (batchEffects.needsProviderInvalidation) { needsProviderInvalidation = true; void activeService?.queryInvalidationThrottler.maybeExecute(); } useStore.getState().applyOrchestrationEvents(uiEvents, environmentId); if (needsProjectUiSync) { const projects = selectProjectsAcrossEnvironments(useStore.getState()); const clientSettings = getClientSettings(); useUiStateStore.getState().syncProjects( projects.map((project) => ({ key: derivePhysicalProjectKey(project), logicalKey: deriveLogicalProjectKeyFromSettings(project, clientSettings), cwd: project.cwd, })), ); } const needsThreadUiSync = events.some( (event) => event.type === "thread.created" || event.type === "thread.deleted", ); if (needsThreadUiSync) { const threads = selectThreadsAcrossEnvironments(useStore.getState()); useUiStateStore.getState().syncThreads( threads.map((thread) => ({ key: scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id)), seedVisitedAt: thread.updatedAt ?? thread.createdAt, })), ); } const draftStore = useComposerDraftStore.getState(); for (const threadId of batchEffects.promoteDraftThreadIds) { markPromotedDraftThreadByRef(scopeThreadRef(environmentId, threadId)); } for (const threadId of batchEffects.clearDeletedThreadIds) { draftStore.clearDraftThread(scopeThreadRef(environmentId, threadId)); useUiStateStore .getState() .clearThreadUi(scopedThreadKey(scopeThreadRef(environmentId, threadId))); } for (const event of events) { if (event.type === "project.deleted") { draftStore.clearProjectDraftThreadId(scopeProjectRef(environmentId, event.payload.projectId)); } } for (const threadId of batchEffects.removeTerminalStateThreadIds) { useTerminalStateStore.getState().removeTerminalState(scopeThreadRef(environmentId, threadId)); } reconcileThreadDetailSubscriptionEvictionForEnvironment(environmentId); } export function applyEnvironmentThreadDetailEvent( event: OrchestrationEvent, environmentId: EnvironmentId, ) { applyRecoveredEventBatch([event], environmentId); } function applyShellEvent(event: OrchestrationShellStreamEvent, environmentId: EnvironmentId) { const threadId = event.kind === "thread-upserted" ? event.thread.id : event.kind === "thread-removed" ? event.threadId : null; const threadRef = threadId ? scopeThreadRef(environmentId, threadId) : null; const previousThread = threadRef ? selectThreadByRef(useStore.getState(), threadRef) : undefined; useStore.getState().applyShellEvent(event, environmentId); switch (event.kind) { case "project-upserted": case "project-removed": syncProjectUiFromStore(); return; case "thread-upserted": syncThreadUiFromStore(); if (!previousThread && threadRef) { markPromotedDraftThreadByRef(threadRef); } if (previousThread?.archivedAt === null && event.thread.archivedAt !== null && threadRef) { useTerminalStateStore.getState().removeTerminalState(threadRef); } reconcileThreadDetailSubscriptionEvictionForThread(environmentId, event.thread.id); evictIdleThreadDetailSubscriptionsToCapacity(); return; case "thread-removed": if (threadRef) { disposeThreadDetailSubscriptionByKey(scopedThreadKey(threadRef)); useComposerDraftStore.getState().clearDraftThread(threadRef); useUiStateStore.getState().clearThreadUi(scopedThreadKey(threadRef)); useTerminalStateStore.getState().removeTerminalState(threadRef); } syncThreadUiFromStore(); return; } } function createEnvironmentConnectionHandlers() { return { applyShellEvent, syncShellSnapshot: (snapshot: OrchestrationShellSnapshot, environmentId: EnvironmentId) => { useStore.getState().syncServerShellSnapshot(snapshot, environmentId); reconcileThreadDetailSubscriptionsForEnvironment( environmentId, snapshot.threads.map((thread) => thread.id), ); reconcileThreadDetailSubscriptionEvictionForEnvironment(environmentId); reconcileSnapshotDerivedState(); }, applyTerminalEvent: (event: TerminalEvent, environmentId: EnvironmentId) => { const threadRef = scopeThreadRef(environmentId, ThreadId.make(event.threadId)); const serverThread = selectThreadByRef(useStore.getState(), threadRef); const hasDraftThread = useComposerDraftStore.getState().getDraftThreadByRef(threadRef) !== null; if ( !shouldApplyTerminalEvent({ serverThreadArchivedAt: serverThread?.archivedAt, hasDraftThread, }) ) { return; } useTerminalStateStore.getState().applyTerminalEvent(threadRef, event); }, }; } function createPrimaryEnvironmentClient( knownEnvironment: ReturnType, ) { const wsBaseUrl = getKnownEnvironmentWsBaseUrl(knownEnvironment); if (!wsBaseUrl) { throw new Error( `Unable to resolve websocket URL for ${knownEnvironment?.label ?? "primary environment"}.`, ); } return createWsRpcClient(new WsTransport(wsBaseUrl)); } function createSavedEnvironmentClient( record: SavedEnvironmentRecord, bearerToken: string, ): WsRpcClient { useSavedEnvironmentRuntimeStore.getState().ensure(record.environmentId); return createWsRpcClient( new WsTransport( () => resolveRemoteWebSocketConnectionUrl({ wsBaseUrl: record.wsBaseUrl, httpBaseUrl: record.httpBaseUrl, bearerToken, }), { onAttempt: () => { setRuntimeConnecting(record.environmentId); }, onOpen: () => { setRuntimeConnected(record.environmentId); }, onError: (message: string) => { useSavedEnvironmentRuntimeStore.getState().patch(record.environmentId, { connectionState: "error", lastError: message, lastErrorAt: isoNow(), }); }, onClose: (details: { readonly code: number; readonly reason: string }) => { setRuntimeDisconnected(record.environmentId, details.reason); }, }, ), ); } async function refreshSavedEnvironmentMetadata( record: SavedEnvironmentRecord, bearerToken: string, client: WsRpcClient, roleHint?: AuthSessionRole | null, configHint?: ServerConfig | null, ): Promise { const [serverConfig, sessionState] = await Promise.all([ configHint ? Promise.resolve(configHint) : client.server.getConfig(), fetchRemoteSessionState({ httpBaseUrl: record.httpBaseUrl, bearerToken, }), ]); useSavedEnvironmentRuntimeStore.getState().patch(record.environmentId, { authState: sessionState.authenticated ? "authenticated" : "requires-auth", descriptor: serverConfig.environment, serverConfig, role: sessionState.authenticated ? (sessionState.role ?? roleHint ?? null) : null, }); } function registerConnection(connection: EnvironmentConnection): EnvironmentConnection { const existing = environmentConnections.get(connection.environmentId); if (existing && existing !== connection) { throw new Error(`Environment ${connection.environmentId} already has an active connection.`); } environmentConnections.set(connection.environmentId, connection); emitEnvironmentConnectionRegistryChange(); return connection; } async function removeConnection(environmentId: EnvironmentId): Promise { const connection = environmentConnections.get(environmentId); if (!connection) { return false; } disposeThreadDetailSubscriptionsForEnvironment(environmentId); environmentConnections.delete(environmentId); emitEnvironmentConnectionRegistryChange(); await connection.dispose(); return true; } function createPrimaryEnvironmentConnection(): EnvironmentConnection { const knownEnvironment = getPrimaryKnownEnvironment(); if (!knownEnvironment?.environmentId) { throw new Error("Unable to resolve the primary environment."); } const existing = environmentConnections.get(knownEnvironment.environmentId); if (existing) { return existing; } return registerConnection( createEnvironmentConnection({ kind: "primary", knownEnvironment, client: createPrimaryEnvironmentClient(knownEnvironment), ...createEnvironmentConnectionHandlers(), }), ); } async function ensureSavedEnvironmentConnection( record: SavedEnvironmentRecord, options?: { readonly client?: WsRpcClient; readonly bearerToken?: string; readonly role?: AuthSessionRole | null; readonly serverConfig?: ServerConfig | null; }, ): Promise { const existing = environmentConnections.get(record.environmentId); if (existing) { return existing; } const bearerToken = options?.bearerToken ?? (await readSavedEnvironmentBearerToken(record.environmentId)); if (!bearerToken) { useSavedEnvironmentRuntimeStore.getState().patch(record.environmentId, { authState: "requires-auth", role: null, connectionState: "disconnected", lastError: "Saved environment is missing its saved credential. Pair it again.", lastErrorAt: isoNow(), }); throw new Error("Saved environment is missing its saved credential."); } const client = options?.client ?? createSavedEnvironmentClient(record, bearerToken); const knownEnvironment = createKnownEnvironment({ id: record.environmentId, label: record.label, source: "manual", target: { httpBaseUrl: record.httpBaseUrl, wsBaseUrl: record.wsBaseUrl, }, }); const connection = createEnvironmentConnection({ kind: "saved", knownEnvironment: { ...knownEnvironment, environmentId: record.environmentId, }, client, refreshMetadata: async () => { await refreshSavedEnvironmentMetadata(record, bearerToken, client); }, onConfigSnapshot: (config) => { useSavedEnvironmentRuntimeStore.getState().patch(record.environmentId, { descriptor: config.environment, serverConfig: config, }); }, onWelcome: (payload) => { useSavedEnvironmentRuntimeStore.getState().patch(record.environmentId, { descriptor: payload.environment, }); }, ...createEnvironmentConnectionHandlers(), }); registerConnection(connection); try { await refreshSavedEnvironmentMetadata( record, bearerToken, client, options?.role ?? null, options?.serverConfig ?? null, ); return connection; } catch (error) { setRuntimeError(record.environmentId, error); await removeConnection(record.environmentId).catch(() => false); throw error; } } async function syncSavedEnvironmentConnections( records: ReadonlyArray, ): Promise { const expectedEnvironmentIds = new Set(records.map((record) => record.environmentId)); const staleEnvironmentIds = [...environmentConnections.values()] .filter((connection) => connection.kind === "saved") .map((connection) => connection.environmentId) .filter((environmentId) => !expectedEnvironmentIds.has(environmentId)); await Promise.all( staleEnvironmentIds.map((environmentId) => disconnectSavedEnvironment(environmentId)), ); await Promise.all( records.map((record) => ensureSavedEnvironmentConnection(record).catch(() => undefined)), ); } function stopActiveService() { activeService?.stop(); activeService = null; } export function subscribeEnvironmentConnections(listener: () => void): () => void { environmentConnectionListeners.add(listener); return () => { environmentConnectionListeners.delete(listener); }; } export function listEnvironmentConnections(): ReadonlyArray { return [...environmentConnections.values()]; } export function readEnvironmentConnection( environmentId: EnvironmentId, ): EnvironmentConnection | null { return environmentConnections.get(environmentId) ?? null; } export function requireEnvironmentConnection(environmentId: EnvironmentId): EnvironmentConnection { const connection = readEnvironmentConnection(environmentId); if (!connection) { throw new Error(`No websocket client registered for environment ${environmentId}.`); } return connection; } export function getPrimaryEnvironmentConnection(): EnvironmentConnection { return createPrimaryEnvironmentConnection(); } export async function disconnectSavedEnvironment(environmentId: EnvironmentId): Promise { const connection = environmentConnections.get(environmentId); if (connection?.kind !== "saved") { return; } useSavedEnvironmentRuntimeStore.getState().clear(environmentId); await removeConnection(environmentId).catch(() => false); } export async function reconnectSavedEnvironment(environmentId: EnvironmentId): Promise { const record = getSavedEnvironmentRecord(environmentId); if (!record) { throw new Error("Saved environment not found."); } const connection = environmentConnections.get(environmentId); if (!connection) { await ensureSavedEnvironmentConnection(record); return; } setRuntimeConnecting(environmentId); try { await connection.reconnect(); } catch (error) { setRuntimeError(environmentId, error); throw error; } } export async function removeSavedEnvironment(environmentId: EnvironmentId): Promise { useSavedEnvironmentRegistryStore.getState().remove(environmentId); await removeSavedEnvironmentBearerToken(environmentId); await disconnectSavedEnvironment(environmentId); } export async function addSavedEnvironment(input: { readonly label: string; readonly pairingUrl?: string; readonly host?: string; readonly pairingCode?: string; }): Promise { const resolvedTarget = resolveRemotePairingTarget({ ...(input.pairingUrl !== undefined ? { pairingUrl: input.pairingUrl } : {}), ...(input.host !== undefined ? { host: input.host } : {}), ...(input.pairingCode !== undefined ? { pairingCode: input.pairingCode } : {}), }); const descriptor = await fetchRemoteEnvironmentDescriptor({ httpBaseUrl: resolvedTarget.httpBaseUrl, }); const environmentId = descriptor.environmentId; if (environmentConnections.has(environmentId)) { throw new Error("This environment is already connected."); } const bearerSession = await bootstrapRemoteBearerSession({ httpBaseUrl: resolvedTarget.httpBaseUrl, credential: resolvedTarget.credential, }); const record: SavedEnvironmentRecord = { environmentId, label: input.label.trim() || descriptor.label, wsBaseUrl: resolvedTarget.wsBaseUrl, httpBaseUrl: resolvedTarget.httpBaseUrl, createdAt: isoNow(), lastConnectedAt: isoNow(), }; await persistSavedEnvironmentRecord(record); const didPersistBearerToken = await writeSavedEnvironmentBearerToken( environmentId, bearerSession.sessionToken, ); if (!didPersistBearerToken) { await ensureLocalApi().persistence.setSavedEnvironmentRegistry( listSavedEnvironmentRecords().map((entry) => ({ environmentId: entry.environmentId, label: entry.label, httpBaseUrl: entry.httpBaseUrl, wsBaseUrl: entry.wsBaseUrl, createdAt: entry.createdAt, lastConnectedAt: entry.lastConnectedAt, })), ); throw new Error("Unable to persist saved environment credentials."); } await ensureSavedEnvironmentConnection(record, { bearerToken: bearerSession.sessionToken, role: bearerSession.role, }); useSavedEnvironmentRegistryStore.getState().upsert(record); return record; } export async function ensureEnvironmentConnectionBootstrapped( environmentId: EnvironmentId, ): Promise { await environmentConnections.get(environmentId)?.ensureBootstrapped(); } export function startEnvironmentConnectionService(queryClient: QueryClient): () => void { if (activeService?.queryClient === queryClient) { activeService.refCount += 1; return () => { if (!activeService || activeService.queryClient !== queryClient) { return; } activeService.refCount -= 1; if (activeService.refCount === 0) { stopActiveService(); } }; } stopActiveService(); needsProviderInvalidation = false; const queryInvalidationThrottler = new Throttler( () => { if (!needsProviderInvalidation) { return; } needsProviderInvalidation = false; void queryClient.invalidateQueries({ queryKey: providerQueryKeys.all }); void queryClient.invalidateQueries({ queryKey: projectQueryKeys.all }); }, { wait: 100, leading: false, trailing: true, }, ); createPrimaryEnvironmentConnection(); const unsubscribeSavedEnvironments = useSavedEnvironmentRegistryStore.subscribe(() => { if (!hasSavedEnvironmentRegistryHydrated()) { return; } void syncSavedEnvironmentConnections(listSavedEnvironmentRecords()); }); void waitForSavedEnvironmentRegistryHydration() .then(() => syncSavedEnvironmentConnections(listSavedEnvironmentRecords())) .catch(() => undefined); activeService = { queryClient, queryInvalidationThrottler, refCount: 1, stop: () => { unsubscribeSavedEnvironments(); queryInvalidationThrottler.cancel(); }, }; return () => { if (!activeService || activeService.queryClient !== queryClient) { return; } activeService.refCount -= 1; if (activeService.refCount === 0) { stopActiveService(); } }; } export async function resetEnvironmentServiceForTests(): Promise { stopActiveService(); for (const key of Array.from(threadDetailSubscriptions.keys())) { disposeThreadDetailSubscriptionByKey(key); } await Promise.all( [...environmentConnections.keys()].map((environmentId) => removeConnection(environmentId)), ); }