1import { prepareComposerDraftsForReload } from "./composerDraftStore";
2import { persistTimelinePositionsForReload } from "./components/chat/timelineScrollAnchoring";
3import { readActiveBrowserRecordingTabIds } from "./browser/browserRecording";
4import { useThemeEditorStore } from "./components/settings/themeEditorStore";
5
6export const PREPARE_UI_REFRESH_EVENT = "t3:prepare-ui-refresh";
7const HIDDEN_UI_SELECTOR = '[data-closed], [aria-hidden="true"], [hidden], [inert]';
8
9/** DOM-owned guests and terminal viewports cannot survive a renderer replacement. */
10export function prepareUiRefresh(): boolean {
11 const selection = document.getSelection();
12 const selectionElement = selection?.anchorNode?.parentElement;
13 if (
14 document.querySelector(
15 "webview, iframe, [data-thread-terminal-drawer], [data-timeline-loading]",
16 ) ||
17 [...document.querySelectorAll('[role="dialog"], [role="alertdialog"]')].some(
18 (dialog) => !dialog.closest(HIDDEN_UI_SELECTOR),
19 ) ||
20 readActiveBrowserRecordingTabIds().size > 0 ||
21 useThemeEditorStore.getState().session !== null ||
22 (selection &&
23 !selection.isCollapsed &&
24 !selectionElement?.closest("[data-ui-refresh-composer]")) ||
25 [...document.querySelectorAll(".cm-editor")].some(
26 (editor) =>
27 !editor.closest("[data-ui-refresh-composer]") &&
28 !editor.closest(HIDDEN_UI_SELECTOR),
29 ) ||
30 [
31 ...document.querySelectorAll(
32 'input:not([type="hidden"]):not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="color"]):not([type="button"]):not([type="submit"]):not([type="reset"]), textarea, [contenteditable=true]',
33 ),
34 ].some(
35 (element) =>
36 !element.closest("[data-ui-refresh-composer]") &&
37 !element.closest(HIDDEN_UI_SELECTOR) &&
38 (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement
39 ? element.value.length > 0
40 : (element.textContent?.length ?? 0) > 0),
41 ) ||
42 [...document.querySelectorAll<HTMLElement>("*")].some(
43 (element) =>
44 (element.scrollTop !== 0 || element.scrollLeft !== 0) &&
45 !element.closest(HIDDEN_UI_SELECTOR) &&
46 !element.matches(".messages-timeline-scroll, [data-tool-group-scroll]") &&
47 !(element.matches(".cm-scroller") && element.closest("[data-ui-refresh-composer]")),
48 )
49 )
50 return false;
51
52 try {
53 if (!window.dispatchEvent(new Event(PREPARE_UI_REFRESH_EVENT, { cancelable: true })))
54 return false;
55 if (!prepareComposerDraftsForReload()) return false;
56 persistTimelinePositionsForReload();
57 return true;
58 } catch {
59 return false;
60 }
61}
62
63export function startAutomaticUiRefresh(options: {
64 readonly pending: () => boolean;
65 readonly apply: () => Promise<unknown>;
66}) {
67 let lastActivity = Date.now();
68 let unfocusedSince = document.hasFocus() && !document.hidden ? null : lastActivity;
69 let composing = false;
70 let applying = false;
71 let disposed = false;
72 let timer: ReturnType<typeof setTimeout> | undefined;
73
74 const check = () => {
75 clearTimeout(timer);
76 if (disposed || applying || !options.pending()) return;
77 const now = Date.now();
78 const idleDelay = 10 * 60_000 - (now - lastActivity);
79 const unfocusedDelay = unfocusedSince === null ? Infinity : 60_000 - (now - unfocusedSince);
80 const delay = Math.min(idleDelay, unfocusedDelay);
81 if (delay > 0) {
82 timer = setTimeout(check, delay);
83 return;
84 }
85 if (composing || !prepareUiRefresh()) {
86 timer = setTimeout(check, 30_000);
87 return;
88 }
89 applying = true;
90 void options
91 .apply()
92 .catch(() => undefined)
93 .finally(() => {
94 applying = false;
95 if (!disposed) timer = setTimeout(check, 30_000);
96 });
97 };
98 const activity = () => {
99 lastActivity = Date.now();
100 check();
101 };
102 const focus = () => {
103 unfocusedSince =
104 document.hasFocus() && !document.hidden ? null : (unfocusedSince ?? Date.now());
105 activity();
106 };
107 const composition = (event: Event) => {
108 composing = event.type === "compositionstart";
109 activity();
110 };
111 const activityEvents = ["pointerdown", "pointermove", "keydown", "wheel", "input", "touchstart"];
112 for (const name of activityEvents)
113 window.addEventListener(name, activity, { capture: true, passive: true });
114 window.addEventListener("focus", focus);
115 window.addEventListener("blur", focus);
116 document.addEventListener("visibilitychange", focus);
117 window.addEventListener("compositionstart", composition, true);
118 window.addEventListener("compositionend", composition, true);
119 check();
120
121 return {
122 check,
123 dispose: () => {
124 disposed = true;
125 clearTimeout(timer);
126 for (const name of activityEvents) window.removeEventListener(name, activity, true);
127 window.removeEventListener("focus", focus);
128 window.removeEventListener("blur", focus);
129 document.removeEventListener("visibilitychange", focus);
130 window.removeEventListener("compositionstart", composition, true);
131 window.removeEventListener("compositionend", composition, true);
132 },
133 };
134}