Skip to content

fix(sidebar): put pinned folder workspaces in the Pinned section - #16799

Open
lafraia wants to merge 4 commits into
stablyai:mainfrom
lafraia:fix/sidebar-pinned-folder-workspace
Open

lafraia wants to merge 4 commits into
stablyai:mainfrom
lafraia:fix/sidebar-pinned-folder-workspace

Conversation

@lafraia

@lafraia lafraia commented Aug 27, 2026 •

Copy link
Copy Markdown

ELI5

Pinning a folder workspace now puts it in the left Pinned list, the same way pinning a git worktree already does.

Visual Proof

Captured from the running Electron app over CDP. Same persisted isPinned: true state on both sides — only the code differs, so this is the fix and not a setup difference.

pinned-folder-workspace-before-after.png

Also verified live, through the real UI rather than the store:

  • Pin from the row's context menu moves the card into Pinned and out of its project group.
  • showPinnedWorktreesInGroups renders both copies, with distinct DOM ids, and exactly one primary active surface; clicking the Pinned copy promotes it and demotes the group copy.
  • Revealing from a fully collapsed sidebar expands Pinned and lands on the card.
  • Shift-click range selection spans Pinned → project group and selects each workspace once.

What Changed

buildRows already treated pin as placement for git worktrees. Folder workspaces had an isPinned flag, a Pin/Unpin menu item and persistence, but the Pinned emitter only accepted Worktree[], so a pinned folder workspace stayed under its project group. Pin is now a placement choice on the same renderable folder-workspace set (#15362), partitioned right beside the existing worktree partition:

const pinnedFolderWorkspaces = renderableFolderWorkspaces.filter((pair) => pair.folderWorkspace.isPinned)
const naturalFolderWorkspaces =
  pinnedDisplayPolicy === 'duplicate-in-groups'
    ? renderableFolderWorkspaces
    : renderableFolderWorkspaces.filter((pair) => !pair.folderWorkspace.isPinned)

FolderWorkspaceRow now carries a required sectionKey, and derives key from it — exactly as WorktreeRow derives rowKey. Rendering the same workspace twice is only safe if the two copies are distinguishable everywhere, and previously they were not: the virtualizer key, the DOM option id, the reveal walkers and the sidebar-order preference each answered "which row is this?" differently. One key now flows through all of them, and the type makes a section-less folder row unrepresentable.

Falling out of that:

  • Pinned header host counts use the same lane helper as the status/PR lanes, so a folder-only Pinned section still scopes to its SSH/runtime host.
  • emitPinnedGroup took 15 positional parameters; it now takes one options object.
  • The duplicate-preference rule (prefer the natural copy, fall back to Pinned) is written once and shared by worktree and folder rows.

Review follow-ups

Reviewed against /readiness-checklist; each of these is a consequence of a folder workspace newly being able to render twice, found in review and fixed here:

  • Reveal over-expanded. expandGroupsForWorktreeReveal persists every toggle it makes. Adding Pinned on top of the project-group chain permanently un-collapsed a section the card is not in. Now an either/or, mirroring getPinnedWorktreeRevealCollapsedGroupKeys.
  • Host badge double-counted a duplicated folder workspace; item rows already deduped for this exact reason.
  • Both copies drew the primary active surface, because the active-surface rule only understood item rows. Generalized to any row that can duplicate — which also gives folder rows the click-promotion worktrees already had.
  • collectRenderedSidebarRowKeys still emitted the pre-change folder key, disagreeing with getRenderRowSidebarKey.
  • useEffectiveCollapsedGroups held the pinned display policy but did not pass it down; the active-surface lookup resolved a ref for every row on the click path.

Why

The menu already said Unpin, so the pin write was working — the card just never moved. Same class of gap as #15362: Pinned was another lane the git-worktree path owned and the folder-workspace path never joined.

Testing

  • Manually tested in the running app (see Visual Proof)
  • Automated tests added

build-rows.pinned-folder-workspaces.test.ts was run against main first: 9 of 10 failed, with no Pinned header for a pinned folder workspace. New coverage for the review follow-ups: reveal either/or per policy, host-badge dedup, and option-id/aria-activedescendant identity for the duplicated row. Navigation fixtures build rows through buildFolderWorkspaceRow so they cannot drift from the real key shape.

pnpm tc clean; pnpm test src/renderer 26,506 passing; pnpm run check:code-quality:changed 0 new findings.

  • macOS unit tests + live app. Linux/Windows: grouping is host-id keyed, no path or shortcut changes.
  • SSH: host counts go through getFolderWorkspaceHostId, the same resolver the status/PR lanes use, so a pinned SSH folder workspace cannot leak into the local host section. The Pinned header's host map was verified identical to the previous local tally for the git-worktree-only case.

Review

  • Cross-platform: no path, shortcut or menu-accelerator changes.
  • Backward compatibility: the row key changed shape, so I traced every persisted UI field. collapsedGroups stores header keys only; folder-workspace rows are never headers, so no saved state references the old key. No mobile or paired-web surface imports these modules — no wire impact.
  • Performance: two extra O(F) partitions per buildRows (memoized, unchanged deps). Folder rows re-key once on upgrade, then only on grouping changes — the same behavior item rows have always had.
  • Security: no IPC, filesystem or auth changes. The one new sink is a DOM id, already encodeURIComponent-wrapped and only ever read by getElementById.

Known gaps (pre-existing, not introduced here)

Folder rows pass neither renameRowKey nor revealHighlight, so rename-on-reveal and the reveal glow do not fire for folder workspaces — identical on main. Folder workspaces still do not join Cmd+Shift+Arrow cycling (that walker is item-row-only). Both are out of scope.

Checklist

  • This PR is small and focused
  • I explained what changed and why (including ELI5)
  • Before/after screenshots attached
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered
  • pnpm typecheck, pnpm test, lint pass locally

Authorship

Original fix and problem diagnosis by @lafraia (commit 1). Commits 2-4 are a takeover pass: the section-key rework, the readiness-checklist review, and its follow-up fixes.

Pinning a folder workspace persisted isPinned and showed Unpin, but
buildRows only collected git worktrees for the Pinned section, so the
card stayed under its project group.

Treat pin as placement on the same renderable set: a pinned folder
workspace moves to Pinned (or duplicates there when the in-groups
policy is on), host counts include it, and reveal expands Pinned.
@coderabbitai

coderabbitai Bot commented Aug 27, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Pinned folder workspaces are partitioned by display policy and emitted in the Pinned section. Folder workspace rows now use section-qualified keys across grouping, rendering, activation, and accessibility. Navigation selects the correct Pinned or project-group section. Duplicate folder workspace rows are deduplicated in sidebar ordering and host counts. Tests cover grouping, reveal behavior, active descendants, row lookup, and rendering.

Merge Risk: 🔵 Low · up to 2f466

Pinned folder workspaces now appear in Pinned, but mixed fallback ordering, collapsed host counts, and accessibility state can be incorrect when duplicate-in-groups is enabled. These are bounded sidebar presentation issues that should be corrected before relying on the new duplicate behavior.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.14% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 37 functions across 29 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The description thoroughly covers the change, rationale, testing, visual proof, review considerations, and checklist. However, it omits the required Linked Issue section and does not provide an issue … Add the required Linked Issue section with a valid issue reference, such as Fixes #15362`` if applicable. Add the Agent skill upstream boundary checkbox and mark it as applicable or not applicable. Include the AI Disclosure section if requi…
✅ Passed checks (3 passed)
Check name Status 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.
Title check ✅ Passed The title clearly and concisely describes the primary change: moving pinned folder workspaces into the Pinned section.
Full details: Description check

Explanation

The description thoroughly covers the change, rationale, testing, visual proof, review considerations, and checklist. However, it omits the required Linked Issue section and does not provide an issue reference, and it omits the Agent skill upstream boundary section from the template.

Resolution

Add the required Linked Issue section with a valid issue reference, such as Fixes #15362`` if applicable. Add the Agent skill upstream boundary checkbox and mark it as applicable or not applicable. Include the AI Disclosure section if required for this contributor.

  • Fix all pre-merge checks with AI

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.

@lafraia

lafraia commented Sep 3, 2026

Copy link
Copy Markdown
Author

@nwparker Would you please take a look at this one? This is a bug that prevents folder workspaces to be pinned in the pinned section.

@nwparker

nwparker commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Do you mind adding some screenshots? Would help me review faster @lafraia !

@lafraia

lafraia commented Sep 4, 2026

Copy link
Copy Markdown
Author

@nwparker sorry to hide most of the sensitive data, but as you can see the folder workspace is pinned but it's not showing in the pinned section (another issue is subagents should show under the pinned item).
image

@nwparker

nwparker commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

you're right. Will get this in

Pinned folder workspaces need two distinct rows when the duplicate policy is
on, which an optional sectionKey only half-modelled: the virtualizer key
diverged from the DOM option id, so aria-activedescendant and the reveal
walkers pointed at an element that no longer existed.

FolderWorkspaceRow now carries a required sectionKey and derives its key from
it, exactly as WorktreeRow does, so one key flows through the virtualizer, the
DOM id, the reveal walkers and the sidebar-order preference.
… surface

Review follow-ups, all consequences of a folder workspace now being able to
render twice:

- reveal expanded the project group on top of Pinned. Under the default policy
  the card renders only in Pinned, and expandGroupsForWorktreeReveal persists
  every toggle, so revealing permanently un-collapsed a section the card is not
  in. Now an either/or, mirroring getPinnedWorktreeRevealCollapsedGroupKeys.
- the host badge counted a duplicated folder workspace twice; item rows already
  deduped for exactly this reason.
- both copies drew the primary active surface, because the active-surface rule
  only understood item rows. Generalized to any row that can duplicate, which
  also gives folder rows the click-promotion worktrees already had.
- collectRenderedSidebarRowKeys still emitted the pre-change folder key,
  disagreeing with getRenderRowSidebarKey.
Two review follow-ups: useEffectiveCollapsedGroups already held the pinned
display policy but did not pass it to getFolderWorkspaceRevealGroupKeys, the
same silent-policy-assumption shape as the row-key divergence above; and the
active-surface lookup resolved a ref for every row before finding its match,
on the one path the sidebar hand-optimizes for pointer latency.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/renderer/src/components/sidebar/worktree-list/grouping/pinned-group-rows.ts (1)

141-149: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Place imported fallback cards after pinned folder rows.

Line 142 inserts each fallback directly after the last worktree item. Folder rows are not included in lastResultIndexByRepoId. With one pinned worktree, one pinned folder workspace, and one fallback candidate, the result becomes worktree → fallback → folder, not worktree → folder → fallback.

Defer fallback insertion until after the folder-workspace block. Add a regression test for this mixed Pinned section.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Team

Run ID: 4041b051-418b-4023-a610-6fc5296245c5

📥 Commits

Reviewing files that changed from the base of the PR and between 8556acb and 2f46605.

📒 Files selected for processing (27)
  • src/renderer/src/components/sidebar/WorktreeList.folder-workspace-rows.test.ts
  • src/renderer/src/components/sidebar/host-section-folder-workspace-counts.test.ts
  • src/renderer/src/components/sidebar/host-section-rows.test.ts
  • src/renderer/src/components/sidebar/host-section-rows.ts
  • src/renderer/src/components/sidebar/worktree-keyboard-cycle.test.ts
  • src/renderer/src/components/sidebar/worktree-list/grouping/build-rows.folder-workspace-lanes.test.ts
  • src/renderer/src/components/sidebar/worktree-list/grouping/build-rows.pinned-folder-workspaces.test.ts
  • src/renderer/src/components/sidebar/worktree-list/grouping/build-rows.ts
  • src/renderer/src/components/sidebar/worktree-list/grouping/group-sections.ts
  • src/renderer/src/components/sidebar/worktree-list/grouping/pinned-group-rows.ts
  • src/renderer/src/components/sidebar/worktree-list/grouping/project-group-sections.ts
  • src/renderer/src/components/sidebar/worktree-list/grouping/row-builders.ts
  • src/renderer/src/components/sidebar/worktree-list/grouping/row-types.ts
  • src/renderer/src/components/sidebar/worktree-list/listing/use-collapsed-groups.ts
  • src/renderer/src/components/sidebar/worktree-list/listing/use-section-rows.ts
  • src/renderer/src/components/sidebar/worktree-list/navigation/active-descendant-option.folder-workspace.test.ts
  • src/renderer/src/components/sidebar/worktree-list/navigation/active-descendant-option.ts
  • src/renderer/src/components/sidebar/worktree-list/navigation/folder-reveal.test.ts
  • src/renderer/src/components/sidebar/worktree-list/navigation/folder-reveal.ts
  • src/renderer/src/components/sidebar/worktree-list/navigation/pending-reveal-inputs.ts
  • src/renderer/src/components/sidebar/worktree-list/navigation/render-row-lookup.folder-workspace.test.ts
  • src/renderer/src/components/sidebar/worktree-list/navigation/render-row-lookup.ts
  • src/renderer/src/components/sidebar/worktree-list/navigation/use-active-row.ts
  • src/renderer/src/components/sidebar/worktree-list/rows/folder-row.tsx
  • src/renderer/src/components/sidebar/worktree-list/viewport/virtual-row-context.ts
  • src/renderer/src/components/sidebar/worktree-sidebar-row-preference.test.ts
  • src/renderer/src/components/sidebar/worktree-sidebar-row-preference.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 5 remain after this review.

Comment on lines +127 to +132
// Why dedupe: a pinned folder workspace renders in Pinned and in its group
// under duplicate-in-groups, but it is still one workspace to the badge.
if (!seenFolderWorkspaceIds.has(row.folderWorkspace.id)) {
count += 1
seenFolderWorkspaceIds.add(row.folderWorkspace.id)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Deduplicate collapsed header counts.

Lines 127-132 deduplicate only materialized folder rows. If Pinned and the natural group are collapsed, flushHeader adds both aggregate header counts and the same folder workspace increases the host badge twice. Carry folder-workspace IDs in header metadata and deduplicate them in flushHeader. Add a test with both sections collapsed.

role="option"
aria-selected={ctx.selectedWorktreeIds.has(folderWorktreeIdentity)}
aria-current={ctx.activeWorktreeId === folderWorktree.id ? 'page' : undefined}
aria-current={isActive ? 'page' : undefined}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Expose only the primary duplicate as current.

With duplicate-in-groups, both copies have the same folderWorktree.id. Both options then receive aria-current="page". Use getActiveSurfaceVariant(row) and set aria-current only for the primary copy.

stasmarkin pushed a commit to stasmarkin/orca that referenced this pull request Oct 26, 2026
Port of stablyai#16799 onto current main, where the row builder moved
to src/shared/sidebar-list.

Pinning a folder workspace persisted isPinned and showed Unpin, but the
Pinned emitter only accepted git worktrees, so the card stayed under its
project group. Pin is now placement on the renderable folder set: a pinned
folder workspace moves to Pinned (or duplicates there under
duplicate-in-groups), and FolderWorkspaceRow carries a sectionKey so the
two copies get distinct row keys, option ids, active surfaces, reveal and
selection order. Folder row keys stay host-qualified.

Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
stasmarkin pushed a commit to stasmarkin/orca that referenced this pull request Nov 1, 2026
Port of stablyai#16799 onto current main, where the row builder moved
to src/shared/sidebar-list.

Pinning a folder workspace persisted isPinned and showed Unpin, but the
Pinned emitter only accepted git worktrees, so the card stayed under its
project group. Pin is now placement on the renderable folder set: a pinned
folder workspace moves to Pinned (or duplicates there under
duplicate-in-groups), and FolderWorkspaceRow carries a sectionKey so the
two copies get distinct row keys, option ids, active surfaces, reveal and
selection order. Folder row keys stay host-qualified.

Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
stasmarkin pushed a commit to stasmarkin/orca that referenced this pull request Nov 3, 2026
Port of stablyai#16799 onto current main, where the row builder moved
to src/shared/sidebar-list.

Pinning a folder workspace persisted isPinned and showed Unpin, but the
Pinned emitter only accepted git worktrees, so the card stayed under its
project group. Pin is now placement on the renderable folder set: a pinned
folder workspace moves to Pinned (or duplicates there under
duplicate-in-groups), and FolderWorkspaceRow carries a sectionKey so the
two copies get distinct row keys, option ids, active surfaces, reveal and
selection order. Folder row keys stay host-qualified.

Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>

This branch has not been deployed

No deployments
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.

2 participants