Skip to content

fix(desktop): v2 file reveal — expand, highlight, scroll, open sidebar#4536

Merged
saddlepaddle merged 4 commits into
mainfrom
fix-pierre-file-reveal
May 14, 2026
Merged

fix(desktop): v2 file reveal — expand, highlight, scroll, open sidebar#4536
saddlepaddle merged 4 commits into
mainfrom
fix-pierre-file-reveal

Conversation

@saddlepaddle
Copy link
Copy Markdown
Collaborator

@saddlepaddle saddlepaddle commented May 14, 2026

Summary

Four fixes to v2 file reveal so opening a file from Quick Open (or any reveal trigger) actually shows the file in the tree:

  • Expand: fixed a race in useFilesTabBridge.fetchDir where the Set-based dedup returned immediately instead of awaiting the in-flight load. Pierre's handle.expand() synchronously notifies subscribers, and our model.subscribe hook fires fetchDir before reveal's own await fetchDir runs — without shared promises, reveal resolved before children landed in knownPaths, so only the first ancestor expanded (apps/) and the rest silently no-op'd.
  • Highlight: focusPath only sets data-item-focused (keyboard focus). Visual row highlight is data-item-selected. FileTree doesn't expose selectOnlyPath, so emulate it via deselect-all + handle.select(). Works for folders too (their handle has the same select() method).
  • Scroll: Pierre auto-scrolls focused rows only when DOM focus lives inside the tree (FileTreeView shouldOwnDomFocus gate) and exposes no public revealPath/scrollTo. Added scrollTreeToRow that replicates Pierre's sort via prepareFileTreeInput + walks visible rows (ancestors-expanded check) to find the index, then sets scrollTop directly. Works regardless of virtualization window.
  • Sidebar: Quick Open called openFilePane directly, so the sidebar/Files tab stayed closed and the reveal happened invisibly. Added handleQuickOpenSelectFile that opens the sidebar + switches to Files before delegating. Tree clicks and other call sites still go through openFilePane unchanged.

Pierre upstream note: a public revealPath/scrollToPath (or even getFocusedIndex) would let us drop the scrollTreeToRow helper. Worth filing.

Test plan

  • Open Quick Open, pick a file deep in the tree — sidebar opens, Files tab shows, ancestors expand, row highlights, tree scrolls so the row is centered
  • Pick a file already visible — row highlights, no scroll jump
  • Pick a file in a different already-expanded subtree — scrolls to it
  • Click a folder in the tree — still toggles expansion (folder highlight doesn't break the click policy)
  • Switch between open tabs — sidebar state preserved (no auto-open on tab switch)
  • Cmd-click a path in terminal output (revealPath) — sidebar opens, file revealed and highlighted

Summary by cubic

Fixes v2 file reveal so opening a file from Quick Open (or any reveal) expands ancestors, highlights and scrolls the row into view, and shows the Files tab. Restores reliable tree click behavior: re-clicks fire, sidebar re-click pins, and Quick Open previews without pinning.

  • Bug Fixes
    • Expand: share in-flight directory loads via a promise Map in useFilesTabBridge.fetchDir so callers await the same fetch; add identity-check cleanup to avoid evicting a fresh promise after a workspace switch.
    • Highlight: emulate select-only via deselect-then-select() + focusPath; works for files and folders.
    • Scroll: add scrollTreeToRow using @pierre/trees prepareFileTreeInput + ancestor-expanded check to compute the visible index and center the row; independent of DOM focus/virtualization.
    • Sidebar: route Quick Open selections through handleQuickOpenSelectFile to open the right sidebar and switch to Files before opening the file.
    • Clicks/pinning: always intercept tree row clicks; split openFilePane (focus existing, no pin) from openFilePaneFromTreeClick (click-again-to-pin) so re-clicks and reopen-after-Cmd+W work and picker selections don’t pin-stick.
    • Echo guard: ignore the reveal-induced onSelectionChange echo when the selected row already matches selectedFilePath to prevent auto-pinning newly opened panes; real keyboard nav still flows.

Written for commit e2329f7. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Quick Open now opens the Files sidebar, focuses the selected file, and can open it in a new tab.
    • New API to distinguish tree-click opens vs other opens so clicks can pin or focus existing file panes.
  • Bug Fixes

    • Reveal/highlight now reliably centers and scrolls selected files into view.
    • Directory loading deduplicated to avoid duplicate listings and stale requests.
    • Row-click handling refined to reliably intercept re-clicks and avoid duplicate selection echoes.

Review Change Stack

- fix race in useFilesTabBridge.fetchDir where the Set-based dedup
  returned without awaiting the in-flight load; Pierre's synchronous
  expand-notify kicked off fetchDir before reveal's own await, so
  reveal resolved before children landed in knownPaths and only the
  first ancestor expanded
- emulate selectOnlyPath via deselect + select on the item handles so
  the revealed row gets data-item-selected; works for folders too
- replicate Pierre's sort + visibility math to compute the visible row
  index and set scrollTop directly, since Pierre auto-scrolls only when
  DOM focus lives inside the tree and exposes no public scrollTo API
- open the right sidebar + switch to the Files tab when the user opens
  a file from Quick Open so the reveal is actually visible
@capy-ai
Copy link
Copy Markdown

capy-ai Bot commented May 14, 2026

Capy auto-review is paused for this organization because the monthly auto-review limit has been reached. Increase the limit or turn it off in billing settings to resume automatic reviews.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 14, 2026

Caution

Review failed

Pull request was closed or merged during review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d8162e9b-0b57-4f75-900f-199a147da0ac

📥 Commits

Reviewing files that changed from the base of the PR and between 8a64559 and e2329f7.

📒 Files selected for processing (1)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/FilesTab/FilesTab.tsx

📝 Walkthrough

Walkthrough

Enhances Files tab selection and visibility: adds a virtualized-tree centering utility, dedupes concurrent directory loads, updates reveal() to select + focus + center rows, adjusts click interception, and wires Quick Open and sidebar tree-clicks to the new navigation callbacks.

Changes

File Selection and Reveal Enhancement

Layer / File(s) Summary
Virtualized tree scrolling utility
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/FilesTab/utils/scrollTreeToRow/scrollTreeToRow.ts, apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/FilesTab/utils/scrollTreeToRow/index.ts, apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/FilesTab/hooks/useFilesTabActions/useFilesTabActions.ts
New scrollTreeToRow plus computeVisibleRowIndex and isPathVisible that find the virtualized scroll container, compute visible row index from Pierre's input, verify ancestor expansion, and center the row; useFilesTabActions.reveal() now selects, focuses, and scrolls the target row using FILE_EXPLORER_ROW_HEIGHT.
Concurrent directory lazy-loading with deduplication
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/FilesTab/hooks/useFilesTabBridge/useFilesTabBridge.ts
Adds inflightDirsRef: Map<string, Promise<void>>, dedupes concurrent fetchDir calls, snapshots versionRef to avoid stale mutations, batch-adds model entries, and clears inflight tracking on workspace/root switches.
Pierre row click interception
apps/desktop/src/renderer/lib/clickPolicy/usePierreRowClickPolicy.ts
Simplifies capture-phase click handling to inspect only action and always intercept when action is present (removed prior tier === "plain" early-return).
Navigation callbacks and UI wiring
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useWorkspaceFileNavigation/useWorkspaceFileNavigation.ts, apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx, apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/FilesTab/FilesTab.tsx
Adds openFilePaneFromTreeClick, updates openFilePane to focus existing panes instead of pinning, wires WorkspaceSidebar.onSelectFile to openFilePaneFromTreeClick, adds handleQuickOpenSelectFile for Quick Open, and guards FilesTab selection handler to ignore reveal-induced selection echoes.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

A rabbit hops through nested trees,
Centers rows with graceful ease,
Promises shared so loads don't race,
Clicks now land in the proper place,
Quick Open opens — sidebar beams 🐰✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.44% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: fixing file reveal functionality in v2 with specific focus on expand, highlight, scroll, and sidebar opening.
Description check ✅ Passed The description is comprehensive and well-structured, covering all template sections including summary, related issues, type of change, testing, and additional notes with detailed technical explanation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-pierre-file-reveal

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


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.

Copy link
Copy Markdown
Contributor

@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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/components/FilesTab/hooks/useFilesTabBridge/useFilesTabBridge.ts:
- Around line 126-131: The finally block currently unconditionally calls
inflightDirsRef.current.delete(relDir) which can remove a newer promise when an
older request resolves; change the cleanup to only delete the entry if the
stored promise equals the local promise (the one assigned to the variable
promise inside the IIFE) so that inflightDirsRef retains newer in-flight
promises for the same relDir; update the finally in the function where
inflightDirsRef, relDir and promise are in scope to conditionally delete only
when inflightDirsRef.current.get(relDir) === promise.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 559a2391-f492-4f3a-9466-1ea4f3b85271

📥 Commits

Reviewing files that changed from the base of the PR and between fcc5676 and 3f663a5.

📒 Files selected for processing (5)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/FilesTab/hooks/useFilesTabActions/useFilesTabActions.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/FilesTab/hooks/useFilesTabBridge/useFilesTabBridge.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/FilesTab/utils/scrollTreeToRow/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/FilesTab/utils/scrollTreeToRow/scrollTreeToRow.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 14, 2026

Greptile Summary

This PR fixes four bugs in the v2 file-reveal flow triggered by Quick Open (or any revealPath call): ancestor expansion now correctly awaits in-flight directory fetches instead of short-circuiting them, visual row highlight is emulated via deselect-all + select(), a new scrollTreeToRow utility replicates Pierre's row-order math to center the target in the viewport, and Quick Open now opens the sidebar/Files tab before delegating so the reveal is actually visible.

  • Expand race fix (useFilesTabBridge): replaces the Set-based in-flight dedup with a Promise-keyed Map so concurrent callers (model.subscribe vs reveal) await the same fetch; the finally block however deletes by key unconditionally, creating a window where a stale promise can evict a newer one added after a workspace reset.
  • Highlight + scroll (useFilesTabActions, scrollTreeToRow): deselect-all + select() emulates selectOnlyPath; scrollTreeToRow replicates Pierre's sort and walks visible rows to compute a scrollTop; both are run inside requestAnimationFrame after all ancestor expands have settled.
  • Sidebar open (page.tsx): handleQuickOpenSelectFile wraps openFilePane with setRightSidebarOpen(true) + setRightSidebarTab(\"files\"); other call sites are unchanged.

Confidence Score: 3/5

The four reveal fixes are directionally correct, but the Promise-Map cleanup in fetchDir has a race that can cause duplicate directory-listing fetches after a workspace switch.

The inflightDirsRef finally block deletes by key rather than by promise identity, so a resolved stale promise from a previous workspace can silently evict a concurrently registered promise for the new workspace, undermining the deduplication guarantee the Map was introduced to provide.

useFilesTabBridge.ts — the finally block in fetchDir needs an identity guard before deleting; scrollTreeToRow.ts — clientHeight may be stale when the sidebar CSS transition is still running.

Important Files Changed

Filename Overview
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/FilesTab/hooks/useFilesTabBridge/useFilesTabBridge.ts Replaces Set-based in-flight dedup with a Promise-Map so concurrent callers await the same fetch; correctly fixes the reveal race, but the finally block unconditionally deletes by key rather than by promise identity, which can evict a newer workspace's promise on workspace switch.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/FilesTab/hooks/useFilesTabActions/useFilesTabActions.ts Adds deselect-all + select emulation for visual highlight and delegates to scrollTreeToRow; logic is sound assuming getSelectedPaths() returns a snapshot array.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/FilesTab/utils/scrollTreeToRow/scrollTreeToRow.ts New utility that replicates Pierre's sort + visibility walk to find a row's visible index and sets scrollTop directly; visibility logic is correct, but isPathVisible is called O(n) times with O(depth) work each, giving O(n×depth) total.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/FilesTab/utils/scrollTreeToRow/index.ts Barrel export for scrollTreeToRow; no issues.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx Adds handleQuickOpenSelectFile to open sidebar + switch to Files tab before delegating to openFilePane; straightforward and correct.

Sequence Diagram

sequenceDiagram
    participant QO as QuickOpen
    participant Page as page.tsx
    participant Actions as useFilesTabActions
    participant Bridge as useFilesTabBridge
    participant Pierre as FileTree (Pierre)
    participant Scroll as scrollTreeToRow

    QO->>Page: onSelectFile(filePath)
    Page->>Page: setRightSidebarOpen(true)
    Page->>Page: setRightSidebarTab("files")
    Page->>Actions: openFilePane(filePath) reveal(absolutePath)

    Actions->>Bridge: await fetchDir("") [root]
    Bridge-->>Actions: root children loaded

    loop for each ancestor segment
        Actions->>Bridge: await fetchDir(parentRel)
        Bridge-->>Actions: children loaded
        Actions->>Pierre: handle.expand()
        Note over Pierre,Bridge: expand() notifies model.subscribe synchronously
        Bridge->>Bridge: fetchDir(dirRel) [returns same in-flight Promise]
        Actions->>Bridge: await fetchDir(acc) [awaits same Promise]
        Bridge-->>Actions: children in knownPaths
    end

    Actions->>Actions: requestAnimationFrame(...)
    Actions->>Pierre: deselect all selected paths
    Actions->>Pierre: getItem(targetKey).select()
    Actions->>Pierre: focusPath(rel)
    Actions->>Scroll: scrollTreeToRow(model, knownPaths, targetKey, rowHeight)
    Scroll->>Pierre: getFileTreeContainer().shadowRoot.querySelector(...)
    Scroll->>Scroll: prepareFileTreeInput + walk visible rows
    Scroll->>Scroll: "scrollEl.scrollTop = centered position"
Loading
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/FilesTab/hooks/useFilesTabBridge/useFilesTabBridge.ts:126-128
**Stale promise can evict a newer promise from `inflightDirsRef` on workspace switch**

`inflightDirsRef.current.delete(relDir)` is unconditional, so if a stale P1 (from the previous workspace) resolves after the map was cleared and a new P2 was registered under the same key, P1's `finally` silently evicts P2. Subsequent concurrent callers then see no in-flight entry and start a redundant P3 fetch, bypassing the deduplication this Map was introduced to provide. The fix is to guard the delete with an identity check:

```ts
} finally {
  if (inflightDirsRef.current.get(relDir) === promise) {
    inflightDirsRef.current.delete(relDir);
  }
}
```

### Issue 2 of 3
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/FilesTab/utils/scrollTreeToRow/scrollTreeToRow.ts:35-45
**`clientHeight` may be 0 when sidebar CSS transition is still running**

`handleQuickOpenSelectFile` opens the sidebar with a state update, then `openFilePane``reveal` awaits `fetchDir` calls. When all directories are already cached (`loadedDirs` hits), those awaits resolve in a single microtask tick, so the `requestAnimationFrame` can fire while the sidebar's open animation is mid-flight and `scrollEl.clientHeight` reads 0. The centering formula `targetTop - (viewportHeight - itemHeight) / 2` then becomes `targetTop + itemHeight/2`, placing the row near the top of the viewport rather than centered once the sidebar fully expands.

### Issue 3 of 3
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/FilesTab/utils/scrollTreeToRow/scrollTreeToRow.ts:49-62
**O(n × depth) visible-index walk on every scroll**

`computeVisibleRowIndex` calls `isPathVisible(path, model)` for every path before the target, and each `isPathVisible` call re-walks all ancestors of that path. For a workspace with thousands of files at several levels of depth this is measurably expensive per reveal, even inside `requestAnimationFrame`. A simple optimization is to track a `Set<string>` of directories already confirmed expanded and skip the ancestor walk for subsequent paths that share the same parent chain.

Reviews (1): Last reviewed commit: "fix(desktop): v2 file reveal — expand, h..." | Re-trigger Greptile

Comment on lines 126 to 128
} finally {
inflightDirsRef.current.delete(relDir);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Stale promise can evict a newer promise from inflightDirsRef on workspace switch

inflightDirsRef.current.delete(relDir) is unconditional, so if a stale P1 (from the previous workspace) resolves after the map was cleared and a new P2 was registered under the same key, P1's finally silently evicts P2. Subsequent concurrent callers then see no in-flight entry and start a redundant P3 fetch, bypassing the deduplication this Map was introduced to provide. The fix is to guard the delete with an identity check:

} finally {
  if (inflightDirsRef.current.get(relDir) === promise) {
    inflightDirsRef.current.delete(relDir);
  }
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/FilesTab/hooks/useFilesTabBridge/useFilesTabBridge.ts
Line: 126-128

Comment:
**Stale promise can evict a newer promise from `inflightDirsRef` on workspace switch**

`inflightDirsRef.current.delete(relDir)` is unconditional, so if a stale P1 (from the previous workspace) resolves after the map was cleared and a new P2 was registered under the same key, P1's `finally` silently evicts P2. Subsequent concurrent callers then see no in-flight entry and start a redundant P3 fetch, bypassing the deduplication this Map was introduced to provide. The fix is to guard the delete with an identity check:

```ts
} finally {
  if (inflightDirsRef.current.get(relDir) === promise) {
    inflightDirsRef.current.delete(relDir);
  }
}
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +35 to +45
const viewportHeight = scrollEl.clientHeight;
const targetTop = visibleIndex * itemHeight;
const targetBottom = targetTop + itemHeight;
const currentTop = scrollEl.scrollTop;
const currentBottom = currentTop + viewportHeight;

if (targetTop >= currentTop && targetBottom <= currentBottom) return true;
scrollEl.scrollTop = Math.max(
0,
targetTop - (viewportHeight - itemHeight) / 2,
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 clientHeight may be 0 when sidebar CSS transition is still running

handleQuickOpenSelectFile opens the sidebar with a state update, then openFilePanereveal awaits fetchDir calls. When all directories are already cached (loadedDirs hits), those awaits resolve in a single microtask tick, so the requestAnimationFrame can fire while the sidebar's open animation is mid-flight and scrollEl.clientHeight reads 0. The centering formula targetTop - (viewportHeight - itemHeight) / 2 then becomes targetTop + itemHeight/2, placing the row near the top of the viewport rather than centered once the sidebar fully expands.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/FilesTab/utils/scrollTreeToRow/scrollTreeToRow.ts
Line: 35-45

Comment:
**`clientHeight` may be 0 when sidebar CSS transition is still running**

`handleQuickOpenSelectFile` opens the sidebar with a state update, then `openFilePane``reveal` awaits `fetchDir` calls. When all directories are already cached (`loadedDirs` hits), those awaits resolve in a single microtask tick, so the `requestAnimationFrame` can fire while the sidebar's open animation is mid-flight and `scrollEl.clientHeight` reads 0. The centering formula `targetTop - (viewportHeight - itemHeight) / 2` then becomes `targetTop + itemHeight/2`, placing the row near the top of the viewport rather than centered once the sidebar fully expands.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +49 to +62
function computeVisibleRowIndex(
targetKey: string,
knownPaths: ReadonlySet<string>,
model: FileTree,
): number {
const prepared = prepareFileTreeInput(Array.from(knownPaths));
let index = 0;
for (const path of prepared.paths) {
if (path === targetKey) {
return isPathVisible(path, model) ? index : -1;
}
if (isPathVisible(path, model)) index++;
}
return -1;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 O(n × depth) visible-index walk on every scroll

computeVisibleRowIndex calls isPathVisible(path, model) for every path before the target, and each isPathVisible call re-walks all ancestors of that path. For a workspace with thousands of files at several levels of depth this is measurably expensive per reveal, even inside requestAnimationFrame. A simple optimization is to track a Set<string> of directories already confirmed expanded and skip the ancestor walk for subsequent paths that share the same parent chain.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/FilesTab/utils/scrollTreeToRow/scrollTreeToRow.ts
Line: 49-62

Comment:
**O(n × depth) visible-index walk on every scroll**

`computeVisibleRowIndex` calls `isPathVisible(path, model)` for every path before the target, and each `isPathVisible` call re-walks all ancestors of that path. For a workspace with thousands of files at several levels of depth this is measurably expensive per reveal, even inside `requestAnimationFrame`. A simple optimization is to track a `Set<string>` of directories already confirmed expanded and skip the ancestor walk for subsequent paths that share the same parent chain.

How can I resolve this? If you propose a fix, please make it concise.

- always intercept file-row clicks in usePierreRowClickPolicy instead of
  deferring plain+pane clicks to Pierre. Pierre's selectOnlyPath no-ops
  when the clicked row is already selected, so re-clicks (click-to-pin,
  reopen after Cmd+W) silently dropped through. Removing the defer
  restores the every-click-fires behavior the pre-Pierre tree had.
- split openFilePane (focus-existing or openPane, no pin — used by the
  Quick Open picker and pane-registry openers) from
  openFilePaneFromTreeClick (pin-on-active wrapper — used by the sidebar
  tree). Picker pre-picks no longer pin-stick the active pane, while
  sidebar keeps the VS-Code click-again-to-pin gesture.
Copy link
Copy Markdown
Contributor

@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.

🧹 Nitpick comments (1)
apps/desktop/src/renderer/lib/clickPolicy/usePierreRowClickPolicy.ts (1)

37-40: ⚡ Quick win

Stale JSDoc contradicts new always-intercept behavior.

The doc block still states that "Unbound tiers and plain 'pane' defer to Pierre's own onSelectionChange", which is the exact behavior the PR removed. The new inline comment at lines 83–86 correctly explains the always-intercept policy; please update this top-level JSDoc to match so future readers aren't misled.

📝 Proposed JSDoc update
- * Unbound tiers and plain "pane" defer to Pierre's own `onSelectionChange`
- * so the visual selection stays in sync; intercepting would swallow the
- * click and leave Pierre out of date.
+ * When `filePolicy.resolve` returns a non-null `action`, the click is always
+ * intercepted (preventDefault + stopPropagation) rather than deferring to
+ * Pierre's `onSelectionChange`. Pierre's `selectOnlyPath` no-ops when the
+ * clicked row is already selected, which would otherwise silently drop
+ * legitimate re-clicks (click-to-pin, reopen after Cmd+W). The app drives
+ * selection explicitly via the reveal flow instead.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/renderer/lib/clickPolicy/usePierreRowClickPolicy.ts` around
lines 37 - 40, Update the stale top-level JSDoc for usePierreRowClickPolicy to
reflect the new always-intercept behavior: remove the line that says "Unbound
tiers and plain 'pane' defer to Pierre's own `onSelectionChange`" and replace it
with a short description that this hook always intercepts clicks (including
unbound tiers and 'pane') and forwards selection changes through the hook
instead of deferring to Pierre; keep or reference the existing inline comment at
lines ~83–86 as supporting detail and ensure the JSDoc mentions the rationale
that intercepting prevents Pierre from getting out of sync.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@apps/desktop/src/renderer/lib/clickPolicy/usePierreRowClickPolicy.ts`:
- Around line 37-40: Update the stale top-level JSDoc for
usePierreRowClickPolicy to reflect the new always-intercept behavior: remove the
line that says "Unbound tiers and plain 'pane' defer to Pierre's own
`onSelectionChange`" and replace it with a short description that this hook
always intercepts clicks (including unbound tiers and 'pane') and forwards
selection changes through the hook instead of deferring to Pierre; keep or
reference the existing inline comment at lines ~83–86 as supporting detail and
ensure the JSDoc mentions the rationale that intercepting prevents Pierre from
getting out of sync.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bb353d3b-9adf-47a0-acbd-12cf593e2596

📥 Commits

Reviewing files that changed from the base of the PR and between 3f663a5 and 45fbcc3.

📒 Files selected for processing (3)
  • apps/desktop/src/renderer/lib/clickPolicy/usePierreRowClickPolicy.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useWorkspaceFileNavigation/useWorkspaceFileNavigation.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx

…sdoc

fetchDir's finally was unconditionally deleting its in-flight map entry. On
a workspace switch the map is cleared and a new promise can land under the
same key, so a late-resolving stale promise would evict the live one and
let subsequent callers start a redundant fetch. Identity-check the entry
before deleting it.

Update usePierreRowClickPolicy jsdoc to reflect the always-intercept
policy (the previous "defer to Pierre's onSelectionChange" line was a
leftover from before the fix).
The reveal flow programmatically selects the just-opened file's row in
Pierre, which fires onSelectionChange synchronously. That echo re-entered
onSelectFile → openFilePaneFromTreeClick, where active === target matched
and pinned the pane we just opened. Result: every freshly opened pane
came out pinned after its reveal completed.

Guard the onSelect handler by skipping when treePath already matches
selectedFilePath (the file we just opened). Real keyboard nav, which
moves selection to a different path, still routes through onSelectFile.
@saddlepaddle saddlepaddle merged commit 55f31db into main May 14, 2026
9 of 10 checks passed
@github-actions
Copy link
Copy Markdown
Contributor

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ✅ Neon database branch

Thank you for your contribution! 🎉

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