Skip to content

Conversation

@brendan-kellam
Copy link
Contributor

@brendan-kellam brendan-kellam commented Jan 15, 2026

Problem

For large repositories (100K+ files), the file tree could take a long time (10s+) to load. The primary bottleneck was because we were loading the entire file tree for the repository, even though a small subset would actually be rendered.

Solution

This PR improves things by only loading the subset of the file tree that is actually opened*, rather than loading everything. The tradeoff with this approach is that subsequent requests need to be made whenever a new folder is opened. When this happens, a "loading..." placeholder is rendered below the folder. Upon testing on a production build, this experience is still pretty snappy (was seeing 50ms load times). I've also added a wa_file_tree_loaded posthog event that includes a durationMs so we can track the performance in the wild.

*opened in this context doesn't necessarily mean rendered. If you open a nested directory and then collapse some parent directory, the nested directory will still be considered open but not rendered. This is needed since if you were to then expand the parent directory, the state of the opened directories should still be preserved.

Before

Screen.Recording.2026-01-15.at.2.06.59.PM.mov

After

Screen.Recording.2026-01-15.at.2.08.09.PM.mov

Fixes #651


Note

Speeds up initial file tree rendering by fetching only opened directories and deferring the rest; adds basic telemetry to measure load times.

  • Change getTree API to accept paths and return a union tree; add getFolderContents, plus normalizePath/isPathValid/buildFileTree helpers and compareFileTreeItems sorter (with tests)
  • Update UI: FileTreePanel tracks openPaths, queries tree on expand (React Query with staleTime: Infinity, placeholder data), emits wa_file_tree_loaded with durationMs; PureFileTreePanel uses openPaths and shows a loading skeleton for unopened/empty children
  • Add PostHog event wa_file_tree_loaded; update types/schemas accordingly; note performance improvement in CHANGELOG.md

Written by Cursor Bugbot for commit 279e044. This will update automatically on new commits. Configure here.

Summary by CodeRabbit

  • New Features

    • Faster file tree loading for large repositories with optimized path handling
    • Added loading skeleton feedback when expanding directories
  • Bug Fixes

    • Fixed error when viewing directories with special path syntax (..)
  • Documentation

    • Updated changelog with performance improvements and bug fixes

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 15, 2026

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

This PR implements lazy-loading for the file tree feature by introducing path-specific API fetching, path validation utilities, and converting file tree components to use controlled expansion state management instead of internal state.

Changes

Cohort / File(s) Summary
Documentation & Changelog
CHANGELOG.md
Added changelog entries documenting improved initial file tree load times for large repositories and a fix for directory viewing errors with special path syntax.
API & Type Definitions
packages/web/src/features/fileTree/api.ts, packages/web/src/features/fileTree/types.ts
Updated getTree signature to accept paths: string[] parameter; added path validation and normalization using isPathValid and normalizePath; introduced new GetFolderContentsRequest and GetFolderContentsResponse types for folder-specific queries.
File Tree Utilities
packages/web/src/features/fileTree/utils.ts, packages/web/src/features/fileTree/utils.test.ts
Added utility functions for path normalization, validation (rejecting .. and null bytes), and hierarchical tree construction from flat lists; includes comprehensive test coverage for all utility behaviors.
File Tree Components
packages/web/src/features/fileTree/components/fileTreePanel.tsx, packages/web/src/features/fileTree/components/pureFileTreePanel.tsx
Refactored from internal collapse state to controlled expansion via openPaths Set; fileTreePanel now manages state and dispatches path changes; PureFileTreePanel receives openPaths and onTreeNodeClicked callback; added tree fetch instrumentation with wa_file_tree_loaded event tracking.
Event Tracking & Minor Updates
packages/web/src/lib/posthogEvents.ts, packages/web/src/app/api/(client)/client.ts
Added new wa_file_tree_loaded PosthogEventMap event with duration tracking; added trailing newline to client.ts.

Sequence Diagram

sequenceDiagram
    participant User
    participant FileTreePanel
    participant API
    participant Utils
    participant UI as PureFileTreePanel

    User->>FileTreePanel: Navigate to repo/revision
    FileTreePanel->>FileTreePanel: Initialize openPaths (empty)
    
    User->>FileTreePanel: Click to expand node
    FileTreePanel->>FileTreePanel: Add node path to openPaths
    FileTreePanel->>API: getTree({repoName, revisionName, paths: [...]})
    
    API->>Utils: Validate paths (isPathValid)
    Utils-->>API: Paths valid
    API->>Utils: Normalize paths
    Utils-->>API: Normalized paths
    API->>API: Execute git ls-tree with specific paths
    API-->>FileTreePanel: Return FileTreeNode hierarchy
    
    FileTreePanel->>Utils: buildFileTree(flatList)
    Utils-->>FileTreePanel: Hierarchical tree
    
    FileTreePanel->>FileTreePanel: Measure fetch duration
    FileTreePanel->>FileTreePanel: Capture wa_file_tree_loaded event
    
    FileTreePanel->>UI: Render with openPaths, onTreeNodeClicked
    UI->>UI: Collapse/expand based on openPaths.has(path)
    UI-->>User: Display tree
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

sourcebot-team

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix: Improve initial file tree load performance' clearly and directly summarizes the main objective of the PR, which is to optimize file tree loading by fetching only opened paths rather than the entire repository tree.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@brendan-kellam brendan-kellam changed the title [wip] fix: Improve file tree performance [wip] fix: Improve initial file tree load performance Jan 15, 2026
@brendan-kellam brendan-kellam changed the title [wip] fix: Improve initial file tree load performance fix: Improve initial file tree load performance Jan 15, 2026
@brendan-kellam brendan-kellam marked this pull request as ready for review January 15, 2026 22:11
@github-actions

This comment has been minimized.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the final PR Bugbot will review for you during this billing cycle

Your free Bugbot reviews will reset on February 14

Details

You are on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle.

To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

@brendan-kellam
Copy link
Contributor Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Jan 15, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@packages/web/src/features/fileTree/utils.test.ts`:
- Around line 33-41: The test description in utils.test.ts for the buildFileTree
unit test contains a grammar typo ("a empty flat list"); update the test title
string in the test(...) call to read "an empty flat list" so the test reads
test('buildFileTree handles an empty flat list', ...) referring to the existing
buildFileTree test case.

In `@packages/web/src/features/fileTree/utils.ts`:
- Around line 43-60: In buildFileTree, intermediate directory nodes are being
created with the full file path (item.path) causing incorrect navigation;
instead compute the node's path incrementally using the parts array and current
index (e.g., join parts.slice(0, i+1) with '/'), assign that computedPath when
creating a new FileTreeNode (use item.path only for leaf nodes where isLeaf is
true), and keep the rest of the logic (variables parts, isLeaf, nodeType, next,
current.children.push) the same so directory nodes have correct cumulative paths
for open-state and navigation matching.
♻️ Duplicate comments (2)
packages/web/src/features/fileTree/components/pureFileTreePanel.tsx (1)

71-77: Indent loading skeleton as a child node.
It should render at depth + 1 to align with the children that will appear there.

🧩 Suggested tweak
-                            {node.type === 'tree' && node.children.length === 0 && openPaths.has(node.path) && renderLoadingSkeleton(depth)}
+                            {node.type === 'tree' && node.children.length === 0 && openPaths.has(node.path) && renderLoadingSkeleton(depth + 1)}
packages/web/src/features/fileTree/components/fileTreePanel.tsx (1)

185-199: LGTM!

The rendering logic correctly handles the three states: pending (skeleton), error (error message), and success (tree panel). The error display issue mentioned in past reviews appears to have been addressed - the current isPending check properly allows errors to display when the initial load fails.

🧹 Nitpick comments (2)
packages/web/src/features/fileTree/api.ts (1)

11-13: Consider consolidating imports from the same module.

The imports from ./utils are split across two lines and could be consolidated for cleaner code.

Suggested fix
 import { FileTreeItem } from './types';
-import { buildFileTree, isPathValid, normalizePath } from './utils';
-import { compareFileTreeItems } from './utils';
+import { buildFileTree, compareFileTreeItems, isPathValid, normalizePath } from './utils';
packages/web/src/features/fileTree/components/fileTreePanel.tsx (1)

99-113: Consider removing openPaths from the dependency array.

The callback re-creates on every openPaths change. Since the has check can be done inside the functional setState, you can eliminate the dependency and improve memoization:

Suggested fix
-    const onTreeNodeClicked = useCallback((node: FileTreeNode) => {
-        if (!openPaths.has(node.path)) {
-            setOpenPaths(current => {
-                const next = new Set(current);
-                next.add(node.path);
-                return next;
-            })
-        } else {
-            setOpenPaths(current => {
-                const next = new Set(current);
-                next.delete(node.path);
-                return next;
-            })
-        }
-    }, [openPaths]);
+    const onTreeNodeClicked = useCallback((node: FileTreeNode) => {
+        setOpenPaths(current => {
+            const next = new Set(current);
+            if (current.has(node.path)) {
+                next.delete(node.path);
+            } else {
+                next.add(node.path);
+            }
+            return next;
+        });
+    }, []);
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dacbe5d and 518e8b5.

📒 Files selected for processing (9)
  • CHANGELOG.md
  • packages/web/src/app/api/(client)/client.ts
  • packages/web/src/features/fileTree/api.ts
  • packages/web/src/features/fileTree/components/fileTreePanel.tsx
  • packages/web/src/features/fileTree/components/pureFileTreePanel.tsx
  • packages/web/src/features/fileTree/types.ts
  • packages/web/src/features/fileTree/utils.test.ts
  • packages/web/src/features/fileTree/utils.ts
  • packages/web/src/lib/posthogEvents.ts
🧰 Additional context used
📓 Path-based instructions (1)
CHANGELOG.md

📄 CodeRabbit inference engine (.cursor/rules/cloud_agent.mdc)

After creating a GitHub PR, create a follow-up commit with a Changelog entry in CHANGELOG.md with a short description of the change. Follow the existing conventions: entries must be parented under a header (Added, Changed, Deprecated, Removed, Fixed, or Security), and entries must include the GitHub pull request id at the end of the line, formatted as #

Files:

  • CHANGELOG.md
🧠 Learnings (1)
📚 Learning: 2025-12-29T18:02:55.419Z
Learnt from: CR
Repo: sourcebot-dev/sourcebot PR: 0
File: .cursor/rules/cloud_agent.mdc:0-0
Timestamp: 2025-12-29T18:02:55.419Z
Learning: Applies to CHANGELOG.md : After creating a GitHub PR, create a follow-up commit with a Changelog entry in CHANGELOG.md with a short description of the change. Follow the existing conventions: entries must be parented under a header (Added, Changed, Deprecated, Removed, Fixed, or Security), and entries must include the GitHub pull request id at the end of the line, formatted as [#<id>](<url>)

Applied to files:

  • CHANGELOG.md
🧬 Code graph analysis (5)
packages/web/src/features/fileTree/utils.test.ts (1)
packages/web/src/features/fileTree/utils.ts (3)
  • normalizePath (3-22)
  • isPathValid (26-33)
  • buildFileTree (35-82)
packages/web/src/features/fileTree/utils.ts (1)
packages/web/src/features/fileTree/types.ts (2)
  • FileTreeNode (43-43)
  • FileTreeItem (28-28)
packages/web/src/features/fileTree/api.ts (1)
packages/web/src/features/fileTree/utils.ts (3)
  • isPathValid (26-33)
  • normalizePath (3-22)
  • compareFileTreeItems (84-89)
packages/web/src/features/fileTree/components/fileTreePanel.tsx (4)
packages/backend/src/utils.ts (1)
  • measure (12-20)
packages/web/src/lib/utils.ts (1)
  • unwrapServiceError (547-554)
packages/web/src/features/fileTree/api.ts (1)
  • getTree (22-84)
packages/web/src/features/fileTree/types.ts (1)
  • FileTreeNode (43-43)
packages/web/src/features/fileTree/components/pureFileTreePanel.tsx (1)
packages/web/src/features/fileTree/types.ts (1)
  • FileTreeNode (43-43)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build
  • GitHub Check: Cursor Bugbot
🔇 Additional comments (12)
packages/web/src/lib/posthogEvents.ts (1)

184-186: LGTM: new event shape is clear and typed.

packages/web/src/features/fileTree/types.ts (1)

3-7: Type additions look consistent with the new API surface.

Also applies to: 16-21, 53-54

CHANGELOG.md (1)

13-17: LGTM!

The changelog entries follow the existing conventions with proper headers and include the required links. Note that the "Fixed" entry references the issue (#531) rather than the PR (#739), which is acceptable since it directly addresses that issue.

packages/web/src/features/fileTree/utils.test.ts (3)

4-14: LGTM!

The normalizePath tests correctly validate the core behaviors: stripping leading slashes, adding trailing slashes, and the edge case for root paths.


16-31: Good coverage of edge cases for path validation.

The tests effectively distinguish between malicious directory traversal patterns (..) and legitimate paths containing dot sequences (like ...), which directly addresses issue #531.


43-93: LGTM!

The test correctly validates tree construction and sorting behavior, ensuring directories appear before files and items are sorted alphabetically within each type.

packages/web/src/features/fileTree/api.ts (4)

39-41: Good security practice: validating paths before git operations.

Path validation correctly happens before any git commands are executed, preventing directory traversal attacks.


48-62: LGTM!

The git command construction properly uses -- to separate paths from options, includes the -t flag to capture directory nodes, and handles the path specifications correctly.


107-110: LGTM!

Path validation and normalization in getFolderContents mirrors the pattern in getTree, ensuring consistent security handling.


141-143: LGTM!

Using the shared compareFileTreeItems comparator ensures consistent sorting behavior across the codebase.

packages/web/src/features/fileTree/components/fileTreePanel.tsx (2)

74-76: LGTM!

Correctly resets the open paths when switching repositories or revisions, preventing stale state from persisting.


80-95: LGTM!

The logic correctly computes and opens all ancestor directories for the current path, ensuring the file is visible in the tree. For blob paths, it properly excludes the filename to open only the containing directory hierarchy.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@brendan-kellam brendan-kellam merged commit 81a061a into main Jan 15, 2026
9 checks passed
@brendan-kellam brendan-kellam deleted the bkellam/file-tree-optimizations-SOU-126 branch January 15, 2026 23:47
return next;
})
}
}, [openPaths]);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale closure prevents folder toggle on rapid clicks

Low Severity

The onTreeNodeClicked callback checks openPaths.has(node.path) using the closure value of openPaths, but the state updates inside use functional updates with current. When a user clicks the same folder rapidly to toggle it, both clicks may see the stale openPaths from the closure and take the same branch (both try to add or both try to delete), preventing the expected toggle behavior. The conditional check needs to happen inside the functional update to use the actual current state.

Fix in Cursor Fix in Web

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.

[bug] File tree can take a long time to load

2 participants