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
+4 -4
View File
@@ -27,7 +27,7 @@ import { explorerFileFromReadResult } from "@/file-explorer/read-result";
import { resolveFilePreviewReadTarget } from "@/file-explorer/preview-target";
import type { WorkspaceFileLocation } from "@/workspace/file-open";
import { useRetainedPanelActive } from "@/components/retained-panel";
import { useAppVisible } from "@/hooks/use-app-visible";
import { useAppActivelyVisible } from "@/hooks/use-app-visible";
import { isFileQueryEnabled } from "@/components/file-pane-enabled";
interface CodeLineProps {
@@ -386,10 +386,10 @@ export function FilePane({
);
// Re-read the file when this pane becomes visible again (#445). `isActive`
// covers tab switches, `isAppVisible` the whole-app background/foreground; the
// gate itself lives in isFileQueryEnabled.
// covers tab switches; active app visibility covers backgrounding and returning
// from another window after an external edit. The gate lives in isFileQueryEnabled.
const isActive = useRetainedPanelActive();
const isAppVisible = useAppVisible();
const isAppVisible = useAppActivelyVisible();
const query = useQuery({
queryKey: ["workspaceFile", serverId, readTarget?.cwd ?? null, readTarget?.path ?? null],
@@ -18,14 +18,14 @@ describe("terminal pane focus claim", () => {
it("waits for both the client and renderer before requesting a claim", () => {
const withoutClient = canRequestFocusClaim({
isWorkspaceFocused: true,
isAppVisible: true,
isAppActivelyVisible: true,
isClientReady: false,
isConnected: true,
isRendererReady: true,
});
const withoutRenderer = canRequestFocusClaim({
isWorkspaceFocused: true,
isAppVisible: true,
isAppActivelyVisible: true,
isClientReady: true,
isConnected: true,
isRendererReady: false,
@@ -37,7 +37,7 @@ describe("terminal pane focus claim", () => {
it("does not deliver a requested claim after the host disconnects", () => {
const disconnected = canRequestFocusClaim({
isWorkspaceFocused: true,
isAppVisible: true,
isAppActivelyVisible: true,
isClientReady: true,
isConnected: false,
isRendererReady: true,
@@ -10,7 +10,7 @@ export interface FocusClaimStep {
interface FocusClaimReadiness {
isWorkspaceFocused: boolean;
isAppVisible: boolean;
isAppActivelyVisible: boolean;
isClientReady: boolean;
isConnected: boolean;
isRendererReady: boolean;
@@ -24,7 +24,7 @@ export const EMPTY_FOCUS_CLAIM_STATE: FocusClaimState = {
export function canRequestFocusClaim(input: FocusClaimReadiness): boolean {
return (
input.isWorkspaceFocused &&
input.isAppVisible &&
input.isAppActivelyVisible &&
input.isClientReady &&
input.isConnected &&
input.isRendererReady
@@ -13,7 +13,7 @@ import type { TerminalInputModeState } from "@getpaseo/protocol/terminal-input-m
import { useTranslation } from "react-i18next";
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style";
import { useAppVisible } from "@/hooks/use-app-visible";
import { useAppActivelyVisible } from "@/hooks/use-app-visible";
import { useStableEvent } from "@/hooks/use-stable-event";
import {
hasPendingTerminalModifiers,
@@ -177,7 +177,7 @@ export function TerminalPane({
onOpenWorkspaceFile,
}: TerminalPaneProps) {
const { t } = useTranslation();
const isAppVisible = useAppVisible();
const isAppActivelyVisible = useAppActivelyVisible();
const { theme } = useUnistyles();
const { settings } = useAppSettings();
const xtermTheme = useMemo(() => toXtermTheme(theme.colors.terminal), [theme]);
@@ -283,7 +283,7 @@ export function TerminalPane({
useEffect(() => {
const canRequest = canRequestFocusClaim({
isWorkspaceFocused,
isAppVisible,
isAppActivelyVisible,
isClientReady: client !== null,
isConnected,
isRendererReady: rendererReadyStreamKey === terminalStreamKey,
@@ -299,7 +299,7 @@ export function TerminalPane({
}
}, [
client,
isAppVisible,
isAppActivelyVisible,
isConnected,
isPaneFocused,
isWorkspaceFocused,
@@ -654,7 +654,7 @@ export function TerminalPane({
let sent = false;
const canSend = canRequestFocusClaim({
isWorkspaceFocused,
isAppVisible,
isAppActivelyVisible,
isClientReady: client !== null,
isConnected,
isRendererReady: true,