From 7a0e653af8f6d76e34fcf863a7cf215b7312a501 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Andr=C3=A9s=20Leiva=20Castillo?= Date: Sat, 20 Jun 2026 21:53:32 -0600 Subject: [PATCH 1/6] fix(fs): cap file list and timeout untracked scan to prevent session init OOM Large repos (100k+ files, unignored __pycache__ / venvs) caused git ls-files --others to hang for minutes and allocate hundreds of MB in the workspace-server process. The resulting GC pressure starved the concurrent session initializationResult() promise, reliably hitting the 30s timeout when adding a large project. Two-part fix in FsService.listRepoFiles: - Abort git ls-files --others after 8 s via AbortController; fall back to [] - Cap combined tracked + untracked array at 50,000 entries before building the directory tree (avoids ~200MB+ allocation for very large repos) Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01TpGjPYBD4pgZpsAHjozzsN --- .../src/services/fs/service.test.ts | 38 ++++++++++++++++-- .../src/services/fs/service.ts | 39 +++++++++++++++++-- 2 files changed, 70 insertions(+), 7 deletions(-) diff --git a/packages/workspace-server/src/services/fs/service.test.ts b/packages/workspace-server/src/services/fs/service.test.ts index ae7cd89a32..e7276e1011 100644 --- a/packages/workspace-server/src/services/fs/service.test.ts +++ b/packages/workspace-server/src/services/fs/service.test.ts @@ -5,20 +5,26 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; vi.mock("@posthog/git/queries", () => ({ getChangedFiles: vi.fn(async () => new Set()), - listAllFiles: vi.fn(async () => []), + listFiles: vi.fn(async () => []), + listUntrackedFiles: vi.fn(async () => []), })); -import { getChangedFiles, listAllFiles } from "@posthog/git/queries"; +import { + getChangedFiles, + listFiles, + listUntrackedFiles, +} from "@posthog/git/queries"; import { FsService } from "./service"; describe("FsService.listRepoFiles", () => { it("derives directory entries alongside files", async () => { vi.mocked(getChangedFiles).mockResolvedValue(new Set()); - vi.mocked(listAllFiles).mockResolvedValue([ + vi.mocked(listFiles).mockResolvedValue([ "a.ts", "src/b.ts", "src/sub/c.ts", ]); + vi.mocked(listUntrackedFiles).mockResolvedValue([]); const service = new FsService(); const entries = await service.listRepoFiles("/repo"); @@ -34,11 +40,12 @@ describe("FsService.listRepoFiles", () => { it("filters directories and files by query substring", async () => { vi.mocked(getChangedFiles).mockResolvedValue(new Set()); - vi.mocked(listAllFiles).mockResolvedValue([ + vi.mocked(listFiles).mockResolvedValue([ "a.ts", "src/b.ts", "src/sub/c.ts", ]); + vi.mocked(listUntrackedFiles).mockResolvedValue([]); const service = new FsService(); const entries = await service.listRepoFiles("/repo", "sub"); @@ -48,6 +55,29 @@ describe("FsService.listRepoFiles", () => { { path: "src/sub/c.ts", kind: "file" }, ]); }); + + it("caps file list at MAX_REPO_FILES when repo is very large", async () => { + vi.mocked(getChangedFiles).mockResolvedValue(new Set()); + const bigList = Array.from({ length: 60_000 }, (_, i) => `file${i}.ts`); + vi.mocked(listFiles).mockResolvedValue(bigList); + vi.mocked(listUntrackedFiles).mockResolvedValue([]); + + const service = new FsService(); + const entries = await service.listRepoFiles("/repo"); + + expect(entries.length).toBeLessThanOrEqual(50_000); + }); + + it("omits untracked files when git ls-files --others is aborted", async () => { + vi.mocked(getChangedFiles).mockResolvedValue(new Set()); + vi.mocked(listFiles).mockResolvedValue(["tracked.ts"]); + vi.mocked(listUntrackedFiles).mockRejectedValue(new Error("AbortError")); + + const service = new FsService(); + const entries = await service.listRepoFiles("/repo"); + + expect(entries.some((e) => e.path === "tracked.ts")).toBe(true); + }); }); describe("FsService repo file IO", () => { diff --git a/packages/workspace-server/src/services/fs/service.ts b/packages/workspace-server/src/services/fs/service.ts index 251109bc80..d9c61086ac 100644 --- a/packages/workspace-server/src/services/fs/service.ts +++ b/packages/workspace-server/src/services/fs/service.ts @@ -1,6 +1,10 @@ import fs from "node:fs/promises"; import path from "node:path"; -import { getChangedFiles, listAllFiles } from "@posthog/git/queries"; +import { + getChangedFiles, + listFiles, + listUntrackedFiles, +} from "@posthog/git/queries"; import { injectable } from "inversify"; import type { BoundedReadResult, DirectoryEntry, FileEntry } from "./schemas"; @@ -8,6 +12,12 @@ import type { BoundedReadResult, DirectoryEntry, FileEntry } from "./schemas"; export class FsService { private static readonly CACHE_TTL = 30000; private static readonly READ_REPO_FILES_CONCURRENCY = 24; + // Large repos (100k+ files) cause GC pressure that starves the session-init + // event loop. Cap the combined tracked + untracked list to bound allocation. + private static readonly MAX_REPO_FILES = 50_000; + // Abort git ls-files --others if it takes too long (unignored venvs, caches, + // or staticfiles directories can make it scan millions of entries). + private static readonly UNTRACKED_TIMEOUT_MS = 8_000; private cache = new Map(); async listDirectory(dirPath: string): Promise { @@ -43,7 +53,7 @@ export class FsService { const changedFiles = await getChangedFiles(repoPath); if (query?.trim()) { - const allFiles = await listAllFiles(repoPath); + const allFiles = await this.fetchAllFiles(repoPath); const directories = this.deriveDirectories(allFiles); const lowerQuery = query.toLowerCase(); const matchingDirs = directories.filter((d) => @@ -64,7 +74,7 @@ export class FsService { return limit ? cached.files.slice(0, limit) : cached.files; } - const files = await listAllFiles(repoPath); + const files = await this.fetchAllFiles(repoPath); const directories = this.deriveDirectories(files); const entries = [ ...this.toDirectoryEntries(directories), @@ -221,6 +231,29 @@ export class FsService { })); } + private async fetchAllFiles(repoPath: string): Promise { + const controller = new AbortController(); + const timer = setTimeout( + () => controller.abort(), + FsService.UNTRACKED_TIMEOUT_MS, + ); + try { + const [tracked, untracked] = await Promise.all([ + listFiles(repoPath), + listUntrackedFiles(repoPath, { abortSignal: controller.signal }).catch( + () => [], + ), + ]); + const combined = tracked.concat(untracked); + if (combined.length > FsService.MAX_REPO_FILES) { + combined.length = FsService.MAX_REPO_FILES; + } + return combined; + } finally { + clearTimeout(timer); + } + } + private deriveDirectories(files: string[]): string[] { const dirs = new Set(); for (const file of files) { From 3a883ac7b4f7b1b18535676997c16067136da8db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Andr=C3=A9s=20Leiva=20Castillo?= Date: Sat, 20 Jun 2026 22:17:25 -0600 Subject: [PATCH 2/6] test(fs): tighten cap assertion and add nested-path case per Greptile review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use toBe(50_000) for flat input — zero derived directories means total === cap - Add test documenting that entries total can exceed 50k when nested paths produce extra directory entries via deriveDirectories (cap is on raw files, not on the final files+dirs result) Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01TpGjPYBD4pgZpsAHjozzsN --- .../src/services/fs/service.test.ts | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/workspace-server/src/services/fs/service.test.ts b/packages/workspace-server/src/services/fs/service.test.ts index e7276e1011..a1e45a262c 100644 --- a/packages/workspace-server/src/services/fs/service.test.ts +++ b/packages/workspace-server/src/services/fs/service.test.ts @@ -58,6 +58,7 @@ describe("FsService.listRepoFiles", () => { it("caps file list at MAX_REPO_FILES when repo is very large", async () => { vi.mocked(getChangedFiles).mockResolvedValue(new Set()); + // Flat paths produce zero derived directory entries, so total === cap. const bigList = Array.from({ length: 60_000 }, (_, i) => `file${i}.ts`); vi.mocked(listFiles).mockResolvedValue(bigList); vi.mocked(listUntrackedFiles).mockResolvedValue([]); @@ -65,7 +66,26 @@ describe("FsService.listRepoFiles", () => { const service = new FsService(); const entries = await service.listRepoFiles("/repo"); - expect(entries.length).toBeLessThanOrEqual(50_000); + expect(entries.length).toBe(50_000); + }); + + it("total entries can exceed MAX_REPO_FILES when derived directories are included", async () => { + vi.mocked(getChangedFiles).mockResolvedValue(new Set()); + // Nested paths cause deriveDirectories to add parent directory entries on + // top of the capped 50k file entries, so the returned total is > 50k. + const bigList = Array.from( + { length: 60_000 }, + (_, i) => `src/sub${i}/file.ts`, + ); + vi.mocked(listFiles).mockResolvedValue(bigList); + vi.mocked(listUntrackedFiles).mockResolvedValue([]); + + const service = new FsService(); + const entries = await service.listRepoFiles("/repo"); + + const fileEntries = entries.filter((e) => e.kind === "file"); + expect(fileEntries.length).toBe(50_000); + expect(entries.length).toBeGreaterThan(50_000); }); it("omits untracked files when git ls-files --others is aborted", async () => { From 7719f2535573d85ca29da0a8adfbe2d6e4daca56 Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Sun, 12 Jul 2026 22:03:10 -0700 Subject: [PATCH 3/6] remove comments from fs service --- packages/workspace-server/src/services/fs/service.test.ts | 3 --- packages/workspace-server/src/services/fs/service.ts | 4 ---- 2 files changed, 7 deletions(-) diff --git a/packages/workspace-server/src/services/fs/service.test.ts b/packages/workspace-server/src/services/fs/service.test.ts index a1e45a262c..3ccf595339 100644 --- a/packages/workspace-server/src/services/fs/service.test.ts +++ b/packages/workspace-server/src/services/fs/service.test.ts @@ -58,7 +58,6 @@ describe("FsService.listRepoFiles", () => { it("caps file list at MAX_REPO_FILES when repo is very large", async () => { vi.mocked(getChangedFiles).mockResolvedValue(new Set()); - // Flat paths produce zero derived directory entries, so total === cap. const bigList = Array.from({ length: 60_000 }, (_, i) => `file${i}.ts`); vi.mocked(listFiles).mockResolvedValue(bigList); vi.mocked(listUntrackedFiles).mockResolvedValue([]); @@ -71,8 +70,6 @@ describe("FsService.listRepoFiles", () => { it("total entries can exceed MAX_REPO_FILES when derived directories are included", async () => { vi.mocked(getChangedFiles).mockResolvedValue(new Set()); - // Nested paths cause deriveDirectories to add parent directory entries on - // top of the capped 50k file entries, so the returned total is > 50k. const bigList = Array.from( { length: 60_000 }, (_, i) => `src/sub${i}/file.ts`, diff --git a/packages/workspace-server/src/services/fs/service.ts b/packages/workspace-server/src/services/fs/service.ts index d9c61086ac..fc4e720b32 100644 --- a/packages/workspace-server/src/services/fs/service.ts +++ b/packages/workspace-server/src/services/fs/service.ts @@ -12,11 +12,7 @@ import type { BoundedReadResult, DirectoryEntry, FileEntry } from "./schemas"; export class FsService { private static readonly CACHE_TTL = 30000; private static readonly READ_REPO_FILES_CONCURRENCY = 24; - // Large repos (100k+ files) cause GC pressure that starves the session-init - // event loop. Cap the combined tracked + untracked list to bound allocation. private static readonly MAX_REPO_FILES = 50_000; - // Abort git ls-files --others if it takes too long (unignored venvs, caches, - // or staticfiles directories can make it scan millions of entries). private static readonly UNTRACKED_TIMEOUT_MS = 8_000; private cache = new Map(); From 49bb7ffb3806df0552387964d16d38f8c4d197b3 Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Sun, 12 Jul 2026 22:20:04 -0700 Subject: [PATCH 4/6] consolidate file-list cap and timeout into listAllFiles --- packages/git/src/queries.test.ts | 42 ++++++++++++++++ packages/git/src/queries.ts | 36 +++++++++++--- .../src/services/fs/service.test.ts | 48 ++++++------------- .../src/services/fs/service.ts | 38 ++++----------- 4 files changed, 96 insertions(+), 68 deletions(-) diff --git a/packages/git/src/queries.test.ts b/packages/git/src/queries.test.ts index 623df45b2e..859cf44b89 100644 --- a/packages/git/src/queries.test.ts +++ b/packages/git/src/queries.test.ts @@ -13,6 +13,7 @@ import { getChangedFilesDetailed, getGitBusyState, getLinkedWorktreeMainPath, + listAllFiles, remoteBranchExists, splitUnifiedDiffByFile, } from "./queries"; @@ -597,3 +598,44 @@ describe("getLinkedWorktreeMainPath", () => { expect(getLinkedWorktreeMainPath(worktreeDir)).toBeNull(); }); }); + +describe("listAllFiles", () => { + let repoDir: string; + + afterEach(async () => { + if (repoDir) { + await rm(repoDir, { recursive: true, force: true }); + } + }); + + it("combines tracked and untracked files uncapped by default", async () => { + repoDir = await setupRepo(); + await writeFile(path.join(repoDir, "untracked.txt"), "content"); + + const files = await listAllFiles(repoDir); + + expect(files.sort()).toEqual(["file.txt", "untracked.txt"]); + }); + + it("truncates to maxFiles", async () => { + repoDir = await setupRepo(); + const git = createGitClient(repoDir); + await writeFile(path.join(repoDir, "b.txt"), "content"); + await writeFile(path.join(repoDir, "c.txt"), "content"); + await git.add(["b.txt", "c.txt"]); + await git.commit("add more files"); + + const files = await listAllFiles(repoDir, { maxFiles: 2 }); + + expect(files.length).toBe(2); + }); + + it("keeps untracked files over tracked ones when truncating", async () => { + repoDir = await setupRepo(); + await writeFile(path.join(repoDir, "untracked.txt"), "content"); + + const files = await listAllFiles(repoDir, { maxFiles: 1 }); + + expect(files).toEqual(["untracked.txt"]); + }); +}); diff --git a/packages/git/src/queries.ts b/packages/git/src/queries.ts index 84708d59c8..1306858ff6 100644 --- a/packages/git/src/queries.ts +++ b/packages/git/src/queries.ts @@ -1181,15 +1181,39 @@ export async function listUntrackedFiles( ); } +export interface ListAllFilesOptions { + maxFiles?: number; + timeoutMs?: number; +} + export async function listAllFiles( baseDir: string, - options?: CreateGitClientOptions, + options?: ListAllFilesOptions, ): Promise { - const [tracked, untracked] = await Promise.all([ - listFiles(baseDir, options), - listUntrackedFiles(baseDir, options), - ]); - return [...tracked, ...untracked]; + const { maxFiles, timeoutMs } = options ?? {}; + const controller = + timeoutMs !== undefined ? new AbortController() : undefined; + const timer = + controller && timeoutMs !== undefined + ? setTimeout(() => controller.abort(), timeoutMs) + : undefined; + try { + const [tracked, untracked] = await Promise.all([ + listFiles(baseDir, { abortSignal: controller?.signal }).catch( + (): string[] => [], + ), + listUntrackedFiles(baseDir, { abortSignal: controller?.signal }).catch( + (): string[] => [], + ), + ]); + const combined = untracked.concat(tracked); + if (maxFiles !== undefined && combined.length > maxFiles) { + combined.splice(maxFiles); + } + return combined; + } finally { + if (timer) clearTimeout(timer); + } } // Tracked + untracked files containing `pattern` (literal, case-insensitive). diff --git a/packages/workspace-server/src/services/fs/service.test.ts b/packages/workspace-server/src/services/fs/service.test.ts index 3ccf595339..0309c3ca4f 100644 --- a/packages/workspace-server/src/services/fs/service.test.ts +++ b/packages/workspace-server/src/services/fs/service.test.ts @@ -5,26 +5,20 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; vi.mock("@posthog/git/queries", () => ({ getChangedFiles: vi.fn(async () => new Set()), - listFiles: vi.fn(async () => []), - listUntrackedFiles: vi.fn(async () => []), + listAllFiles: vi.fn(async () => []), })); -import { - getChangedFiles, - listFiles, - listUntrackedFiles, -} from "@posthog/git/queries"; +import { getChangedFiles, listAllFiles } from "@posthog/git/queries"; import { FsService } from "./service"; describe("FsService.listRepoFiles", () => { it("derives directory entries alongside files", async () => { vi.mocked(getChangedFiles).mockResolvedValue(new Set()); - vi.mocked(listFiles).mockResolvedValue([ + vi.mocked(listAllFiles).mockResolvedValue([ "a.ts", "src/b.ts", "src/sub/c.ts", ]); - vi.mocked(listUntrackedFiles).mockResolvedValue([]); const service = new FsService(); const entries = await service.listRepoFiles("/repo"); @@ -40,12 +34,11 @@ describe("FsService.listRepoFiles", () => { it("filters directories and files by query substring", async () => { vi.mocked(getChangedFiles).mockResolvedValue(new Set()); - vi.mocked(listFiles).mockResolvedValue([ + vi.mocked(listAllFiles).mockResolvedValue([ "a.ts", "src/b.ts", "src/sub/c.ts", ]); - vi.mocked(listUntrackedFiles).mockResolvedValue([]); const service = new FsService(); const entries = await service.listRepoFiles("/repo", "sub"); @@ -56,26 +49,26 @@ describe("FsService.listRepoFiles", () => { ]); }); - it("caps file list at MAX_REPO_FILES when repo is very large", async () => { + it("passes the file cap and timeout through to listAllFiles", async () => { vi.mocked(getChangedFiles).mockResolvedValue(new Set()); - const bigList = Array.from({ length: 60_000 }, (_, i) => `file${i}.ts`); - vi.mocked(listFiles).mockResolvedValue(bigList); - vi.mocked(listUntrackedFiles).mockResolvedValue([]); + vi.mocked(listAllFiles).mockResolvedValue([]); const service = new FsService(); - const entries = await service.listRepoFiles("/repo"); + await service.listRepoFiles("/repo"); - expect(entries.length).toBe(50_000); + expect(listAllFiles).toHaveBeenCalledWith("/repo", { + maxFiles: 50_000, + timeoutMs: 8_000, + }); }); - it("total entries can exceed MAX_REPO_FILES when derived directories are included", async () => { + it("total entries can exceed the file cap when derived directories are included", async () => { vi.mocked(getChangedFiles).mockResolvedValue(new Set()); - const bigList = Array.from( - { length: 60_000 }, + const cappedList = Array.from( + { length: 50_000 }, (_, i) => `src/sub${i}/file.ts`, ); - vi.mocked(listFiles).mockResolvedValue(bigList); - vi.mocked(listUntrackedFiles).mockResolvedValue([]); + vi.mocked(listAllFiles).mockResolvedValue(cappedList); const service = new FsService(); const entries = await service.listRepoFiles("/repo"); @@ -84,17 +77,6 @@ describe("FsService.listRepoFiles", () => { expect(fileEntries.length).toBe(50_000); expect(entries.length).toBeGreaterThan(50_000); }); - - it("omits untracked files when git ls-files --others is aborted", async () => { - vi.mocked(getChangedFiles).mockResolvedValue(new Set()); - vi.mocked(listFiles).mockResolvedValue(["tracked.ts"]); - vi.mocked(listUntrackedFiles).mockRejectedValue(new Error("AbortError")); - - const service = new FsService(); - const entries = await service.listRepoFiles("/repo"); - - expect(entries.some((e) => e.path === "tracked.ts")).toBe(true); - }); }); describe("FsService repo file IO", () => { diff --git a/packages/workspace-server/src/services/fs/service.ts b/packages/workspace-server/src/services/fs/service.ts index fc4e720b32..64e53589af 100644 --- a/packages/workspace-server/src/services/fs/service.ts +++ b/packages/workspace-server/src/services/fs/service.ts @@ -1,10 +1,6 @@ import fs from "node:fs/promises"; import path from "node:path"; -import { - getChangedFiles, - listFiles, - listUntrackedFiles, -} from "@posthog/git/queries"; +import { getChangedFiles, listAllFiles } from "@posthog/git/queries"; import { injectable } from "inversify"; import type { BoundedReadResult, DirectoryEntry, FileEntry } from "./schemas"; @@ -13,7 +9,7 @@ export class FsService { private static readonly CACHE_TTL = 30000; private static readonly READ_REPO_FILES_CONCURRENCY = 24; private static readonly MAX_REPO_FILES = 50_000; - private static readonly UNTRACKED_TIMEOUT_MS = 8_000; + private static readonly LIST_FILES_TIMEOUT_MS = 8_000; private cache = new Map(); async listDirectory(dirPath: string): Promise { @@ -49,7 +45,7 @@ export class FsService { const changedFiles = await getChangedFiles(repoPath); if (query?.trim()) { - const allFiles = await this.fetchAllFiles(repoPath); + const allFiles = await this.listAllFilesBounded(repoPath); const directories = this.deriveDirectories(allFiles); const lowerQuery = query.toLowerCase(); const matchingDirs = directories.filter((d) => @@ -70,7 +66,7 @@ export class FsService { return limit ? cached.files.slice(0, limit) : cached.files; } - const files = await this.fetchAllFiles(repoPath); + const files = await this.listAllFilesBounded(repoPath); const directories = this.deriveDirectories(files); const entries = [ ...this.toDirectoryEntries(directories), @@ -227,27 +223,11 @@ export class FsService { })); } - private async fetchAllFiles(repoPath: string): Promise { - const controller = new AbortController(); - const timer = setTimeout( - () => controller.abort(), - FsService.UNTRACKED_TIMEOUT_MS, - ); - try { - const [tracked, untracked] = await Promise.all([ - listFiles(repoPath), - listUntrackedFiles(repoPath, { abortSignal: controller.signal }).catch( - () => [], - ), - ]); - const combined = tracked.concat(untracked); - if (combined.length > FsService.MAX_REPO_FILES) { - combined.length = FsService.MAX_REPO_FILES; - } - return combined; - } finally { - clearTimeout(timer); - } + private listAllFilesBounded(repoPath: string): Promise { + return listAllFiles(repoPath, { + maxFiles: FsService.MAX_REPO_FILES, + timeoutMs: FsService.LIST_FILES_TIMEOUT_MS, + }); } private deriveDirectories(files: string[]): string[] { From c64f9ba43b5e9c1f576e0c09106bc19e27a0875f Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Mon, 13 Jul 2026 16:49:11 -0700 Subject: [PATCH 5/6] keep tracked files when untracked scan times out --- packages/git/src/queries.test.ts | 9 +++++++++ packages/git/src/queries.ts | 4 +--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/git/src/queries.test.ts b/packages/git/src/queries.test.ts index 859cf44b89..763bc55bd8 100644 --- a/packages/git/src/queries.test.ts +++ b/packages/git/src/queries.test.ts @@ -638,4 +638,13 @@ describe("listAllFiles", () => { expect(files).toEqual(["untracked.txt"]); }); + + it("returns tracked files when the untracked scan times out", async () => { + repoDir = await setupRepo(); + await writeFile(path.join(repoDir, "untracked.txt"), "content"); + + const files = await listAllFiles(repoDir, { timeoutMs: 0 }); + + expect(files).toContain("file.txt"); + }); }); diff --git a/packages/git/src/queries.ts b/packages/git/src/queries.ts index 1306858ff6..aa08c2fbac 100644 --- a/packages/git/src/queries.ts +++ b/packages/git/src/queries.ts @@ -1199,9 +1199,7 @@ export async function listAllFiles( : undefined; try { const [tracked, untracked] = await Promise.all([ - listFiles(baseDir, { abortSignal: controller?.signal }).catch( - (): string[] => [], - ), + listFiles(baseDir).catch((): string[] => []), listUntrackedFiles(baseDir, { abortSignal: controller?.signal }).catch( (): string[] => [], ), From 117568c9e4adf0ff4ea5be1f766d7cb639cc1c4e Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Mon, 13 Jul 2026 16:49:58 -0700 Subject: [PATCH 6/6] prefer tracked files when capping repo file list --- packages/git/src/queries.test.ts | 4 ++-- packages/git/src/queries.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/git/src/queries.test.ts b/packages/git/src/queries.test.ts index 763bc55bd8..7cee73847f 100644 --- a/packages/git/src/queries.test.ts +++ b/packages/git/src/queries.test.ts @@ -630,13 +630,13 @@ describe("listAllFiles", () => { expect(files.length).toBe(2); }); - it("keeps untracked files over tracked ones when truncating", async () => { + it("keeps tracked files over untracked ones when truncating", async () => { repoDir = await setupRepo(); await writeFile(path.join(repoDir, "untracked.txt"), "content"); const files = await listAllFiles(repoDir, { maxFiles: 1 }); - expect(files).toEqual(["untracked.txt"]); + expect(files).toEqual(["file.txt"]); }); it("returns tracked files when the untracked scan times out", async () => { diff --git a/packages/git/src/queries.ts b/packages/git/src/queries.ts index aa08c2fbac..10148770c9 100644 --- a/packages/git/src/queries.ts +++ b/packages/git/src/queries.ts @@ -1204,7 +1204,7 @@ export async function listAllFiles( (): string[] => [], ), ]); - const combined = untracked.concat(tracked); + const combined = tracked.concat(untracked); if (maxFiles !== undefined && combined.length > maxFiles) { combined.splice(maxFiles); }