Skip to content

Speed up sidebar pin state lookup - #6233

Closed
azooz2003-bit wants to merge 10 commits into
mainfrom
task-large-workspace-scaling
Closed

azooz2003-bit wants to merge 10 commits into
mainfrom
task-large-workspace-scaling

Conversation

@azooz2003-bit

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

Copy link
Copy Markdown
Collaborator

Summary

  • Reuse VerticalTabsSidebarRenderContext.workspaceById when computing sidebar row context-menu pin state.
  • Keep action-time pin validation by building a live workspace dictionary once when the action runs.
  • Add dispatcher coverage for indexed pin-state parity, stale IDs, and duplicate targets.

Benchmark

  • Standalone Swift benchmark, 1000 workspaces, 48 visible rows, 2000 row-render passes: old row pin-state shape 28401.14 ms, new pre-indexed shape 49.26 ms.

Testing

  • git diff --check
  • xcodebuild -project cmux.xcodeproj -scheme cmux-unit -configuration Debug -destination 'platform=macOS' -derivedDataPath /tmp/cmux-sclrow build
  • ./scripts/reload.sh --tag sclrow (cloud reload script was unavailable in this worktree)
  • Tagged preflight: launched sclrow, moved the window to LG HDR 4K, built a 73-workspace sidebar fixture before workspace creation started timing out, verified workspace-action pin and workspace-action unpin on workspace:1, and captured debug.window.screenshot at /var/folders/xw/j2s0lpvj16b4y5_5hsfcphb00000gn/T/cmux-screenshots/sclrow-sidebar_2026-06-16T04-50-00Z_D824AFAB.png.

Follow-up Evidence

  • Live preflight exposed a separate scale bottleneck: after 73 real workspaces, workspace.create took 5016.92 ms then 20745.89 ms, and list-workspaces timed out while logs showed repeated git probe, PR refresh, mobile observer, and sidebar invalidation work.
  • Context-menu right-click itself was not socket-drivable. Computer Use could list the app but failed to capture the tagged window with cgWindowNotFound, so the PR proves the shared pin action and rendered sidebar, while the exact context-menu click remains manual dogfood.

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


Summary by cubic

Speeds up sidebar context‑menu pin state with a pre‑indexed workspace map, cutting 2000 row renders from 28,401 ms to 49 ms. Also speeds up and bounds session scrollback by reading terminal text in‑process, skipping VT export/Base64, prioritizing the selected workspace with per‑window capture limits, and budgeting restored‑scrollback fallback.

  • Refactors

    • Added WorkspaceActionDispatcher.pinState(workspacesById:target) and updated the sidebar to pass renderContext.workspaceById; pinState(in:tabManager:) wraps the indexed path and keeps the live lookup helper private.
    • Introduced encodeBase64 to TerminalController.terminalTextPayload; added readTerminalTextForSessionSnapshot and switched Workspace to this path. Migrated dispatcher tests to Swift Testing with coverage for indexed parity and stale/duplicate targets.
  • Performance

    • Session snapshots bypass VT export/Base64 via in‑process reads and tail to lineLimit when set.
    • Scrollback capture prioritizes the selected workspace and limits work to 8 selected and 8 background captures per window snapshot; restored‑scrollback fallback is disabled when using this budget.

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

Review in cubic

Summary by CodeRabbit

  • Bug Fixes
    • Fixed workspace sidebar “pin” state resolution to use the current render pass’s workspace data, ensuring correct pin/unpin behavior across UI contexts.
    • Updated terminal session/quit snapshot text generation to bypass VT-export and use the in-process text read path.
  • Refactor
    • Improved pin-state and pin/unpin target resolution by switching to dictionary-based workspace lookups for de-duplicated, order-preserving target IDs.
  • Tests
    • Migrated and expanded workspace pin-state tests to Swift Testing, adding sidebar vs single state parity checks.

@vercel

vercel Bot commented Jun 16, 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 16, 2026 5:13pm
cmux-staging Building Building Preview, Comment Jun 16, 2026 5:13pm

@coderabbitai

coderabbitai Bot commented Jun 16, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

WorkspaceActionDispatcher replaces its TabManager-based live workspace resolution with a [UUID: Workspace] dictionary lookup. A new pinState(workspacesById:target:) overload and liveWorkspaceIds(workspacesById:from:) helper are added; ContentView passes renderContext.workspaceById to the new overload; tests are migrated to Swift Testing and assert parity between the old and new paths. Separately, session snapshot text reads now explicitly disable VT file export.

Changes

Pin-state lookup refactor

Layer / File(s) Summary
Dispatcher refactor and call site integration
Sources/WorkspaceActionDispatcher.swift, Sources/ContentView.swift
pinState(in:target:) and performPinAction(_:in:) now build a workspacesById dict from tabManager.tabs and forward to a new pinState(workspacesById:target:) overload. liveWorkspaceIds(workspacesById:from:) replaces the TabManager-based helper with dictionary-presence filtering and deduplication. ContentView passes renderContext.workspaceById to the new overload at the sidebar context-menu call site.
Test migration and new overload coverage
cmuxTests/WorkspaceActionDispatcherTests.swift
Test file migrated from XCTestCase to Swift Testing (@Suite, @Test, #require, #expect). testSingleAndSidebarTargetsResolveTheSamePinState adds a workspacesById index and asserts that pinState(workspacesById:target:) produces the same result as the existing sidebar and single-target paths. All other test cases converted to Swift Testing assertion style while preserving existing test logic for filtered/stale pin targets and multiple-target pin ordering/unpinning behavior.

Terminal session snapshot VT export behavior

Layer / File(s) Summary
Session snapshot VT export disable
Sources/TerminalController.swift
readTerminalTextForSessionSnapshot passes allowVTExport: false to readTerminalTextForSnapshot, disabling the VT file export fallback and forcing in-process Ghostty text reading for session-save and quit snapshot behavior.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

  • manaflow-ai/cmux#4865: Modifies the same WorkspaceActionDispatcher pinState/performPinAction flow and live-workspace resolution logic.

Poem

🐇 Hop, hop, the tabManager's gone,
A dictionary lookup carries on!
UUIDs filter, deduplicate with care,
The render snapshot's workspaces laid bare.
Pin state resolves with a precomputed map—
And snapshots skip VT's export trap! 🗂️

🚥 Pre-merge checks | ✅ 20 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
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 (20 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately captures the main optimization: using a pre-indexed workspace map to speed up the sidebar pin state lookup, which is the core change across multiple files.
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 All production code changes properly maintain Swift 6 actor isolation: new WorkspaceActionDispatcher methods are @MainActor, Workspace dictionary stays MainActor-bound, and TerminalController chang...
Cmux Swift Blocking Runtime ✅ Passed No blocking/timing-based synchronization primitives introduced. Changes include: ContentView parameter reuse, WorkspaceActionDispatcher dictionary lookup overload, TerminalController allowVTExport...
Cmux Expensive Synchronous Load ✅ Passed No expensive synchronous loaders added to main actor or interactive paths. New pinState(workspacesById:target:) performs only in-memory dictionary lookups and property access on pre-loaded Workspac...
Cmux Cache Substitution Correctness ✅ Passed ContentView.swift freshly constructs renderContext.workspaceById from tabManager.tabs in body's scope each render, satisfying freshness. TerminalController change bypasses VT export caching, forcin...
Cmux No Hacky Sleeps ✅ Passed Check scope excludes Swift code; all PR changes are in Swift files (.swift), covered by separate swift-blocking-runtime rule instead.
Cmux Algorithmic Complexity ✅ Passed PR reuses pre-built workspaceById dictionary (O(N) once per render) instead of rebuilding per row (O(N×M) total), reducing pin-state lookup from O(1) per row via expensive dict construction to O(M)...
Cmux Swift Concurrency ✅ Passed PR introduces no legacy async patterns: new WorkspaceActionDispatcher.pinState overload is synchronous with @MainActor; TerminalController change is parameter-only on synchronous functions; test fi...
Cmux Swift @Concurrent ✅ Passed All Swift changes follow concurrent annotation rules: new @MainActor synchronous methods (pinState, performPinAction, liveWorkspaceIds) are properly isolated and called from UI-bound contexts; no n...
Cmux Swift File And Package Boundaries ✅ Passed All Swift file changes comply with package boundary rules: (1) ContentView.swift (16,714 lines) touches incidentally with +1/-1 change—existing oversized file; (2) TerminalController.swift (14,829...
Cmux Swift Logging ✅ Passed No logging violations found. Changes to WorkspaceActionDispatcher.swift, ContentView.swift, TerminalController.swift, and WorkspaceActionDispatcherTests.swift introduce no print, debugPrint, dump,...
Cmux User-Facing Error Privacy ✅ Passed PR contains no new user-facing error messages, alerts, or sensitive data. Changes are refactoring only: indexed workspace lookups, parameter changes, and test migration with no user-visible text mo...
Cmux Full Internationalization ✅ Passed PR introduces no new user-facing text strings, string catalog entries, or web UI changes. Tests (excluded), refactoring of pin-state functions, and pre-existing localized strings remain unchanged.
Cmux Swiftui State Layout ✅ Passed PR passes SwiftUI state layout rules: contentMenuPinState is a value snapshot (PinState struct), not an observable; TabItemView receives snapshots and closures per snapshot-boundary rule; no render...
Cmux Architecture Rethink ✅ Passed PR reuses immutable renderContext.workspaceById snapshot for pin-state computation instead of repeated TabManager lookups; adds indexed method overload with clear ownership, maintains invariants, u...
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR introduces no new auxiliary windows. Changes are: refactored pin state to use indexed workspace lookup, added helper method overloads, modified terminal snapshot parameter, and migrated tests to...
Cmux Source Artifacts ✅ Passed All 4 changed files are hand-written source code and tests intentionally part of the product (Sources/ and cmuxTests/ directories). No artifacts, logs, screenshots, temp files, or generated code we...
Description check ✅ Passed The PR description comprehensively covers changes, testing methodology, and performance benchmarks, though it lacks a demo video and incomplete checklist.

✏️ 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 task-large-workspace-scaling

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 16, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR speeds up the sidebar context-menu pin-state lookup by reusing the already-indexed renderContext.workspaceById map instead of rebuilding a dictionary from tabManager.tabs on every row render. It also adds a per-window scrollback capture budget (8 selected + 8 background panels) and switches the plain-text fallback read path away from the socket-based Base64 round-trip to an in-process read.

  • Pin-state indexing: WorkspaceActionDispatcher gains a pinState(workspacesById:) overload; the sidebar passes the render context's pre-built map; performPinAction still builds a fresh dictionary at action time for safety. Tests migrated to Swift Testing with new stale/duplicate target coverage.
  • Scrollback budget: TabManager.sessionSnapshot now allocates at most 8 live captures for the selected workspace's panels and 8 for background panels, passed down through claimScrollbackCapture closures.
  • In-process terminal read: readPlainTerminalTextForSnapshot replaces the readTerminalTextBase64 socket path with readTerminalTextRawSnapshot + terminalTextPayload(encodeBase64: false), removing the Base64 encode/decode cycle for the fallback path.

Confidence Score: 4/5

Safe to merge after addressing the scrollback cache eviction issue in Workspace.swift; the pin-state and terminal-read changes are solid.

The allowFallbackScrollback change in Workspace.swift causes terminalSnapshotScrollback to call restoredTerminalScrollbackByPanelId.removeValue(forKey:) for every panel that is denied a budget slot. Because this cache is the only in-memory carrier of previously-restored scrollback between snapshots, workspaces ranked beyond the 8-background-panel budget will silently and permanently lose their persisted scrollback after the first background save.

Sources/Workspace.swift — the allowFallbackScrollback condition on line 506 and its interaction with terminalSnapshotScrollback's cache-clearing else branch.

Important Files Changed

Filename Overview
Sources/WorkspaceActionDispatcher.swift Adds pinState(workspacesById:target:) so callers with a pre-built map can skip re-iterating tabManager.tabs; liveWorkspaceIds refactored to accept the dictionary; performPinAction builds a fresh dictionary at action time for safety. Logic is correct.
Sources/ContentView.swift Switches sidebar row context-menu pin-state computation to pinState(workspacesById:) reusing renderContext.workspaceById; straightforward hot-path optimization with no logic change.
Sources/TabManager.swift Adds per-window scrollback capture budget (8 selected + 8 background) with hardcoded inline constants; budget-denied panels silently lose their previously-restored scrollback cache entry (see Workspace.swift finding).
Sources/Workspace.swift Propagates claimScrollbackCapture and usesScrollbackCaptureBudget into panel snapshots; the allowFallbackScrollback condition now suppresses the restoredTerminalScrollbackByPanelId fallback when budget is in use, causing permanent eviction of previously-persisted scrollback for budget-denied panels.
Sources/TerminalController.swift Adds encodeBase64 flag to terminalTextPayload; readPlainTerminalTextForSnapshot switches from socket-based readTerminalTextBase64 to in-process readTerminalTextRawSnapshot; new readTerminalTextForSessionSnapshot thin wrapper. Changes look correct.
cmuxTests/WorkspaceActionDispatcherTests.swift Migrated from XCTest to Swift Testing; adds indexed-path parity test and a new test covering stale/duplicate target filtering. Coverage looks solid.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant SB as VerticalTabsSidebar (row render)
    participant RC as VerticalTabsSidebarRenderContext
    participant WAD as WorkspaceActionDispatcher
    participant TM as TabManager
    participant WS as Workspace

    SB->>RC: workspaceById (pre-indexed)
    SB->>WAD: pinState(workspacesById:, target:)
    Note over WAD: O(1) dict lookup, no tabs scan

    Note over TM: On session snapshot
    TM->>TM: build claimScrollbackCapture() closure
    TM->>WS: sessionSnapshot(claimScrollbackCapture:, usesScrollbackCaptureBudget: true)
    WS->>WS: claimScrollbackCapture() → Bool
    alt budget available
        WS->>WS: readTerminalTextForSessionSnapshot()
        WS->>WS: terminalSnapshotScrollback(allowFallbackScrollback: true)
    else budget denied
        WS->>WS: terminalSnapshotScrollback(allowFallbackScrollback: false)
        Note over WS: restoredTerminalScrollbackByPanelId entry removed
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant SB as VerticalTabsSidebar (row render)
    participant RC as VerticalTabsSidebarRenderContext
    participant WAD as WorkspaceActionDispatcher
    participant TM as TabManager
    participant WS as Workspace

    SB->>RC: workspaceById (pre-indexed)
    SB->>WAD: pinState(workspacesById:, target:)
    Note over WAD: O(1) dict lookup, no tabs scan

    Note over TM: On session snapshot
    TM->>TM: build claimScrollbackCapture() closure
    TM->>WS: sessionSnapshot(claimScrollbackCapture:, usesScrollbackCaptureBudget: true)
    WS->>WS: claimScrollbackCapture() → Bool
    alt budget available
        WS->>WS: readTerminalTextForSessionSnapshot()
        WS->>WS: terminalSnapshotScrollback(allowFallbackScrollback: true)
    else budget denied
        WS->>WS: terminalSnapshotScrollback(allowFallbackScrollback: false)
        Note over WS: restoredTerminalScrollbackByPanelId entry removed
    end
Loading

Reviews (7): Last reviewed commit: "Budget restored scrollback fallback" | Re-trigger Greptile

Comment on lines 92 to 96
@MainActor
private static func liveWorkspaceIds(
in tabManager: TabManager,
static func liveWorkspaceIds(
workspacesById: [UUID: Workspace],
from workspaceIds: [UUID]
) -> [UUID] {

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 liveWorkspaceIds was private before this PR. No external caller was added — the tests exercise it only indirectly through pinState(workspacesById:) — so removing private needlessly widens the internal API surface of WorkspaceActionDispatcher.

Suggested change
@MainActor
private static func liveWorkspaceIds(
in tabManager: TabManager,
static func liveWorkspaceIds(
workspacesById: [UUID: Workspace],
from workspaceIds: [UUID]
) -> [UUID] {
@MainActor
private static func liveWorkspaceIds(
workspacesById: [UUID: Workspace],
from workspaceIds: [UUID]
) -> [UUID] {

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!

Comment thread Sources/Workspace.swift
capturedScrollback: capturedScrollback,
includeScrollback: includeScrollback,
allowFallbackScrollback: shouldPersistScrollback || allowDebugFallbackScrollback || hasRestoredScrollbackFallback
allowFallbackScrollback: shouldCaptureScrollback || allowDebugFallbackScrollback || (!usesScrollbackCaptureBudget && hasRestoredScrollbackFallback)

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 Budget-denied panels permanently lose previously-restored scrollback

When usesScrollbackCaptureBudget = true and a panel's claimScrollbackCapture() returns false, shouldCaptureScrollback is false, so allowFallbackScrollback collapses to false || false || (false && hasRestoredScrollbackFallback) = false. terminalSnapshotScrollback then receives capturedScrollback: nil and allowFallbackScrollback: false, causing resolvedSnapshotTerminalScrollback to return nil, which triggers restoredTerminalScrollbackByPanelId.removeValue(forKey: panelId).

This permanently wipes the in-memory scrollback cache entry for every budget-denied panel on each session snapshot. Workspaces ranked beyond the 8 background slots will lose all previously-persisted scrollback after the first background save, even though the intent of the budget is only to limit live terminal reads, not to evict data already in hand.

Restoring hasRestoredScrollbackFallback unconditionally preserves the existing stored value without incurring any terminal read:

allowFallbackScrollback: shouldCaptureScrollback || allowDebugFallbackScrollback || hasRestoredScrollbackFallback

@lawrencecchen lawrencecchen added the stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening. label Sep 23, 2026
@github-project-automation github-project-automation Bot moved this from Todo to Done in cmux backlog Sep 23, 2026

This branch was successfully deployed

1 active deployment
Preview – cmux — bd325d0f Deployed Jun 16, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants