1import { useAtomValue } from "@effect/atom-react";
2import { deriveReportedModelSelection } from "@t3tools/client-runtime/state/thread-execution";
3
4import { appAtomRegistry } from "./atom-registry";
5import type {
6 EnvironmentProject,
7 EnvironmentThread,
8 EnvironmentThreadShell,
9} from "@t3tools/client-runtime/state/shell";
10import type {
11 EnvironmentId,
12 ScopedProjectRef,
13 ScopedThreadRef,
14 ServerConfig,
15 ThreadId,
16} from "@t3tools/contracts";
17import { hasThreadLinks, relabelThreadLinks } from "@t3tools/shared/threadLinks";
18import { Atom } from "effect/reactivity";
19import { useMemo } from "react";
20
21import { environmentProjects } from "./projects";
22import { environmentServerConfigsAtom, serverEnvironment } from "./server";
23import { environmentThreadDetails, environmentThreadShells } from "./threads";
24
25const EMPTY_PROJECT_ATOM = Atom.make<EnvironmentProject | null>(null).pipe(
26 Atom.withLabel("mobile-project:empty"),
27);
28const EMPTY_THREAD_SHELL_ATOM = Atom.make<EnvironmentThreadShell | null>(null).pipe(
29 Atom.withLabel("mobile-thread-shell:empty"),
30);
31const EMPTY_SERVER_CONFIG_ATOM = Atom.make<ServerConfig | null>(null).pipe(
32 Atom.withLabel("mobile-server-config:empty"),
33);
34const EMPTY_THREAD_TITLES: ReadonlyMap<ThreadId, string> = new Map();
35const EMPTY_THREAD_TITLES_ATOM = Atom.make(EMPTY_THREAD_TITLES).pipe(
36 Atom.withLabel("mobile-thread-titles:empty"),
37);
38
39/** Thread titles in one environment. Emits when a title changes, not on every shell update. */
40const threadTitlesAtom = Atom.family((environmentId: EnvironmentId) => {
41 let previous = EMPTY_THREAD_TITLES;
42 return Atom.make((get) => {
43 const index = get(environmentThreadShells.environmentThreadIndexAtom(environmentId));
44 const unchanged =
45 index.size === previous.size &&
46 Array.from(index).every(([threadId, shell]) => previous.get(threadId) === shell.title);
47 if (!unchanged) {
48 previous = new Map(Array.from(index, ([threadId, shell]) => [threadId, shell.title]));
49 }
50 return previous;
51 }).pipe(Atom.withLabel(`mobile-thread-titles:${environmentId}`));
52});
53
54/** Resolves when the project event reaches the live client store. */
55export function waitForProject(
56 ref: ScopedProjectRef,
57 timeoutMs = 10_000,
58): Promise<EnvironmentProject | null> {
59 const atom = environmentProjects.projectAtom(ref);
60 const current = appAtomRegistry.get(atom);
61 if (current !== null) return Promise.resolve(current);
62 return new Promise((resolve) => {
63 let unsubscribe: (() => void) | null = null;
64 const timeout = setTimeout(() => {
65 unsubscribe?.();
66 resolve(null);
67 }, timeoutMs);
68 const finish = (project: EnvironmentProject | null) => {
69 if (project === null) return;
70 clearTimeout(timeout);
71 unsubscribe?.();
72 resolve(project);
73 };
74 unsubscribe = appAtomRegistry.subscribe(atom, finish);
75 finish(appAtomRegistry.get(atom));
76 });
77}
78
79export function useProjects(): ReadonlyArray<EnvironmentProject> {
80 return useAtomValue(environmentProjects.projectsAtom);
81}
82
83export function useThreadShells(): ReadonlyArray<EnvironmentThreadShell> {
84 return useAtomValue(environmentThreadShells.threadShellsAtom);
85}
86
87export function useNavigationThreadShells(): ReadonlyArray<EnvironmentThreadShell> {
88 return useAtomValue(environmentThreadShells.navigationThreadShellsAtom);
89}
90
91export function useProject(ref: ScopedProjectRef | null): EnvironmentProject | null {
92 return useAtomValue(ref === null ? EMPTY_PROJECT_ATOM : environmentProjects.projectAtom(ref));
93}
94
95export function useThreadShell(ref: ScopedThreadRef | null): EnvironmentThreadShell | null {
96 return useAtomValue(
97 ref === null ? EMPTY_THREAD_SHELL_ATOM : environmentThreadShells.threadShellAtom(ref),
98 );
99}
100
101export function useChildThreadInputs(ref: ScopedThreadRef) {
102 return useAtomValue(environmentThreadShells.childThreadInputsAtom(ref));
103}
104
105/** `markdown` with each thread link labeled by the thread's current title in `environmentId`. */
106export function useLiveThreadLinkLabels(markdown: string, environmentId: EnvironmentId): string {
107 const titles = useAtomValue(
108 hasThreadLinks(markdown) ? threadTitlesAtom(environmentId) : EMPTY_THREAD_TITLES_ATOM,
109 );
110 return useMemo(
111 () =>
112 titles.size === 0
113 ? markdown
114 : relabelThreadLinks(markdown, (threadId) => titles.get(threadId)),
115 [markdown, titles],
116 );
117}
118
119export function useEnvironmentServerConfig(
120 environmentId: EnvironmentId | null,
121): ServerConfig | null {
122 return useAtomValue(
123 environmentId === null
124 ? EMPTY_SERVER_CONFIG_ATOM
125 : serverEnvironment.configValueAtom(environmentId),
126 );
127}
128
129export function useServerConfigs(): ReadonlyMap<EnvironmentId, ServerConfig> {
130 return useAtomValue(environmentServerConfigsAtom);
131}
132
133const selectReportedModelSelection = (thread: EnvironmentThread | null) =>
134 thread === null ? null : deriveReportedModelSelection(thread.projection);
135
136export function useThreadReportedModelSelection(ref: ScopedThreadRef) {
137 return useAtomValue(environmentThreadDetails.threadAtom(ref), selectReportedModelSelection);
138}