Skip to content

Suppress right-edge scroll bar on TUI alt-screen and add disable setting (fixes 2728) - #2729

Merged
austinywang merged 15 commits into
mainfrom
issue-2728-nvim-scrollbar-overlap
Apr 14, 2026
Merged

austinywang merged 15 commits into
mainfrom
issue-2728-nvim-scrollbar-overlap

Conversation

@austinywang

@austinywang austinywang commented Apr 8, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #2728

Root cause

  • Auto-hide terminal scroll bar fixes 2674 #2678 switched the terminal scroll bar to auto-hide, but cmux still renders the overlay on top of the terminal viewport.
  • Full-screen TUI apps such as nvim, htop, less, tmux, lazygit, and k9s repaint continuously, so AppKit keeps the overlay scroller effectively visible.
  • Because the overlay sits inside the terminal surface bounds, it covers the rightmost cell column and can obscure editor sign columns and in-app scroll indicators.

What changed

  • cmux now suppresses the right-edge terminal scroll bar for alternate-screen-style TUI surfaces by using Ghostty's per-surface scrollbar telemetry. In the embedded surface path, those full-screen TUI states surface as a viewport with no extra scrollback (total <= len), so the wrapper treats that as the alt-screen/no-scrollback signal and hides the bar for that surface.
  • Added a global Settings > Terminal toggle, Show Terminal Scroll Bar, which defaults to on.
  • Added a per-workspace override in the workspace context menu under Workspace Settings > Hide Terminal Scroll Bar.
  • Persisted the per-workspace override in the existing workspace session snapshot/store so it survives restart/restore.
  • Reserved a right gutter whenever terminal scrollbar support is enabled by shrinking the Ghostty surface width before pushing geometry back into libghostty. That keeps the scrollbar out of the rightmost terminal cell column and keeps the PTY width stable instead of adding/removing a column when switching between shell scrollback and alt-screen TUIs.
  • Added settings.json support and docs/schema for terminal.showScrollBar.

Behavior note

  • In nvim and other alt-screen TUIs, the scrollbar itself is hidden.
  • The default behavior still keeps a reserved right gutter so the terminal grid does not reflow when scrollback appears or disappears.
  • If you want full-bleed TUI content with no reserved gutter, turn off the terminal scrollbar globally in Settings > Terminal > Show Terminal Scroll Bar or per workspace via Workspace Settings > Hide Terminal Scroll Bar.

Why the auto-hide from #2678 was not enough

  • Auto-hide only changes when the overlay fades; it does not change where cmux paints the scrollbar.
  • For repaint-heavy TUIs, the overlay rarely gets a chance to disappear, so the visual overlap remains even though static shell content looks fine.
  • This PR fixes the overlap in two ways: hide the bar for alt-screen/no-scrollback TUI surfaces, and reserve width outside the terminal grid when scrollbar support is enabled.

How to use the new setting

  • Global: open Settings > Terminal and toggle Show Terminal Scroll Bar.
  • Per workspace: right-click a workspace in the sidebar, then use Workspace Settings > Hide Terminal Scroll Bar.
  • File-managed config: set terminal.showScrollBar in ~/.config/cmux/settings.json.

Manual test checklist

  • Open nvim and confirm the rightmost cell column is unobstructed.
  • Open htop and confirm the rightmost cell column is unobstructed.
  • Open a long-scrolling shell session and confirm the terminal scroll bar still appears.
  • Toggle Show Terminal Scroll Bar in Settings and confirm the bar and reserved gutter disappear immediately.
  • Restart cmux and confirm the chosen setting persists.

Validation

  • Built with ./scripts/reload.sh --tag nvim-scrollbar-overlap.
  • Per repo policy, local tests were not run.

Note

Medium Risk
Changes terminal scroll bar visibility and sizing logic, plus adds new persisted settings and workspace state; risk is mainly UI/layout regressions and session restore compatibility issues.

Overview
Updates the embedded Ghostty terminal to only show the right-edge scroll bar when scrollback is available, suppressing it automatically for alternate-screen/TUI states (based on Ghostty scrollbar telemetry) and retiling/layouting when visibility changes.

Adds a global Settings > Terminal > Show Terminal Scroll Bar toggle (backed by new TerminalScrollBarSettings, terminal.showScrollBar file-managed setting, schema/docs updates, and localized strings) and a per-workspace context-menu override (Workspace Settings > Hide Terminal Scroll Bar) that is persisted in session snapshots and applied on restore.

Reviewed by Cursor Bugbot for commit da1b45d. Bugbot is set up for automated code reviews on this repo. Configure here.

@vercel

vercel Bot commented Apr 8, 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 13, 2026 11:18pm

@coderabbitai

coderabbitai Bot commented Apr 8, 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 global and per-workspace setting to show/hide the terminal right-edge scrollbar, plus UI, localization, settings/schema/docs, session persistence, context-menu wiring, and terminal view geometry/visibility synchronization.

Changes

Cohort / File(s) Summary
Localization
Resources/Localizable.xcstrings
Added 6 new localized keys for workspace context menu and Terminal settings (contextMenu.workspaceSettings*, settings.section.terminal, settings.terminal.scrollBar*) with EN/JA entries.
App Settings & UI
Sources/cmuxApp.swift, web/app/.../page.tsx, web/data/cmux-settings.schema.json
Introduced TerminalScrollBarSettings, Settings toggle (terminal.showScrollBar) with AppStorage binding, reset handling, settings UI section, and docs/schema updates including example config.
Settings File Store
Sources/KeyboardShortcutSettingsFileStore.swift
Added terminal.showScrollBar to supported JSON paths, parsing/validation, default template inclusion, and notify-on-change only when stored UserDefaults mutates (also on restore).
Sidebar / Context Menu
Sources/ContentView.swift
Added "Workspace Settings" submenu with "Hide Terminal Scroll Bar" toggle; helpers compute per-context-menu target state and apply changes via TabManager.
Workspace, TabManager & Session
Sources/Workspace.swift, Sources/SessionPersistence.swift, Sources/TabManager.swift
Persisted terminalScrollBarHidden on Workspace with setter & change notification; added snapshot field; TabManager API setWorkspaceTerminalScrollBarHidden; included property in session fingerprinting.
Terminal View / Geometry
Sources/GhosttyTerminalView.swift, Sources/AppDelegate.swift
Added workspace-scoped observer and global preference observer; introduced shouldShowTerminalScrollBar() predicate, resync/retile on visibility flips, adjusted sizing to reserve scrollbar width, and refined surface refresh sequencing.
Web docs/schema example
web/app/[locale]/docs/configuration/page.tsx, web/data/cmux-settings.schema.json
Inserted terminal section into docs schema order and example; added terminal.showScrollBar to JSON schema with description and default.

Sequence Diagram

sequenceDiagram
    participant User
    participant ContentView
    participant TabManager
    participant Workspace
    participant Defaults as UserDefaults
    participant Terminal as GhosttyTerminalView

    User->>ContentView: open context menu / toggle "Hide Terminal Scroll Bar"
    ContentView->>TabManager: setWorkspaceTerminalScrollBarHidden(tabId, hidden)
    TabManager->>Workspace: setTerminalScrollBarHidden(hidden)
    Workspace->>Workspace: update published state & post notification
    Defaults->>Terminal: TerminalScrollBarSettings.isVisible()
    Workspace->>Terminal: workspace notification (terminalScrollBarHidden changed)
    Terminal->>Terminal: shouldShowTerminalScrollBar() (global + workspace + content)
    Terminal->>Terminal: scrollView.tile() / synchronizeGeometryAndContent()
    Terminal->>User: redraw terminal with updated scrollbar visibility
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 I nudged a toggle with a hop and a twitch,
The right edge cleared—no more hidden niche.
Workspace whispers, global tune,
Gutter freed beneath the moon.
Hop—neovim can see its line anew. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description check ✅ Passed The pull request description is comprehensive and includes all required template sections: Summary (root cause, changes, behavior notes), Testing (manual checklist), and Review Trigger block. However, no demo video is provided and the Testing section lacks detail on how changes were tested locally.
Title check ✅ Passed The pull request has a clear, descriptive title that concisely summarizes the changes: fixing scrollbar overlap issues and adding disable/control settings.
Linked Issues check ✅ Passed The PR is explicitly titled 'Fixes #2728' and the description comprehensively addresses all objectives from the linked issue: global toggle, per-workspace override, file-managed config, persistence, and localization.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing terminal scrollbar visibility controls and related settings persistence. No unrelated refactors, dependency updates, or out-of-scope modifications detected in the file-by-file summaries.

✏️ 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 issue-2728-nvim-scrollbar-overlap

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 8, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes the right-edge scrollbar overlap for full-screen TUI apps (nvim, htop, etc.) by suppressing the overlay scroller when Ghostty reports no additional scrollback (total <= len), reserving a right gutter when the bar IS shown so it never sits over terminal cells, and adding both a global Settings toggle (terminal.showScrollBar) and a per-workspace context-menu override that survives session restore. All three findings are P2 style/robustness suggestions that do not block merge.

Confidence Score: 5/5

Safe to merge; all findings are P2 style/robustness suggestions with no blocking correctness issues.

The core logic — suppressing the scrollbar on total <= len and reserving gutter width — is sound. Persistence, settings wiring, localization, schema, and docs are all consistent. The three comments are non-blocking: overly broad UserDefaults observer, initial-nil scrollbar returning false, and the removed isolation comment for handlePreferredScrollerStyleChange.

Sources/GhosttyTerminalView.swift — broad UserDefaults observer and initial-nil scrollbar behavior worth a second look.

Vulnerabilities

No security concerns identified. The feature reads/writes only boolean UserDefaults keys under a clearly-namespaced key (terminal.showScrollBar); no user-supplied data is passed to the OS, no new network paths are opened, and no privilege boundaries are crossed.

Important Files Changed

Filename Overview
Sources/GhosttyTerminalView.swift Core scrollbar suppression logic: adds shouldShowTerminalScrollBar(), gutter reservation, workspace/settings lookup, and a broad UserDefaults.didChangeNotification observer; three P2 concerns noted.
Sources/Workspace.swift Adds terminalScrollBarHidden: Bool published property with setTerminalScrollBarHidden() that guards duplicate writes and schedules geometry reconcile; serialization correctly stores nil for the default (non-hidden) case.
Sources/SessionPersistence.swift Adds optional terminalScrollBarHidden: Bool? to SessionWorkspaceSnapshot; backward-compatible with nil defaulting to false on restore.
Sources/cmuxApp.swift Adds TerminalScrollBarSettings enum with isVisible() helper and the Settings UI toggle wired to @AppStorage; reset path correctly updated.
Sources/ContentView.swift Adds Workspace Settings context menu item with checkmark toggling; context menus are ephemeral so stale-state impact of reading tabManager.tabs in the label closure is negligible.
Sources/KeyboardShortcutSettingsFileStore.swift Wires terminal.showScrollBar into the settings file parser; follows the existing section-parsing pattern.
Sources/TabManager.swift Adds setWorkspaceTerminalScrollBarHidden() forwarding method and includes terminalScrollBarHidden in the workspace hasher.
web/data/cmux-settings.schema.json Adds terminal section with showScrollBar boolean property consistent with default true and the implementation.

Sequence Diagram

sequenceDiagram
    participant G as Ghostty (libghostty)
    participant SV as GhosttySurfaceScrollView
    participant W as Workspace
    participant UD as UserDefaults

    Note over G,SV: Scrollbar telemetry path
    G->>SV: handleScrollbarUpdate(notification)
    SV->>SV: shouldShowTerminalScrollBar() [before]
    SV->>SV: surfaceView.scrollbar = scrollbar
    SV->>SV: shouldShowTerminalScrollBar() [after]
    alt visibility changed
        SV->>SV: synchronizeGeometryAndContent()
    else no change
        SV->>SV: synchronizeScrollView()
    end

    Note over W,SV: Per-workspace toggle path
    W->>W: setTerminalScrollBarHidden(hidden)
    W->>W: scheduleTerminalGeometryReconcile()
    W-->>SV: layout() triggered by AppKit
    SV->>SV: synchronizeGeometryAndContent()
    SV->>W: owningWorkspace()?.terminalScrollBarHidden

    Note over UD,SV: Global settings path
    UD->>SV: UserDefaults.didChangeNotification (any key)
    SV->>SV: handleTerminalScrollBarPreferenceChange()
    SV->>SV: synchronizeGeometryAndContent()
    SV->>UD: TerminalScrollBarSettings.isVisible()
Loading

Reviews (1): Last reviewed commit: "Suppress terminal scrollbar for TUI work..." | Re-trigger Greptile

Comment thread Sources/GhosttyTerminalView.swift Outdated
Comment thread Sources/GhosttyTerminalView.swift
Comment thread Sources/GhosttyTerminalView.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.

Caution

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

⚠️ Outside diff range comments (1)
Sources/GhosttyTerminalView.swift (1)

11277-11292: ⚠️ Potential issue | 🟠 Major

Compare against the current scroller state, not surfaceView.scrollbar.

GhosttyNSView.flushPendingScrollbar() already writes the new telemetry before posting this notification, so Line 11281 is reading the new state, not the previous one. That makes wasVisible and isVisible equal, so entering/leaving alt-screen or receiving the first scrollback packet never triggers synchronizeGeometryAndContent(). The gutter/scroller state then stays stale until some unrelated layout pass happens.

Suggested fix
     private func handleScrollbarUpdate(_ notification: Notification) {
         guard let scrollbar = notification.userInfo?[GhosttyNotificationKey.scrollbar] as? GhosttyScrollbar else {
             return
         }
-        let wasVisible = shouldShowTerminalScrollBar()
+        let wasVisible = scrollView.hasVerticalScroller
         if pendingExplicitWheelScroll {
             userScrolledAwayFromBottom = scrollbar.offset + scrollbar.len < scrollbar.total
             allowExplicitScrollbarSync = true
             pendingExplicitWheelScroll = false
         }
         surfaceView.scrollbar = scrollbar
-        let isVisible = shouldShowTerminalScrollBar()
+        let isVisible = terminalScrollBarAllowedBySettings() && scrollbar.total > scrollbar.len
         if wasVisible != isVisible {
             _ = synchronizeGeometryAndContent()
             return
         }
         synchronizeScrollView()
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/GhosttyTerminalView.swift` around lines 11277 - 11292, The handler
reads the "new" scroller state via shouldShowTerminalScrollBar() (which reflects
flushed telemetry) so wasVisible is wrong; instead capture the previous scroller
state from surfaceView.scrollbar before applying the notification. In
handleScrollbarUpdate, store the existing surfaceView.scrollbar (e.g.
previousScrollbar = surfaceView.scrollbar) and compute wasVisible from that
previousScrollbar, then assign surfaceView.scrollbar = scrollbar and compute
isVisible via shouldShowTerminalScrollBar() as before; keep the existing
pendingExplicitWheelScroll / userScrolledAwayFromBottom /
allowExplicitScrollbarSync logic intact.
🧹 Nitpick comments (1)
Sources/Workspace.swift (1)

6491-6491: Harden with private(set) to prevent accidental direct mutations.

The backing terminalScrollBarHidden property is still writable from anywhere in the file. While a search confirms no current direct assignments bypass setTerminalScrollBarHidden(_:), make the property private(set) to enforce that the setter—which triggers scheduleTerminalGeometryReconcile()—is always the path for mutations. This prevents future changes from accidentally updating the state without resizing the Ghostty surface.

Also applies to: 7529-7533

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

In `@Sources/Workspace.swift` at line 6491, Make the Published property
terminalScrollBarHidden read-only externally by changing its declaration to use
private(set) so callers cannot directly assign it; ensure all mutations go
through the existing setter method setTerminalScrollBarHidden(_:) which calls
scheduleTerminalGeometryReconcile(). Also apply the same private(set) hardening
to the other similar Published property controlled by its own setter (the one
referenced in the review at lines 7529-7533) so that all external changes must
use the corresponding setter that triggers geometry reconciliation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 11277-11292: The handler reads the "new" scroller state via
shouldShowTerminalScrollBar() (which reflects flushed telemetry) so wasVisible
is wrong; instead capture the previous scroller state from surfaceView.scrollbar
before applying the notification. In handleScrollbarUpdate, store the existing
surfaceView.scrollbar (e.g. previousScrollbar = surfaceView.scrollbar) and
compute wasVisible from that previousScrollbar, then assign
surfaceView.scrollbar = scrollbar and compute isVisible via
shouldShowTerminalScrollBar() as before; keep the existing
pendingExplicitWheelScroll / userScrolledAwayFromBottom /
allowExplicitScrollbarSync logic intact.

---

Nitpick comments:
In `@Sources/Workspace.swift`:
- Line 6491: Make the Published property terminalScrollBarHidden read-only
externally by changing its declaration to use private(set) so callers cannot
directly assign it; ensure all mutations go through the existing setter method
setTerminalScrollBarHidden(_:) which calls scheduleTerminalGeometryReconcile().
Also apply the same private(set) hardening to the other similar Published
property controlled by its own setter (the one referenced in the review at lines
7529-7533) so that all external changes must use the corresponding setter that
triggers geometry reconciliation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b85456f9-294e-42fc-9f60-e4d8bb1a9a51

📥 Commits

Reviewing files that changed from the base of the PR and between fc721af and 9abf20c.

📒 Files selected for processing (10)
  • Resources/Localizable.xcstrings
  • Sources/ContentView.swift
  • Sources/GhosttyTerminalView.swift
  • Sources/KeyboardShortcutSettingsFileStore.swift
  • Sources/SessionPersistence.swift
  • Sources/TabManager.swift
  • Sources/Workspace.swift
  • Sources/cmuxApp.swift
  • web/app/[locale]/docs/configuration/page.tsx
  • web/data/cmux-settings.schema.json

Comment thread Sources/GhosttyTerminalView.swift
Comment thread Sources/Workspace.swift

@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.

2 issues found across 10 files

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="Sources/ContentView.swift">

<violation number="1" location="Sources/ContentView.swift:13285">
P2: `TabItemView` now reads `tabManager` from its body via `allTargetWorkspacesHideTerminalScrollBar`, which breaks the documented rule to avoid `tabManager` in the body (equatable typing hot path). Precompute this flag outside the body and pass it in as a stored parameter instead.</violation>
</file>

<file name="Sources/GhosttyTerminalView.swift">

<violation number="1" location="Sources/GhosttyTerminalView.swift:9066">
P2: `UserDefaults.didChangeNotification` fires for every defaults write (workspace renames, theme changes, sidebar settings, etc.), not just scroll-bar preference changes. Each `GhosttySurfaceScrollView` instance will run a full `synchronizeGeometryAndContent()` pass on every unrelated write. In a session with many splits this creates unnecessary layout churn. Filter by checking whether the scroll-bar key actually changed, or use `KVO`/`AppStorage` scoped to the specific key.</violation>
</file>

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

Comment thread Sources/ContentView.swift Outdated
Comment thread Sources/GhosttyTerminalView.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: 1

Caution

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

⚠️ Outside diff range comments (1)
Sources/ContentView.swift (1)

9973-10027: ⚠️ Potential issue | 🟠 Major

Recompute the current hidden state in the handler instead of trusting the snapshotted Bool.

allContextMenuWorkspacesHideTerminalScrollBar is derived once in VerticalTabsSidebar and then reused as currentlyHidden for the mutation. These rows refresh through their own observation path, so that snapshot can lag the real terminalScrollBarHidden values and make the toggle flip the selected workspaces the wrong way. The action should derive the current aggregate state from targetIds right before mutating.

Suggested change
-                toggleWorkspaceTerminalScrollBarHidden(
-                    targetIds: targetIds,
-                    currentlyHidden: allContextMenuWorkspacesHideTerminalScrollBar
-                )
+                toggleWorkspaceTerminalScrollBarHidden(targetIds: targetIds)
...
-    private func toggleWorkspaceTerminalScrollBarHidden(targetIds: [UUID], currentlyHidden: Bool) {
-        let hideScrollBar = !currentlyHidden
+    private func toggleWorkspaceTerminalScrollBarHidden(targetIds: [UUID]) {
+        let workspacesById = Dictionary(uniqueKeysWithValues: tabManager.tabs.map { ($0.id, $0) })
+        let currentlyHidden = !targetIds.isEmpty &&
+            targetIds.allSatisfy { workspacesById[$0]?.terminalScrollBarHidden == true }
+        let hideScrollBar = !currentlyHidden
         for targetId in targetIds {
             tabManager.setWorkspaceTerminalScrollBarHidden(tabId: targetId, hidden: hideScrollBar)
         }
     }

Based on learnings: In TabItemView in ContentView.swift, use Equatable + .equatable() and keep reactive row state off parent-diff-only inputs.

Also applies to: 13286-13301, 14006-14011

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

In `@Sources/ContentView.swift` around lines 9973 - 10027, The computed Bool
allContextMenuWorkspacesHideTerminalScrollBar should not be captured and used as
the authoritative "currentlyHidden" in the toggle handler; instead recompute the
aggregate from the actual workspace IDs right before mutating (e.g. use
contextMenuWorkspaceIds.allSatisfy { tabsById[$0]?.terminalScrollBarHidden ==
true } inside the action that flips terminalScrollBarHidden), update
VerticalTabsSidebar/TabItemView callsites to use that recomputed value in the
mutation, and make TabItemView inputs equatable (use Equatable + .equatable())
so row state is driven by its own observed values rather than parent
snapshot-only inputs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 9066-9072: The view currently only observes
TerminalScrollBarSettings.didChangeNotification; also add an observer for
workspace-level changes to immediately react when
Workspace.terminalScrollBarHidden is toggled (e.g., observe the Workspace change
notification or KVO the workspace's terminalScrollBarHidden) and invoke
handleTerminalScrollBarPreferenceChange() so updateNSView/resync logic runs
immediately; mirror this change wherever the same observer block is added (the
other occurrence around lines 11351-11380) and ensure the new observer is
cleaned up alongside the existing ones.

---

Outside diff comments:
In `@Sources/ContentView.swift`:
- Around line 9973-10027: The computed Bool
allContextMenuWorkspacesHideTerminalScrollBar should not be captured and used as
the authoritative "currentlyHidden" in the toggle handler; instead recompute the
aggregate from the actual workspace IDs right before mutating (e.g. use
contextMenuWorkspaceIds.allSatisfy { tabsById[$0]?.terminalScrollBarHidden ==
true } inside the action that flips terminalScrollBarHidden), update
VerticalTabsSidebar/TabItemView callsites to use that recomputed value in the
mutation, and make TabItemView inputs equatable (use Equatable + .equatable())
so row state is driven by its own observed values rather than parent
snapshot-only inputs.
🪄 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: 0793045f-26d6-4e13-9900-f8cd1b63e700

📥 Commits

Reviewing files that changed from the base of the PR and between 9abf20c and 7cbd2b8.

📒 Files selected for processing (5)
  • Sources/ContentView.swift
  • Sources/GhosttyTerminalView.swift
  • Sources/KeyboardShortcutSettingsFileStore.swift
  • Sources/Workspace.swift
  • Sources/cmuxApp.swift
🚧 Files skipped from review as they are similar to previous changes (3)
  • Sources/KeyboardShortcutSettingsFileStore.swift
  • Sources/Workspace.swift
  • Sources/cmuxApp.swift

Comment thread Sources/GhosttyTerminalView.swift
Comment thread Sources/cmuxApp.swift

@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 5 files (changes from recent commits).

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="Sources/GhosttyTerminalView.swift">

<violation number="1" location="Sources/GhosttyTerminalView.swift:9067">
P2: Listening only to `TerminalScrollBarSettings.didChangeNotification` can miss scrollbar preference changes from direct `@AppStorage` writes (e.g. settings reset), leaving terminal scrollbar geometry stale until another relayout trigger occurs.</violation>
</file>

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

Comment thread Sources/GhosttyTerminalView.swift
@austinywang

Copy link
Copy Markdown
Contributor Author

Follow-up push: 85cf4140

Addressed the remaining review feedback:

  • recompute the workspace scrollbar hidden state at action time instead of trusting the snapshotted row bool
  • emit TerminalScrollBarSettings.didChangeNotification from settings reset when the global value actually changes
  • post a targeted workspace scrollbar preference notification and have each hosted terminal surface observe its owning workspace so per-workspace toggles apply immediately without waiting for unrelated layout churn

All open review threads on these items are resolved.

@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 `@Sources/GhosttyTerminalView.swift`:
- Around line 9401-9406: When reattaching a TerminalSurface in
attachSurface(_:), after setting cachedOwningWorkspace and calling
updateWorkspaceTerminalScrollBarObserver(workspace) immediately apply the
current terminalScrollBarHidden value for that workspace so the gutter/scroller
state is updated right away; locate the logic that applies
terminalScrollBarHidden elsewhere (the code that reads terminalScrollBarHidden
and updates surfaceView/gutter visibility) and invoke it here (e.g. call the
function that applies workspace scroll bar visibility or directly set
surfaceView visibility using the terminalScrollBarHidden value for
cachedOwningWorkspace) so the newly attached surface reflects the correct
workspace state immediately.
- Around line 11395-11420: The gutter/PTY width isn't reconciled when Ghostty's
scrollbar config changes; update refreshHostBackgroundAfterGhosttyConfigReload()
so that after calling synchronizeScrollbarAppearance() it also forces a
geometry/layout reconciliation driven by
terminalScrollBarAllowedBySettings()/shouldShowTerminalScrollBar()/terminalScrollBarReservedWidth():
e.g. dispatch to main thread and call surfaceView.setNeedsLayout() (or
invalidateIntrinsicContentSize()/layoutSubtreeIfNeeded()) and notify the
workspace (owningWorkspace()?.updateTerminalLayout() or similar) so the
gutter/PTY width is recomputed immediately when scrollbar config reloads.
🪄 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: b2e328e7-fa7f-4378-963e-4e893c85ce82

📥 Commits

Reviewing files that changed from the base of the PR and between 7cbd2b8 and 85cf414.

📒 Files selected for processing (4)
  • Sources/ContentView.swift
  • Sources/GhosttyTerminalView.swift
  • Sources/Workspace.swift
  • Sources/cmuxApp.swift
✅ Files skipped from review due to trivial changes (1)
  • Sources/ContentView.swift
🚧 Files skipped from review as they are similar to previous changes (2)
  • Sources/cmuxApp.swift
  • Sources/Workspace.swift

Comment thread Sources/GhosttyTerminalView.swift
Comment thread Sources/GhosttyTerminalView.swift Outdated
@austinywang

Copy link
Copy Markdown
Contributor Author

Follow-up push: db18978e

Addressed the two new GitHub review findings:

  • reattaching a terminal surface now immediately reconciles geometry so the current workspace scroll-bar override applies without waiting for a later layout or scrollbar packet
  • Ghostty config reload now performs the scrollbar/gutter geometry reconcile inside refreshHostBackgroundAfterGhosttyConfigReload(), and the caller no longer does a redundant second reconcile pass

The new review threads are resolved.

@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)
Sources/Workspace.swift (1)

6485-6487: Mark this notification constant nonisolated to avoid forcing observer registration code into main-actor context.

Workspace.terminalScrollBarHiddenDidChangeNotification is implicitly main-actor-isolated because it's a static let in an @MainActor class. Since Notification.Name is a value type and Sendable, marking it nonisolated static let allows GhosttyTerminalView and other non-main-actor code to reference it without async overhead.

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

In `@Sources/Workspace.swift` around lines 6485 - 6487,
Workspace.terminalScrollBarHiddenDidChangeNotification is currently
main-actor-isolated because it's a static let on an `@MainActor` class; change its
declaration to be nonisolated (i.e., declare it as nonisolated static let
terminalScrollBarHiddenDidChangeNotification = ...) so non-main-actor code like
GhosttyTerminalView can reference this Notification.Name without async/await or
hopping to the main actor; keep the value type and initialization the same.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/ContentView.swift`:
- Around line 13173-13178: The TabItemView currently reads tabManager.tabs
inside its private helper allTargetWorkspacesHideTerminalScrollBar, violating
the .equatable() / Equatable precomputed-inputs rule and causing stale
checkmarks; move that boolean computation into VerticalTabsSidebar where you
have access to the tab list, compute a single Bool (e.g.
allTargetsHideTerminalScrollBar) for each TabItemView, add that Bool as a stored
initializer parameter to TabItemView, include it in TabItemView’s Equatable ==
comparison, and remove any direct reads of tabManager or notificationStore from
TabItemView’s body (replace calls to allTargetWorkspacesHideTerminalScrollBar
and similar logic around lines 13173 and 13285-13299 with the injected
parameter).

In `@Sources/GhosttyTerminalView.swift`:
- Around line 11351-11359: The guard in updateWorkspaceTerminalScrollBarObserver
currently can early-return when both observedWorkspaceTerminalScrollBar and
workspace are nil while workspaceTerminalScrollBarObserver is non-nil, skipping
cleanup; change the early-return logic so it only returns when the
observedWorkspaceTerminalScrollBar === workspace AND
workspaceTerminalScrollBarObserver == nil (i.e., nothing to do), ensuring that
when workspaceTerminalScrollBarObserver is non-nil you proceed to remove the
observer and set workspaceTerminalScrollBarObserver = nil.

---

Nitpick comments:
In `@Sources/Workspace.swift`:
- Around line 6485-6487: Workspace.terminalScrollBarHiddenDidChangeNotification
is currently main-actor-isolated because it's a static let on an `@MainActor`
class; change its declaration to be nonisolated (i.e., declare it as nonisolated
static let terminalScrollBarHiddenDidChangeNotification = ...) so non-main-actor
code like GhosttyTerminalView can reference this Notification.Name without
async/await or hopping to the main actor; keep the value type and initialization
the same.
🪄 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: c3c710bb-724b-479d-ab18-48b5ac0d8e04

📥 Commits

Reviewing files that changed from the base of the PR and between db18978 and 83739b4.

📒 Files selected for processing (3)
  • Sources/ContentView.swift
  • Sources/GhosttyTerminalView.swift
  • Sources/Workspace.swift

Comment thread Sources/ContentView.swift Outdated
Comment thread Sources/GhosttyTerminalView.swift
@austinywang

Copy link
Copy Markdown
Contributor Author

Follow-up push: 765b9eae

Addressed the remaining minor review comments:

  • the workspace scrollbar checkmark is once again precomputed in VerticalTabsSidebar and passed into TabItemView, so the .equatable() row stays on precomputed inputs only
  • updateWorkspaceTerminalScrollBarObserver now cleans up a stale observer token even if the weak workspace reference has already gone nil

The remaining minor review threads are resolved.

@austinywang

Copy link
Copy Markdown
Contributor Author

Fixed the CI/build break from the last review-follow-up patch. The sidebar now restores the parent-scope workspace lookup used for the precomputed terminal-scrollbar context-menu state, so stays on the precomputed-inputs path and compiles again.

Comment thread Sources/KeyboardShortcutSettingsFileStore.swift
@cubic-dev-ai

cubic-dev-ai Bot commented Apr 10, 2026

Copy link
Copy Markdown

You're iterating quickly on this pull request. To help protect your rate limits, cubic has paused automatic reviews on new pushes for now—when you're ready for another review, comment @cubic-dev-ai review.

Comment thread Sources/Workspace.swift

@cursor cursor 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is kicking off a free cloud agent to fix this issue. This run is complimentary, but you can enable autofix for all future PRs in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 09b4bf0. Configure here.

Comment thread Sources/GhosttyTerminalView.swift Outdated
@austinywang

Copy link
Copy Markdown
Contributor Author

Follow-up push: da1b45d5

Addressed the remaining open review thread in GhosttyTerminalView:

  • handleScrollbarUpdate now uses the canonical shouldShowTerminalScrollBar() predicate instead of duplicating the inline visibility formula, so future scrollbar-visibility conditions stay consistent across the runtime update path and the shared reconcile path

Validation:

  • Built with ./scripts/reload.sh --tag issue-2728-nvim-scrollbar-overlap
  • Per repo policy, local tests were not run

@austinywang
austinywang merged commit 2073e2e into main Apr 14, 2026
21 of 22 checks passed
rodchristiansen pushed a commit to rodchristiansen/cmux that referenced this pull request Sep 2, 2026
…scrollbar-overlap

Suppress right-edge scroll bar on TUI alt-screen and add disable setting (fixes 2728)

This branch was successfully deployed

1 active deployment
Preview — da1b45d5 Deployed Apr 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.

Right-edge bar overlaps neovim scrollbar — please add a way to disable it (regression after #2678?)

1 participant