| 1 | import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react"; |
| 2 | import type { InboxSnapshotLike } from "./composerInboxQueue"; |
| 3 | import { app } from "./bridge"; |
| 4 | import { |
| 5 | guidanceFromInboxSnapshot, |
| 6 | hydrateEmptyGuidancePreviews, |
| 7 | inboxSnapshotBelongsToScope, |
| 8 | localGuidanceFallback, |
| 9 | mergeGuidanceSnapshot, |
| 10 | } from "./composerInboxQueue"; |
| 11 | import { onInboxChanged } from "./inboxEvents"; |
| 12 | import type { PendingGuidance } from "../components/ComposerGuidanceShelf"; |
| 13 | |
| 14 | export function useComposerInboxRefresh( |
| 15 | tabId: string | undefined, |
| 16 | draftKey: string, |
| 17 | guidanceDraftKey: string, |
| 18 | inboxSessionKey: string, |
| 19 | previewKey: string, |
| 20 | retryNonce: number, |
| 21 | running: boolean, |
| 22 | applyQueue: (items: PendingGuidance[]) => void, |
| 23 | collapse: () => void, |
| 24 | bump: () => void, |
| 25 | runtimeRevision?: number, |
| 26 | ) { |
| 27 | const inboxSessionKeyRef = useRef(inboxSessionKey); |
| 28 | const [snapshot, setSnapshot] = useState<InboxSnapshotLike>(); |
| 29 | const appliedRevisionRef = useRef<{ scope: string; revision: number }>({ scope: inboxSessionKey, revision: -1 }); |
| 30 | const clearQueue = useCallback(() => applyQueue([]), [applyQueue]); |
| 31 | useLayoutEffect(() => { |
| 32 | if (inboxSessionKeyRef.current === inboxSessionKey) return; |
| 33 | inboxSessionKeyRef.current = inboxSessionKey; |
| 34 | appliedRevisionRef.current = { scope: inboxSessionKey, revision: -1 }; |
| 35 | setSnapshot(undefined); |
| 36 | collapse(); |
| 37 | clearQueue(); |
| 38 | }, [draftKey, inboxSessionKey, clearQueue, collapse]); |
| 39 | const acceptSnapshot = useCallback((snap: InboxSnapshotLike) => { |
| 40 | if (inboxSessionKeyRef.current !== inboxSessionKey || !inboxSnapshotBelongsToScope(snap.sessionPath, inboxSessionKey)) return false; |
| 41 | const revision = Number(snap.revision ?? -1); |
| 42 | if (!Number.isFinite(revision) || (appliedRevisionRef.current.scope === inboxSessionKey && revision < appliedRevisionRef.current.revision)) return false; |
| 43 | appliedRevisionRef.current = { scope: inboxSessionKey, revision }; |
| 44 | setSnapshot(snap); |
| 45 | applyQueue(mergeGuidanceSnapshot(guidanceFromInboxSnapshot(snap), localGuidanceFallback(previewKey))); |
| 46 | return true; |
| 47 | }, [inboxSessionKey, applyQueue, previewKey]); |
| 48 | useEffect(() => onInboxChanged((changed) => { |
| 49 | if (changed.tabId && tabId && changed.tabId !== tabId) return; |
| 50 | if (!inboxSnapshotBelongsToScope(changed.sessionPath, inboxSessionKey)) return; |
| 51 | if (changed.revision !== undefined |
| 52 | && appliedRevisionRef.current.scope === inboxSessionKey |
| 53 | && changed.revision <= appliedRevisionRef.current.revision) return; |
| 54 | bump(); |
| 55 | }), [tabId, inboxSessionKey, bump]); |
| 56 | useEffect(() => { |
| 57 | if (guidanceDraftKey !== draftKey) return; |
| 58 | let live = true; |
| 59 | const fallback = localGuidanceFallback(previewKey); |
| 60 | if (typeof app.InboxSnapshot !== "function") { |
| 61 | applyQueue(fallback); |
| 62 | return; |
| 63 | } |
| 64 | void app.InboxSnapshot(tabId || "").then((snap) => { |
| 65 | if (!live || !inboxSnapshotBelongsToScope(snap?.sessionPath, inboxSessionKey)) return; |
| 66 | if (!acceptSnapshot(snap)) return; |
| 67 | const revision = Number(snap?.revision ?? -1); |
| 68 | const durable = guidanceFromInboxSnapshot(snap); |
| 69 | applyQueue(mergeGuidanceSnapshot(durable, fallback)); |
| 70 | void hydrateEmptyGuidancePreviews(durable, (id) => app.ReadInboxItem(tabId || "", id)).then((hydrated) => { |
| 71 | if (live && appliedRevisionRef.current.revision === revision && hydrated.some((item) => item.text.trim())) { |
| 72 | applyQueue(mergeGuidanceSnapshot(hydrated, fallback)); |
| 73 | } |
| 74 | }); |
| 75 | }).catch(() => { |
| 76 | // A transport failure must not erase the last authoritative queue. |
| 77 | if (live && appliedRevisionRef.current.revision < 0) applyQueue(localGuidanceFallback(previewKey)); |
| 78 | }); |
| 79 | return () => { live = false; }; |
| 80 | }, [draftKey, guidanceDraftKey, previewKey, running, tabId, retryNonce, inboxSessionKey, applyQueue, runtimeRevision, acceptSnapshot]); |
| 81 | return { snapshot, acceptSnapshot }; |
| 82 | } |
| 83 |