[Bug]: Files tree forgets which folders were expanded on every remount#12041

openedSep 16, 2026byCcristip73

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/web

Steps to reproduce

  1. Open a thread in a repo with a deep directory layout.
  2. Open the Files surface in the right panel and expand your way down to something like apps/web/src/components/files/.
  3. Switch the right panel to another tab (Diff, Terminal, Preview), or switch to another thread and back. A window reload does it too.
  4. Return to the Files surface.

Expected behavior

The tree comes back the way I left it, with the same folders expanded, so I can keep working from where I was.

Actual behavior

The tree is fully collapsed again. Every folder on the path has to be re-opened by hand, every single time.

This is not a stale-cache problem, the state is simply never stored. In apps/web/src/components/files/FileBrowserPanel.tsx:287 expansion lives in a ref that is recreated on mount:

ts
1const expandedPathsRef = useRef(new Set<string>());

Mobile has the same shape in apps/mobile/src/features/files/FileTreeBrowser.tsx:122:

ts
1const [expandedPaths, setExpandedPaths] = useState<ReadonlySet<string>>(() => new Set());

Nothing writes either one to storage. The surrounding surface already persists plenty: open file tabs per thread in apps/web/src/rightPanelStore.ts (t3code:right-panel-state:v2), and even the explorer pane's own open/closed flag in apps/web/src/components/files/FilePreviewPanel.tsx:112 (t3code.fileExplorerOpen). So the tree's expansion is the odd one out, and it is the piece that costs the most clicks to rebuild.

Impact

Minor bug or occasional failure

Version or commit

0.0.41-nightly.20260910.1486

Environment

macOS 26.x, T3 Code Desktop (Nightly) 0.0.41-nightly.20260910.1486

Logs or stack traces

shell
1

Workaround

None. Re-expand the path manually each time.

  • #8759 covers the right panel losing scroll position and diff folds. Same family of state loss, different state. Filing separately because the fix there (#8826) was already flagged as too broad, and this one is a self-contained addition to FileBrowserPanel.
  • #3733 (preserve reasoning/tool trace expanded state when switching threads) and #4227 (expand/collapse all in the file surface) both landed, so persisting UI expansion is established behavior elsewhere in the app.

GPT 6 Astra via Codex in T3 Code.

Triage

Confirmed on current main (b900fc94e) and on the reporter’s 0.0.41-nightly.20260910.1486. This is a real web/desktop (and mobile) UX bug, not a stale-cache problem and not a wash of #8759 / #8826 / #8968. The tree’s expansion is session-only and the Files surface is torn down whenever it is not the active right-panel tab.

useFileTree is created with initialExpansion: "closed". The Pierre model is the expansion store. expandedPathsRef is only a shadow set so newly expanded folders get load(). Both die on unmount. Nothing writes either one to storage.

ts
1// apps/web/src/components/files/FileBrowserPanel.tsx
2initialExpansion: "closed",
3const expandedPathsRef = useRef(new Set<string>());

ChatView renders only the active right-panel surface. Switching to Diff, Terminal, or Preview unmounts FilePreviewPanel, which unmounts FileBrowserPanel. Hiding the explorer (t3code.fileExplorerOpen) does the same: showExplorer ? <FileBrowserPanel /> : null. Thread switch and reload remount it as well.

The report slightly overstates “fully collapsed” on the file surface (kind === "file"). Remount re-expands ancestors of the open file via the reveal effect. That does not restore:

  • folders the user opened that are not on that file’s path
  • the Files singleton (kind === "files", selectedPath === null)
  • expandAll (useState(false))

#8968 only keeps Pierre rows alive across in-place treePaths updates. It does not survive unmount. #11527 made remount worse: children are fetched per expanded folder, so a collapsed remount also drops the directory cache in useDirectoryEntries.

Mobile matches: FileTreeBrowser holds expandedPaths in useState and is keyed by [environmentId, cwd]. Leaving the Files route or remounting the navigator pane starts from new Set(). Selecting a file re-expands ancestors only.

WorkWhat it didWhy this still happens
Open #8759 / open #8826Diff scroll + collapsed files across right-panel tabsSame family of state loss, different state. #8826 was already called too broad; do not fold the Files tree into it.
Merged #8968Incremental treePaths so refresh does not rebuild the treeSurvives refresh, not tab/thread/reload remount.
Merged #8889 (closed #4227 / #6794)Expand/collapse allexpandAll is also useState. One more thing the remount forgets.
Merged #11527On-demand folder listingRestore must load() each persisted path from the root down.
Closed #3733Persist reasoning/tool-trace expansion across thread switchesPrecedent that expansion is durable elsewhere. Not this tree.

rightPanelStore already says each feature owns its durable resource state. Open file tabs (t3code:right-panel-state:v2) and the explorer open flag (t3code.fileExplorerOpen) persist. The tree is the odd one out. No open PR persists Files expansion.

Suggested fix

Keep this a small persistence bugfix on the Files tree. Do not keep every right-panel surface mounted, and do not wait for #8826.

  1. Persist the expanded path set (and expandAll if you want that button to survive too), keyed by environmentId + cwd. Workspace scope matches the tree. localStorage, same idea as t3code.fileExplorerOpen. Do not put this in rightPanelStore.
  2. On mount, restore after paths exist. For each persisted directory: load(path), then item.expand() on the Pierre model (web) / setExpandedPaths (mobile). Parent-before-child. Skip paths that are gone.
  3. Write on toggle, not only on unmount. The current model.subscribe already sees expand/collapse; persist from there. Prune paths that left directoryPaths.
  4. Mobile: same store in FileTreeBrowser so the Files route and the thread navigator pane come back expanded.
  5. Tests: remount FileBrowserPanel with a stored deep path and assert load + expand for each ancestor. Empty/missing storage stays closed. A selected file still reveals even when storage is empty.
  6. Visual close: expand a deep folder on the Files singleton, switch to Diff and back, reload — same folders open. Repeat on mobile after leaving Files.

Do not treat keep-alive of inactive right-panel surfaces as the fix. That is the #8826 scope problem. Persisting a path set is enough.

Workaround

Stay on the Files tab. If a file is already open, that path re-reveals; sibling folders do not. No in-app toggle.

Classification: bug · accepted · minor (web/desktop/mobile Files tree; extra clicks, no data loss)
Labels: add bug, accepted, via-triage
Discord tags: ui, mobile-general

Synced from GitHub

I'd like to take this. I'll follow the direction in the maintainer triage above and open a focused PR with before/after evidence, noting the model and harness used.

Existing fix: #18211 explicitly references this report and persists Files-tree expansion per environment/workspace across remounts and reloads. Its current head is 2fcd03ece8ef; all 30 reported checks pass, with human approval still outstanding. No duplicate PR opened. Mobile is outside that PR’s scope. Leaving this issue open until the existing fix lands.