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 {