From 203e0ff553bd53c2e40030af035ce58099d41915 Mon Sep 17 00:00:00 2001 From: Cameron Chisholm Date: Fri, 30 Oct 2026 19:36:28 +0000 Subject: [PATCH] perf(mobile): opening a file reads it alongside its metadata On a remote host the file viewer waited for the metadata lookup before asking for the contents, so every uncached text file paid two serial round trips. Read both at once; a directory answers the read with a cheap not-a-file error that its view never shows. --- .../features/files/ThreadFilesRouteScreen.tsx | 16 +++++++--------- .../src/features/files/filePath.test.ts | 9 +++++++++ apps/mobile/src/features/files/filePath.ts | 19 +++++++++++++++++++ 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx index 117dbc44ede58..4844063f5d16a 100644 --- a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx +++ b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx @@ -68,6 +68,7 @@ import { isMarkdownPreviewFile, isSvgImagePreviewFile, isVideoPreviewFile, + needsWorkspaceFileContents, } from "./filePath"; import { useWorkspaceFileAssetUrlState } from "./workspaceFileAssetUrl"; @@ -707,15 +708,12 @@ export function ThreadFileScreen(props: ThreadFileRouteScreenProps) { const handleRetryPreview = () => { void assetPreview.refresh().finally(() => setPreviewRevision((current) => current + 1)); }; - const needsFileContents = - !metadataPending && - directoryPath === null && - relativePath !== null && - !isVideoFile && - !isAudioFile && - (resolvedActiveMode === "source" || - isMarkdownPreviewFile(relativePath) || - filePreviewDelimiter({ name: relativePath }) !== null); + // Metadata is needed only to distinguish directories; file contents can load concurrently. + const needsFileContents = needsWorkspaceFileContents({ + relativePath, + metadataKind: metadata.data?.kind, + activeMode: resolvedActiveMode, + }); const fileQuery = useEnvironmentQuery( canReadFiles && environmentId !== null && diff --git a/apps/mobile/src/features/files/filePath.test.ts b/apps/mobile/src/features/files/filePath.test.ts index 0a5fd943e88f8..46b26d7e3bf3d 100644 --- a/apps/mobile/src/features/files/filePath.test.ts +++ b/apps/mobile/src/features/files/filePath.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vite-plus/test"; import { fileRoutePathSegments, + needsWorkspaceFileContents, resolveFileLinkPath, resolveWorkspaceRelativeFilePath, } from "./filePath"; @@ -49,3 +50,11 @@ describe("resolveWorkspaceRelativeFilePath", () => { expect(resolveWorkspaceRelativeFilePath(null, "/repo/main.ts")).toBeNull(); }); }); + +describe("needsWorkspaceFileContents", () => { + it("reads a text file while its metadata is still pending, but not a directory", () => { + const input = { relativePath: "package.json", activeMode: "source" } as const; + expect(needsWorkspaceFileContents({ ...input, metadataKind: undefined })).toBe(true); + expect(needsWorkspaceFileContents({ ...input, metadataKind: "directory" })).toBe(false); + }); +}); diff --git a/apps/mobile/src/features/files/filePath.ts b/apps/mobile/src/features/files/filePath.ts index d889e88825830..e38ff86fa6656 100644 --- a/apps/mobile/src/features/files/filePath.ts +++ b/apps/mobile/src/features/files/filePath.ts @@ -1,3 +1,5 @@ +import type { FilesystemEntryMetadata } from "@t3tools/contracts"; +import { filePreviewDelimiter } from "@t3tools/shared/delimitedPreview"; import { joinPath, resolvePathLinkTarget } from "@t3tools/shared/fileLinks"; import { isWorkspaceAudioPreviewPath, @@ -109,6 +111,23 @@ export function isMarkdownPreviewFile(path: string): boolean { return /\.(?:md|mdx)$/i.test(path.split(/[?#]/, 1)[0] ?? ""); } +export function needsWorkspaceFileContents(input: { + readonly relativePath: string | null; + readonly metadataKind: FilesystemEntryMetadata["kind"] | undefined; + readonly activeMode: "preview" | "source"; +}): boolean { + const path = input.relativePath; + return ( + input.metadataKind !== "directory" && + path !== null && + !isVideoPreviewFile(path) && + !isAudioPreviewFile(path) && + (input.activeMode === "source" || + isMarkdownPreviewFile(path) || + filePreviewDelimiter({ name: path }) !== null) + ); +} + export function fileHeaderSubtitle(projectName: string, relativePath: string): string { const parentDir = relativePath.slice( 0,