Skip to content

feat(files): load the Files panel one folder at a time, sort it, filter locally - #633

Draft
shenlvkang-collab wants to merge 3 commits into
Ark0N:masterfrom
shenlvkang-collab:feat/files-panel-lazy-tree
Draft

shenlvkang-collab wants to merge 3 commits into
Ark0N:masterfrom
shenlvkang-collab:feat/files-panel-lazy-tree

Conversation

@shenlvkang-collab

Copy link
Copy Markdown
Contributor

Stacked on #631 (path picker download/upload). Review that one first; this PR's own change is the last commit, e52338d.

Problem

Opening the Files panel walks the whole workspace (depth 5, up to 5,000 nodes) on every open, refresh and session switch. Every keystroke in its search box (after the 250 ms debounce) walks it again on the server. On a slow mount this is the whole cost of the panel. Measured on WSL /mnt/d (9P, a stat per entry) against one real workspace (3,723 files, 1,277 dirs, truncated):

Files panel today Path picker, same root
open 13.6 s / 14.1 s, 645 KB 0.34 s / 0.47 s, 15 KB
one search 4.3 s —

What changed

  • Lazy listing: GET /api/sessions/:id/files?lazy=1[&path=<workspace-relative dir>] lists one directory.
    • Folders come back without children, which means "not loaded".
    • Every entry carries mtimeMs.
    • path is workspace-confined through validateSessionFilePath() (realpath).
    • A hidden segment needs showHidden.
    • The generated-folder excludes do not apply, since nothing is walked: node_modules costs one row.
    • The non-lazy response is byte-for-byte unchanged.
  • Panel: loads the root lazily and fetches a folder when it is expanded.
    • A loading row shows while it loads; a failure row says why and retries on collapse and expand.
    • The same treeEpoch/owner/showHidden guards as the root load, so a listing that lands after a refresh, session switch or hidden toggle is dropped.
  • Sort menu: name A→Z / Z→A, newest / oldest first, folders always first. Per device, the same modes as the path picker.
  • Search: typing filters the nodes already loaded, in the browser, with no request (ordered-substring match with *, never a RegExp).
    • Status reads N matches in loaded folders · Enter searches the whole workspace.
    • Enter runs the existing server search (COD-236/341), unchanged.
    • A folder hit that the lazy tree has not loaded yet opens by loading each ancestor in turn.
  • Expand All opens only loaded folders, so it never fans out into one request per folder.

Validation

  • New test/routes/file-tree-lazy.test.ts (6, real temp dirs) covers: one-level shape and order, mtime, generated folders listed, subfolder paths, hidden opt-in, traversal and symlink escape refused, missing folder (404) and a file (400), and the unchanged default tree.

  • New lazy block in test/file-browser-search.test.ts (6) covers:

    • expand-once loading
    • a stale listing after refresh is dropped
    • error then retry
    • date sort with folders first, persisted
    • local filter with no request, unloaded folders not searched, and Enter → server search
    • lazy reveal of an unloaded search hit

    The existing 94 tests in that file pass; 9 expected root-load URLs changed from depth=5 to lazy=1.

  • The file-routes, path-picker, hidden-toggle and five i18n suites pass (292 tests across 14 files). Typecheck, lint, Prettier and frontend syntax pass.

  • Playwright against a built server, 13 checks:

    • the root load is lazy and only the top level shows
    • oldest-first sort works
    • expanding fetches one folder
    • the local filter sends no request and shows loaded matches with their ancestors
    • a file in an unloaded folder is not found locally
    • Enter's workspace search finds it
    • clearing restores the tree
    • no page errors

🤖 Generated with Claude Code

shenlvkang-collab and others added 3 commits October 15, 2026 01:44
The path picker (mobile Path key) is the fast way to browse: one folder per
request, any allowed root. It could not move files, so the Files panel was
still needed for that, and a desktop browser could not reach the picker at all.

- GET /api/filesystem/download: any regular file the picker can list, sent as
  an attachment.
- POST /api/filesystem/upload: one multipart file into the folder on screen.
  Same-origin check (shared with paste-image), per-IP rate bucket, bounded
  folder probe, sanitized basename, blocked-name check, O_EXCL|O_NOFOLLOW
  creation with " (n)" suffixes (never overwrites, never follows a planted
  symlink), partial files unlinked. 1GB cap (CODEMAN_MAX_PICKER_UPLOAD_BYTES).
- Both resolve through resolveFilesystemPickerPath(), so they reach exactly
  what browse can list, including the multi-user root scoping.
- Picker `fileTransfer` option: a download link per file row and an Upload
  button (multi-select, XHR progress) that reloads the folder and selects the
  last upload. On for the Path key and the new quick-browser button in the
  Files panel header; Link Existing stays browse-only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The key already exists further down the table; an object literal keeps the
last one, so the first copy was dead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…er locally

Opening the Files panel walked the whole workspace (depth 5, up to 5,000
nodes) on every open, refresh and session switch, and every keystroke in its
search box re-walked it on the server. On WSL /mnt/d (9P, a stat per entry)
that measured 13.6-14.1 s to open a 3,700-file workspace and 4.3 s per
search, against 0.34-0.47 s for the path picker's one-folder listing of the
same root.

- `GET /api/sessions/:id/files?lazy=1[&path=<dir>]` lists one directory:
  folders without `children` (not loaded), `mtimeMs` on every entry,
  workspace-confined through validateSessionFilePath(), hidden segments
  need showHidden. The non-lazy response is unchanged.
- The panel loads the root lazily and fetches a folder when it is expanded
  (loading row, error row that retries on collapse/expand, epoch-guarded).
- A sort menu (name A-Z/Z-A, newest/oldest first, folders first), per
  device, same modes as the path picker.
- Typing filters the already-loaded nodes in the browser with no request
  (ordered-substring `*` match, never a RegExp); Enter runs the existing
  workspace search. A folder hit the lazy tree has not loaded yet is opened
  by loading each ancestor in turn. Expand All opens only loaded folders.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant