import { useAtomValue } from "@effect/atom-react"; import type { EnvironmentProject, EnvironmentThread, EnvironmentThreadShell, } from "@t3tools/client-runtime/state/shell"; import { type EnvironmentThreadStatus, mergeEnvironmentThread, } from "@t3tools/client-runtime/state/threads"; import type { ScopedProjectRef, ScopedThreadRef, ServerConfig } from "@t3tools/contracts"; import type { EnvironmentId } from "@t3tools/contracts"; import { Atom } from "effect/unstable/reactivity"; import { useMemo } from "react"; import { appAtomRegistry } from "../rpc/atomRegistry"; import { environmentProjects } from "./projects"; import { environmentServerConfigsAtom } from "./server"; import { allEnvironmentProjectSnapshotsReadyAtom, allEnvironmentShellsBootstrappedAtom, } from "./shell"; import { environmentThreadDetails, environmentThreadShells } from "./threads"; const EMPTY_THREAD_REFS: ReadonlyArray = Object.freeze([]); const EMPTY_PROJECT_ATOM = Atom.make(null).pipe( Atom.withLabel("web-project:empty"), ); const EMPTY_THREAD_REFS_ATOM = Atom.make(EMPTY_THREAD_REFS).pipe( Atom.withLabel("web-thread-refs:empty"), ); const EMPTY_THREAD_SHELL_ATOM = Atom.make(null).pipe( Atom.withLabel("web-thread-shell:empty"), ); const EMPTY_THREAD_DETAIL_ATOM = Atom.make(null).pipe( Atom.withLabel("web-thread-detail:empty"), ); const EMPTY_THREAD_STATUS_ATOM = Atom.make("empty").pipe( Atom.withLabel("web-thread-status:empty"), ); const activeEnvironmentIdAtom = Atom.make(null).pipe( Atom.keepAlive, Atom.withLabel("web-active-environment-id"), ); export function useActiveEnvironmentId(): EnvironmentId | null { return useAtomValue(activeEnvironmentIdAtom); } export function setActiveEnvironmentId(environmentId: EnvironmentId | null): void { appAtomRegistry.set(activeEnvironmentIdAtom, environmentId); } export function useThreadRefs(): ReadonlyArray { return useAtomValue(environmentThreadShells.threadRefsAtom); } export function useEnvironmentThreadRefs( environmentId: EnvironmentId | null, ): ReadonlyArray { return useAtomValue( environmentId === null ? EMPTY_THREAD_REFS_ATOM : environmentThreadShells.environmentThreadRefsAtom(environmentId), ); } export function useProjects(): ReadonlyArray { return useAtomValue(environmentProjects.projectsAtom); } export function useServerConfigs(): ReadonlyMap { return useAtomValue(environmentServerConfigsAtom); } export function useThreadShells(): ReadonlyArray { return useAtomValue(environmentThreadShells.threadShellsAtom); } export function useAllEnvironmentShellsBootstrapped(): boolean { return useAtomValue(allEnvironmentShellsBootstrappedAtom); } export function useAllEnvironmentProjectSnapshotsReady(): boolean { return useAtomValue(allEnvironmentProjectSnapshotsReadyAtom); } export function useThreadShellsForProjectRefs( refs: ReadonlyArray, ): ReadonlyArray { return useAtomValue(environmentThreadShells.threadShellsForProjectRefsAtom(refs)); } export function useProject(ref: ScopedProjectRef | null): EnvironmentProject | null { return useAtomValue(ref === null ? EMPTY_PROJECT_ATOM : environmentProjects.projectAtom(ref)); } export function useThreadShell(ref: ScopedThreadRef | null): EnvironmentThreadShell | null { return useAtomValue( ref === null ? EMPTY_THREAD_SHELL_ATOM : environmentThreadShells.threadShellAtom(ref), ); } export function useThreadDetail(ref: ScopedThreadRef | null): EnvironmentThread | null { return useAtomValue( ref === null ? EMPTY_THREAD_DETAIL_ATOM : environmentThreadDetails.detailAtom(ref), ); } export function useThreadStatus(ref: ScopedThreadRef | null): EnvironmentThreadStatus { return useAtomValue( ref === null ? EMPTY_THREAD_STATUS_ATOM : environmentThreadDetails.statusAtom(ref), ); } export function resolveThreadDetailRef( ref: ScopedThreadRef | null, options: { shellExists: boolean; waitForShell: boolean; }, ): ScopedThreadRef | null { return ref !== null && (!options.waitForShell || options.shellExists) ? ref : null; } /** Detail collections composed with shell-authoritative thread/workspace metadata. */ export function useThread( ref: ScopedThreadRef | null, options?: { /** * Client-reserved draft thread ids do not exist on the server until the * first send. Waiting for the shell index avoids polling the detail * endpoint for an intentionally missing thread during that window. */ waitForShell?: boolean; }, ): EnvironmentThread | null { const shell = useThreadShell(ref); const detail = useThreadDetail( resolveThreadDetailRef(ref, { shellExists: shell !== null, waitForShell: options?.waitForShell === true, }), ); return useMemo(() => mergeEnvironmentThread(detail, shell), [detail, shell]); } export function readProject(ref: ScopedProjectRef): EnvironmentProject | null { return appAtomRegistry.get(environmentProjects.projectAtom(ref)); } export function readProjects(): ReadonlyArray { return appAtomRegistry.get(environmentProjects.projectsAtom); } /** Resolves when the project event reaches the live client store. */ export function waitForProject( ref: ScopedProjectRef, timeoutMs = 10_000, ): Promise { const current = readProject(ref); if (current !== null) return Promise.resolve(current); return new Promise((resolve, reject) => { let unsubscribe: (() => void) | null = null; const timeout = setTimeout(() => { unsubscribe?.(); reject(new Error("The project did not appear in the desktop app.")); }, timeoutMs); const finish = (project: EnvironmentProject | null) => { if (project === null) return; clearTimeout(timeout); unsubscribe?.(); resolve(project); }; unsubscribe = appAtomRegistry.subscribe(environmentProjects.projectAtom(ref), finish); finish(readProject(ref)); }); } export function readThreadShell(ref: ScopedThreadRef): EnvironmentThreadShell | null { return appAtomRegistry.get(environmentThreadShells.threadShellAtom(ref)); } /** Whether the environment's server understands thread.settle/unsettle. False for pre-settlement servers (capability defaults false on decode), so clients under version skew fall back instead of erroring. */ export function readEnvironmentSupportsSettlement(environmentId: EnvironmentId): boolean { return ( appAtomRegistry.get(environmentServerConfigsAtom).get(environmentId)?.environment.capabilities .threadSettlement === true ); } /** Whether the environment's server understands thread.snooze/unsnooze. Same version-skew contract as settlement. */ export function readEnvironmentSupportsSnooze(environmentId: EnvironmentId): boolean { return ( appAtomRegistry.get(environmentServerConfigsAtom).get(environmentId)?.environment.capabilities .threadSnooze === true ); } /** Whether the environment's server understands thread.pin/unpin. Same version-skew contract as settlement. */ export function readEnvironmentSupportsPinning(environmentId: EnvironmentId): boolean { return ( appAtomRegistry.get(environmentServerConfigsAtom).get(environmentId)?.environment.capabilities .threadPinning === true ); } /** Whether the environment's server understands thread title regeneration. Same version-skew contract as settlement. */ export function readEnvironmentSupportsTitleRegeneration(environmentId: EnvironmentId): boolean { return ( appAtomRegistry.get(environmentServerConfigsAtom).get(environmentId)?.environment.capabilities .threadTitleRegeneration === true ); } /** Whether the environment's server understands thread.pin.reorder (and orderKey on thread.pin). Same version-skew contract as settlement. */ export function readEnvironmentSupportsPinReorder(environmentId: EnvironmentId): boolean { return ( appAtomRegistry.get(environmentServerConfigsAtom).get(environmentId)?.environment.capabilities .threadPinReorder === true ); } export function readEnvironmentSupportsActiveReorder(environmentId: EnvironmentId): boolean { return ( appAtomRegistry.get(environmentServerConfigsAtom).get(environmentId)?.environment.capabilities .threadActiveReorder === true ); } export function readEnvironmentThreadRefs( environmentId: EnvironmentId, ): ReadonlyArray { return appAtomRegistry.get(environmentThreadShells.environmentThreadRefsAtom(environmentId)); } export function readThreadShells(): ReadonlyArray { return appAtomRegistry.get(environmentThreadShells.threadShellsAtom); }