Keep focused agent timelines live and catch up instantly (#2196)

* fix(app): keep focused agent timelines live

Window focus was conflated with app visibility, so switching OS windows could remove the selected timeline subscription. Separate those signals, grace every visibility-driven removal, and cover retained history until authoritative catch-up completes.

* fix(app): keep timeline catch-up failures non-blocking

* fix(app): surface timeline catch-up failures

* fix(app): preserve file preview refocus

* fix(app): preserve optimistic catch-up flow
This commit is contained in:
Mohamed Boudra
2026-07-18 16:30:59 +02:00
committed by GitHub
parent fcaa84f0e4
commit 7917532716
26 changed files with 527 additions and 64 deletions
+37 -15
View File
@@ -1,19 +1,24 @@
import { useSyncExternalStore } from "react";
import { AppState } from "react-native";
import { getIsAppActivelyVisible } from "@/utils/app-visibility";
import { getIsAppActivelyVisible, getIsAppVisible } from "@/utils/app-visibility";
import { isWeb } from "@/constants/platform";
let current = getIsAppActivelyVisible();
const listeners = new Set<() => void>();
let visible = getIsAppVisible();
let activelyVisible = getIsAppActivelyVisible();
const visibilityListeners = new Set<() => void>();
const activeVisibilityListeners = new Set<() => void>();
function notify(): void {
const next = getIsAppActivelyVisible();
if (next === current) {
return;
const nextVisible = getIsAppVisible();
if (nextVisible !== visible) {
visible = nextVisible;
for (const listener of visibilityListeners) listener();
}
current = next;
for (const listener of listeners) {
listener();
const nextActivelyVisible = getIsAppActivelyVisible();
if (nextActivelyVisible !== activelyVisible) {
activelyVisible = nextActivelyVisible;
for (const listener of activeVisibilityListeners) listener();
}
}
@@ -29,15 +34,32 @@ if (isWeb && typeof document !== "undefined") {
window.addEventListener("blur", notify);
}
function subscribe(listener: () => void): () => void {
listeners.add(listener);
return () => listeners.delete(listener);
function subscribeToVisibility(listener: () => void): () => void {
visibilityListeners.add(listener);
return () => visibilityListeners.delete(listener);
}
function getSnapshot(): boolean {
return current;
function subscribeToActiveVisibility(listener: () => void): () => void {
activeVisibilityListeners.add(listener);
return () => activeVisibilityListeners.delete(listener);
}
function getVisibilitySnapshot(): boolean {
return visible;
}
function getActiveVisibilitySnapshot(): boolean {
return activelyVisible;
}
export function useAppVisible(): boolean {
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
return useSyncExternalStore(subscribeToVisibility, getVisibilitySnapshot, getVisibilitySnapshot);
}
export function useAppActivelyVisible(): boolean {
return useSyncExternalStore(
subscribeToActiveVisibility,
getActiveVisibilitySnapshot,
getActiveVisibilitySnapshot,
);
}