From 301f798f15d72628849056ee92b026d91f66d750 Mon Sep 17 00:00:00 2001 From: ashrafchowdury Date: Fri, 4 Sep 2026 06:11:53 +0600 Subject: [PATCH 1/4] fix(files): exclude hidden files from the Files configuration count and list The Files section counted and listed dot-prefixed paths (.env, .claude/, .gitignore), which is plumbing rather than user content and made the count misleading. The count is a server-side scalar, so the backend's curated flat view (limit=0 count and order=recent list) now drops hidden paths; the recency branch's own hidden filter folds into it. The summary hook drops them from its record-log recents and depth=1 root fallback, since depth=1 must keep serving them to the browse explorer. Raw listings, the browse tree, depth=1 levels, storage and runtime are unchanged: the explorer still shows hidden files behind its show-hidden toggle. Closes #6027 --- api/oss/src/core/mounts/service.py | 19 +++-- .../tests/pytest/unit/test_mounts_file_ops.py | 75 ++++++++++++++++++- .../src/drive/useSessionDrive.ts | 26 +++++-- .../tests/unit/summary-drive-path.test.ts | 31 ++++++++ 4 files changed, 135 insertions(+), 16 deletions(-) create mode 100644 web/packages/agenta-entities/tests/unit/summary-drive-path.test.ts diff --git a/api/oss/src/core/mounts/service.py b/api/oss/src/core/mounts/service.py index e33e6454158..7f51d8a43a7 100644 --- a/api/oss/src/core/mounts/service.py +++ b/api/oss/src/core/mounts/service.py @@ -251,8 +251,9 @@ def _is_git_plumbing(path: str) -> bool: def _is_hidden_path(path: str) -> bool: """A dot-prefixed (hidden) file or folder anywhere in the path — `.claude/…`, `.gitignore`, etc. - Mirrors the web `isHiddenPath`. Dropped from the RECENCY view only (it is meant to read like - "what did I just work on", not dotfile plumbing); the browsable tree still lists them (dimmed).""" + Mirrors the web `isHiddenPath`. Dropped from the curated FLAT view (count + recency): those read + like "what is in my drive / what did I just work on", not dotfile plumbing. The browsable tree + and `depth=1` levels still list them (dimmed), behind the UI's "show hidden" toggle.""" return any( segment.startswith(".") for segment in path.strip("/").split("/") if segment ) @@ -1069,6 +1070,8 @@ async def list_files( output) are pruned, runner-internal artifacts are hidden, and — for perf — the flat/recency modes descend level-by-level pruning ignored DIRECTORIES at the store layer instead of enumerating a `node_modules` dump. Pruning drives both the count and the tree in that mode. + The curated FLAT view (count + recency) additionally drops dot-prefixed paths; the browse and + `depth=1` views keep them, so the explorer can still show them behind its own toggle. `include_gitignored` (git_aware only) surfaces `.gitignore`-matched files again — the UI's "show git-ignored files" toggle — while STILL hiding `.git` plumbing and runner internals. @@ -1272,15 +1275,19 @@ async def _count_children(sub_rel: str) -> Tuple[str, int]: f for f in files if not _path_gitignored(f.path, False, specs) ] files = [f for f in files if not _is_internal_mount_path(f.path)] + # Dotfile plumbing (`.claude/…`, `.gitignore`, `.env`) is not user content, so the + # curated flat view — the "N files" badge AND the recency list it labels — leaves it + # out entirely. The browsable tree still lists it (dimmed, behind the UI's "show + # hidden" toggle), which is why this drops here and not in the browse/`depth=1` views. + files = [f for f in files if not _is_hidden_path(f.path)] total = len(files) if count_only: return MountFileList(files=[], total=total, total_capped=truncated) if order == "recent": if git_aware: - # Drop dotfile plumbing (`.claude/…`, `.gitignore`) — the recency list reads as - # "what did I just work on" — then roll a fresh directory into one folder row. - visible = [f for f in files if not _is_hidden_path(f.path)] - entries = _rollup_recent_entries(visible, limit) + # Hidden/internal plumbing is already gone above; roll a fresh directory into + # one folder row so the list reads as "what did I just work on". + entries = _rollup_recent_entries(files, limit) return MountFileList(files=entries, total=total) # RAW recency: newest object-store mtime first, no rollup/hidden pruning. files.sort(key=lambda f: f.mtime or 0, reverse=True) diff --git a/api/oss/tests/pytest/unit/test_mounts_file_ops.py b/api/oss/tests/pytest/unit/test_mounts_file_ops.py index eff508265d1..783047a24c5 100644 --- a/api/oss/tests/pytest/unit/test_mounts_file_ops.py +++ b/api/oss/tests/pytest/unit/test_mounts_file_ops.py @@ -632,12 +632,83 @@ async def test_git_aware_recency_descent_prunes_gitignored_dirs(self): listing = await service.list_files( project_id=pid, mount_id=mid, order="path", limit=100, git_aware=True ) + # `.gitignore` itself is a dotfile, so the curated flat view drops it as hidden plumbing. assert {f.path for f in listing.files} == { - ".gitignore", "api/main.py", "web/index.ts", } - assert listing.total == 3 + assert listing.total == 2 + + async def test_git_aware_flat_listing_excludes_hidden_paths(self): + # Dot-prefixed files and directories are plumbing, not user content: the curated flat view + # leaves them out of both the listing and its `total` (#6027). + mount = _make_mount() + service, pid, mid = _make_service(mount) + for path in [ + "notes.md", + ".env", + ".claude/settings.json", + "src/.hidden/keep.txt", + "src/main.py", + ]: + await service.write_file( + project_id=pid, mount_id=mid, path=path, content=b"x" + ) + + listing = await service.list_files( + project_id=pid, mount_id=mid, order="path", limit=100, git_aware=True + ) + + assert {f.path for f in listing.files} == {"notes.md", "src/main.py"} + assert listing.total == 2 + + async def test_git_aware_count_only_excludes_hidden_paths(self): + # The "N files" badge reads the count-only total, so it must agree with the list above. + mount = _make_mount() + service, pid, mid = _make_service(mount) + for path in ["notes.md", ".env", ".claude/settings.json"]: + await service.write_file( + project_id=pid, mount_id=mid, path=path, content=b"x" + ) + + listing = await service.list_files( + project_id=pid, mount_id=mid, limit=0, git_aware=True + ) + + assert listing.total == 1 + assert listing.files == [] + + async def test_raw_flat_listing_keeps_hidden_paths(self): + # The hidden-file pruning is part of the CURATED view only — the raw contract still lists + # everything that is stored, so other API consumers see the mount as it really is. + mount = _make_mount() + service, pid, mid = _make_service(mount) + for path in ["notes.md", ".env"]: + await service.write_file( + project_id=pid, mount_id=mid, path=path, content=b"x" + ) + + listing = await service.list_files( + project_id=pid, mount_id=mid, order="path", limit=100, git_aware=False + ) + + assert {f.path for f in listing.files} == {"notes.md", ".env"} + + async def test_git_aware_shallow_listing_keeps_hidden_paths(self): + # `depth=1` backs the browsable explorer, which shows hidden entries (dimmed) behind its own + # toggle — so the pruning above must NOT reach this view. + mount = _make_mount() + service, pid, mid = _make_service(mount) + for path in ["notes.md", ".env"]: + await service.write_file( + project_id=pid, mount_id=mid, path=path, content=b"x" + ) + + listing = await service.list_files( + project_id=pid, mount_id=mid, depth=1, git_aware=True + ) + + assert {f.path for f in listing.files} == {"notes.md", ".env"} async def test_raw_listing_keeps_git_and_ignored_by_default(self): # Default (git_aware=False) is the plain-endpoint contract: EVERY object under the prefix, incl. diff --git a/web/packages/agenta-entities/src/drive/useSessionDrive.ts b/web/packages/agenta-entities/src/drive/useSessionDrive.ts index b281085c128..a304b49afef 100644 --- a/web/packages/agenta-entities/src/drive/useSessionDrive.ts +++ b/web/packages/agenta-entities/src/drive/useSessionDrive.ts @@ -17,7 +17,13 @@ import { } from "@agenta/entities/session" import {agentMountQueryFamily} from "./agentDrive" -import {cleanPath, driveFileStats, isInternalDrivePath, relativeTime} from "./driveTree" +import { + cleanPath, + driveFileStats, + isHiddenPath, + isInternalDrivePath, + relativeTime, +} from "./driveTree" /** The agent's durable mount is symlinked into the session cwd under this name (runner: * `AGENT_FILES_LINK_NAME`). Its files live in a SEPARATE mount/prefix, so the drive folds them in @@ -379,6 +385,10 @@ const SUMMARY_LATEST_LIMIT = 5 * - The COUNT is a BOUNDED `limit=0` scan per mount (`total`/`total_capped`): the backend stops * after a cap and reports "N+", so the "N files" badge never blocks on enumerating a huge tree. * + * Neither the count nor the lists include hidden (dot-prefixed) paths — this chrome is about user + * content, not plumbing (#6027). The browse explorer still lists them, behind its "show hidden" + * toggle; nothing about storage or what the agent can reach changes. + * * Returns the same {@link SessionDriveData} shape so consumers are unchanged. */ export function useSessionDriveSummary(sessionId: string, artifactId?: string): SessionDriveData { @@ -399,10 +409,10 @@ export function useSessionDriveSummary(sessionId: string, artifactId?: string): const recordRecency = useAtomValue(sessionRecordFileRecencyAtomFamily(sessionId)) // The record log's tool paths AS DRIVE PATHS: sandbox workspace root stripped, mount origin - // tagged, anything naming no drive file dropped (see `drivePathFromToolPath`). Filtered on the - // mount-relative path, before the fold prefix, exactly as the root listing filters its own. - // ONE derivation, shared with the gate below, so the gate can't withhold the root-listing - // fallback over a row the list then drops. + // tagged, anything naming no drive file dropped (see `drivePathFromToolPath`). Filtered by + // `isSummaryDrivePath` on the mount-relative path, before the fold prefix, exactly as the root + // listing below filters its own. ONE derivation, shared with the gate below, so the gate can't + // withhold the root-listing fallback over a row the list then drops. const recordFiles = useMemo( () => [...recordRecency.entries()] @@ -410,7 +420,7 @@ export function useSessionDriveSummary(sessionId: string, artifactId?: string): .filter( (entry): entry is {resolved: DriveToolPath; at: number} => entry.resolved !== null && - isListableDrivePath(entry.resolved.path, { + isSummaryDrivePath(entry.resolved.path, { fromAgentMount: entry.resolved.origin === "agent", }), ), @@ -484,9 +494,9 @@ export function useSessionDriveSummary(sessionId: string, artifactId?: string): // `agent-files` from the session mount is the fold marker and goes, the same name from the // agent mount is a real directory and stays. const rootEntries: MountFile[] = [ - ...(rootQuery.data ?? []).filter((f) => isListableDrivePath(f.path)), + ...(rootQuery.data ?? []).filter((f) => isSummaryDrivePath(f.path)), ...(agentRootQuery.data ?? []) - .filter((f) => isListableDrivePath(f.path, {fromAgentMount: true})) + .filter((f) => isSummaryDrivePath(f.path, {fromAgentMount: true})) .map((f) => ({ ...f, path: `${agentPrefix}${cleanPath(f.path)}`, diff --git a/web/packages/agenta-entities/tests/unit/summary-drive-path.test.ts b/web/packages/agenta-entities/tests/unit/summary-drive-path.test.ts new file mode 100644 index 00000000000..09fbdbdc7a3 --- /dev/null +++ b/web/packages/agenta-entities/tests/unit/summary-drive-path.test.ts @@ -0,0 +1,31 @@ +import {describe, expect, it} from "vitest" + +import {isListableDrivePath, isSummaryDrivePath} from "../../src/drive/useSessionDrive" + +describe("isSummaryDrivePath", () => { + it("keeps ordinary user files", () => { + expect(isSummaryDrivePath("notes.md")).toBe(true) + expect(isSummaryDrivePath("src/main.py")).toBe(true) + }) + + it("drops hidden paths at any depth, so they leave the count and the list together (#6027)", () => { + expect(isSummaryDrivePath(".env")).toBe(false) + expect(isSummaryDrivePath(".claude/settings.json")).toBe(false) + expect(isSummaryDrivePath("src/.hidden/keep.txt")).toBe(false) + }) + + it("still drops what isListableDrivePath drops", () => { + expect(isSummaryDrivePath("agents/sessions/x.json")).toBe(false) + expect(isSummaryDrivePath("agent-files")).toBe(false) + }) + + it("keeps a real `agent-files` directory from the agent mount", () => { + expect(isSummaryDrivePath("agent-files", {fromAgentMount: true})).toBe(true) + }) + + it("is strictly narrower than isListableDrivePath — hidden is the only difference", () => { + // The browse explorer asks the wider question; it lists hidden files behind its own toggle. + expect(isListableDrivePath(".gitignore")).toBe(true) + expect(isSummaryDrivePath(".gitignore")).toBe(false) + }) +}) From 67297336be2da0d81301923921cf262827f30edf Mon Sep 17 00:00:00 2001 From: ashrafchowdury Date: Fri, 4 Sep 2026 13:40:00 +0600 Subject: [PATCH 2/4] fix(files): count the file budget in files the drive will actually show The count cap bounded the DESCENT in raw objects but reported the count after curation, so the two were measured in different units. A drive with a large .claude/ or agents/ tree spent its whole budget on files that were then filtered away, and reported a needless "N+" when its real files could have been counted exactly. Hiding dotfiles from the count made that worse. Prune hidden and runner-internal directories during the walk, where .git and gitignored directories were already pruned, so the budget covers what the caller counts. The file-level filters stay for the matching file in a kept directory, which no directory prune can reach. --- api/oss/src/core/mounts/service.py | 35 +++++++--- .../tests/pytest/unit/test_mounts_file_ops.py | 68 +++++++++++++++++++ 2 files changed, 93 insertions(+), 10 deletions(-) diff --git a/api/oss/src/core/mounts/service.py b/api/oss/src/core/mounts/service.py index 7f51d8a43a7..08fb82f79db 100644 --- a/api/oss/src/core/mounts/service.py +++ b/api/oss/src/core/mounts/service.py @@ -960,9 +960,10 @@ async def _list_pruned_files( mount_base: str, cap: Optional[int] = None, ) -> Tuple[List[StoreObject], List[Tuple[str, "pathspec.PathSpec"]], bool]: - """Enumerate a mount's FILES by descending the tree LEVEL BY LEVEL, skipping `.git` and - gitignored DIRECTORIES at the store layer — so a dependency dump (`node_modules`, tens of - thousands of objects) is never enumerated at all. The flat `recursive=True` listing cannot + """Enumerate a mount's FILES by descending the tree LEVEL BY LEVEL, skipping every directory + the curated view discards — `.git`, gitignored, runner-internal (`agents/`) and hidden + (dot-prefixed) — at the store layer, so a dependency dump (`node_modules`, tens of thousands + of objects) is never enumerated at all. The flat `recursive=True` listing cannot exclude a prefix, so it must scan every object; this walks only what survives, listing sibling directories concurrently (bounded by `_LIST_CONCURRENCY`) so wall-clock tracks the tree DEPTH, not the object count. Each level's `.gitignore` files are read before that level's children are @@ -970,6 +971,9 @@ async def _list_pruned_files( `cap` early-stops the descent once that many files are collected — for a bounded COUNT of a pathologically large (non-ignored) tree, so the cost never runs away regardless of contents. + Because the prunes above run DURING the walk, what `cap` budgets is (near enough) the files + the caller will actually count, so a drive only reports "N+" when it genuinely holds that + many VISIBLE files. Returns (kept StoreObjects, specs, truncated). `truncated` is True when the `cap` stopped the walk early (the real count is higher). The caller still applies FILE-level gitignore for @@ -1034,7 +1038,16 @@ async def _shallow(prefix: str): ).rstrip("/") if not dir_rel: continue - if _is_git_plumbing(dir_rel) or _path_gitignored(dir_rel, True, specs): + # Every directory whose files the curated view would discard anyway. Pruning them + # HERE, not after the walk, is what keeps `cap` a budget of COUNTABLE files: a big + # `.claude/` or `agents/` tree would otherwise spend the budget and then be filtered + # out, reporting a needless "N+" on a drive that could be counted exactly. + if ( + _is_git_plumbing(dir_rel) + or _is_internal_mount_path(dir_rel) + or _is_hidden_path(dir_rel) + or _path_gitignored(dir_rel, True, specs) + ): continue visited.add(sub_prefix) frontier.append(sub_prefix) @@ -1267,18 +1280,20 @@ async def _count_children(sub_rel: str) -> Tuple[str, int]: for o in store_files ] if git_aware: - # Whole-directory pruning happened at the store level; a `.git` file or a gitignored - # FILE inside a KEPT directory (e.g. a stray `*.pyc`) still needs dropping here. + # The descent already pruned these as whole DIRECTORIES. What is left to drop is the + # matching FILE sitting in a KEPT directory — a stray `*.pyc`, a root `.gitignore` or + # `.env`, a `.agenta-*` marker — which no directory prune can reach. + # + # Hidden files leave the curated flat view entirely: this is the "N files" badge and + # the recency list it labels, which are about user content, not plumbing. The + # browsable tree still lists them (dimmed, behind the UI's "show hidden" toggle), + # which is why they drop here and not in the browse/`depth=1` views. files = [f for f in files if not _is_git_plumbing(f.path)] if specs: files = [ f for f in files if not _path_gitignored(f.path, False, specs) ] files = [f for f in files if not _is_internal_mount_path(f.path)] - # Dotfile plumbing (`.claude/…`, `.gitignore`, `.env`) is not user content, so the - # curated flat view — the "N files" badge AND the recency list it labels — leaves it - # out entirely. The browsable tree still lists it (dimmed, behind the UI's "show - # hidden" toggle), which is why this drops here and not in the browse/`depth=1` views. files = [f for f in files if not _is_hidden_path(f.path)] total = len(files) if count_only: diff --git a/api/oss/tests/pytest/unit/test_mounts_file_ops.py b/api/oss/tests/pytest/unit/test_mounts_file_ops.py index 783047a24c5..8a8e879c1a5 100644 --- a/api/oss/tests/pytest/unit/test_mounts_file_ops.py +++ b/api/oss/tests/pytest/unit/test_mounts_file_ops.py @@ -678,6 +678,74 @@ async def test_git_aware_count_only_excludes_hidden_paths(self): assert listing.total == 1 assert listing.files == [] + async def test_hidden_tree_does_not_spend_the_count_budget(self, monkeypatch): + # `cap` bounds the DESCENT, but the number it produces is the VISIBLE file count — so the + # two have to be measured in the same unit. A hidden directory is pruned during the walk, + # not counted and then filtered away afterwards; otherwise a drive with a big `.claude/` + # reports a needless "N+" when its real files could have been counted exactly. + monkeypatch.setattr(mounts_service_module, "_COUNT_CAP", 5) + mount = _make_mount() + service, pid, mid = _make_service(mount) + for i in range(10): + await service.write_file( + project_id=pid, mount_id=mid, path=f".claude/f{i}.json", content=b"x" + ) + for path in ["a.txt", "b.txt"]: + await service.write_file( + project_id=pid, mount_id=mid, path=path, content=b"x" + ) + + listing = await service.list_files( + project_id=pid, mount_id=mid, limit=0, git_aware=True + ) + + assert listing.total == 2 + assert listing.total_capped is False + + async def test_internal_tree_does_not_spend_the_count_budget(self, monkeypatch): + # Same rule for the runner-owned `agents/` namespace, which can hold a whole transcript + # workspace: pruned during the walk, so it never crowds out the real files. + monkeypatch.setattr(mounts_service_module, "_COUNT_CAP", 5) + mount = _make_mount() + service, pid, mid = _make_service(mount) + for i in range(10): + await service.write_file( + project_id=pid, + mount_id=mid, + path=f"agents/sessions/s{i}.json", + content=b"x", + ) + await service.write_file( + project_id=pid, mount_id=mid, path="a.txt", content=b"x" + ) + + listing = await service.list_files( + project_id=pid, mount_id=mid, limit=0, git_aware=True + ) + + assert listing.total == 1 + assert listing.total_capped is False + + async def test_count_still_caps_on_a_genuinely_large_visible_tree( + self, monkeypatch + ): + # The budget still bites when the files really are visible — the prunes narrow what counts, + # they do not remove the bound. + monkeypatch.setattr(mounts_service_module, "_COUNT_CAP", 5) + mount = _make_mount() + service, pid, mid = _make_service(mount) + for i in range(12): + await service.write_file( + project_id=pid, mount_id=mid, path=f"docs/f{i}.md", content=b"x" + ) + + listing = await service.list_files( + project_id=pid, mount_id=mid, limit=0, git_aware=True + ) + + assert listing.total_capped is True + assert listing.total >= 5 + async def test_raw_flat_listing_keeps_hidden_paths(self): # The hidden-file pruning is part of the CURATED view only — the raw contract still lists # everything that is stored, so other API consumers see the mount as it really is. From f7476f795ba456ed5219279f6934c50fa602c837 Mon Sep 17 00:00:00 2001 From: ashrafchowdury Date: Fri, 4 Sep 2026 15:12:24 +0600 Subject: [PATCH 3/4] fix(files): define the summary predicate and keep dotfiles off the count budget isSummaryDrivePath was called at three sites but its definition never landed on this branch, so the package did not compile and the new tests could not run. The count cap also still charged hidden files sitting at a kept directory's own root, which no directory prune can reach. A root holding a few dotfiles could exhaust the budget and report "N+" for a drive that was countable exactly. The level scan now skips them, after reading .gitignore, which is itself hidden. Comments trimmed to the one-line rule in web/AGENTS.md. --- api/oss/src/core/mounts/service.py | 12 ++++- .../tests/pytest/unit/test_mounts_file_ops.py | 47 +++++++++++++++++++ .../src/drive/useSessionDrive.ts | 17 ++++--- 3 files changed, 68 insertions(+), 8 deletions(-) diff --git a/api/oss/src/core/mounts/service.py b/api/oss/src/core/mounts/service.py index 08fb82f79db..953f23c01b1 100644 --- a/api/oss/src/core/mounts/service.py +++ b/api/oss/src/core/mounts/service.py @@ -1003,17 +1003,27 @@ async def _shallow(prefix: str): subdir_prefixes: List[str] = [] for level_files, level_subdirs in listings: for obj in level_files: - kept.append(obj) rel = ( obj.key[len(mount_base) :] if obj.key.startswith(mount_base) else obj.key ) + # Read BEFORE the prune below: `.gitignore` is itself a hidden file. if rel == ".gitignore" or rel.endswith("/.gitignore"): dir_rel = ( "" if rel == ".gitignore" else rel[: -len("/.gitignore")] ) gitignore_reads.append((dir_rel, obj.key)) + # Charge `cap` only for files the caller can actually count, so a root full of + # dotfiles can't report "N+" over an exactly countable drive. Gitignored files + # still pass here; their specs are not in scope until the level is read. + if ( + _is_git_plumbing(rel) + or _is_internal_mount_path(rel) + or _is_hidden_path(rel) + ): + continue + kept.append(obj) subdir_prefixes.extend(level_subdirs) # Bounded COUNT: enough to know it's "more than the cap" — stop before descending further. diff --git a/api/oss/tests/pytest/unit/test_mounts_file_ops.py b/api/oss/tests/pytest/unit/test_mounts_file_ops.py index 8a8e879c1a5..ebda4dbd3f6 100644 --- a/api/oss/tests/pytest/unit/test_mounts_file_ops.py +++ b/api/oss/tests/pytest/unit/test_mounts_file_ops.py @@ -702,6 +702,53 @@ async def test_hidden_tree_does_not_spend_the_count_budget(self, monkeypatch): assert listing.total == 2 assert listing.total_capped is False + async def test_root_dotfiles_do_not_spend_the_count_budget(self, monkeypatch): + # The directory prune cannot reach a hidden file sitting at a KEPT directory's root, so the + # level scan has to skip it too. Otherwise five root dotfiles exhaust the budget and the + # drive reports "1+" for a count it could state exactly. + monkeypatch.setattr(mounts_service_module, "_COUNT_CAP", 5) + mount = _make_mount() + service, pid, mid = _make_service(mount) + for path in [ + ".env", + ".dockerignore", + ".coderabbit.yaml", + ".prettierrc", + ".npmrc", + "a.txt", + ]: + await service.write_file( + project_id=pid, mount_id=mid, path=path, content=b"x" + ) + + listing = await service.list_files( + project_id=pid, mount_id=mid, limit=0, git_aware=True + ) + + assert listing.total == 1 + assert listing.total_capped is False + + async def test_gitignore_still_applies_when_it_is_pruned_from_the_count(self): + # `.gitignore` is a hidden file, so the level scan skips it — but its RULES must still be + # read first, or the files it ignores would start counting. + mount = _make_mount() + service, pid, mid = _make_service(mount) + for path, body in [ + (".gitignore", b"ignored.txt\n"), + ("ignored.txt", b"x"), + ("kept.txt", b"x"), + ]: + await service.write_file( + project_id=pid, mount_id=mid, path=path, content=body + ) + + listing = await service.list_files( + project_id=pid, mount_id=mid, order="path", limit=100, git_aware=True + ) + + assert {f.path for f in listing.files} == {"kept.txt"} + assert listing.total == 1 + async def test_internal_tree_does_not_spend_the_count_budget(self, monkeypatch): # Same rule for the runner-owned `agents/` namespace, which can hold a whole transcript # workspace: pruned during the walk, so it never crowds out the real files. diff --git a/web/packages/agenta-entities/src/drive/useSessionDrive.ts b/web/packages/agenta-entities/src/drive/useSessionDrive.ts index a304b49afef..705225fdbce 100644 --- a/web/packages/agenta-entities/src/drive/useSessionDrive.ts +++ b/web/packages/agenta-entities/src/drive/useSessionDrive.ts @@ -84,6 +84,11 @@ export const isListableDrivePath = (path: string, opts?: {fromAgentMount?: boole return opts?.fromAgentMount === true || rel !== AGENT_FILES_DIR } +/** {@link isListableDrivePath} minus hidden paths: the summary counts user content, not plumbing + * (#6027). The browse explorer asks the wider question, so it still lists them. */ +export const isSummaryDrivePath = (path: string, opts?: {fromAgentMount?: boolean}): boolean => + isListableDrivePath(path, opts) && !isHiddenPath(path) + /** True when a listing holds BOTH agent and session files — the only time the origin tags/filter * carry information (a single-origin drive doesn't need them). */ export const driveHasMixedOrigins = (files: {path: string}[]): boolean => { @@ -385,9 +390,7 @@ const SUMMARY_LATEST_LIMIT = 5 * - The COUNT is a BOUNDED `limit=0` scan per mount (`total`/`total_capped`): the backend stops * after a cap and reports "N+", so the "N files" badge never blocks on enumerating a huge tree. * - * Neither the count nor the lists include hidden (dot-prefixed) paths — this chrome is about user - * content, not plumbing (#6027). The browse explorer still lists them, behind its "show hidden" - * toggle; nothing about storage or what the agent can reach changes. + * Counts and lists exclude hidden paths (see {@link isSummaryDrivePath}). * * Returns the same {@link SessionDriveData} shape so consumers are unchanged. */ @@ -409,10 +412,10 @@ export function useSessionDriveSummary(sessionId: string, artifactId?: string): const recordRecency = useAtomValue(sessionRecordFileRecencyAtomFamily(sessionId)) // The record log's tool paths AS DRIVE PATHS: sandbox workspace root stripped, mount origin - // tagged, anything naming no drive file dropped (see `drivePathFromToolPath`). Filtered by - // `isSummaryDrivePath` on the mount-relative path, before the fold prefix, exactly as the root - // listing below filters its own. ONE derivation, shared with the gate below, so the gate can't - // withhold the root-listing fallback over a row the list then drops. + // tagged, anything naming no drive file dropped (see `drivePathFromToolPath`). Filtered on the + // mount-relative path, before the fold prefix, exactly as the root listing filters its own. + // ONE derivation, shared with the gate below, so the gate can't withhold the root-listing + // fallback over a row the list then drops. const recordFiles = useMemo( () => [...recordRecency.entries()] From a93dbf7a44d06955a5677bc76149d22df99632d1 Mon Sep 17 00:00:00 2001 From: ashrafchowdury Date: Fri, 4 Sep 2026 15:28:58 +0600 Subject: [PATCH 4/4] style(files): cut the summary predicate docstring to one line web/AGENTS.md caps in-code comments at one short line. --- web/packages/agenta-entities/src/drive/useSessionDrive.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/web/packages/agenta-entities/src/drive/useSessionDrive.ts b/web/packages/agenta-entities/src/drive/useSessionDrive.ts index 705225fdbce..a20db63dbd4 100644 --- a/web/packages/agenta-entities/src/drive/useSessionDrive.ts +++ b/web/packages/agenta-entities/src/drive/useSessionDrive.ts @@ -84,8 +84,7 @@ export const isListableDrivePath = (path: string, opts?: {fromAgentMount?: boole return opts?.fromAgentMount === true || rel !== AGENT_FILES_DIR } -/** {@link isListableDrivePath} minus hidden paths: the summary counts user content, not plumbing - * (#6027). The browse explorer asks the wider question, so it still lists them. */ +/** {@link isListableDrivePath} minus hidden paths; the explorer asks the wider one (#6027). */ export const isSummaryDrivePath = (path: string, opts?: {fromAgentMount?: boolean}): boolean => isListableDrivePath(path, opts) && !isHiddenPath(path)