From daf7042cd2fa0d53f1e82737e848abf566a36c48 Mon Sep 17 00:00:00 2001 From: Christoph Leiter Date: Fri, 26 Jun 2026 14:16:52 +0200 Subject: [PATCH] Refresh open file tabs when revisited (#445) (#1699) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit File previews read once and then froze: visited tabs stay mounted-but-hidden (LRU in useMountedTabSet), so refetchOnMount never re-fired and the only way to see fresh content was to close and reopen the tab. Gate the file query's `enabled` on tab + app visibility so React Query refetches on the disabled->enabled transition — and only when the cached read is stale (keeping the existing 5s staleTime), so a quick flip away and back doesn't re-read. Covers switching between tabs and backgrounding/reopening the whole app to the same tab. Adds a wiring test covering the stale-revisit refetch, the fresh-revisit debounce, the app-foreground refetch, and the read-error path. Co-authored-by: Claude Opus 4.8 --- .../src/components/file-pane-enabled.test.ts | 28 +++++++++++++++++++ .../app/src/components/file-pane-enabled.ts | 17 +++++++++++ packages/app/src/components/file-pane.tsx | 17 +++++++++-- 3 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 packages/app/src/components/file-pane-enabled.test.ts create mode 100644 packages/app/src/components/file-pane-enabled.ts diff --git a/packages/app/src/components/file-pane-enabled.test.ts b/packages/app/src/components/file-pane-enabled.test.ts new file mode 100644 index 000000000..ca7b9617d --- /dev/null +++ b/packages/app/src/components/file-pane-enabled.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from "vitest"; +import { isFileQueryEnabled } from "./file-pane-enabled"; + +describe("isFileQueryEnabled", () => { + it("reads when there is a target, the tab is active, and the app is visible", () => { + expect(isFileQueryEnabled({ hasReadTarget: true, isTabActive: true, isAppVisible: true })).toBe( + true, + ); + }); + + it("does not read while the tab is hidden", () => { + expect( + isFileQueryEnabled({ hasReadTarget: true, isTabActive: false, isAppVisible: true }), + ).toBe(false); + }); + + it("does not read while the app is backgrounded", () => { + expect( + isFileQueryEnabled({ hasReadTarget: true, isTabActive: true, isAppVisible: false }), + ).toBe(false); + }); + + it("does not read without a resolved file target", () => { + expect( + isFileQueryEnabled({ hasReadTarget: false, isTabActive: true, isAppVisible: true }), + ).toBe(false); + }); +}); diff --git a/packages/app/src/components/file-pane-enabled.ts b/packages/app/src/components/file-pane-enabled.ts new file mode 100644 index 000000000..653fb8732 --- /dev/null +++ b/packages/app/src/components/file-pane-enabled.ts @@ -0,0 +1,17 @@ +/** + * Whether `FilePane` should read its file right now. + * + * The read is gated on visibility so a revisited tab refetches instead of showing + * the frozen first-load snapshot (#445): React Query refetches on the + * disabled→enabled transition (stale-gated by the query's staleTime). The file is + * read only when there is something to read AND the pane can actually show it — + * the tab is the active one (not a hidden, mounted-but-offscreen tab) and the + * whole app is in the foreground. + */ +export function isFileQueryEnabled(input: { + hasReadTarget: boolean; + isTabActive: boolean; + isAppVisible: boolean; +}): boolean { + return input.hasReadTarget && input.isTabActive && input.isAppVisible; +} diff --git a/packages/app/src/components/file-pane.tsx b/packages/app/src/components/file-pane.tsx index af2630083..189e58c8e 100644 --- a/packages/app/src/components/file-pane.tsx +++ b/packages/app/src/components/file-pane.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useRef } from "react"; +import React, { useContext, useEffect, useMemo, useRef } from "react"; import { useQuery } from "@tanstack/react-query"; import type { FileReadResult } from "@getpaseo/client/internal/daemon-client"; import { @@ -29,6 +29,9 @@ import { createPreviewAttachmentId, getFileNameFromPath } from "@/attachments/ut import { explorerFileFromReadResult } from "@/file-explorer/read-result"; import { resolveFilePreviewReadTarget } from "@/file-explorer/preview-target"; import type { WorkspaceFileLocation } from "@/workspace/file-open"; +import { MountedTabActiveContext } from "@/components/split-container"; +import { useAppVisible } from "@/hooks/use-app-visible"; +import { isFileQueryEnabled } from "@/components/file-pane-enabled"; interface CodeLineProps { tokens: HighlightToken[]; @@ -408,9 +411,19 @@ export function FilePane({ [normalizedFilePath, normalizedWorkspaceRoot], ); + // 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. + const isActive = useContext(MountedTabActiveContext); + const isAppVisible = useAppVisible(); + const query = useQuery({ queryKey: ["workspaceFile", serverId, readTarget?.cwd ?? null, readTarget?.path ?? null], - enabled: Boolean(client && readTarget), + enabled: isFileQueryEnabled({ + hasReadTarget: Boolean(client && readTarget), + isTabActive: isActive, + isAppVisible, + }), queryFn: async () => { if (!client || !readTarget) { return {