Skip to content

Keep selected workspace visible in sidebar - #3152

Merged
lawrencecchen merged 2 commits into
mainfrom
task-sidebar-workspace-scroll-into-view
Apr 25, 2026
Merged

lawrencecchen merged 2 commits into
mainfrom
task-sidebar-workspace-scroll-into-view

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Apr 25, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Keep selected workspace rows visible by observing selected workspace changes in the sidebar.
  • Add a UI regression test for repeated Cmd+N overflow and Cmd+1 navigation back to the first workspace.

Testing

  • ./scripts/setup.sh
  • ./scripts/reload.sh --tag sbscroll

Issues

  • Task: user-reported workspace sidebar navigation visibility bug

Summary by cubic

Keep the selected workspace visible in the sidebar by auto-scrolling the active row into view and reserving space below the titlebar controls. Refactors the sidebar into smaller pieces and adds a UI test to prevent regressions.

  • Bug Fixes

    • Auto-scroll the selected workspace on appear, selection changes, and workspace list updates using ScrollViewReader; reserve a top visibility inset with safeAreaInset to keep rows clear of the titlebar; use stable .id(tab.id) and defer scrolling until rows lay out via SidebarWorkspaceRowIdsPreferenceKey to prevent jitter.
    • Add WorkspaceSidebarScrollUITests to verify visibility during repeated Cmd+N and Cmd+1, and ensure the first row stays below the titlebar controls.
  • Refactors

    • Split rendering into workspaceScrollArea, workspaceScrollContent, workspaceRows, and workspaceRow; introduce WorkspaceListRenderContext; centralize scroll logic in requestSelectedWorkspaceScroll/flushPendingSelectedWorkspaceScroll and track laidOutWorkspaceRowIds/pendingSelectedWorkspaceScrollId.

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

Summary by CodeRabbit

  • Bug Fixes

    • Sidebar now auto-scrolls to keep the selected workspace row visible and clear of the titlebar overlay during creation, reorder, and selection changes.
  • Refactor

    • Workspace sidebar rendering reorganized to centralize list state and enable reliable programmatic scrolling when selection or visible items change.
  • Tests

    • Added UI tests that exercise many workspaces and verify the sidebar keeps the selected row visible.

@vercel

vercel Bot commented Apr 25, 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 Apr 25, 2026 9:06am

@coderabbitai

coderabbitai Bot commented Apr 25, 2026 •

Copy link
Copy Markdown

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

Adds a new UI test that verifies sidebar row visibility during workspace navigation and refactors the workspace sidebar rendering to use a WorkspaceListRenderContext and ScrollViewReader-driven conditional programmatic scrolling.

Changes

Cohort / File(s) Summary
Project Configuration
GhosttyTabs.xcodeproj/project.pbxproj
Registers new UI test source cmuxUITests/WorkspaceSidebarScrollUITests.swift by adding PBXFileReference, PBXBuildFile, and including it in the cmuxUITests target/group.
Sidebar Scroll Logic
Sources/ContentView.swift
Refactors VerticalTabsSidebar rendering into helper methods backed by WorkspaceListRenderContext; replaces inline ScrollView/ForEach with ScrollViewReader and stable .id(tab.id); conditionally calls scrollTo(selectedWorkspaceId) when that ID exists in the visible workspaceIds; extracts per-row derived state into a workspaceRow helper and configures NSScrollView insets.
UI Test Suite
cmuxUITests/WorkspaceSidebarScrollUITests.swift
Adds WorkspaceSidebarScrollUITests containing testWorkspaceSelectionKeepsSidebarRowVisible; launches app with deterministic args/env, polls for windows and row hittability, sends keyboard commands (Cmd+N, Cmd+1) to navigate workspaces, and asserts visibility and titlebar clearance for rows.

Sequence Diagram

sequenceDiagram
    participant UITest as UITest
    participant ControlSock as ControlSocket
    participant App as App/UI
    participant ScrollProxy as ScrollViewReader
    participant Row as WorkspaceRow

    UITest->>ControlSock: request create/select workspace (via test control channel)
    ControlSock->>App: deliver workspace create/select command
    App->>App: update workspace list and selectedWorkspaceId
    App->>ScrollProxy: request scrollSelectedWorkspaceIntoView(selectedWorkspaceId)
    ScrollProxy->>App: check renderContext.workspaceIds contains selectedWorkspaceId
    alt selected ID present
        ScrollProxy->>Row: proxy.scrollTo(selectedWorkspaceId)
        Row-->>ScrollProxy: row becomes visible/hittable
    else selected ID missing
        ScrollProxy-->>App: no-op
    end
    UITest->>App: query row hittability / check frame for titlebar clearance
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped the sidebar lanes tonight,

I nudged the view so rows stay in sight,
Tests tap keys and widgets gleam,
Scrolls align like carrot dream,
A happy rabbit nods, "All right!"

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

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.
Description check ❓ Inconclusive The PR description includes summary of changes, testing instructions, and an auto-generated summary with detailed information about bug fixes, refactors, and test additions. However, several template sections are missing. Add a demo video link or note if not applicable, and ensure all checklist items are marked. Clarify the specific testing verification steps beyond script execution.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: keeping selected workspaces visible in the sidebar through auto-scrolling functionality.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch task-sidebar-workspace-scroll-into-view

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 Apr 25, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a user-reported bug where the selected workspace row could scroll out of view in the sidebar. It wraps the sidebar ScrollView in a ScrollViewReader and adds .onAppear / .onChange hooks that call proxy.scrollTo(selectedWorkspaceId) whenever the selection or tab list changes, and adds a UI regression test exercising the Cmd+N overflow → Cmd+1 navigation scenario.

Confidence Score: 4/5

Safe to merge; only P2 findings present — the onAppear scroll timing edge case and the two-commit policy deviation.

No P0 or P1 issues. Two P2 findings: a potential no-op scroll on initial appearance before layout completes, and a regression test commit policy deviation per CLAUDE.md. Neither blocks correctness for the common path.

Sources/ContentView.swift — onAppear scroll timing; cmuxUITests/WorkspaceSidebarScrollUITests.swift — two-commit policy.

Important Files Changed

Filename Overview
Sources/ContentView.swift Wraps sidebar ScrollView in ScrollViewReader and adds onChange/onAppear hooks to keep the selected workspace row visible; onAppear scroll may fire before layout is complete.
cmuxUITests/WorkspaceSidebarScrollUITests.swift New UI regression test for sidebar scroll visibility; test and fix are shipped together, violating the two-commit regression test policy in CLAUDE.md.
GhosttyTabs.xcodeproj/project.pbxproj Registers WorkspaceSidebarScrollUITests.swift in the Xcode project — straightforward addition with no issues.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[ScrollViewReader wraps ScrollView] --> B{Trigger}
    B -->|onAppear| C[scrollSelectedWorkspaceIntoView]
    B -->|onChange: selectedTabId| C
    B -->|onChange: tabs.map id| C
    C --> D{selectedWorkspaceId in workspaceIds?}
    D -->|No| E[return — no-op]
    D -->|Yes| F[proxy.scrollTo selectedWorkspaceId]
    F --> G[SwiftUI scrolls minimum needed to reveal row]
Loading

Comments Outside Diff (1)

  1. Sources/ContentView.swift, line 308-309 (link)

    P2 onAppear scroll may be a no-op before layout completes

    scrollSelectedWorkspaceIntoView is called inside .onAppear of the ScrollView, but at that point SwiftUI has not yet placed the child row views — proxy.scrollTo(_:) silently does nothing if the target ID hasn't been laid out yet. The onChange(of: tabManager.selectedTabId) handlers cover in-session changes fine, but on initial appearance (e.g., window restore, cold launch with the 20th workspace selected) the sidebar may open at the top instead of the selected row. A common workaround is to defer the scroll by one run-loop tick with DispatchQueue.main.async { scrollSelectedWorkspaceIntoView(...) }, giving SwiftUI time to complete the first layout pass before issuing the scroll.

Reviews (1): Last reviewed commit: "Keep selected workspace visible in sideb..." | Re-trigger Greptile

Comment on lines +1 to +5
import Foundation
import XCTest

final class WorkspaceSidebarScrollUITests: XCTestCase {
private var socketPath = ""

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 Regression test commit policy: fix and test shipped together

CLAUDE.md requires a two-commit structure for regression tests — Commit 1 adds only the failing test (so CI goes red, proving the test catches the bug), Commit 2 adds the fix (CI goes green). This PR introduces both the scrollSelectedWorkspaceIntoView fix in ContentView.swift and the new WorkspaceSidebarScrollUITests in the same change, so CI history cannot demonstrate that the test would have caught the regression.

Context Used: CLAUDE.md (source)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed by rewriting the PR into the required two-commit shape: first commit is test-only, second commit contains the implementation.

— Claude Code

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmuxUITests/WorkspaceSidebarScrollUITests.swift`:
- Around line 70-77: The test builds an accessibility label using a zero-based
workspace index which mismatches the app's 1-based label; update the label
construction in the test (the code that sets let label = "\(title), workspace
\(index) of \(count)") to use "\(index + 1)" instead of "\(index)"; ensure this
change is applied where pollUntil calls app.descendants(matching:
.any).matching(NSPredicate(...)) to match the app's localized workspace label
format.
- Around line 217-240: The sendLine() reader currently returns at the first
newline, truncating multi-line responses; update sendLine() (the loop using
accumulator, poll(&pollDescriptor,...), read(fd,...)) to continue reading until
read() returns 0 (peer closed EOF) or the deadline elapses instead of returning
on the first '\n', so accumulator contains the full multi-line reply before
splitting in workspaceLines(); alternatively, after writing the request you can
call shutdown(fd, SHUT_WR) to signal EOF to the server and then read until
read() == 0 so the full multi-line response is received.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4d97f8b3-cbb9-4430-8e7a-ff0ef0c60dc0

📥 Commits

Reviewing files that changed from the base of the PR and between 51741a5 and 6c743ed.

📒 Files selected for processing (3)
  • GhosttyTabs.xcodeproj/project.pbxproj
  • Sources/ContentView.swift
  • cmuxUITests/WorkspaceSidebarScrollUITests.swift

Comment thread cmuxUITests/WorkspaceSidebarScrollUITests.swift Outdated
Comment thread cmuxUITests/WorkspaceSidebarScrollUITests.swift Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 issue found across 3 files

You’re at about 96% of the monthly review limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="cmuxUITests/WorkspaceSidebarScrollUITests.swift">

<violation number="1" location="cmuxUITests/WorkspaceSidebarScrollUITests.swift:234">
P2: Do not return on the first newline here; `list_workspaces` responses are multi-line, so this truncates the payload to one row and makes workspace counting incorrect.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

if let chunk = String(bytes: buffer[0..<count], encoding: .utf8) {
accumulator.append(chunk)
if let newline = accumulator.firstIndex(of: "\n") {
return String(accumulator[..<newline])

@cubic-dev-ai cubic-dev-ai Bot Apr 25, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Do not return on the first newline here; list_workspaces responses are multi-line, so this truncates the payload to one row and makes workspace counting incorrect.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At cmuxUITests/WorkspaceSidebarScrollUITests.swift, line 234:

<comment>Do not return on the first newline here; `list_workspaces` responses are multi-line, so this truncates the payload to one row and makes workspace counting incorrect.</comment>

<file context>
@@ -0,0 +1,242 @@
+                if let chunk = String(bytes: buffer[0..<count], encoding: .utf8) {
+                    accumulator.append(chunk)
+                    if let newline = accumulator.firstIndex(of: "\n") {
+                        return String(accumulator[..<newline])
+                    }
+                }
</file context>
Fix with Cubic

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not applicable on the current branch: this test does not read list_workspaces or parse multi-line socket responses.

— Claude Code

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks for the feedback! I've saved this as a new learning to improve future reviews.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
cmuxUITests/WorkspaceSidebarScrollUITests.swift (2)

84-99: Minor: guard against busy-looping if condition is cheap and RunLoop returns early.

RunLoop.current.run(until:) can return earlier than the requested date if there are no input sources, turning the polling loop into a near-tight CPU loop. Adding a small Thread.sleep floor (or using XCTWaiter/expectation(for:evaluatedWith:)) keeps wall-clock pacing predictable in CI.

♻️ Suggested tweak
-            RunLoop.current.run(until: Date().addingTimeInterval(interval))
+            RunLoop.current.run(until: Date().addingTimeInterval(interval))
+            // Floor the cadence so the loop pace is stable on CI hosts where the
+            // run loop has no input sources to schedule against.
+            Thread.sleep(forTimeInterval: max(0, interval / 2))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmuxUITests/WorkspaceSidebarScrollUITests.swift` around lines 84 - 99, The
pollUntil function can busy-loop because RunLoop.current.run(until:) may return
early; modify pollUntil (the function named pollUntil) to enforce a minimum
sleep after the RunLoop call (e.g., Thread.sleep(forTimeInterval: min(interval,
0.01)) or a fixed small floor like 0.01s) so the loop cannot spin tightly when
condition() is cheap or RunLoop returns immediately; keep the existing
timeout/interval semantics and only add the small sleep after
RunLoop.current.run(until:).

58-65: Scope the predicate query to sidebar cells to reduce traversal cost and false positives.

app.descendants(matching: .any) walks the entire accessibility tree on every poll iteration (~20 polls/sec × 20 workspaces). Matching against .cell/.button (and ideally a sidebar container) is significantly faster and avoids matching nested labels (e.g., a tooltip or a duplicated subview that contains the same "workspace X of Y" substring). Also consider LIKE/MATCHES with anchors, or label ENDSWITH %@, since the localized template is "<title>, workspace <i> of <n>" — CONTAINS "workspace 2 of 2" will also accidentally match "workspace 2 of 22" if the count grows.

♻️ Proposed refactor
-        let position = "workspace \(index) of \(count)"
-        return pollUntil(timeout: timeout) {
-            let row = app.descendants(matching: .any)
-                .matching(NSPredicate(format: "label CONTAINS %@", position))
-                .firstMatch
-            return row.exists && row.isHittable
-        }
+        let suffix = "workspace \(index) of \(count)"
+        return pollUntil(timeout: timeout) {
+            // Restrict to cells/buttons and anchor on the end of the label to avoid
+            // matching "workspace 2 of 22" when looking for "workspace 2 of 2".
+            let predicate = NSPredicate(format: "label ENDSWITH %@", suffix)
+            let row = app.cells.matching(predicate).firstMatch
+            return row.exists && row.isHittable
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmuxUITests/WorkspaceSidebarScrollUITests.swift` around lines 58 - 65, The
predicate is too broad: change the query in the closure (where you currently
call app.descendants(matching: .any) and create row) to scope to the sidebar
cell/button elements (e.g., matching .cell or .button and, if available, the
sidebar container view) instead of the entire accessibility tree; also tighten
the predicate from CONTAINS to a boundary-aware match (e.g., ENDSWITH / LIKE /
MATCHES with anchors or exact formatting for the localized template) so
pollUntil only inspects sidebar rows and avoids false positives like "workspace
2 of 22" and reduces traversal cost.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@cmuxUITests/WorkspaceSidebarScrollUITests.swift`:
- Around line 84-99: The pollUntil function can busy-loop because
RunLoop.current.run(until:) may return early; modify pollUntil (the function
named pollUntil) to enforce a minimum sleep after the RunLoop call (e.g.,
Thread.sleep(forTimeInterval: min(interval, 0.01)) or a fixed small floor like
0.01s) so the loop cannot spin tightly when condition() is cheap or RunLoop
returns immediately; keep the existing timeout/interval semantics and only add
the small sleep after RunLoop.current.run(until:).
- Around line 58-65: The predicate is too broad: change the query in the closure
(where you currently call app.descendants(matching: .any) and create row) to
scope to the sidebar cell/button elements (e.g., matching .cell or .button and,
if available, the sidebar container view) instead of the entire accessibility
tree; also tighten the predicate from CONTAINS to a boundary-aware match (e.g.,
ENDSWITH / LIKE / MATCHES with anchors or exact formatting for the localized
template) so pollUntil only inspects sidebar rows and avoids false positives
like "workspace 2 of 22" and reduces traversal cost.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 66f49f7b-7a3a-4101-a09e-b68935942c66

📥 Commits

Reviewing files that changed from the base of the PR and between 5730096 and 8abd8cd.

📒 Files selected for processing (3)
  • GhosttyTabs.xcodeproj/project.pbxproj
  • Sources/ContentView.swift
  • cmuxUITests/WorkspaceSidebarScrollUITests.swift
✅ Files skipped from review due to trivial changes (2)
  • GhosttyTabs.xcodeproj/project.pbxproj
  • Sources/ContentView.swift

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
cmuxUITests/WorkspaceSidebarScrollUITests.swift (1)

28-31: Redundant assertion duplicates final loop iteration.

The loop's last iteration (expectedCount == workspaceCount == 20) already asserts waitForWorkspaceRowHittable(index: 20, count: 20, ...) with the same timeout. This block re-checks the exact same condition immediately after, with no intervening action. Drop it (or replace with something the loop didn't already cover, e.g., asserting that workspace 1 is not hittable to prove the list actually overflowed).

♻️ Proposed cleanup
-        XCTAssertTrue(
-            waitForWorkspaceRowHittable(index: workspaceCount, count: workspaceCount, app: app, timeout: 6.0),
-            "Expected the newly selected bottom workspace to be visible"
-        )
-
         app.typeKey("1", modifierFlags: [.command])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmuxUITests/WorkspaceSidebarScrollUITests.swift` around lines 28 - 31, Remove
the redundant final XCTAssert that repeats the same condition already checked in
the loop: the call asserting waitForWorkspaceRowHittable(index: workspaceCount,
count: workspaceCount, app: app, timeout: 6.0) duplicates the loop's last
iteration. Either delete that assertion entirely or replace it with a
complementary check (for example assert that waitForWorkspaceRowHittable(index:
1, count: workspaceCount, app: app, timeout: 1.0) is false or that workspace 1
is not hittable) so the test actually verifies overflow behavior rather than
repeating the same check; adjust calls to waitForWorkspaceRowHittable and
workspaceCount accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmuxUITests/WorkspaceSidebarScrollUITests.swift`:
- Around line 68-81: The helper launchAndEnsureForeground currently hides launch
failures by wrapping app.launch() in a non-strict XCTExpectFailure and then
treating .runningBackground as success; change it to stop suppressing launch
errors and instead poll for app.state == .runningForeground with a real timeout
(e.g., loop/sleep with deadline) after calling app.launch(), narrowing any
XCTExpectFailure to just the flaky activation call if absolutely necessary;
treat .runningBackground as a failure (call XCTFail with a clear message
including app.state.rawValue) so tests surface the real launch/activation
problem, and ensure any expected-failure annotation only wraps the minimal flaky
activation verification rather than the entire launch sequence in
launchAndEnsureForeground.
- Around line 47-60: The waitForWorkspaceRowHittable query is too broad and uses
a substring match that can collide; change it to target a specific element type
(e.g., .cells, .rows, or .staticTexts) instead of .any and use an anchored
predicate like "label BEGINSWITH %@" or an equality check so the position string
matches exactly (avoid "CONTAINS"); update the query in
waitForWorkspaceRowHittable to restrict descendants(matching: .cell) (or the
correct row element type) and use NSPredicate(format: "label BEGINSWITH %@",
position) or "label == %@", then check that that element’s exists && isHittable
to avoid matching inner text descendants or indexed collisions.

---

Nitpick comments:
In `@cmuxUITests/WorkspaceSidebarScrollUITests.swift`:
- Around line 28-31: Remove the redundant final XCTAssert that repeats the same
condition already checked in the loop: the call asserting
waitForWorkspaceRowHittable(index: workspaceCount, count: workspaceCount, app:
app, timeout: 6.0) duplicates the loop's last iteration. Either delete that
assertion entirely or replace it with a complementary check (for example assert
that waitForWorkspaceRowHittable(index: 1, count: workspaceCount, app: app,
timeout: 1.0) is false or that workspace 1 is not hittable) so the test actually
verifies overflow behavior rather than repeating the same check; adjust calls to
waitForWorkspaceRowHittable and workspaceCount accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3e218a45-5562-44d2-b134-65ba1564d2e0

📥 Commits

Reviewing files that changed from the base of the PR and between 8abd8cd and b1429db.

📒 Files selected for processing (3)
  • GhosttyTabs.xcodeproj/project.pbxproj
  • Sources/ContentView.swift
  • cmuxUITests/WorkspaceSidebarScrollUITests.swift
✅ Files skipped from review due to trivial changes (1)
  • GhosttyTabs.xcodeproj/project.pbxproj
🚧 Files skipped from review as they are similar to previous changes (1)
  • Sources/ContentView.swift

Comment thread cmuxUITests/WorkspaceSidebarScrollUITests.swift
Comment thread cmuxUITests/WorkspaceSidebarScrollUITests.swift Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
Sources/ContentView.swift (1)

9453-9455: Pass snapshots/actions into TabItemView instead of the stores.

This refactor moves a lot of row state up, but Line 9454 and Line 9455 still inject tabManager and notificationStore into TabItemView. That keeps the row below the ForEach boundary while reading global stores in its body/context menu, which undermines the invalidation isolation this split is trying to achieve. Please finish the decoupling here by threading immutable row snapshots plus action closures instead of the store references.

As per coding guidelines, "In any SwiftUI panel whose body contains a LazyVStack / LazyHStack / List / ForEach of rows, no view below that boundary may hold a reference to an ObservableObject / @Observable store ... Rows and drop-gaps receive immutable value snapshots plus closure action bundles only to prevent re-render feedback loops."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ContentView.swift` around lines 9453 - 9455, TabItemView is still
receiving Observable stores (tabManager, notificationStore); change its init and
call site so it accepts an immutable TabSnapshot (or similar row model) and a
bundle of action closures (e.g., onSelectTab, onCloseTab, onToggleNotification)
instead of the store references; update the ForEach to build those snapshots and
closures from tabManager/notificationStore outside the row boundary and pass
only the value and closures into TabItemView, and modify TabItemView's stored
properties and any usages of tabManager/notificationStore inside its
body/context menu to use the passed-in snapshot and closures.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@Sources/ContentView.swift`:
- Around line 9453-9455: TabItemView is still receiving Observable stores
(tabManager, notificationStore); change its init and call site so it accepts an
immutable TabSnapshot (or similar row model) and a bundle of action closures
(e.g., onSelectTab, onCloseTab, onToggleNotification) instead of the store
references; update the ForEach to build those snapshots and closures from
tabManager/notificationStore outside the row boundary and pass only the value
and closures into TabItemView, and modify TabItemView's stored properties and
any usages of tabManager/notificationStore inside its body/context menu to use
the passed-in snapshot and closures.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2a9ec654-0f3a-4427-8048-5a83637d2983

📥 Commits

Reviewing files that changed from the base of the PR and between b1429db and 1e467f4.

📒 Files selected for processing (3)
  • GhosttyTabs.xcodeproj/project.pbxproj
  • Sources/ContentView.swift
  • cmuxUITests/WorkspaceSidebarScrollUITests.swift
✅ Files skipped from review due to trivial changes (1)
  • GhosttyTabs.xcodeproj/project.pbxproj
🚧 Files skipped from review as they are similar to previous changes (1)
  • cmuxUITests/WorkspaceSidebarScrollUITests.swift

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2b1bf0d1b8

ℹ️ 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".

Comment thread Sources/ContentView.swift Outdated
Comment on lines +9346 to +9348
.onChange(of: renderContext.workspaceIds) { _, tabIds in
scrollSelectedWorkspaceIntoView(scrollProxy, workspaceIds: tabIds)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid scrolling on every workspace-order change

The new onChange for workspaceIds triggers scrollTo(selectedTabId) even when the selected workspace did not change, so any reorder/add/remove operation can forcibly jump the sidebar back to the selected row. In particular, SidebarTabDropDelegate.performDrop mutates tabManager.tabs without changing selectedTabId, so dropping a dragged row can snap the list away from the drop area and disrupt repeated reorders when the selected workspace is elsewhere. This should be gated to selection-driven changes (or to cases where the selected row actually became non-visible).

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed by gating workspace-id-change scrolling to the case where the selected workspace newly appears in the list. Pure reorder/list changes no longer force-scroll back to the selected row.

— Claude Code

@lawrencecchen
lawrencecchen force-pushed the task-sidebar-workspace-scroll-into-view branch from b094e9c to e194ceb Compare April 25, 2026 09:05
@lawrencecchen
lawrencecchen merged commit 631a9af into main Apr 25, 2026
19 checks passed
@lawrencecchen
lawrencecchen deleted the task-sidebar-workspace-scroll-into-view branch April 25, 2026 09:15

This branch was successfully deployed

1 active deployment
Preview — e194ceb4 Deployed Apr 25, 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