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
+43 -1
View File
@@ -1,7 +1,15 @@
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
import type { TFunction } from "i18next";
import { SquarePen } from "lucide-react-native";
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
import React, {
memo,
useCallback,
useEffect,
useMemo,
useRef,
useState,
useSyncExternalStore,
} from "react";
import { useTranslation } from "react-i18next";
import { ActivityIndicator, Text, View } from "react-native";
import ReanimatedAnimated from "react-native-reanimated";
@@ -14,6 +22,7 @@ import { AgentStreamView, type AgentStreamViewHandle } from "@/agent-stream/view
import { ArchivedAgentCallout } from "@/components/archived-agent-callout";
import { FileDropZone } from "@/components/file-drop/file-drop-zone";
import { useRetainedPanelActive } from "@/components/retained-panel";
import { SidebarCallout } from "@/components/sidebar-callout";
import { Composer } from "@/composer";
import { AgentModeControl } from "@/composer/agent-controls/mode-control";
import { RewindComposerRestoreProvider } from "@/components/rewind/composer-restore";
@@ -751,6 +760,26 @@ function ChatAgentContent({
const agentHistorySyncGeneration = useSessionStore((state) =>
agentId ? (state.sessions[serverId]?.agentHistorySyncGeneration?.get(agentId) ?? -1) : -1,
);
const viewedTimelineSync = useSessionStore(
(state) => state.sessions[serverId]?.viewedTimelineSync ?? null,
);
const subscribeToVisibilityCatchUp = useCallback(
(listener: () => void) => viewedTimelineSync?.subscribe(listener) ?? (() => {}),
[viewedTimelineSync],
);
const readTimelineStatus = useCallback(
() =>
!agentId || !viewedTimelineSync
? ("ready" as const)
: viewedTimelineSync.getAgentTimelineStatus(agentId),
[agentId, viewedTimelineSync],
);
const timelineStatus = useSyncExternalStore(
subscribeToVisibilityCatchUp,
readTimelineStatus,
readTimelineStatus,
);
const visibilityCatchUpStatus = isPaneVisible ? timelineStatus : "ready";
const hasActiveCreateHandoff = useCreateFlowStore((state) =>
findActiveCreateHandoff({ pendingByDraftId: state.pendingByDraftId, serverId, agentId }),
);
@@ -862,6 +891,7 @@ function ChatAgentContent({
isArchivingCurrentAgent,
isHistorySyncing,
needsAuthoritativeSync,
visibilityCatchUpStatus,
continuity,
hasHydratedHistoryBefore,
},
@@ -1004,6 +1034,7 @@ function ChatAgentContent({
viewState.tag === "ready" &&
viewState.sync.status === "catching_up" &&
viewState.sync.ui === "overlay";
const showHistorySyncError = viewState.tag === "ready" && viewState.sync.status === "sync_error";
return (
<ChatAgentReadyContent
@@ -1023,6 +1054,7 @@ function ChatAgentContent({
handleComposerHeightChange={handleComposerHeightChange}
handleMessageSent={handleMessageSent}
showHistorySyncOverlay={showHistorySyncOverlay}
showHistorySyncError={showHistorySyncError}
cwd={agentCwd}
onAttentionInputFocus={attentionController.clearOnInputFocus}
onAttentionPromptSend={attentionController.clearOnPromptSend}
@@ -1048,6 +1080,7 @@ const ChatAgentReadyContent = memo(function ChatAgentReadyContent({
handleComposerHeightChange,
handleMessageSent,
showHistorySyncOverlay,
showHistorySyncError,
cwd,
onAttentionInputFocus,
onAttentionPromptSend,
@@ -1069,6 +1102,7 @@ const ChatAgentReadyContent = memo(function ChatAgentReadyContent({
handleComposerHeightChange: (height: number) => void;
handleMessageSent: () => void;
showHistorySyncOverlay: boolean;
showHistorySyncError: boolean;
cwd: string;
onAttentionInputFocus: () => void;
onAttentionPromptSend: () => void;
@@ -1140,6 +1174,14 @@ const ChatAgentReadyContent = memo(function ChatAgentReadyContent({
<FileDropZone style={styles.container} disabled={isArchivingCurrentAgent}>
{contentContainer}
{showHistorySyncError ? (
<SidebarCallout
title={t("agentPanel.states.timelineSyncFailed")}
variant="error"
testID="agent-timeline-sync-error"
/>
) : null}
{composerSection}
{showHistorySyncOverlay ? (