Repository navigation
Remove sidebar row-ids preference aggregation feeding the layout livelock (#2586) - #5708
Conversation
…elock The workspace sidebar published SidebarWorkspaceRowIdsPreferenceKey from every row and group header, reduced it sidebar-wide on every layout pass, and wrote the aggregated set into @State from onPreferenceChange to gate selected-workspace scroll-into-view on "row is laid out". During lazy layout churn (scroll, tab switch) that preference value changes on every pass, so each pass scheduled another transaction from inside GraphHost.flushTransactions; once the cycle stopped converging the main thread never returned to the run loop (100% CPU, unbounded autorelease growth, CLI dispatch_sync deadlock). ScrollViewProxy.scrollTo resolves .id() values in lazy containers without the row being realized, and unknown ids are a no-op, so the laid-out gate is unnecessary: scroll pending requests unconditionally and delete the preference key, the per-row emitters, the sidebar-wide onPreferenceChange, and the laidOutWorkspaceRowIds state. Same class-elimination as #5325 did for the sibling SidebarWorkspaceRowFramePreferenceKey (drop-target anchors), which no longer appears in hang samples since it shipped. Issue: #2586 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThe PR removes PreferenceKey-based layout tracking for sidebar workspace rows and instead tracks a single pendingSelectedWorkspaceScrollId; scrolling now occurs immediately and unconditionally via ScrollViewProxy, with collapsed-group redirects handled by a computed scroll target. ChangesWorkspace scrolling refactoring
Sequence Diagram(s)sequenceDiagram
participant User
participant ContentView
participant ScrollViewProxy
participant GroupHeaderAnchor
User->>ContentView: change selected workspace
ContentView->>ContentView: set pendingSelectedWorkspaceScrollId
ContentView->>ScrollViewProxy: flushPendingSelectedWorkspaceScroll(scrollTarget)
alt workspace belongs to collapsed group
ScrollViewProxy->>GroupHeaderAnchor: scrollTo(groupAnchorId)
else normal workspace
ScrollViewProxy->>ScrollViewProxy: scrollTo(workspaceId)
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Suggested reviewers
Poem
Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (2 errors, 1 warning)
✅ Passed checks (18 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 830838bd69
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| proxy.scrollTo(selectedWorkspaceId) | ||
| pendingSelectedWorkspaceScrollId = nil |
There was a problem hiding this comment.
Keep pending scrolls for hidden group children
When the selected workspace is a non-anchor child of a collapsed group (for example after the non-focus socket/CLI collapse path, which intentionally does not change selection), renderContext.workspaceIds still contains that UUID even though SidebarWorkspaceRenderItem.renderItems does not render a view with that .id. scrollTo is therefore an unknown-id no-op, but this now clears pendingSelectedWorkspaceScrollId; expanding the group later does not change workspaceIds or selectedTabId, so the selected row is never scrolled into view. Previously the pending request survived until the row preference appeared after expansion.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Addressed in 68adaa7: hidden members of a collapsed group now resolve their scroll target to the group header (anchor workspace id) from render-context data, so the scroll lands where the workspace lives instead of silently no-oping. Deliberately not restoring the deferred-scroll-on-expand: that required tracking what the lazy layout realized, which is the feedback edge this PR removes.
Greptile SummaryThis PR removes
Confidence Score: 5/5Safe to merge: the change deletes a well-identified feedback loop rather than patching it, the replacement API is the platform's intended mechanism, and the collapsed-group scroll target is decided from model data that is always present. The deletion of SidebarWorkspaceRowIdsPreferenceKey and its surrounding @State write removes the exact mechanism driving the livelock, and the new unconditional scrollTo path is simpler with no new state or observers introduced. scrollTargetWorkspaceId is pure and deterministic; the three unit tests cover the behaviorally distinct cases. The only pre-existing gap — the implicit coupling between the workspace-order-change notification path and onChange(workspaceIds) as its flush trigger — was already noted in a prior review and is unchanged by this PR. No files require special attention; all changes are deletions or small, self-contained additions. Important Files Changed
Reviews (3): Last reviewed commit: "Import Foundation in SidebarState for UU..." | Re-trigger Greptile |
…sed group Codex review: a member of a collapsed group renders no row, so scrollTo with its UUID is a no-op, and clearing the pending request dropped the old deferred-scroll-on-expand behavior. Resolve the scroll target from render-context data instead: hidden members target their group header (anchor workspace id), which is always present. Never decided from what the lazy layout realized, so it cannot re-enter the preference/layout feedback cycle. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lPolicy Keeps ContentView.swift under the Swift file length budget (workflow-guard-tests) and makes the collapsed-group fallback unit testable. Adds coverage: no group / expanded group target the workspace itself, collapsed group targets the group anchor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
#5708 removed SidebarWorkspaceRowIdsPreferenceKey, its per-row .preference emitters, the sidebar-wide onPreferenceChange aggregation, and laidOutWorkspaceRowIds — the cmux-owned edge of the #2586 sidebar layout livelock — and made selected-workspace scroll-into-view call scrollTo unconditionally. This branch's overflow fix had layered a row-layout *completeness* gate on top of that same removed per-row IDs aggregation. Reconcile by dropping the completeness machinery entirely (it depended on the deleted key) and keeping only: - a single whole-content GeometryReader that measures the rows container height once (not a per-row preference reduce), deduped by SidebarWorkspaceRowsMeasurement.isEquivalent so sub-pixel jitter from agent-driven row re-renders never writes @State — so it cannot re-feed the #2586 transaction cycle; and - the finite empty-area height that fixes the original #3241 phantom scrollbar. Also simplify SidebarWorkspaceScrollLayout (drop rowsOverflow, rowsLayoutCompleteness, and the completeness enum), restore the ghostty pointer to origin/main, and update the layout tests for the simplified API. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…n, ECC M to L (#5727) * WIP snapshot: working tree preserved after repo/.git loss (2026-06-09 incident) * Restore sidebar scroll files to origin/main The repo-loss snapshot commit flattened a pre-#5708 working tree on top of origin/main, accidentally reverting the merged sidebar row-ids preference removal (#5708). These four files are unrelated to the QR work; restore them to main. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Restructure compact QR coding per cmux file-organization policy Replace the CmxAttachTicketCompactCoding namespace enum with an injectable CmxAttachTicketCompactCoder struct, move each compact DTO (CompactAttachTicket, CompactAttachRoute, CompactAttachEndpoint) into its own file, and turn the pure private static helpers into file-scope private functions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Scope compact-attach helpers onto their owning types per package conventions --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…021 + 6026) (#6033) * Sidebar: remove whole-content rows-height measurement (fixes layout livelock) Replace the LazyVStack background GeometryReader -> SidebarWorkspaceRowsHeightPreferenceKey -> @State workspaceRowsMeasurement -> emptyAreaHeight round trip with SidebarRowsFillLayout, a custom Layout that places the rows at their natural height and stretches the empty drop/tap area to fill the remaining viewport from its own concrete bounds, in one geometry pass with no state writes. The preference write during layout fed a non-converging relayout transaction: main thread pinned 100%+ in GraphHost.flushTransactions -> LazySubviewPlacements.placeSubviews -> LazyStack.place -> ForEachList.applyNodes. A fresh 2026-06-12 capture on stable 0.64.15 (which already contains the mitigations from #5708, #5846, #5855, and #5859) shows the identical signature: 128% CPU, 400 threads, debug socket refusing connections, 3344/3715 main-thread samples inside flushTransactions. The rows-height key is the last live write-during-layout edge in the sidebar after #5325 (frame anchors) and #5708 (row IDs) removed their siblings. Same approach as #5852, re-ported on top of the #5846 pixel-alignment work (contentMinHeight flooring is kept; only the empty-area math moves into the Layout). Fixes #5764. Helps #2586, #5570, #5845. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: create bundled helper directory before install * Sidebar: replace render-item String id with an allocation-free Hashable enum ForEach(renderItems, id: \.id) gathers every row's identifier on each list diff, and the sidebar re-diffs all rows per update. The previous computed String id ("workspace.\(uuid.uuidString)") allocated and formatted a fresh 36-char string per access; SidebarWorkspaceRenderItem.id.getter was the hottest app-owned frame in the #5764 livelock spindump. SidebarWorkspaceRenderItemID is a two-case enum over UUID: identity compare and hash with zero heap allocation, and group headers can never collide with workspace rows on the same UUID (same guarantee the string prefixes gave). Identity values are unchanged in meaning, so row lifetime and animations are unaffected; nothing persisted the string form (the only consumers are the ForEach key path and scrollTo, which targets the explicit inner .id(tab.id) UUIDs, not the ForEach identity). Pure per-pass cost cut for #5764, #5845, #2586; complements the structural loop fix in #6019. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Sidebar: make rows height-stable under agent churn (no height animation, eager markdown) Two changes that stop agent activity from continuously varying sidebar row heights, which kept re-feeding the sidebar-wide layout/measurement cycle at animation frame rate (#5764, #5845): 1. Remove the three implicit .animation(value:) modifiers on agent-mutable snapshot fields (latestLog, progress, metadataBlocks.count) and reduce the four height-moving .transition(.opacity.combined(.move(edge: .top))) modifiers in TabItemView's log/progress/metadata sections to .transition(.opacity). While a row-height animation runs, every frame produces a different LazyVStack content height; with dozens of agent sessions some row is always animating. Content changes now apply in one discrete layout pass. 2. SidebarMetadataMarkdownBlockRow parsed its markdown in onAppear into @State: a guaranteed nil -> attributed swap (and height change) on every first appearance of every block scrolling in. It now renders inline via a new SidebarMetadataMarkdownRenderer with a bounded (512-entry) memo cache, so the FIRST render is already attributed and appearance performs no state write and no height change. Matches the SidebarWorkspaceDescriptionText sibling, plus memoization to keep repeat body evals cheap and growth bounded. WWDC backing: lazy rows must be height-stable after appearing; initialize row state in the initializer, not onAppear (WWDC26 "Dive into lazy stacks", 321); keep body cheap / precompute (WWDC23 10160, WWDC25 306). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Cache failed parses with updateValue (subscript assignment drops nil values) With [String: AttributedString?], `cache[markdown] = parsed` removes the key when parsed is nil, so unparseable blocks re-parsed on every body eval and appended phantom keys to insertionOrder, mis-evicting valid entries once at capacity. Caught by Greptile on the PR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Byte-bound the metadata markdown cache (autoreview P1) The 512-entry cap bounded entry count but not retained bytes. Metadata blocks are agent/control-socket supplied and uncapped at this boundary, so a key churning large unique markdown could keep hundreds of big payloads alive after the workspace metadata was overwritten or cleared (worse than the old row-local @State, which released on update). Skip caching blocks over 4096 UTF-8 bytes: they parse inline each eval (rare, still attributed from the first frame), and total retained cache bytes are now bounded by capacity * maxCacheableBytes regardless of churn. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Plain-text fallback for oversized metadata blocks (autoreview P1) Parsing >4KB blocks inline (previous commit) removed the retention but moved the cost to CPU: TabItemView.body re-runs on snapshot changes under agent churn, so a large block reparsed each time. Return nil for oversized blocks instead, so the row falls back to the existing Text(block.markdown) plain path: no parse, no retention, and height-stable (the result never changes for a given block, so no nil->attributed swap). Small blocks still cache and render as markdown. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Size the sidebar empty area from an explicit viewport, not the layout proposal (autoreview P2) SidebarRowsFillLayout derived its container height from proposal.replacingUnspecifiedDimensions(). A vertical ScrollView leaves the scroll-axis height unspecified, so that fell back to a 10pt placeholder and the empty area collapsed to 0 whenever the rows fit the viewport — dropping the blank area below the last row out of the double-click/drop target. Pass the viewport height (minHeight, the floored content height the call site already computes from the scroll geometry) into the layout explicitly and size the empty area from it. New emptyAreaFillHeight(viewportHeight:rowsHeight:) overload encodes container = max(viewport, rows). Verified at runtime via temporary instrumentation (since removed): rows fit -> viewport=628 rows=421 empty=207 and rows=370 empty=258; rows overflow -> viewport=628 rows=676 empty=0. Added unit coverage for both the fit and overflow viewport paths. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…n, ECC M to L (#5727) * WIP snapshot: working tree preserved after repo/.git loss (2026-06-09 incident) * Restore sidebar scroll files to origin/main The repo-loss snapshot commit flattened a pre-#5708 working tree on top of origin/main, accidentally reverting the merged sidebar row-ids preference removal (manaflow-ai/cmux#5708). These four files are unrelated to the QR work; restore them to main. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Restructure compact QR coding per cmux file-organization policy Replace the CmxAttachTicketCompactCoding namespace enum with an injectable CmxAttachTicketCompactCoder struct, move each compact DTO (CompactAttachTicket, CompactAttachRoute, CompactAttachEndpoint) into its own file, and turn the pure private static helpers into file-scope private functions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Scope compact-attach helpers onto their owning types per package conventions --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…021 + 6026) (#6033) * Sidebar: remove whole-content rows-height measurement (fixes layout livelock) Replace the LazyVStack background GeometryReader -> SidebarWorkspaceRowsHeightPreferenceKey -> @State workspaceRowsMeasurement -> emptyAreaHeight round trip with SidebarRowsFillLayout, a custom Layout that places the rows at their natural height and stretches the empty drop/tap area to fill the remaining viewport from its own concrete bounds, in one geometry pass with no state writes. The preference write during layout fed a non-converging relayout transaction: main thread pinned 100%+ in GraphHost.flushTransactions -> LazySubviewPlacements.placeSubviews -> LazyStack.place -> ForEachList.applyNodes. A fresh 2026-06-12 capture on stable 0.64.15 (which already contains the mitigations from manaflow-ai/cmux#5708, manaflow-ai/cmux#5846, manaflow-ai/cmux#5855, and manaflow-ai/cmux#5859) shows the identical signature: 128% CPU, 400 threads, debug socket refusing connections, 3344/3715 main-thread samples inside flushTransactions. The rows-height key is the last live write-during-layout edge in the sidebar after manaflow-ai/cmux#5325 (frame anchors) and manaflow-ai/cmux#5708 (row IDs) removed their siblings. Same approach as manaflow-ai/cmux#5852, re-ported on top of the manaflow-ai/cmux#5846 pixel-alignment work (contentMinHeight flooring is kept; only the empty-area math moves into the Layout). Fixes manaflow-ai/cmux#5764. Helps manaflow-ai/cmux#2586, manaflow-ai/cmux#5570, manaflow-ai/cmux#5845. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: create bundled helper directory before install * Sidebar: replace render-item String id with an allocation-free Hashable enum ForEach(renderItems, id: \.id) gathers every row's identifier on each list diff, and the sidebar re-diffs all rows per update. The previous computed String id ("workspace.\(uuid.uuidString)") allocated and formatted a fresh 36-char string per access; SidebarWorkspaceRenderItem.id.getter was the hottest app-owned frame in the manaflow-ai/cmux#5764 livelock spindump. SidebarWorkspaceRenderItemID is a two-case enum over UUID: identity compare and hash with zero heap allocation, and group headers can never collide with workspace rows on the same UUID (same guarantee the string prefixes gave). Identity values are unchanged in meaning, so row lifetime and animations are unaffected; nothing persisted the string form (the only consumers are the ForEach key path and scrollTo, which targets the explicit inner .id(tab.id) UUIDs, not the ForEach identity). Pure per-pass cost cut for manaflow-ai/cmux#5764, manaflow-ai/cmux#5845, manaflow-ai/cmux#2586; complements the structural loop fix in manaflow-ai/cmux#6019. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Sidebar: make rows height-stable under agent churn (no height animation, eager markdown) Two changes that stop agent activity from continuously varying sidebar row heights, which kept re-feeding the sidebar-wide layout/measurement cycle at animation frame rate (manaflow-ai/cmux#5764, manaflow-ai/cmux#5845): 1. Remove the three implicit .animation(value:) modifiers on agent-mutable snapshot fields (latestLog, progress, metadataBlocks.count) and reduce the four height-moving .transition(.opacity.combined(.move(edge: .top))) modifiers in TabItemView's log/progress/metadata sections to .transition(.opacity). While a row-height animation runs, every frame produces a different LazyVStack content height; with dozens of agent sessions some row is always animating. Content changes now apply in one discrete layout pass. 2. SidebarMetadataMarkdownBlockRow parsed its markdown in onAppear into @State: a guaranteed nil -> attributed swap (and height change) on every first appearance of every block scrolling in. It now renders inline via a new SidebarMetadataMarkdownRenderer with a bounded (512-entry) memo cache, so the FIRST render is already attributed and appearance performs no state write and no height change. Matches the SidebarWorkspaceDescriptionText sibling, plus memoization to keep repeat body evals cheap and growth bounded. WWDC backing: lazy rows must be height-stable after appearing; initialize row state in the initializer, not onAppear (WWDC26 "Dive into lazy stacks", 321); keep body cheap / precompute (WWDC23 10160, WWDC25 306). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Cache failed parses with updateValue (subscript assignment drops nil values) With [String: AttributedString?], `cache[markdown] = parsed` removes the key when parsed is nil, so unparseable blocks re-parsed on every body eval and appended phantom keys to insertionOrder, mis-evicting valid entries once at capacity. Caught by Greptile on the PR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Byte-bound the metadata markdown cache (autoreview P1) The 512-entry cap bounded entry count but not retained bytes. Metadata blocks are agent/control-socket supplied and uncapped at this boundary, so a key churning large unique markdown could keep hundreds of big payloads alive after the workspace metadata was overwritten or cleared (worse than the old row-local @State, which released on update). Skip caching blocks over 4096 UTF-8 bytes: they parse inline each eval (rare, still attributed from the first frame), and total retained cache bytes are now bounded by capacity * maxCacheableBytes regardless of churn. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Plain-text fallback for oversized metadata blocks (autoreview P1) Parsing >4KB blocks inline (previous commit) removed the retention but moved the cost to CPU: TabItemView.body re-runs on snapshot changes under agent churn, so a large block reparsed each time. Return nil for oversized blocks instead, so the row falls back to the existing Text(block.markdown) plain path: no parse, no retention, and height-stable (the result never changes for a given block, so no nil->attributed swap). Small blocks still cache and render as markdown. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Size the sidebar empty area from an explicit viewport, not the layout proposal (autoreview P2) SidebarRowsFillLayout derived its container height from proposal.replacingUnspecifiedDimensions(). A vertical ScrollView leaves the scroll-axis height unspecified, so that fell back to a 10pt placeholder and the empty area collapsed to 0 whenever the rows fit the viewport — dropping the blank area below the last row out of the double-click/drop target. Pass the viewport height (minHeight, the floored content height the call site already computes from the scroll geometry) into the layout explicitly and size the empty area from it. New emptyAreaFillHeight(viewportHeight:rowsHeight:) overload encodes container = max(viewport, rows). Verified at runtime via temporary instrumentation (since removed): rows fit -> viewport=628 rows=421 empty=207 and rows=370 empty=258; rows overflow -> viewport=628 rows=676 empty=0. Added unit coverage for both the fit and overflow viewport paths. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Summary
SidebarWorkspaceRowIdsPreferenceKeyentirely: the per-row/per-group-header.preferenceemitters, the sidebar-wide.onPreferenceChangeaggregation, and thelaidOutWorkspaceRowIds@State.ScrollViewProxy.scrollTounconditionally for pending requests.scrollToresolves.id()values in lazy containers without the target row being realized, and unknown ids are a harmless no-op, so the "wait until the row is laid out" gate was unnecessary.Why
This is the surviving cmux-owned edge in the #2586 sidebar layout livelock. Fresh evidence from today (0.64.14-nightly, macOS 26.5, 36 workspaces, no groups — well below the 100-workspace scale discussed earlier): scrolling the sidebar pegged the main thread at 100% inside a single
GraphHost.flushTransactions()that never returned to the run loop. Twosamplecaptures minutes apart show the identical signature:NSHostingView.beginTransactionrun-loop observer callback, loopingrunTransaction→LazySubviewPlacements.placeSubviewsover the sidebarForEach.SidebarWorkspaceRowIdsPreferenceKey.reduce→Set.formUnionin the hot loop (the Fix sidebar workspace frame collection virtualization #5325-gatedSidebarWorkspaceRowFramePreferenceKeyis absent, consistent with vlechemin's 0.64.13 table).LazyLayoutCacheItem.AllItemsPhaseMutation→LazyLayoutViewCache.updateItemPhases→propagate_dirty, plusLazyLayoutViewCache.signalPrefetch→NSHostingView.requestUpdatequeuing the next transaction.NSCompositeAppearance, 8.6MNSMutableDictionary).Mechanism: the preference value is the set of currently-realized lazy rows, which changes on essentially every layout pass while scrolling. Each change re-runs the N-row
reduceand firesonPreferenceChange, which writes@Statefrom inside the update cycle, scheduling another transaction inside the sameflushTransactionsloop. Once lazy phase churn keeps the realized-row set oscillating, the cycle stops converging and the main thread never escapes.This is the same class-elimination #5325 applied to the sibling drop-target anchor preference: delete the steady-state per-row preference traffic instead of patching timing. With this PR the workspace sidebar publishes no per-row preferences at rest at all.
How this is principled vs hacky
Principled: it deletes the feedback mechanism rather than debouncing or gating it, and the replacement (
scrollToon lazy-container ids) is the API's intended use. Residual risk: if some macOS build fails to resolve an unrealized row id, scroll-into-view for far-off-screen rows would silently no-op —cmuxUITests/WorkspaceSidebarScrollUITests(Cmd+1 from the bottom of a 20-workspace overflow list) covers exactly that path on CI.Testing
WorkspaceSidebarScrollUITestsviatest-e2e.yml(covers selected-row scroll-into-view incl. off-screen Cmd+1 and Move-to-Top): run pending below.rowidsfor dogfood.Issues
dispatch_syncdeadlock and any non-cmux-owned residual churn remain tracked there)Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.Note
Medium Risk
Changes lazy-sidebar scroll behavior (no longer gated on realized rows); collapsed-group targeting is new but model-driven, with UI tests covering off-screen scroll paths.
Overview
Removes the sidebar layout feedback loop that caused main-thread livelock (#2586) by deleting
SidebarWorkspaceRowIdsPreferenceKey, per-row/group.preferenceemitters, the sidebar.onPreferenceChangehandler, and@State laidOutWorkspaceRowIds.Selected-workspace scroll-into-view no longer waits for lazy rows to report as laid out.
flushPendingSelectedWorkspaceScrollalways callsScrollViewProxy.scrollToon an id resolved from model data viaSidebarSelectedWorkspaceScrollPolicy.scrollTargetWorkspaceId(selected workspace, or the group header anchor when the group is collapsed and the member row has no.id). Scroll helpers now takeWorkspaceListRenderContextinstead of raw id lists or preference-derived row sets.Unit tests cover the new scroll-target mapping for ungrouped, expanded-group, and collapsed-group cases.
Reviewed by Cursor Bugbot for commit 5bab011. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by cubic
Removes the sidebar row-id preference aggregation and scroll gate to break the #2586 layout livelock. Scrolling now always targets the selected workspace (or its group header when collapsed) via
ScrollViewProxy.scrollTo.SidebarWorkspaceRowIdsPreferenceKey, per-row/group.preferences, sidebar.onPreferenceChange, andlaidOutWorkspaceRowIds.scrollTowith a policy-resolved id; lazy id resolution handles unrealized rows and unknown ids as a no-op. Collapsed-group selections map to the group header viaSidebarSelectedWorkspaceScrollPolicy.scrollTargetWorkspaceId; tests cover no-group/expanded/collapsed cases.FoundationinSidebarStateforUUID.Written for commit 5bab011. Summary will update on new commits.
Summary by CodeRabbit