1import { useAtomValue } from "@effect/atom-react";
2import { useEffect } from "react";
3import { Atom } from "effect/reactivity";
4
5import {
6 reconcilePendingThreadOrder,
7 type PendingThreadOrder,
8} from "../features/threads/threadOrder";
9import { getThreadListV2OrderedSection } from "../features/threads/threadListV2";
10import { appAtomRegistry } from "./atom-registry";
11import { threadListEnvironmentsAtom } from "./server";
12import { environmentThreadShells } from "./threads";
13import { queuedThreadKeysAtom } from "./use-thread-outbox";
14
15// Covers lifecycle commands before a cross-section move can acquire an order hold.
16export const threadArrangementOpenAtom = Atom.make(false).pipe(Atom.keepAlive);
17
18export const threadDropBusyAtom = Atom.make(false).pipe(Atom.keepAlive);
19
20export const pendingThreadOrderAtom = Atom.make<PendingThreadOrder | null>(null).pipe(
21 Atom.keepAlive,
22);
23
24export function usePendingThreadOrder(nowMinute: string, snoozeWakeTick: number) {
25 const pending = useAtomValue(pendingThreadOrderAtom);
26 // A timed wake can change section membership without a shell event. Use the
27 // lists' existing clocks to retire that hold and re-enable their move menus.
28 useEffect(() => {
29 getPendingThreadOrder();
30 }, [nowMinute, snoozeWakeTick]);
31 return pending;
32}
33
34let refreshPendingOrder: (() => void) | undefined;
35
36/** Shared by Home and the navigation sidebar, including their action guards. */
37export function getPendingThreadOrder(): PendingThreadOrder | null {
38 refreshPendingOrder?.();
39 return appAtomRegistry.get(pendingThreadOrderAtom);
40}
41
42export function beginPendingThreadOrder(pending: PendingThreadOrder) {
43 const unsubscribers: (() => void)[] = [];
44 const cancel = () => {
45 if (refreshPendingOrder !== refresh) return;
46 refreshPendingOrder = undefined;
47 for (const unsubscribe of unsubscribers) unsubscribe();
48 appAtomRegistry.set(pendingThreadOrderAtom, null);
49 };
50 const refresh = () => {
51 if (refreshPendingOrder !== refresh) return;
52 const current = appAtomRegistry.get(pendingThreadOrderAtom);
53 if (current === null) return;
54 const capabilities = appAtomRegistry.get(threadListEnvironmentsAtom);
55 const ordered = getThreadListV2OrderedSection({
56 threads: appAtomRegistry.get(environmentThreadShells.threadShellsAtom),
57 section: current.section,
58 now: new Date().toISOString(),
59 queuedThreadKeys: appAtomRegistry.get(queuedThreadKeysAtom),
60 settlementEnvironmentIds: capabilities.settlementEnvironmentIds,
61 snoozeEnvironmentIds: capabilities.snoozeEnvironmentIds,
62 });
63 const next = reconcilePendingThreadOrder(current, ordered);
64 if (next === null) cancel();
65 else if (next !== current) appAtomRegistry.set(pendingThreadOrderAtom, next);
66 };
67 refreshPendingOrder = refresh;
68 appAtomRegistry.set(pendingThreadOrderAtom, pending);
69 unsubscribers.push(
70 appAtomRegistry.subscribe(environmentThreadShells.threadShellsAtom, refresh),
71 appAtomRegistry.subscribe(threadListEnvironmentsAtom, refresh),
72 appAtomRegistry.subscribe(queuedThreadKeysAtom, refresh),
73 );
74 return {
75 isPending: () => {
76 refresh();
77 return refreshPendingOrder === refresh;
78 },
79 complete: () => {
80 if (refreshPendingOrder !== refresh) return;
81 const current = appAtomRegistry.get(pendingThreadOrderAtom);
82 if (current !== null) {
83 appAtomRegistry.set(pendingThreadOrderAtom, { ...current, commandsComplete: true });
84 refresh();
85 }
86 },
87 cancel,
88 };
89}