Skip to content

Sidebar: make rows height-stable under agent churn (no height animation, eager markdown) - #6026

Closed
azooz2003-bit wants to merge 3 commits into
mainfrom
issue-5764-row-height-churn
Closed

azooz2003-bit wants to merge 3 commits into
mainfrom
issue-5764-row-height-churn

Conversation

@azooz2003-bit

@azooz2003-bit azooz2003-bit commented Jun 13, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

Agent activity continuously varies sidebar row heights, which keeps re-feeding the sidebar-wide layout/measurement cycle at animation frame rate (#5764, #5845). Two mechanisms:

  1. Three implicit .animation(.easeInOut(0.2), value:) modifiers on agent-mutable snapshot fields (latestLog, progress != nil, metadataBlocks.count) plus height-moving .move(edge: .top) transitions in TabItemView's log/progress/metadata sections. While a row-height animation runs, every frame produces a different LazyVStack content height; with dozens of agent sessions some row is always animating, so the whole-sidebar layout re-runs at display refresh rate for the full 0.2s after every log/progress/metadata event.
  2. SidebarMetadataMarkdownBlockRow parsed its markdown in onAppear into @State: a guaranteed nil -> attributed swap (and intrinsic height change) on every first appearance of every metadata block scrolling in. Agent-heavy workspaces, the livelock precondition, are exactly the rows with metadata blocks.

Fix

  1. Remove the three .animation(value:) modifiers; reduce the four TabItemView transitions to .transition(.opacity) (the fifth, in the extension sidebar, is untouched). Content changes now apply in one discrete layout pass; rows are bouncier-less but height-stable.
  2. New SidebarMetadataMarkdownRenderer (own file, keeps ContentView under its length budget): inline rendering 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 existing SidebarWorkspaceDescriptionText inline-parse sibling; the cache bound also prevents unbounded growth in long agent sessions.

WWDC backing: lazy rows must be height-stable after appearing and row state belongs in the initializer, not onAppear (WWDC26 "Dive into lazy stacks"); keep body cheap / precompute (WWDC23 10160, WWDC25 306).

Companion to #6019 (structural loop-edge removal) and #6021 (cheap ForEach identity); all three are independent.

Preflighted on tag hstable: markdown metadata block (report_meta_block with bold/italic/list) renders attributed from the first frame; sidebar drags + workspace creation responsive. Localization audit: no user-facing strings added or changed.

Note on tests: the regression is per-frame layout churn, not testable as a unit assertion; the existing snapshot/UI suites cover rendering. Behavioral gate is the trace diff in dogfood (accepted workspaceRowsMeasurement writes drop from per-frame to per-event, per the validation plan in #5764).

🤖 Generated with Claude Code


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

Stabilizes sidebar row heights during agent churn to stop per-frame layout re-measurement and keep scrolling responsive. Addresses issue-5764 by removing height animations and rendering metadata markdown eagerly with a bounded cache.

  • Bug Fixes
    • Removed implicit animations on latestLog, progress, and metadataBlocks.count; simplified transitions to .opacity in TabItemView to apply changes in one layout pass.
    • Added SidebarMetadataMarkdownRenderer with a 512-entry memo cache to render attributed text on first paint, replacing onAppear parsing and height changes.
    • Ensured the cache stores failed parses via updateValue, preventing re-parsing and incorrect evictions at capacity.

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

Review in cubic

Summary by CodeRabbit

  • Improvements
    • Smoother, more predictable sidebar animations by simplifying transitions and removing implicit lifecycle-driven animations.
    • Faster, more responsive metadata markdown rendering using an in-memory cached renderer and direct inline rendering to eliminate extra lifecycle updates.
  • Refactor
    • Simplified view update paths by deriving rendered content during view computation rather than via state-driven callbacks.

…on, 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>
@vercel

vercel Bot commented Jun 13, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jun 13, 2026 1:26am
cmux-staging Building Building Preview, Comment Jun 13, 2026 1:26am

@coderabbitai

coderabbitai Bot commented Jun 13, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a4688137-8b66-4d41-8830-d5877de2b297

📥 Commits

Reviewing files that changed from the base of the PR and between fc42869 and c8e71c1.

📒 Files selected for processing (1)
  • cmux.xcodeproj/project.pbxproj

📝 Walkthrough

Walkthrough

Adds a bounded in-memory memoization cache for rendered markdown (SidebarMetadataMarkdownRenderer), wires the new file into the Xcode project, and refactors ContentView to call the renderer inline while simplifying transitions and removing implicit animations.

Changes

Markdown Caching and Integration

Layer / File(s) Summary
Markdown memoization cache implementation
Sources/SidebarMetadataMarkdownRenderer.swift
New @MainActor SidebarMetadataMarkdownRenderer with static [String: AttributedString?] cache, insertion-order array, capacity 512, and rendered(_:) that returns cached results, parses on miss, stores (including nil), and evicts oldest when full.
Xcode project build configuration
cmux.xcodeproj/project.pbxproj
Adds SidebarMetadataMarkdownRenderer.swift as a PBXBuildFile and PBXFileReference, includes it in the main "Sources" group, and registers it in the app target's PBXSourcesBuildPhase.
ContentView markdown and transition updates
Sources/ContentView.swift
Calls SidebarMetadataMarkdownRenderer.rendered(...) inline in the markdown row body (removing prior @State lifecycle rendering). Replaces multiple combined opacity+move transitions with .transition(.opacity) and removes three .animation(..., value:) modifiers, replacing them with explanatory comments about animation behavior.

🎯 3 (Moderate) | ⏱️ ~20 minutes

🐰 I cached the markdown, snug and neat,
No extra parsing, quick to greet,
Transitions pared down, subtle and light,
The sidebar hums with tidy might,
A rabbit's nod to code done right.


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Cmux Swiftui State Layout ❌ Error SidebarMetadataMarkdownBlockRow.body calls SidebarMetadataMarkdownRenderer.rendered, which mutates static cache/insertionOrder via cache.updateValue during render. Avoid render-time side effects: compute/store attributed markdown outside body (e.g., in the row initializer) or populate the memo cache in a non-render lifecycle, so body only reads computed values.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (19 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: stabilizing sidebar row heights by removing height animations and implementing eager markdown rendering with caching.
Description check ✅ Passed The description is comprehensive with Problem/Fix sections, technical rationale, WWDC backing, companion PR references, and validation details, though it lacks demo video, explicit local testing confirmation, and incomplete checklist items.
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.
Cmux Swift Actor Isolation ✅ Passed New SidebarMetadataMarkdownRenderer is an @MainActor enum with static bounded cache; ContentView calls it inline and removes .animation modifiers—no background/MainActor isolation issues found.
Cmux Swift Blocking Runtime ✅ Passed PR diff (ContentView.swift + SidebarMetadataMarkdownRenderer.swift) contains no semaphore waits, sleeps/Task.sleep, DispatchQueue.main.sync/asyncAfter, or manual locks; only SwiftUI transitions and...
Cmux Expensive Synchronous Load ✅ Passed Verified Sources/ContentView.swift and Sources/SidebarMetadataMarkdownRenderer.swift contain no RestorableAgentSessionIndex.load() calls; changes are SwiftUI transitions + inline markdown memoiza...
Cmux Cache Substitution Correctness ✅ Passed Renderer memoizes parsed AttributedString by markdown; rendered() returns cache hit and otherwise parses (cold fallback) with bounded capacity, and this is UI rendering, not persistence/history/und...
Cmux No Hacky Sleeps ✅ Passed PR #6026 changes only Swift files and pbxproj (3 files listed); no TypeScript/JavaScript/shell/build runtime scripts were modified to introduce hacky sleeps.
Cmux Algorithmic Complexity ✅ Passed New SidebarMetadataMarkdownRenderer caches markdown in a [String: AttributedString?] dict with a bounded 512-entry FIFO (insertionOrder/removeFirst); no unbounded scans/sorts added.
Cmux Swift Concurrency ✅ Passed PR diff shows no DispatchQueue/Task/Combine/@published or completion-handler patterns; added SidebarMetadataMarkdownRenderer is synchronous @MainActor memoized parsing.
Cmux Swift @Concurrent ✅ Passed New Swift file is @MainActor with only synchronous rendered(_:); no nonisolated async or @concurrent annotations are introduced/used in the changed Swift sources.
Cmux Swift File And Package Boundaries ✅ Passed New Sources/SidebarMetadataMarkdownRenderer.swift is only 45 lines and solely memoized markdown parsing (bounded 512 cache); ContentView changes are small UI transition/inline usage with no mixed p...
Cmux Swift Logging ✅ Passed Touched Swift files contain no print/debugPrint/dump; only an NSLog is inside #if DEBUG in ContentView.swift, and the new renderer adds no Logger constants.
Cmux User-Facing Error Privacy ✅ Passed Diff (origin/main..HEAD) only changes SwiftUI transitions/animations and metadata markdown rendering (new 512-entry renderer); no user-facing errors/alerts/recovery/API error copy added or altered.
Cmux Full Internationalization ✅ Passed Diff (main..PR head) only touched ContentView.swift, new SidebarMetadataMarkdownRenderer.swift, and pbxproj; added code has no user-facing String/Text literals; no i18n/web/strings catalogs changed.
Cmux Architecture Rethink ✅ Passed Renderer is @MainActor with bounded memo cache; ContentView now computes attributed markdown inline (no onAppear state), and no sleeps/locks/asyncAfter/polling/observers were added.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR #6026 only changes ContentView.swift transitions and adds SidebarMetadataMarkdownRenderer.swift + pbxproj; diff contains no NSWindow/NSPanel/WindowGroup or cmuxAuxiliaryWindowIdentifiers assignm...
Cmux Source Artifacts ✅ Passed PR #6026 changes only Sources/ContentView.swift, Sources/SidebarMetadataMarkdownRenderer.swift, and cmux.xcodeproj/project.pbxproj; no build outputs/caches/logs/screenshots/temporary artifacts added.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-5764-row-height-churn

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.

@greptile-apps

greptile-apps Bot commented Jun 13, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes per-frame sidebar layout churn in agent-heavy sessions by removing implicit height animations from agent-mutable snapshot fields and replacing deferred onAppear markdown parsing with an eager, bounded memo cache.

  • Removes three .animation(.easeInOut(0.2), value:) modifiers and simplifies four TabItemView transitions to .opacity, ensuring row height changes land in a single discrete layout pass rather than animating over every display-refresh frame.
  • Adds SidebarMetadataMarkdownRenderer, a @MainActor enum with a 512-entry FIFO cache, so the first render of each metadata block row is already attributed; the updateValue fix correctly stores failed parses as nil entries, preventing phantom-key eviction churn for unparseable blocks.

Confidence Score: 5/5

Safe to merge — the animation removal and eager markdown rendering changes are narrowly scoped to the sidebar row layout path with no regressions identified.

The two root-cause fixes (animation removal, inline rendering with a bounded cache) are each self-contained and well-reasoned. The cache correctly handles nil failed-parse entries via updateValue, the @mainactor isolation is sound, and the ContentView changes touch only transition/animation modifiers and the onAppear state pattern. No new correctness or concurrency issues are introduced by this PR.

No files require special attention. SidebarMetadataMarkdownRenderer.swift is the most novel change but its logic is straightforward and the previously-noted concerns were either fixed (nil eviction) or are bounded constants (FIFO eviction cost).

Important Files Changed

Filename Overview
Sources/SidebarMetadataMarkdownRenderer.swift New @mainactor bounded cache for markdown rendering; nil-valued failed-parse entries are correctly stored via updateValue. Previously-flagged O(n) removeFirst() eviction remains; otherwise cache semantics are sound.
Sources/ContentView.swift Four transitions simplified to .opacity, three implicit height-animating .animation(value:) modifiers removed, and SidebarMetadataMarkdownBlockRow rewritten to call the new renderer inline; changes are focused and correct.
cmux.xcodeproj/project.pbxproj Registers SidebarMetadataMarkdownRenderer.swift in the app target; straightforward project file update with no concerns.

Reviews (3): Last reviewed commit: "Merge origin/main (pick up workflow-guar..." | Re-trigger Greptile

Comment on lines +37 to +38
cache[markdown] = parsed
insertionOrder.append(markdown)

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.

P1 Swift dictionary subscript assignment semantics silently drop nil values: cache[markdown] = parsed when parsed is nil is equivalent to cache.removeValue(forKey: markdown) — the key is never stored. insertionOrder.append(markdown) still runs unconditionally, so a markdown string that fails to parse is appended to insertionOrder on every call but never inserted into cache. Once insertionOrder.count >= capacity, evictions fire against phantom keys, prematurely ejecting valid cache entries. For a workspace that has any malformed metadata block, every body re-evaluation retries the parse and grows insertionOrder with a duplicate — defeating the height-stability goal of this PR for those rows.

Suggested change
cache[markdown] = parsed
insertionOrder.append(markdown)
cache.updateValue(parsed, forKey: markdown)
insertionOrder.append(markdown)

Comment on lines +22 to +36
private static var insertionOrder: [String] = []
private static let capacity = 512

static func rendered(_ markdown: String) -> AttributedString? {
if let hit = cache[markdown] {
return hit
}
let parsed = try? AttributedString(
markdown: markdown,
options: .init(interpretedSyntax: .full)
)
if insertionOrder.count >= capacity, let oldest = insertionOrder.first {
insertionOrder.removeFirst()
cache.removeValue(forKey: oldest)
}

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.

P2 Array.removeFirst() is O(n) — it shifts all remaining elements left. On a full 512-entry cache every eviction copies up to 511 pointers on the main actor. In agent-heavy sessions with many distinct blocks the eviction path can fire on every body pass for uncached keys. A simple head-index or ring-buffer gives O(1) removal.

Suggested change
private static var insertionOrder: [String] = []
private static let capacity = 512
static func rendered(_ markdown: String) -> AttributedString? {
if let hit = cache[markdown] {
return hit
}
let parsed = try? AttributedString(
markdown: markdown,
options: .init(interpretedSyntax: .full)
)
if insertionOrder.count >= capacity, let oldest = insertionOrder.first {
insertionOrder.removeFirst()
cache.removeValue(forKey: oldest)
}
private static var insertionOrder: [String] = []
private static var insertionHead: Int = 0
private static let capacity = 512
static func rendered(_ markdown: String) -> AttributedString? {
if let hit = cache[markdown] {
return hit
}
let parsed = try? AttributedString(
markdown: markdown,
options: .init(interpretedSyntax: .full)
)
if insertionOrder.count >= capacity {
let oldest = insertionOrder[insertionHead % capacity]
cache.removeValue(forKey: oldest)
insertionOrder[insertionHead % capacity] = markdown
insertionHead += 1
} else {
insertionOrder.append(markdown)
}

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

…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>
@azooz2003-bit

Copy link
Copy Markdown
Collaborator Author

Good catch on the optional-dictionary subscript footgun — fixed in fc42869 with cache.updateValue(parsed, forKey:) so failed parses are cached too (no per-eval re-parse, no phantom insertionOrder keys). Tag hstable rebuilt with the fix.

@azooz2003-bit

Copy link
Copy Markdown
Collaborator Author

Re CodeRabbit's "Cmux Swiftui State Layout" error (render-time mutation of the static memo cache): the rule targets the feedback-loop class — writes from body to OBSERVABLE state (@published / @State / store writes) that re-dirty the view graph and spin layout. A private static dictionary is not observable: no view depends on it, so mutating it during render cannot invalidate anything; it is memoization, same category as Foundation's own caches that run under body all the time. The suggested alternative (populate the cache in a non-render lifecycle, e.g. onAppear or the row initializer with @State) is exactly the height-changing appear-time write this PR removes.

The principled long-term home for the parse is the snapshot builder (attributed string computed when the metadata block is ingested, carried on the snapshot), which also removes the inline parse from SidebarWorkspaceDescriptionText — noted as a follow-up; it touches the typing-latency-sensitive snapshot path so it is deliberately out of scope here. The docstring-coverage warning is CodeRabbit's generic default, not a repo gate.

@azooz2003-bit

Copy link
Copy Markdown
Collaborator Author

Superseded by the combined PR #6033 (this PR's change is included verbatim there), opened to run a single CI/review/dogfood pass across all three sidebar-perf fixes. Closing; track 6033.

azooz2003-bit added a commit that referenced this pull request Jun 13, 2026
…(Fix 5: F9, F11)

F9 — SidebarWorkspaceFrameAnchorModifier used if/else around `content`, so the
drop-target frame anchor was structurally present only while collecting. Flipping
isEnabled at drag start/end (shouldCollectWorkspaceDropTargets) gave every
materialized row a new structural identity, recreating each TabItemView subtree
(dropping per-row @State, forcing fresh snapshot builds + relayout) inside the
drag transaction. Make it branchless: always apply anchorPreference, emit
`isEnabled ? [id: anchor] : [:]`. Row identity stays stable across the flip; the
emitted preference is byte-identical to before while dragging, and the
virtualization-defeating *reader* stays gated on the drag (#5325), so emitting an
empty dict when idle costs nothing.

F11 — SidebarWorkspaceDescriptionText carried `.id(description)`, keying the
view's identity to agent-updated text, so every description rewrite tore down and
recreated the view (markdown re-parse, layout caches) on top of the in-place
re-render the text change already triggers. Delete it; the view updates in place.

Verified on tag rowapat: simulated sidebar drag drives the frame-anchor path
(drop indicator tracked across all steps, socket 31ms, zero layout-loop symbols);
markdown description renders bold/italic and wraps with no .id.

Deferred from Fix 5 with rationale: F8 (per-row rowHeight @State feeding the drop
delegate) and F13 (per-row Finder .task pre-warm) — both now low-value post-#6026
(rows are height-stable) and both carry real regression surface (reorder
hit-testing via a SidebarTabDropDelegate interface change; Reveal-in-Finder is
`.disabled(url == nil)` gated on the pre-warm), so they warrant a focused
follow-up rather than riding with these contained changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
azooz2003-bit added a commit that referenced this pull request Jun 13, 2026
…lazy Finder resolve (Fix 5: F9/F11/F13) (#6052)

* Sidebar: branchless frame-anchor modifier + drop the description .id (Fix 5: F9, F11)

F9 — SidebarWorkspaceFrameAnchorModifier used if/else around `content`, so the
drop-target frame anchor was structurally present only while collecting. Flipping
isEnabled at drag start/end (shouldCollectWorkspaceDropTargets) gave every
materialized row a new structural identity, recreating each TabItemView subtree
(dropping per-row @State, forcing fresh snapshot builds + relayout) inside the
drag transaction. Make it branchless: always apply anchorPreference, emit
`isEnabled ? [id: anchor] : [:]`. Row identity stays stable across the flip; the
emitted preference is byte-identical to before while dragging, and the
virtualization-defeating *reader* stays gated on the drag (#5325), so emitting an
empty dict when idle costs nothing.

F11 — SidebarWorkspaceDescriptionText carried `.id(description)`, keying the
view's identity to agent-updated text, so every description rewrite tore down and
recreated the view (markdown re-parse, layout caches) on top of the in-place
re-render the text change already triggers. Delete it; the view updates in place.

Verified on tag rowapat: simulated sidebar drag drives the frame-anchor path
(drop indicator tracked across all steps, socket 31ms, zero layout-loop symbols);
markdown description renders bold/italic and wraps with no .id.

Deferred from Fix 5 with rationale: F8 (per-row rowHeight @State feeding the drop
delegate) and F13 (per-row Finder .task pre-warm) — both now low-value post-#6026
(rows are height-stable) and both carry real regression surface (reorder
hit-testing via a SidebarTabDropDelegate interface change; Reveal-in-Finder is
`.disabled(url == nil)` gated on the pre-warm), so they warrant a focused
follow-up rather than riding with these contained changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Sidebar: resolve Finder dir at context-menu time, drop per-row pre-warm (Fix 5: F13)

Each TabItemView ran `.task(id: finderDirectoryCacheKey)` that stat'd the
workspace's directory (FileManager.fileExists) on every row appearance, just to
populate the `.disabled(finderDirectoryURL == nil)` gate on the "Show in Finder"
context-menu item. Scrolling N workspaces issued N disk stats + N deferred row
invalidations.

"Show in Finder" already re-validates the directory at click time
(WorkspaceFinderDirectoryOpener.openInFinder), so the pre-warm only gated the
item's enabled state. Resolve it synchronously while the context menu builds
(one stat per right-click, user-initiated) via the new
WorkspaceFinderDirectoryResolver.existingDirectoryURLNow. Removes the per-row
@State, the `.task`, and the now-dead WorkspaceFinderDirectoryCache /
CacheKey / cache(for:). Behavior unchanged: item disabled when the dir is
missing, reveals it when present.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Sidebar: read row height from a reference cell, not @State (Fix 5: F8)

Each sidebar row (TabItemView) and group header (SidebarWorkspaceGroupHeaderView)
kept its measured height in @State, written from a background GeometryReader on
appearance (a guaranteed 1 -> realHeight write) and on every later height change.
That write forced a second full body evaluation of the row inside the lazy
placement pass — and the write was load-bearing: the drop delegate captured the
height at body-eval time, so without the re-eval it would have been stuck at 1.

Replace @State with SidebarRowHeightStore, a tiny reference cell the probe
mutates in place (no invalidation). The drop delegates
(SidebarTabDropDelegate, SidebarWorkspaceGroupHeaderDropDelegate) now hold the
store and read `height` LAZILY at drop time via a computed property, so they
always see the current measurement without a body re-eval reconstructing them.
Drop targets with no measured row (empty area, first-row gap) pass a nil store;
the extension-sidebar delegates keep their fixed constants.

Height value at drop time is unchanged (same measurement the probe always fed),
so reorder hit-testing behavior is preserved — only the read path changes.
Verified at runtime: drags over rows + group header mount the delegates, app
responsive (64ms), zero layout-loop symbols, sidebar renders. Real-drag drop
position needs human dogfood (no socket path drives DropInfo hit-testing).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* F13 fix: gate Show-in-Finder on configured path, no body-time stat (autoreview P1)

The previous F13 commit resolved the Finder directory with a synchronous
existence stat in the context-menu builder, assuming it ran only on right-click.
SwiftUI evaluates the `.contextMenu { }` items as part of the row body, so that
reintroduced a synchronous FileManager.fileExists on the main actor for every
row body eval of a configured workspace — exactly the hot-path filesystem IO the
sidebar avoids.

Gate the menu item on whether a directory is configured
(WorkspaceFinderDirectoryResolver.path, in-memory, IO-free) and defer the stat
to the click action, where WorkspaceFinderDirectoryOpener already re-validates
and beeps if the directory is gone. Removes the synchronous helper. Behavior
unchanged for existing directories; a configured-but-deleted directory now shows
an enabled item that beeps on click instead of a disabled item.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Revert "Sidebar: read row height from a reference cell, not @State (Fix 5: F8)"

This reverts commit 3aaf643.

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview – cmux — c8e71c1b Deployed Jun 13, 2026 by vercel[bot]
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