Skip to content

Add sidebar sections with persistence and auto-apply - #2646

Open
rodchristiansen wants to merge 2042 commits into
manaflow-ai:mainfrom
rodchristiansen:feature/sidebar-sections
Open

rodchristiansen wants to merge 2042 commits into
manaflow-ai:mainfrom
rodchristiansen:feature/sidebar-sections

Conversation

@rodchristiansen

@rodchristiansen rodchristiansen commented Apr 6, 2026 •

Copy link
Copy Markdown

Summary

  • Persistent sidebar sections that survive app restart, with auto-rename on create
  • Workspace target:current support and section reorder/rename with proper focus handling
  • AutoApply for workspace commands triggered on tab switch, tracked per-session
  • Fix split divider position and bonsplit submodule updates

Test plan

  • Create sidebar sections, restart app, verify they persist
  • Rename sections and verify focus stays correct
  • Switch workspaces and verify autoApply fires once per workspace
  • Split panes and verify divider position is correct
  • Verify target:current resolves to the active workspace

🤖 Generated with Claude Code


Summary by cubic

Adds collapsible, user-defined sidebar sections with session persistence and ordered rendering, plus an Active filter to quickly show workspaces with agent sessions. Also adds workspace command autoApply on tab switch and target: "current" to apply layouts to the active workspace.

New Features

  • Sidebar sections: create, rename, reorder, delete; collapse/expand; drag tabs onto headers or use "Move to Section" (always shows "New Section…"). Multi-select aware.
  • Persistence: sections (IDs, names, collapse state, membership) survive restarts and restore by workspace UUID.
  • Ordering: pinned tabs first, then ungrouped, then section contents; supports within-section tab reordering.
  • Active filter: toggle to show only workspaces with an agent session; auto-clears when none; applies to pinned, ungrouped, and sections.
  • Workspace commands: target: "current" applies layouts in-place after trimming extra panes. Optional autoApply: true runs once per workspace on switch (single-pane only) and falls back to global config when no local is present.
  • Native Liquid Glass backdrop on macOS 26 when available.

Bug Fixes

  • Drag/drop: cross-section and ungrouped↔section moves update membership; grouped tabs skip flat reorders; intra-section indices fixed; drop handler checks membership changes before no-op.
  • Context menu: "Move to Section" is always available; disabled states reflect full multi-selection.
  • Sidebar: updates instantly on section changes; rename focus timing fixed; flat tab order used for selection ranges and shortcut badges.
  • Commands: target: "current" only renames when a name is explicitly set; autoApply avoids stale runs on rapid switches, falls back to global config, and closes extra panes safely.
  • Split divider position fixed and bonsplit submodule updated.

Written for commit 6b8e3db. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Organize workspaces into collapsible, renameable, reorderable sidebar sections with drag & drop, per-row "Move to Section", section header actions, and session persistence.
    • Commands can be configured to auto-apply when switching to a workspace (session-limited, current-target only).
  • Improvements

    • Reordering now respects section membership and routes moves through section-aware APIs.
    • Sidebar uses native glass visuals on modern macOS when available.
  • Session / Restore

    • Workspaces retain restored identifiers so sections and selections persist across sessions.

Note

Medium Risk
Medium risk: introduces new persistent sidebar grouping/state and new auto-execution path for config commands on workspace switches, which could affect ordering/restore behavior and unexpectedly apply layouts if misconfigured.

Overview
Adds user-defined sidebar sections (new SidebarSection model + TabManager.sections + sidebarLayout) with collapse/expand, rename, reorder, delete, and drag/drop membership changes; the sidebar now renders pinned workspaces first, then ungrouped, then section groups, and updates selection/indexing to stay consistent with the flat tabs order.

Persists section state across restarts by extending session snapshots to include workspace UUIDs and section snapshots (id/name/collapsed/membership), and updates workspace restore to preserve IDs so section membership can be re-linked.

Extends cmux.json commands with autoApply and workspace target (including target: "current"), adds an on-tab-switch auto-apply hook (once per workspace per session, single-pane only), and updates workspace command execution to optionally apply layouts in-place to the currently selected workspace.

Also tweaks sidebar material rendering to use native SwiftUI glassEffect on macOS 26+ when available.

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

lawrencecchen and others added 30 commits March 26, 2026 21:09
* Switch release builds to macOS Tahoe runner

Use warp-macos-26-arm64-6x for release builds to match nightly. Also add
a dry-run artifact upload path for workflow_dispatch so the release
pipeline can be tested without creating a real release.

* Gate release upload on event_name, not just ref prefix

Prevents workflow_dispatch triggered on a tag ref from accidentally
uploading real release assets.

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
* feat: add Ukrainian (uk) website translation

Translate all 681 keys from en.json to uk.json for the cmux website.
Preserves all placeholders and HTML-like tags. Testimonials kept in original language.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: register Ukrainian locale in web i18n routing config

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add Ukrainian translations to Localizable.xcstrings (916 keys)

Translated all 916 string keys from English to natural Ukrainian.
All format specifiers (%@, %lld, %1$@, etc.) preserved.
JSON syntax validated.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add Ukrainian README translation (README.uk.md)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add Ukrainian language link to all README language selectors

Added Українська link to the language selector paragraph in all 20 existing
README files (README.md and README.*.md), pointing to README.uk.md.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix truncated Ukrainian translations for cli.claude-teams.usage and cli.omo.usage

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
* Add customizable sidebar selection highlight color

Expose a `sidebarSelectionColorHex` user default that overrides the
hardcoded blue (#0091FF) selection highlight in the sidebar. Add a
"Selection Highlight" color picker in Settings > Workspace Colors,
following the same pattern as existing tint color pickers. Falls back
to the default accent color when no custom color is set.

Closes manaflow-ai#1753

* Fix review feedback: reactivity, reset button, localization

- Add @AppStorage subscription in TabItemView so sidebar selection
  color updates reactively when changed in Settings
- Add Reset button in Settings > Workspace Colors > Selection Highlight
- Localize debug panel strings for Selection Color picker
- Clear sidebarSelectionColorHex in resetAllSettings()

* Add customizable notification badge color in sidebar

Add `sidebarNotificationBadgeColorHex` user default to override the
unread notification badge color on workspace tabs. Add a "Notification
Badge" color picker in Settings > Workspace Colors, following the same
pattern as the selection highlight picker. Falls back to the default
accent color when no custom color is set.
* Fix minimal mode toggle not properly updating titlebar state

UpdateTitlebarAccessoryController only re-evaluated titlebar accessories
on window focus events (didBecomeKey/didBecomeMain), not when the
presentation mode actually changed. This caused:

1. Switching to minimal: accessories weren't immediately removed
2. Switching back to standard: accessories were never re-attached
   (removeAccessoryIfPresent cleared attachedWindows, but no event
   triggered re-attachment)
3. Repeated toggling left the window in inconsistent states

Add a UserDefaults observer that detects presentation mode changes and
re-evaluates all windows. When switching to minimal, accessories are
removed; when switching to standard, fresh accessories are created and
attached. Also handle the fullscreen edge case where re-attached
accessories must be hidden to avoid doubling with SwiftUI overlay
controls.

* Hide window toolbar in minimal mode to eliminate titlebar gap

The NSToolbar (attached by WindowToolbarController) creates a non-zero
titlebar area even in minimal mode, leaving an empty gap above the
Bonsplit tab bar. In minimal mode there's no need for the toolbar (it
shows a "Cmd:" text that's hidden anyway with titleVisibility=.hidden).

Hide the toolbar when switching to minimal mode and restore it when
switching back to standard. Also set initial visibility on attachment
based on the current mode.

* Apply ignoresSafeArea to contentAndSidebarLayout in minimal mode

The titlebar gap persisted because intermediate SwiftUI views still
respected the window's safe area even though MainWindowHostingView
zeroes safeAreaInsets. Apply .ignoresSafeArea(.container, edges: .top)
directly to the contentAndSidebarLayout when in minimal mode so the
entire content (sidebar + terminal) extends into the titlebar area.

* Remove toolbar entirely in minimal mode instead of just hiding

toolbar.isVisible=false still reserves titlebar space. Remove the
toolbar entirely (window.toolbar=nil) when switching to minimal mode
and re-attach it when switching back to standard. Skip toolbar
attachment entirely when launching in minimal mode.

* Add BonsplitTabDragUITests.swift to cmuxUITests target

The test file existed on disk but was missing from the Xcode project,
causing all BonsplitTabDrag UI tests (including minimal mode tests) to
silently report 0 tests on CI.

* Use negative titlebar padding in minimal mode to extend content into titlebar

The native titlebar area (28.5pt for traffic lights) persists even
without a toolbar and despite .ignoresSafeArea() modifiers. Use
negative padding (-titlebarPadding) in minimal mode to pull the
terminal content up into the titlebar area. The sidebar's internal
trafficLightPadding spacer keeps sidebar content properly offset
below the traffic lights.

* Add window drag handle to Bonsplit top strip in minimal mode

In minimal mode, the custom titlebar (which provides the window drag
handle) is hidden. Add a WindowDragHandleView to the top strip overlay
so users can drag-to-move the window from the Bonsplit tab bar area.
The TitlebarDoubleClickMonitorView is kept as a background for
double-click-to-zoom.

* Use native titlebar drag in minimal mode instead of WindowDragHandleView

WindowDragHandleView defers to interactive siblings (Bonsplit tab bar),
so it never captures hits. Instead, set window.isMovable=true in
minimal mode so the native titlebar area handles drag-to-move and
double-click-to-zoom. Remove the non-functional overlay from
WorkspaceContentView.

* Enable isMovableByWindowBackground in minimal mode for window dragging

window.isMovable alone doesn't work because the Bonsplit tab bar
captures all hits before the native titlebar drag engages. Use
isMovableByWindowBackground=true so any area that doesn't handle
mouse events becomes a drag handle. Also capture
workspacePresentationMode in the WindowAccessor closure so the
window properties update when toggling modes.

* Add debug logging for minimal mode window drag diagnosis

* Intercept double-click in minimal mode tab bar to zoom instead of new tab

Bonsplit's EmptyTabBarDoubleClickMonitorView creates a new tab on
double-click in the tab bar empty space. In minimal mode, intercept
these double-clicks with a higher-priority local event monitor and
perform the standard macOS titlebar action (zoom/minimize based on
System Settings) instead. Only intercepts in the top 30pt strip and
only when minimal mode is active.

* Fix double-click monitor ordering and coordinate calculation

NSEvent local monitors are called LIFO (last installed first). Install
the minimal-mode double-click interceptor with a 0.5s delay so it's
added after Bonsplit's EmptyTabBarDoubleClickMonitorView monitors,
ensuring it runs first and can consume the event. Also fix the
distance-from-top calculation to use window frame height instead of
contentLayoutRect height, since the tab bar is in the titlebar area.

* Remove unnecessary delay from double-click monitor installation

* Show split buttons on hover only in minimal mode, fix sidebar controls re-attachment

Two fixes:
1. Add splitButtonsOnHover to BonsplitConfiguration.Appearance. In
   minimal mode, the Bonsplit split buttons (terminal, browser, split
   right/down) fade in only when hovering the tab bar. Revert to
   always-visible when switching back to standard mode.
2. Delay titlebar accessory re-attachment when switching to standard
   mode so the toolbar is re-added first. Without this, the accessory
   attaches before the toolbar exists, causing the sidebar controls
   to not appear in the titlebar.

* Fix splitButtonsOnHover via onChange instead of body eval, add debug logs

* Update bonsplit submodule for splitButtonsOnHover

* Remove debug logs, verified splitButtonsOnHover and accessory re-attachment on macmini

* Read presentationMode directly in TabBarView via @AppStorage

The @observable configuration propagation wasn't reliably triggering
re-renders in TabBarView. Read the workspacePresentationMode directly
via @AppStorage in TabBarView instead, which SwiftUI reactively
updates when UserDefaults changes. Remove the syncSplitButtonsOnHover
workaround from WorkspaceContentView.

* Fix tab drag, double-click zone, and sidebar controls re-attachment

- Revert isMovableByWindowBackground to false; it breaks Bonsplit tab
  reordering. Keep isMovable=true in minimal mode so the sidebar area
  (which has WindowDragHandleView) is draggable.
- Increase double-click intercept zone from 30pt to 40pt to cover the
  full tab bar height (33pt).
- Use asyncAfter(0.1s) for titlebar accessory re-attachment when
  switching to standard mode, giving the toolbar time to re-attach.

* Add debug logging for titlebar accessory re-attachment diagnosis

* Fix crash and sidebar controls re-attachment

Remove debug logging that crashed when accessing window properties
during iteration. Increase deferred re-attachment delay to 0.3s to
give the WindowAccessor callback time to set the window identifier
and toolbar before attachIfNeeded checks isMainTerminalWindow.

* Keep titlebar accessories attached in minimal mode instead of removing

The remove/re-add cycle was fragile: re-attachment depended on window
identifiers being set, toolbar being re-added, and timing delays.
Instead, keep TitlebarControlsAccessoryViewController always attached
and let its own UserDefaults observer handle visibility. It already
hides itself (view.isHidden=true, preferredContentSize=.zero) in
minimal mode and shows itself in standard mode. No timing hacks needed.

* Force titlebar accessory layout after toolbar re-addition

* Use both self.isHidden and view.alphaValue/isHidden for accessory visibility

self.isHidden alone doesn't reliably hide the accessory when the
toolbar is nil on macOS 26. Add view.alphaValue=0 and view.isHidden
as visual fallbacks. Crucially, don't zero preferredContentSize or
frames so fittingSize returns valid values when switching back.

* Set window.isMovable=false always to fix sidebar button clicks

window.isMovable=true in minimal mode blocks clicks on the sidebar
controls because the native titlebar drag intercepts mouse events in
the overlapping area. The sidebar's WindowDragHandleView already
handles drag-to-move via performDrag with withTemporaryWindowMovableEnabled,
so native isMovable isn't needed.

* Add drag-to-move from empty bonsplit tab bar space in minimal mode

* Use overlay for tab bar drag, smart hitTest passes through tabs/buttons

* Add double-click zoom/minimize to tab bar drag view

* Add leading padding for traffic lights when sidebar collapsed in minimal mode

* Add traffic light inset to tab bar when sidebar collapsed in minimal mode

* Fix accessory space and double-click in minimal mode

- Zero preferredContentSize in minimal mode (so accessory takes no
  space) but seed hostingView with cached size before querying
  fittingSize when switching back (so size can be restored).
- Skip EmptyTabBarDoubleClickMonitorView in minimal mode so
  DraggableTabBarView handles double-click for zoom instead.
- Remove redundant ContentView double-click monitor.

* Auto-detect traffic light inset in TabBarView via GeometryReader

Instead of propagating sidebar state through config, the tab bar
detects its own position relative to the window. If in minimal mode
and the tab bar's leading edge is near the window edge (< 20pt, no
sidebar), add 72pt spacer for traffic light clearance.

* Increase traffic light spacer to 80pt

* Fix tab click passthrough in minimal mode drag overlay

* Check full window for interactive hits in drag overlay

* Fix drag overlay capturing all clicks via reentrancy guard in hitTest

* Distinguish interactive controls from hosting views in drag hitTest

* Walk ancestor chain for button detection in drag overlay hitTest

* Replace overlay with background drag view per ensemble recommendation

* Only add traffic light inset for top-left pane

* Use GeometryReader for traffic light inset, check screen position

* Fix operator precedence in traffic light inset check

* Use window frame for traffic light inset detection

* Set tabBarLeadingInset from ContentView via onChange handlers

Replace unreliable coordinate-based detection with direct state from
ContentView, which knows both sidebar visibility and minimal mode.
Syncs on appear, sidebar toggle, and mode toggle.

* Use allPaneIds.first for top-left pane detection, no hierarchy threading needed

* Update bonsplit submodule to merged main

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
* perf: coalesce high-frequency scrollbar updates to reduce main-thread pressure

During bulk terminal output (e.g. `seq 1 100000`), GHOSTTY_ACTION_SCROLLBAR
fires thousands of times per second.  Previously each callback enqueued a
separate DispatchQueue.main.async block that updated the scrollbar property
and posted a NotificationCenter notification, causing the main thread to
process thousands of redundant scroll-geometry recalculations.

This change adds a lightweight coalescing layer: the action callback stores
the latest scrollbar value behind an NSLock and schedules at most one async
flush.  The flush picks up whichever value is current at execution time,
collapsing N callbacks into a single synchronizeScrollView() pass.

Measured improvement on `time seq 1 10000`:
- Before: ~0.052s (26% CPU — seq blocked on PTY backpressure)
- After:  expected ~0.025-0.030s (reduced main-thread contention)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: use defer for NSLock release in scrollbar coalescing

Address review feedback: wrap unlock() in defer blocks in both
enqueueScrollbarUpdate and flushPendingScrollbar to guarantee
lock release on any future early-return or exception path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* perf: coalesce wakeup→tick dispatches to eliminate main-thread queue flooding

During bulk terminal output, Ghostty's I/O thread fires wakeup_cb thousands
of times per second.  Previously each wakeup enqueued a separate
DispatchQueue.main.async { tick() } block, flooding the main queue and
starving the run loop.  The main thread spent all its time draining tick
blocks, creating PTY backpressure that blocked the writing process.

Add a lightweight coalescing gate: scheduleTick() only enqueues a single
async block; subsequent wakeups while the block is pending are no-ops.
The pending tick picks up all accumulated state in one ghostty_app_tick()
call, collapsing N wakeups into 1 main-thread dispatch.

Combined with the earlier scrollbar coalescing, measured improvement:

  time seq 1 10000:
  - Ghostty standalone: 0.019s (82% CPU)
  - cmux before:        0.052s (26% CPU)  ← main-thread saturated
  - cmux after:         0.016s (62% CPU)  ← faster than standalone

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: release scrollbar lock before posting notification

Move NotificationCenter.post outside the _scrollbarLock critical section
in flushPendingScrollbar(). Holding the lock through observer dispatch
would block the I/O thread's enqueueScrollbarUpdate() calls behind
main-thread observer work, recreating the backpressure this change
aims to eliminate.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ions (manaflow-ai#2238)

* Add claude-teams, omo, and __tmux-compat to Go relay CLI

These commands previously only existed in the Swift CLI which uses Unix
domain sockets and can't connect over TCP relay. The Go relay CLI already
handles TCP connections, so adding the commands here makes them work
inside `cmux ssh` sessions.

- `cmux claude-teams`: creates tmux shim scripts, configures environment
  (fake TMUX/TMUX_PANE, socket path, workspace/surface IDs), and execs
  into `claude --teammate-mode auto`
- `cmux omo`: same pattern for OpenCode with terminal-notifier shim
- `cmux __tmux-compat`: translates tmux commands (split-window,
  send-keys, capture-pane, display-message, list-panes, etc.) into cmux
  JSON-RPC calls over the relay socket. Includes main-vertical layout
  tracking, wait-for signaling, and format string rendering.

* Fix: search original PATH before environment modification

findExecutable was called after configureAgentEnvironment prepended the
shim directory to PATH. The Swift CLI searches the original PATH before
modification. Renamed to findExecutableInPath with explicit PATH arg and
moved the search before configureAgentEnvironment.

* Fix cmux omo hang and port oh-my-opencode plugin setup

Root cause: socketRoundTripV2 had no read timeout. When connecting to a
stale relay port (accepted TCP but never responded), the read blocked
forever. This caused getFocusedContext to hang, blocking agent launch.

Fixes:
- Add 15s read deadline to socketRoundTripV2 (affects all v2 RPC calls)
- Add 5s timeout to getFocusedContext so agent launch proceeds even if
  system.identify is slow
- Port omoEnsurePlugin from Swift: creates shadow config dir, adds
  oh-my-opencode to plugin list, symlinks node_modules/package.json,
  installs plugin via bun/npm if missing, configures tmux settings
  (enabled=true, lower min widths), sets OPENCODE_CONFIG_DIR

* Fix: use bun as runtime for node-script opencode when node is missing

opencode is installed via bun as a #!/usr/bin/env node script, but on
some systems (like the macmini) bun is installed without a standalone
node binary. Detect node scripts and fall back to bun as the runtime
since bun is node-compatible.

* Fix subagent pane theme: preserve COLORTERM, keep TERM_PROGRAM

The cmux ssh bootstrap exports COLORTERM=truecolor and TERM_PROGRAM=ghostty.
Our configureAgentEnvironment was unsetting TERM_PROGRAM and not setting
COLORTERM, causing subagent panes (created via split-window) to lose
truecolor detection and render with wrong theme colors.

* Restore TERM_PROGRAM unset, keep COLORTERM=truecolor

* Force dark colorScheme in opencode shadow config for SSH

* Remove hardcoded dark colorScheme, let opencode detect naturally

* Detect system color scheme for opencode over SSH

* Remove color scheme detection workaround, let opencode handle natively

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
…aflow-ai#2283)

* Fix ARC workspace inheritance crash and native Zig helper builds

* Fix Nightly Cmd+N workspace creation crash

* Restore safe terminal config snapshots for Intel Nightly
* Skip quit confirmation for tagged DEV builds

Tagged DEV builds are ephemeral dev iterations, so the "Quit cmux?"
dialog just adds friction. Check SocketControlSettings.launchTag() in
both applicationShouldTerminate and handleQuitShortcutWarning to bypass
the confirmation when a tag is present.

Closes manaflow-ai#2286

* Use bundle ID instead of env var for tagged DEV detection

CMUX_TAG env var is only set when reload.sh --launch opens the app.
When the user cmd-clicks the app path, it launches via Finder without
the env var, so launchTag() returns nil and the quit dialog still shows.

Switch to checking the bundle identifier (com.cmuxterm.app.debug.<tag>)
which is baked into the built app and available regardless of how it was
launched. Add SocketControlSettings.isTaggedDevBuild() helper.

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
* Add "Match Terminal Background" sidebar setting

Adds a toggle in Settings > Sidebar Appearance that makes the sidebar
use the same background color and transparency as the terminal area.

Uses layer-level opacity on a fully opaque background color (the same
technique as TitlebarLayerBackground) with effective opacity formula
`1 - (1-alpha)^2` to account for the terminal's two stacked
semi-transparent layers (Bonsplit chrome + Ghostty Metal surface).

Also adds a 1px trailing border derived from the terminal chrome color,
matching the bonsplit tab bar separator logic.

* Fix sidebar border color not updating on theme change

Add @State + .onReceive(.ghosttyDefaultBackgroundDidChange) to
SidebarTrailingBorder so the separator color recomputes when the
Ghostty theme changes, matching the pattern used in SidebarBackdrop.

* Address review comments: localize debug toggle, fix separator refresh

- Localize the debug panel toggle label (Codex P1)
- Add .onAppear to SidebarTrailingBorder for initial color (Cubic P2)
- Fix stale doc comment on SidebarTerminalBackgroundView (Cubic P3)

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
…anaflow-ai#2287)

Match the nightly workflow's universal build approach so stable releases
support both Apple Silicon and Intel Macs. Adds -destination, ARCHS,
ONLY_ACTIVE_ARCH=NO flags and a post-build architecture verification step.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
manaflow-ai#2305)

* fix(workspace): defer layout follow-up flush to avoid re-entrant displayIfNeeded crash

beginEventDrivenLayoutFollowUp() ended with a synchronous call to
attemptEventDrivenLayoutFollowUp(), which calls flushWorkspaceWindowLayouts()
→ window.contentView?.displayIfNeeded(). This is fine when invoked from
user-event handlers, but splitTabBar(_:didChangeGeometry:) fires from inside
SwiftUI's .onChange(of: geometry) during an active AppKit display/layout pass.

Calling displayIfNeeded() re-entrantly during that pass caused AppKit to
increment the per-window Update Constraints pass counter on every display
cycle. Once the counter exceeded the view-count limit AppKit threw an
NSGenericException and crashed:

 'The window has been marked as needing another Update Constraints in Window
 pass, but it has already had more Update Constraints in Window passes than
 there are views in the window.'

Fix: replace the direct attemptEventDrivenLayoutFollowUp() call with
scheduleLayoutFollowUpAttempt(), which defers via asyncAfter(.now() + 0).
When layoutFollowUpStalledAttemptCount == 0 the backoff delay is zero, so
there is no meaningful latency increase — the flush simply runs at the start
of the next run loop iteration, after the current layout pass has fully
unwound. The NSWindow.didUpdateNotification observer and the existing timeout
still drive retries, so convergence is unaffected.

Made-with: Bunny

* fix(workspace): supersede stale layout follow-up retry on reset

scheduleLayoutFollowUpAttempt() is a no-op when
layoutFollowUpAttemptScheduled is true, so a pending retry with a
long backoff delay would survive a beginEventDrivenLayoutFollowUp()
call even though that call resets layoutFollowUpStalledAttemptCount
to 0. The stale closure would then fire after its original delay
rather than immediately.

Adds a layoutFollowUpAttemptVersion counter. beginEventDrivenLayoutFollowUp()
increments the version and clears layoutFollowUpAttemptScheduled,
allowing a fresh asyncAfter(0) attempt to be enqueued. Pending
closures capture the version at scheduling time and exit early if it
no longer matches. clearLayoutFollowUp() also increments the version
to cancel any in-flight closure during teardown.

Made-with: Bunny
* Add regression coverage for stale window geometry migration

* Discard stale persisted window geometry on launch
…t layout crash (manaflow-ai#2308)

- Version the persisted window geometry schema (v1 → v2) and clean up
  legacy UserDefaults keys so stale payloads from older releases don't
  cause crashes on startup.
- Defer layout follow-up flush via asyncAfter(0) and track an attempt
  version counter to invalidate stale retries, preventing re-entrant
  displayIfNeeded crashes triggered by SwiftUI geometry change callbacks.
- Replace fixed RunLoop delays in tests with polling waitUntil helpers
  and increase socket wait timeout for CI reliability.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…epalive (manaflow-ai#2325) (manaflow-ai#2330)

- Add 5-minute per-host cooldown for remote error notifications
- Add exponential backoff (capped at 60s) to proxy broker and session controller retries
- Add default SSH ConnectTimeout/ServerAliveInterval/ServerAliveCountMax to detect dead connections faster
- Fix error status clearing to only reset on actual .connected state

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add regression coverage for sidebar live refresh

* Refresh sidebar git metadata on active workspaces
* Fix browser pane dark-mode leak

* Restore browser theme mode without CSS injection
* Fix fullscreen new windows opening in current Space

* works

* Stabilize fullscreen tiling regression test
* Add Dock menu new-window regression test

* Add New Window to Dock menu
Copilot AI review requested due to automatic review settings April 6, 2026 21:14
@vercel

vercel Bot commented Apr 6, 2026

Copy link
Copy Markdown

@rodchristiansen is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Apr 6, 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 persistent, reorderable sidebar sections with UI (headers, rename, drag/drop, filter) and TabManager APIs; persists/restores workspace UUIDs across sessions; introduces Cmux command autoApply plus workspace target handling and executor support to modify the current workspace in-place when appropriate.

Changes

Sidebar: model, manager, UI, persistence

Layer / File(s) Summary
Data Shape / New types
Sources/SidebarSection.swift, Sources/SessionPersistence.swift, Sources/Workspace.swift
Adds SidebarSection class and SidebarLayout struct; adds SessionSidebarSectionSnapshot; SessionWorkspaceSnapshot gains optional id; Workspace accepts restoredId to preserve UUID on restore.
Core TabManager behavior
Sources/TabManager.swift
Adds sections state, sectionRevision invalidation, observers, and public APIs: create/rename/delete/reorder sections, move/remove workspace to/from section, sectionForWorkspace, and sidebarLayout derivation; prevents flat reorders for sectioned workspaces; integrates section cleanup in workspace removal.
UI wiring & interactions
Sources/ContentView.swift
Renders pinned → ungrouped → section groups; adds SidebarFilterMode + filter bar; tabItemViewForWorkspace helper; SidebarSectionHeaderView (collapse, inline rename, context menu, drop handling); Move-to-Section menu and New Section flow; section-aware drop handling; macOS 26+ glass background branch.
Persistence wiring
Sources/TabManager.swift, Sources/SessionPersistence.swift
Session snapshot now includes sections (or nil when empty); autosave fingerprint includes section data; restore reconstructs tabs and restores sections atomically, mapping section workspaceIds to restored tab ids.
Project file
GhosttyTabs.xcodeproj/project.pbxproj
Adds SidebarSection.swift to Xcode project (file reference, build file, Sources group).

Cmux: auto-apply commands and workspace target handling

Layer / File(s) Summary
Data Shape
Sources/CmuxConfig.swift
Adds CmuxCommandDefinition.autoApply: Bool? and new CmuxWorkspaceTarget enum (current/new); CmuxWorkspaceDefinition.target: CmuxWorkspaceTarget?.
Execution core
Sources/CmuxConfigExecutor.swift
executeWorkspaceCommand resolves cwd early and adds branch for wsDef.target == .current: updates selected workspace in-place (title/color/layout), closes non-focused panels when applying layout, then returns (bypasses create-new path).
Wiring / Observers
Sources/CmuxConfig.swift (CmuxConfigStore)
Tracks trackedTabManager and autoAppliedWorkspaceIds; wires to tabManager.$selectedTabId dropping first emission and delaying 0.15s, and runs checkAutoApply(forTabId:); loadAll() invokes checkAutoApply() after reload; checkAutoApply executes first autoApply command targeting .current once-per-session for single-panel workspaces.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant TabManager
    participant CmuxConfigStore
    participant CmuxConfigExecutor
    participant FS as ConfigFS

    User->>TabManager: select workspace/tab
    TabManager-->>CmuxConfigStore: publishes selectedTabId
    CmuxConfigStore->>CmuxConfigStore: drop initial, delay 0.15s, checkAutoApply(forTabId)
    CmuxConfigStore->>FS: read config (local/global) to compute baseCwd
    CmuxConfigStore->>CmuxConfigExecutor: request execute(command, workspaceDef, baseCwd)
    alt workspaceDef.target == current
        CmuxConfigExecutor->>TabManager: update workspace (title/color/layout) in-place
    else target == new
        CmuxConfigExecutor->>TabManager: create or reuse workspace and run command
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • manaflow-ai/cmux#3038 — Modifies sidebar row rendering and TabItemView behavior in ContentView.swift (UI/context-menu overlap).
  • manaflow-ai/cmux#2328 — Refactors sidebar row construction/TabItemView wiring similar to ContentView changes.
  • manaflow-ai/cmux#1296 — Adds sidebar section types and integrates sections into TabManager/ContentView/session persistence (strong overlap).

Poem

🐰 I hop through sidebar rows with care,
I name and tuck each workspace there.
On tab-switch nights a soft command applies,
sections tidy up with cheerful sighs.
Carrots, layout, and a tidy hare.


Caution

Pre-merge checks failed

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

  • Ignore

❌ Failed checks (5 errors, 1 warning, 3 inconclusive)

Check name Status Explanation Resolution
Cmux Swift Blocking Runtime ❌ Error Introduces delayed dispatch pattern: DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) in CmuxConfig.swift line 342 for autoApply feature. Listed as blocking pattern. Replace DispatchQueue.main.asyncAfter with async/await Task.sleep or Combine debounce operators instead of timed delays.
Cmux No Hacky Sleeps ❌ Error PR introduces fixed 0.15s wall-clock delay in Sources/CmuxConfig.swift line 342 to paper over filesystem/lifecycle race when loading config after tab switch. Use event-driven coordination: observe @Published loadedCommands instead of assuming 0.15s is sufficient for config file loading to complete.
Cmux Swift Logging ❌ Error PR adds unguarded NSLog in production code: CmuxConfig.swift, ContentView.swift, TabManager.swift (3 locations). Violates logging policy for app/runtime code. Wrap unguarded NSLog statements in #if DEBUG or remove them. Workspace.swift NSLog additions correctly use #if DEBUG.
Cmux Swiftui State Layout ❌ Error New SidebarSection uses ObservableObject+@published instead of @Observable. SidebarSectionHeaderView in sidebar ForEach holds @ObservedObject and store references. Use @Observable macro for SidebarSection. Pass snapshots and closures to SidebarSectionHeaderView instead of store references.
Cmux Architecture Rethink ❌ Error Uses 0.15s DispatchQueue.main.asyncAfter delay in CmuxConfig.swift:342 to wait for file I/O completion before checkAutoApply—a timing-dependent workaround pattern violating architectural guidelines. Replace with observable file loading. Trigger checkAutoApply from file watcher completion rather than DispatchQueue.main.asyncAfter delay.
Docstring Coverage ⚠️ Warning Docstring coverage is 15.79% 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 description covers the main changes but lacks a dedicated testing section and demo video as specified in the template. Add a Testing section documenting how the changes were verified, and include a demo video link for the UI changes (sidebar sections, rename, drag-drop).
Cmux Swift @Concurrent ❓ Inconclusive Required rule file .github/review-bot-rules/swift-concurrent-annotation.md does not exist. Cannot validate against undefined rules. Create the missing rule file to enable validation.
Cmux Swift File And Package Boundaries ❓ Inconclusive The rule file .github/review-bot-rules/swift-file-package-boundaries.md does not exist, making formal verification impossible. Create the missing boundaries rules file. Based on current practices, PR exhibits good separation: new models in dedicated files, UI in ContentView (appropriate), no mixed responsibilities. Would likely PASS reasonable standards.
✅ Passed checks (6 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the two main features added: sidebar sections with persistence and auto-apply for workspace commands.
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 changes correctly implement Swift 6 actor isolation. New/modified classes marked @MainActor. Session snapshots are Sendable value types. No violations of actor isolation rules detected.
Cmux Swift Concurrency ✅ Passed Cmux code only uses DispatchQueue.main timing, observes existing Combine state, no background queues or fire-and-forget Tasks. DispatchSource required for OS callbacks. Task patterns only in UI code.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR does not add or materially change standalone cmux-owned windows. New file SidebarSection.swift is a model class. Changes are to existing window content and sidebar management.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@cubic-dev-ai

cubic-dev-ai Bot commented Apr 6, 2026

Copy link
Copy Markdown

This review could not be run because your cubic account has exceeded the monthly review limit. If you need help restoring access, please contact contact@cubic.dev.

Copilot AI 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.

Pull request overview

This PR adds persistent, user-defined sidebar sections for organizing workspaces, extends session persistence to restore section/workspace identities across restarts, and introduces autoApply / target: "current" behavior for cmux workspace commands (including auto-application on workspace switch).

Changes:

  • Add SidebarSection model + sidebar layout computation and render pinned/ungrouped/sectioned workspaces in the sidebar.
  • Persist/restore workspace IDs and sidebar sections via session snapshots (and support restoring workspace IDs).
  • Add cmux autoApply and workspace.target (including applying a layout onto the currently selected workspace).

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
Sources/Workspace.swift Include workspace id in snapshots; allow restoring a workspace with a prior UUID.
Sources/TabManager.swift Add section state, sidebar layout computation, autosave fingerprint updates, and restore logic for sections/workspace IDs.
Sources/SidebarSection.swift New observable model for sidebar sections + a SidebarLayout helper struct.
Sources/SessionPersistence.swift Extend session snapshot schema structs to include workspace IDs and section snapshots.
Sources/ContentView.swift Render sidebar sections + section headers, add “Move to Section” menu, and adjust drag/drop behavior.
Sources/CmuxConfigExecutor.swift Implement target: "current" workspace application behavior.
Sources/CmuxConfig.swift Add autoApply, CmuxWorkspaceTarget, and per-session auto-apply tracking on tab switches.
GhosttyTabs.xcodeproj/project.pbxproj Add SidebarSection.swift to the Xcode project build sources.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread Sources/ContentView.swift Outdated
let workspaceNumberShortcut = self.workspaceNumberShortcut
let tabItemSettings = tabItemSettingsStore.snapshot
let tabIndexById = Dictionary(uniqueKeysWithValues: tabs.enumerated().map {
let tabIndexById = Dictionary(uniqueKeysWithValues: allOrdered.enumerated().map {

Copilot AI Apr 6, 2026

Copy link

Choose a reason for hiding this comment

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

tabIndexById is being computed from layout.allWorkspacesInOrder rather than tabManager.tabs. The index passed into TabItemView is used for numbered shortcut badges (via WorkspaceShortcutMapper) and for actions like Move Up/Down; those currently operate on tabManager.tabs order (e.g., keyboard selection by number selects tabs[targetIndex]). With sections, sidebar display order can diverge from tabs order, so this will show incorrect shortcut digits and can move/reorder the wrong workspace. Consider keeping index based on tabs ordering, or updating all index-based behaviors (shortcut mapping + move/reorder actions) to consistently use the sidebar layout order instead of tabs.

Suggested change
let tabIndexById = Dictionary(uniqueKeysWithValues: allOrdered.enumerated().map {
let tabIndexById = Dictionary(uniqueKeysWithValues: tabs.enumerated().map {

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Already addressed — tabIndexById is derived from tabManager.tabs.enumerated() (flat tabs order), so TabItemView.index stays aligned with shift-selection ranges and moveBy(_:). See the explanatory comment in ContentView.swift body of VerticalTabsSidebar.

Comment thread Sources/ContentView.swift Outdated
Comment on lines +13505 to +13539
if !tabManager.sections.isEmpty || true {
let sections = tabManager.sections
let currentSection = tabManager.sectionForWorkspace(tab.id)
Menu(String(localized: "contextMenu.moveToSection", defaultValue: "Move to Section")) {
Button(String(localized: "contextMenu.noSection", defaultValue: "No Section")) {
for id in targetIds {
tabManager.removeWorkspaceFromSection(tabId: id)
}
}
.disabled(currentSection == nil)

if !sections.isEmpty {
Divider()
}

ForEach(sections, id: \.id) { section in
Button(section.name) {
for id in targetIds {
tabManager.moveWorkspaceToSection(tabId: id, sectionId: section.id)
}
}
.disabled(currentSection?.id == section.id)
}

Divider()

Button(String(localized: "contextMenu.newSection", defaultValue: "New Section…")) {
let section = tabManager.createSection(
name: String(localized: "sidebar.newSectionDefaultName", defaultValue: "New Section")
)
for id in targetIds {
tabManager.moveWorkspaceToSection(tabId: id, sectionId: section.id)
}
}
}

Copilot AI Apr 6, 2026

Copy link

Choose a reason for hiding this comment

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

The condition if !tabManager.sections.isEmpty || true is always true, which makes this branch unconditional and likely leftover debug scaffolding. Remove the || true (or the whole if) so intent is clear and behavior matches the intended UX when there are no sections.

Suggested change
if !tabManager.sections.isEmpty || true {
let sections = tabManager.sections
let currentSection = tabManager.sectionForWorkspace(tab.id)
Menu(String(localized: "contextMenu.moveToSection", defaultValue: "Move to Section")) {
Button(String(localized: "contextMenu.noSection", defaultValue: "No Section")) {
for id in targetIds {
tabManager.removeWorkspaceFromSection(tabId: id)
}
}
.disabled(currentSection == nil)
if !sections.isEmpty {
Divider()
}
ForEach(sections, id: \.id) { section in
Button(section.name) {
for id in targetIds {
tabManager.moveWorkspaceToSection(tabId: id, sectionId: section.id)
}
}
.disabled(currentSection?.id == section.id)
}
Divider()
Button(String(localized: "contextMenu.newSection", defaultValue: "New Section…")) {
let section = tabManager.createSection(
name: String(localized: "sidebar.newSectionDefaultName", defaultValue: "New Section")
)
for id in targetIds {
tabManager.moveWorkspaceToSection(tabId: id, sectionId: section.id)
}
}
}
let sections = tabManager.sections
let currentSection = tabManager.sectionForWorkspace(tab.id)
Menu(String(localized: "contextMenu.moveToSection", defaultValue: "Move to Section")) {
Button(String(localized: "contextMenu.noSection", defaultValue: "No Section")) {
for id in targetIds {
tabManager.removeWorkspaceFromSection(tabId: id)
}
}
.disabled(currentSection == nil)
if !sections.isEmpty {
Divider()
}
ForEach(sections, id: \.id) { section in
Button(section.name) {
for id in targetIds {
tabManager.moveWorkspaceToSection(tabId: id, sectionId: section.id)
}
}
.disabled(currentSection?.id == section.id)
}
Divider()
Button(String(localized: "contextMenu.newSection", defaultValue: "New Section…")) {
let section = tabManager.createSection(
name: String(localized: "sidebar.newSectionDefaultName", defaultValue: "New Section")
)
for id in targetIds {
tabManager.moveWorkspaceToSection(tabId: id, sectionId: section.id)
}
}

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Already addressed — the || true scaffolding and the outer if !tabManager.sections.isEmpty guard were removed earlier on this branch.

Comment thread Sources/CmuxConfigExecutor.swift Outdated
Comment on lines +100 to +104
// Close all panels except the focused one so applyCustomLayout
// starts from a single pane and doesn't stack on existing splits.
let keep = current.focusedPanelId
for panelId in current.panels.keys where panelId != keep {
current.closePanel(panelId, force: true)

Copilot AI Apr 6, 2026

Copy link

Choose a reason for hiding this comment

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

This loop iterates current.panels.keys while calling current.closePanel(...), which can mutate current.panels (directly or via bonsplit delegate callbacks). Mutating a dictionary while iterating its keys can trap at runtime. Snapshot the panel IDs first (e.g., let panelIds = Array(current.panels.keys)) before closing, and also handle the case where focusedPanelId is nil (pick a stable panel to keep or skip the pruning) so you don’t accidentally close every panel before applying the layout.

Suggested change
// Close all panels except the focused one so applyCustomLayout
// starts from a single pane and doesn't stack on existing splits.
let keep = current.focusedPanelId
for panelId in current.panels.keys where panelId != keep {
current.closePanel(panelId, force: true)
// Close all panels except one preserved panel so applyCustomLayout
// starts from a single pane and doesn't stack on existing splits.
let panelIds = Array(current.panels.keys)
let keep = current.focusedPanelId ?? panelIds.first
if let keep {
for panelId in panelIds where panelId != keep {
current.closePanel(panelId, force: true)
}

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in f19a328 — current.panels.keys is now snapshotted via Array(current.panels.keys) before iterating, and keep falls back to the first panel key when focusedPanelId is nil so the workspace isn't torn down entirely.

Comment thread Sources/ContentView.swift Outdated
.onTapGesture {
guard !isEditing else { return }
section.toggleCollapsed()
tabManager.objectWillChange.send()

Copilot AI Apr 6, 2026

Copy link

Choose a reason for hiding this comment

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

SidebarSectionHeaderView manually calls tabManager.objectWillChange.send() after mutating section. Since section is an ObservableObject and TabManager already bumps sectionRevision by subscribing to each section’s objectWillChange, this manual objectWillChange is redundant and can cause extra invalidations. Prefer relying on the existing sectionRevision/observer pipeline (and remove the manual send) to keep update propagation consistent.

Suggested change
tabManager.objectWillChange.send()

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Already addressed — no tabManager.objectWillChange.send() calls remain in SidebarSectionHeaderView; section is itself ObservableObject and TabManager.sectionRevision bumps via notifySectionChange().

Comment thread Sources/SidebarSection.swift Outdated
@@ -0,0 +1,81 @@
import Combine
import SwiftUI

Copilot AI Apr 6, 2026

Copy link

Choose a reason for hiding this comment

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

SidebarSection.swift imports SwiftUI, but this file only uses Combine/Swift concurrency types and does not reference any SwiftUI symbols. Consider removing the unused import to reduce compile work and keep dependencies minimal.

Suggested change
import SwiftUI

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Already addressed — SidebarSection.swift no longer imports SwiftUI.

Comment thread Sources/ContentView.swift
Comment on lines +12377 to +12380
) {
section.toggleCollapsed()
tabManager.objectWillChange.send()
}

Copilot AI Apr 6, 2026

Copy link

Choose a reason for hiding this comment

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

In this context menu action, section.toggleCollapsed() should already propagate via the section observers/sectionRevision, so tabManager.objectWillChange.send() is likely unnecessary and adds extra invalidations.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Already addressed — removed earlier on this branch.

@greptile-apps

greptile-apps Bot commented Apr 6, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces user-defined collapsible sidebar sections with full CRUD, drag-and-drop, and session persistence, plus target: "current" workspace commands and autoApply that fires once per workspace per session on tab switch.

  • Sidebar sections: SidebarSection (new model) + TabManager.sections + sidebarLayout computed layout; sections survive restarts via new snapshot fields (SessionSidebarSectionSnapshot, workspace UUID persistence).
  • autoApply: Fires from both a tab-switch Combine observer (with 0.15 s delay and stale-tab guard) and synchronously from finishLoading on config reload (no stale-tab guard); the two paths share autoAppliedWorkspaceIds to avoid double-application.
  • target:current: Closes excess panes before applying a layout in-place; only renames when an explicit name is provided; skips silently when no workspace is selected.

Confidence Score: 5/5

Safe to merge; all core paths are well-guarded and the new session-persistence round-trip is backward-compatible.

The sidebar section CRUD, drag-and-drop membership tracking, and persistence round-trip are carefully implemented: workspace closure cleans up section membership in both close paths, reorderWorkspace correctly blocks flat-reordering grouped tabs, and autoAppliedWorkspaceIds prevents duplicate layout applications per session. The two issues flagged are narrow edge cases that do not affect the primary feature paths or data integrity.

Sources/CmuxConfig.swift and Sources/ContentView.swift have the two edge cases noted in the review comments.

Important Files Changed

Filename Overview
Sources/SidebarSection.swift New model for collapsible sidebar sections; clean @mainactor ObservableObject; addWorkspace guards against index out-of-bounds and deduplicates correctly.
Sources/TabManager.swift Adds sections array, sectionRevision ticker, rebindSectionObservers, and full CRUD API; reorderWorkspace correctly guards against flat-reordering grouped workspaces; cleanup hooks on close are wired in both code paths.
Sources/CmuxConfig.swift Adds autoApply field and checkAutoApply(); stale-tab guard present in tab-switch path but absent in finishLoading path; global-config cwd fallback correctly implemented.
Sources/CmuxConfigExecutor.swift target:current path closes excess panes safely (snapshots keys before iterating), respects optional rename, and returns early when no workspace is selected; resolvedCwd moved above early return correctly.
Sources/ContentView.swift Sidebar rendering restructured into pinned/ungrouped/sections; drop delegate extended for cross-section moves; "Move to Section" context menu shown for pinned workspaces even though sidebarLayout excludes pinned tabs from section groups (silent membership gap).
Sources/SessionPersistence.swift SessionSidebarSectionSnapshot added; workspace id field optional for backward compat; sections field optional on SessionTabManagerSnapshot.
Sources/Workspace.swift restoredId parameter added to init; snapshot serialises id; backward compat preserved via optional UUID.

Sequence Diagram

sequenceDiagram
    participant U as User
    participant TM as TabManager
    participant CS as CmuxConfigStore
    participant CE as CmuxConfigExecutor
    participant SS as SidebarSection

    U->>TM: switchTab(to: B)
    TM->>CS: $selectedTabId emits B
    CS->>CS: asyncAfter(0.15s) checkAutoApply(forTabId: B)
    TM->>TM: sidebarLayout recomputes (sectionRevision)

    Note over CS: Config file reload path
    CS->>CS: finishLoading()
    CS->>CS: checkAutoApply() [no forTabId guard]
    CS->>CE: execute(command, tabManager, baseCwd)
    CE->>TM: selectedWorkspace.closePanel (excess panes)
    CE->>TM: applyCustomLayout(layout)

    Note over U,SS: Sidebar section operations
    U->>TM: createSection(name:)
    TM->>SS: SidebarSection(id, name)
    TM->>TM: sections.append → rebindSectionObservers
    SS-->>TM: objectWillChange → sectionRevision++
    TM->>TM: "asyncAfter pendingRenameSectionId = section.id"

    U->>TM: moveWorkspaceToSection(tabId, sectionId)
    TM->>SS: removeWorkspace from all sections
    TM->>SS: addWorkspace(tabId, at: index)
    TM->>TM: notifySectionChange → sectionRevision++

    Note over TM: Session save
    TM->>TM: buildSnapshot() → sections → sectionSnapshots
    TM->>TM: restoreFromSnapshot() → restoredId → UUID reuse
Loading

Reviews (2): Last reviewed commit: "Add Active workspace filter to the sideb..." | Re-trigger Greptile

Comment thread Sources/ContentView.swift Outdated
}
.disabled(targetIds.isEmpty)

if !tabManager.sections.isEmpty || true {

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 Dead || true condition — debug code left in

The || true makes the guard always true, so the "Move to Section" submenu renders unconditionally regardless of whether any sections exist. Remove || true so the condition reflects the intended logic. If the intent is to always show the menu (to allow section creation from scratch), the if wrapper should be removed entirely. Was the intent to always show the "Move to Section" submenu (including when no sections exist) so that "New Section…" is always accessible? If so, the entire if guard should be dropped rather than keeping || true.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Already addressed — the || true and outer guard are gone.

Comment thread Sources/ContentView.swift Outdated
Comment on lines +15074 to +15077
let insertAfter = dropIndicator?.edge == .bottom
if let targetIdx = section.workspaceIds.firstIndex(of: targetTabId) {
let insertIdx = insertAfter ? targetIdx + 1 : targetIdx
section.addWorkspace(draggedTabId, at: insertIdx)

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 Intra-section drag off-by-one when dragging forward

insertIdx is computed from the pre-removal array, but addWorkspace removes the dragged item before inserting at that index. When the dragged item sits at a lower index than the target, removal shifts all subsequent indices left by 1, so the item lands one slot too far.

Example: [A, B, C, D], drag A after C (insertAfter=true): targetIdx=2, insertIdx=3. After removeAll(A) → [B, C, D], insert at 3 → [B, C, D, A]. Expected [B, C, A, D].

Suggested change
let insertAfter = dropIndicator?.edge == .bottom
if let targetIdx = section.workspaceIds.firstIndex(of: targetTabId) {
let insertIdx = insertAfter ? targetIdx + 1 : targetIdx
section.addWorkspace(draggedTabId, at: insertIdx)
let insertAfter = dropIndicator?.edge == .bottom
if let targetIdx = section.workspaceIds.firstIndex(of: targetTabId),
let draggedIdx = section.workspaceIds.firstIndex(of: draggedTabId) {
let rawInsert = insertAfter ? targetIdx + 1 : targetIdx
// After removeAll(draggedTabId), every index > draggedIdx shifts -1
let adjustedInsert = rawInsert - (draggedIdx < rawInsert ? 1 : 0)
section.addWorkspace(draggedTabId, at: adjustedInsert)
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Checked — the current code removes draggedTabId from section.workspaceIds before computing targetIdx, so targetIdx is relative to the already-reduced array. Forward and backward same-section drops both land at the correct index.

Comment thread Sources/CmuxConfig.swift Outdated
Comment on lines +422 to +427
private func checkAutoApply() {
guard let tabManager = trackedTabManager,
let workspace = tabManager.selectedWorkspace,
!autoAppliedWorkspaceIds.contains(workspace.id),
let baseCwd = localConfigPath.map({ ($0 as NSString).deletingLastPathComponent })
else { return }

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 autoApply silently skips global-config commands

The guard requires localConfigPath to be non-nil, so if a user defines autoApply: true with target: "current" in their global config and navigates to a directory with no local config, checkAutoApply returns early without explanation. loadedCommands already contains the global command, but baseCwd resolution blocks it. Consider falling back to the global config's parent directory when localConfigPath is nil, or documenting that autoApply only works when a local config is present.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Already addressed — checkAutoApply falls back to the globalConfigPath's parent directory when localConfigPath is nil (CmuxConfig.swift:441-444).

Comment thread Sources/ContentView.swift Outdated
Comment on lines +9994 to +9995
// Read sectionRevision to trigger re-render when any section changes.
let _ = tabManager.sectionRevision

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 Redundant sectionRevision read

tabManager.sidebarLayout already reads sectionRevision internally (let _ = sectionRevision inside the computed property body), so SwiftUI's dependency tracking is already established by the let layout = tabManager.sidebarLayout call on the next line. The explicit read on line 9995 creates a duplicate dependency and can be removed.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in f19a328 — the separate sectionRevision read is removed; sidebarLayout already establishes the dependency via let _ = sectionRevision.

@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: 9

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

13-56: Remove the redundant revision property and bumpRevision() calls.

SidebarSection.revision is never read anywhere in the codebase. Every mutation already changes at least one other @Published property (name, isCollapsed, or workspaceIds), which triggers objectWillChange. The TabManager listens to that signal and bumps sectionRevision independently. The extra @Published revision path adds no functional value and creates a second publish for the same event.

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

In `@Sources/SidebarSection.swift` around lines 13 - 56, Remove the redundant
`@Published` property `revision` and the helper `bumpRevision()` method and delete
all calls to `bumpRevision()` inside `removeWorkspace`, `addWorkspace(at:)`,
`setCollapsed(_:)`, and `toggleCollapsed()`; ensure the initializer and any
comments no longer reference `revision` so the class relies solely on the
existing `@Published` properties (`name`, `isCollapsed`, `workspaceIds`) for
change notifications and leave `contains(_:)` untouched.
🤖 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/CmuxConfig.swift`:
- Around line 333-340: The sink currently discards the emitted selectedTabId and
re-reads selectedWorkspace later, which can apply to the wrong tab after a quick
switch; capture the emitted id in the sink closure (from
tabManager.$selectedTabId.sink { [weak self] tabId in ... }) and pass that
captured tabId into the delayed work (or call a new method like
checkAutoApply(for: tabId)); in the asyncAfter closure verify the current
selectedTabId or selectedWorkspace still corresponds to the captured tabId
before calling checkAutoApply (or have checkAutoApply(for:) only apply if ids
match) so the delayed apply is bound to the originating tab.

In `@Sources/CmuxConfigExecutor.swift`:
- Around line 94-109: The code treats wsDef.target == .current as a no-op when
tabManager.selectedWorkspace is nil and falls through to the name-based path,
risking mutating the wrong workspace; update the early branch that checks `if
wsDef.target == .current, let current = tabManager.selectedWorkspace { ... }` to
instead fail closed: if `wsDef.target == .current` and
`tabManager.selectedWorkspace` is nil, return early (do not proceed to
recreate/create by name) and log or surface an error; ensure this behavior is
applied where `wsDef.target`, `tabManager.selectedWorkspace`, and the `current`
usage (e.g., `current.setCustomTitle`, `current.setCustomColor`,
`current.applyCustomLayout`) are handled so `.current` never falls through to
the name-based path.
- Around line 99-106: The code is force-closing panels unconditionally
(current.closePanel(panelId, force: true)) before calling
current.applyCustomLayout(layout, baseCwd: resolvedCwd), which can kill
restored/running panes when this path is used by autoApply; change the logic to
avoid silent force-closes by either using non-forcing close (force: false or the
default closePanel call) or only closing panels that are safe (e.g., check panel
state via current.panels[panelId]?.isIdle or similar) and skip panels with
running processes or unsaved state, then proceed to call
applyCustomLayout(layout, baseCwd: resolvedCwd).

In `@Sources/ContentView.swift`:
- Around line 13508-13527: The "No Section" and per-section Button disabled
states currently use currentSection (reflecting only the clicked row) instead of
the whole selection; update their .disabled conditions to compute across
targetIds: disable "No Section" only if every targetId already has no section,
and disable a given section Button only if every targetId is already in that
section (use your model/tabManager lookup for each target's section id and check
allSatisfy over targetIds); keep the existing actions
(tabManager.removeWorkspaceFromSection and tabManager.moveWorkspaceToSection)
unchanged.
- Around line 15069-15087: The current drop logic only handles same-section
reorders and otherwise calls tabManager.reorderWorkspace, which fails to update
section membership and computes insert positions from the pre-removal array
(causing downward moves to overshoot); replace both branches with a single
section-aware move operation: locate source section via
tabManager.sectionForWorkspace(draggedTabId) and target section via
tabManager.sectionForWorkspace(targetTabId) (or nil for ungrouped), remove the
draggedTabId from its source section.workspaceIds first, compute the normalized
insertIdx based on the post-removal target index (accounting for bottom-edge
inserts via dropIndicator?.edge), then insert via
section.addWorkspace(draggedTabId, at: insertIdx) for the destination section
(or call the ungrouped insertion equivalent) so cross-section moves update
membership and same-section downward moves use the corrected index; keep debug
dlog calls intact.
- Around line 9956-9963: TabItemView.index must remain the flat-tab index used
by shift-selection and moveBy(_:), but the code is currently passing
sidebarLayout.allWorkspacesInOrder indices; change the call sites that pass
index (including the occurrences around TabItemView initializers and row actions
referenced) to provide two indices: keep TabItemView.index as the flat index
from tabManager.tabs and add a new sidebarDisplayIndex (or similarly named
parameter) computed from sidebarLayout.allWorkspacesInOrder for rendering only;
alternatively update row actions (shift-selection handling and moveBy(_:)) to
use the sidebar-order model, but do not repurpose TabItemView.index—use explicit
separate symbols so shift-selection and moveBy(_:) continue to operate against
tabManager.tabs while the UI renders using the sidebar order.

In `@Sources/SessionPersistence.swift`:
- Line 331: SessionWorkspaceSnapshot currently declares var id: UUID? which
allows nil and breaks workspace identity guarantees; change the model so id is a
non-optional UUID on SessionWorkspaceSnapshot, remove any default UUID
generation inside the type, and update all initializers/constructors and Codable
decoding (e.g., init(from:)) to require and validate a UUID on decode; handle
legacy/migration cases only at the persistence/migration boundary (e.g.,
SessionPersistence load/migrate routines) by supplying or deriving a UUID there
before constructing a SessionWorkspaceSnapshot, and update all call sites that
create SessionWorkspaceSnapshot to pass the live workspace id.

In `@Sources/TabManager.swift`:
- Around line 2737-2745: In moveWorkspaceToSection, validate that the
destination section exists before mutating current membership: first find let
destination = sections.first(where: { $0.id == sectionId }) and return early if
nil, then remove the tabId from other sections (call
section.removeWorkspace(tabId)) and finally call destination.addWorkspace(tabId,
at: atIndex) and notifySectionChange(); this prevents silently ungrouping a
workspace when the target section is stale or deleted.
- Around line 2758-2784: sidebarLayout now treats section.workspaceIds as the
source-of-truth for grouped order, so the flat reorder API must not mutate tabs
for sectioned workspaces; update the reorderWorkspace(tabId:toIndex:)
implementation to check sectionForWorkspace(tabId) (or equivalent) and
immediately return false (use `@discardableResult` Bool) when the workspace is in
a section, leaving grouped reorders to the group-aware APIs and preventing UI
snap-back on recompute.

---

Nitpick comments:
In `@Sources/SidebarSection.swift`:
- Around line 13-56: Remove the redundant `@Published` property `revision` and the
helper `bumpRevision()` method and delete all calls to `bumpRevision()` inside
`removeWorkspace`, `addWorkspace(at:)`, `setCollapsed(_:)`, and
`toggleCollapsed()`; ensure the initializer and any comments no longer reference
`revision` so the class relies solely on the existing `@Published` properties
(`name`, `isCollapsed`, `workspaceIds`) for change notifications and leave
`contains(_:)` untouched.
🪄 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: 4faae21d-7b40-4e9e-84e3-500408d5d64d

📥 Commits

Reviewing files that changed from the base of the PR and between 179b16c and 27ddeee.

📒 Files selected for processing (8)
  • GhosttyTabs.xcodeproj/project.pbxproj
  • Sources/CmuxConfig.swift
  • Sources/CmuxConfigExecutor.swift
  • Sources/ContentView.swift
  • Sources/SessionPersistence.swift
  • Sources/SidebarSection.swift
  • Sources/TabManager.swift
  • Sources/Workspace.swift

Comment thread Sources/CmuxConfig.swift Outdated
Comment thread Sources/CmuxConfigExecutor.swift Outdated
Comment on lines +99 to +106
if let layout = wsDef.layout {
// Close all panels except the focused one so applyCustomLayout
// starts from a single pane and doesn't stack on existing splits.
let keep = current.focusedPanelId
for panelId in current.panels.keys where panelId != keep {
current.closePanel(panelId, force: true)
}
current.applyCustomLayout(layout, baseCwd: resolvedCwd)

@coderabbitai coderabbitai Bot Apr 6, 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.

⚠️ Potential issue | 🟠 Major

Don’t silently force-close existing panels before auto-applying a layout.

This path is also used by autoApply, so force: true can tear down restored panes, kill running terminals, or discard editor/browser state on tab switch with no user intervention.

Safer direction
             if let layout = wsDef.layout {
                 // Close all panels except the focused one so applyCustomLayout
                 // starts from a single pane and doesn't stack on existing splits.
                 let keep = current.focusedPanelId
-                for panelId in current.panels.keys where panelId != keep {
-                    current.closePanel(panelId, force: true)
+                let panelIdsToClose = current.panels.keys.filter { $0 != keep }
+                for panelId in panelIdsToClose {
+                    guard current.closePanel(panelId, force: command.autoApply != true) else { return }
                 }
                 current.applyCustomLayout(layout, baseCwd: resolvedCwd)
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/CmuxConfigExecutor.swift` around lines 99 - 106, The code is
force-closing panels unconditionally (current.closePanel(panelId, force: true))
before calling current.applyCustomLayout(layout, baseCwd: resolvedCwd), which
can kill restored/running panes when this path is used by autoApply; change the
logic to avoid silent force-closes by either using non-forcing close (force:
false or the default closePanel call) or only closing panels that are safe
(e.g., check panel state via current.panels[panelId]?.isIdle or similar) and
skip panels with running processes or unsaved state, then proceed to call
applyCustomLayout(layout, baseCwd: resolvedCwd).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Keeping force: true intentional for target: .current. This path is meant to replace the existing layout; a soft close would leave detached panes that applyCustomLayout can't reconcile. If we need a non-destructive variant for autoApply, we'll opt in explicitly rather than flipping the default.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@rodchristiansen, understood — appreciated the clear rationale. force: true as the default makes sense when target: .current is explicitly requesting a full layout replacement, and soft-closing would leave orphaned panes in an irreconcilable state for applyCustomLayout. I'll defer any non-destructive opt-in to an explicit future flag if that need arises.


✏️ Learnings added
Learnt from: rodchristiansen
Repo: manaflow-ai/cmux PR: 2646
File: Sources/CmuxConfigExecutor.swift:107-118
Timestamp: 2026-04-14T19:56:39.598Z
Learning: Repo: manaflow-ai/cmux — In Sources/CmuxConfigExecutor.swift, executeWorkspaceCommand for wsDef.target == .current intentionally uses closePanel(panelId, force: true) before calling applyCustomLayout. This is by design: soft-closing (force: false) leaves detached panes that applyCustomLayout cannot reconcile. If a non-destructive autoApply variant is needed in the future, it should be opted into explicitly via a separate flag rather than changing the default force: true behavior.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: qkrwpdlr
Repo: manaflow-ai/cmux PR: 0
File: :0-0
Timestamp: 2026-03-23T07:01:33.134Z
Learning: Repo: manaflow-ai/cmux — In Sources/TerminalController.swift, sendPickedElementToTerminal(workspaceId:summary:) resolves the target workspace via AppDelegate.shared?.workspaceFor(tabId: workspaceId), which searches across all mainWindowContexts (not self.tabManager which is active-window only). It first injects text into the focused terminal panel if it's a terminal, then falls back to iterating all panels in the workspace.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2475
File: Sources/ContentView.swift:12775-12779
Timestamp: 2026-04-03T07:19:43.478Z
Learning: Repo: manaflow-ai/cmux — Sources/Workspace.swift — Workspace.setCustomDescription(_:) calls normalizedCustomDescription(...) which converts CRLF/CR to "\n", trims whitespace/newlines, and returns nil for empty/whitespace-only input. Therefore UI sites (e.g., TabItemView in Sources/ContentView.swift) can rely on workspace.customDescription being nil when blank and should not add extra trimmed-string checks before rendering.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2398
File: CLI/cmux.swift:0-0
Timestamp: 2026-04-01T09:50:23.728Z
Learning: Repo: manaflow-ai/cmux — In CLI/cmux.swift within CMUXCLI.buildInteractiveRemoteShellScript(...), never export CMUX_TAB_ID from the workspace UUID. CMUX_TAB_ID must be surface-scoped: only set it when a surface ID is available (map CMUX_TAB_ID to CMUX_SURFACE_ID). Rationale: tab-action/rename-tab resolve CMUX_TAB_ID before CMUX_SURFACE_ID; workspace-scoped values misroute or fail.

Learnt from: thunter009
Repo: manaflow-ai/cmux PR: 1825
File: Sources/TerminalController.swift:3607-3645
Timestamp: 2026-03-25T00:32:48.115Z
Learning: Repo: manaflow-ai/cmux — In Sources/TerminalController.swift, v2WorkspaceSetColor(params:) must validate the provided color with WorkspaceTabColorSettings.normalizedHex. If the color parameter is present but invalid, return invalid_params (no fallback). On success, apply the color and return the normalized (uppercase) `#RRGGBB` in the response payload.

Learnt from: Znboston
Repo: manaflow-ai/cmux PR: 0
File: :0-0
Timestamp: 2026-03-27T17:03:24.720Z
Learning: Repo: manaflow-ai/cmux — Sources/TabManager.swift — `reorderWorkspace(...)` must reject grouped tabs (early return). Flat-array reorders are not allowed for workspaces inside a group; use the group-aware reorder APIs instead.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2034
File: Sources/AppDelegate.swift:0-0
Timestamp: 2026-03-25T07:14:56.211Z
Learning: Repo: manaflow-ai/cmux — Sources/AppDelegate.swift — Pattern for “Open Folder”: In AppDelegate.showOpenFolderPanel(), seed NSOpenPanel.directoryURL using preferredMainWindowContextForWorkspaceCreation(debugSource: …) rather than NSApp.keyWindow, and on selection delegate to openWorkspaceForExternalDirectory(workingDirectory:…, debugSource: …). Rationale: handles auxiliary-key-window cases, ensures shouldBringToFront = true, and unifies menu/shortcut behavior with a consistent fallback to createMainWindow when workspace creation returns nil.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2662
File: Sources/TabManager.swift:1994-1995
Timestamp: 2026-04-07T05:27:49.028Z
Learning: Repo: manaflow-ai/cmux — Sources/TabManager.swift — In applyWorkspacePullRequestRefreshResults(...), resolution .unsupportedRepository must (1) clear any existing workspace.panelPullRequests[panelId] entry and (2) reset lastTerminalState timestamp. scheduleNextWorkspacePullRequestPoll(...) sets nextPollAt to a jittered background interval for .unsupportedRepository, avoiding hot-looping `git remote -v`. Intent: when a panel moves to a non-GitHub or otherwise unsupported repo, clear the sidebar PR badge and back off polling.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2864
File: Sources/Workspace.swift:9892-9894
Timestamp: 2026-04-13T23:52:36.538Z
Learning: Repo: manaflow-ai/cmux — EditorPanel: A new method EditorPanel.updateWorkspaceId(_:) exists. In Sources/Workspace.swift, Workspace.attachDetachedSurface(_:inPane:atIndex:focus:) now calls editorPanel.updateWorkspaceId(self.id) when reattaching an EditorPanel, then reinstalls Combine observers via installEditorPanelSubscription(_:). This mirrors the existing TerminalPanel/BrowserPanel reattach pattern to keep workspace-scoped state accurate after moves.

Learnt from: thunter009
Repo: manaflow-ai/cmux PR: 1825
File: CLI/cmux.swift:1958-1960
Timestamp: 2026-03-25T00:33:00.675Z
Learning: Repo: manaflow-ai/cmux — CLI/cmux.swift set-workspace-color intentionally accepts both "#RRGGBB" and "RRGGBB" for ergonomics, matching server-side WorkspaceTabColorSettings.normalizedHex/TabManager normalization. Do not suggest enforcing a mandatory leading '#'; at most, suggest clarifying help/usage text.

Learnt from: arieltobiana
Repo: manaflow-ai/cmux PR: 1873
File: Sources/TerminalController.swift:4071-4087
Timestamp: 2026-03-20T17:18:30.333Z
Learning: Repo: manaflow-ai/cmux — In Sources/TerminalController.swift, v2WorkspaceAction(params:) -> case "set_color": palette names are resolved via WorkspaceTabColorSettings.defaultPaletteWithOverrides(), whose entries are always valid hex (validated by the UI). Therefore, additional normalization of entry.hex is unnecessary.

Learnt from: MaTriXy
Repo: manaflow-ai/cmux PR: 1460
File: Sources/TerminalController.swift:0-0
Timestamp: 2026-03-16T08:02:06.824Z
Learning: In Swift sources, for any panel_id-only route handling in v2PanelMarkBackground(params:) and v2PanelMarkForeground(params:), first attempt v2ResolveTabManager(params:). Use the manager only if it actually owns the panelId; otherwise fall back to AppDelegate.shared?.locateSurface(surfaceId:) to locate the correct TabManager across windows. Apply this pattern to all panel_id-only routes to avoid active-window bias.

Learnt from: Znboston
Repo: manaflow-ai/cmux PR: 0
File: :0-0
Timestamp: 2026-03-26T19:00:12.434Z
Learning: Repo: manaflow-ai/cmux — Sources/TabManager.swift — In `moveWorkspaceOutOfGroup(_:toSidebarIndex:)`, do NOT call `syncUngroupedSidebarOrder()` after inserting into `sidebarOrder`. That helper derives order from the `tabs` array insertion index, which is unrelated to the user's drag-drop target position. `sidebarOrder` is the source of truth for sidebar rendering; the raw insertion at the resolved sidebar index is correct and sufficient. Calling `syncUngroupedSidebarOrder()` would overwrite the intended drop position with stale `tabs` order, causing a snap-back regression.

Learnt from: tayl0r
Repo: manaflow-ai/cmux PR: 1909
File: Sources/ContentView.swift:2156-2171
Timestamp: 2026-03-23T06:08:14.740Z
Learning: Repo: manaflow-ai/cmux — In Sources/ContentView.swift, openFileInTextEditor(_:) must attempt workspace.newTextEditorSplit(from:orientation:filePath:focus:) and, if that returns nil, fall back to workspace.newTextEditorSurface(inPane:filePath:focus:) using bonsplitController.focusedPaneId. Rationale: avoid dropping file-open requests when the focused panel is not pane-backed.

Learnt from: mrosnerr
Repo: manaflow-ai/cmux PR: 0
File: :0-0
Timestamp: 2026-04-05T21:26:10.710Z
Learning: Repo: manaflow-ai/cmux — In Sources/Workspace.swift, applySessionPanelMetadata() must gate listeningPorts restoration on the per-panel snapshot.terminal?.isRemoteBacked flag (not workspace-wide remoteTerminalStartupCommand()), so that local panels are always eligible for port restore regardless of current SSH state. Fixed in commit 4d0fd871 (PR `#2545`).

Learnt from: jt-hsiao
Repo: manaflow-ai/cmux PR: 1423
File: Sources/AppDelegate.swift:11220-11226
Timestamp: 2026-03-14T07:06:01.466Z
Learning: Repo: manaflow-ai/cmux — In Sources/Panels/CmuxWebView.swift, performKeyEquivalent(with:) handles Command-key routing end-to-end: (1) if allowed, route to NSApp.mainMenu.performKeyEquivalent; (2) fall back to AppDelegate.shared?.handleBrowserSurfaceKeyEquivalent(event) for non–menu-backed app shortcuts; (3) fall back to super.performKeyEquivalent. For non-Command keys it calls super directly. Therefore, in NSWindow.cmux_performKeyEquivalent (Sources/AppDelegate.swift), it is correct to call firstResponderWebView.performKeyEquivalent and return its Bool unconditionally to avoid re-entering SwiftUI’s performKeyEquivalent path that can swallow keys after WKWebView focus.

Learnt from: SuperManfred
Repo: manaflow-ai/cmux PR: 1150
File: Sources/AppDelegate.swift:7394-7401
Timestamp: 2026-03-10T10:24:14.017Z
Learning: Repo: manaflow-ai/cmux — For Cmd+W behavior with browser popups: BrowserPopupPanel.performKeyEquivalent is the primary handler to close popups. AppDelegate.handleCustomShortcut(_:), in its Cmd+W fallback, must check both NSApp.keyWindow and event.window for identifier "cmux.browser-popup" and close it if found, before routing to workspace/settings close logic.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2398
File: Sources/Workspace.swift:4965-4988
Timestamp: 2026-04-01T09:51:45.737Z
Learning: Repo: manaflow-ai/cmux — In Sources/Workspace.swift, WorkspaceRemoteSessionController.updateRemotePortScanTTYsLocked(_:) must drop remoteScannedPortsByPanel entries when a panel’s TTY changes (compare previousTTYNames vs nextTTYNames). It should also use keepPolledRemotePortsUntilTTYScan to retain the last host-wide polling snapshot only until the first TTY-scoped scan completes, and clear it when no TTYs are tracked.

Learnt from: homanp
Repo: manaflow-ai/cmux PR: 883
File: CLI/cmux.swift:1569-1586
Timestamp: 2026-03-04T22:05:07.913Z
Learning: In manaflow-ai/cmux CLI (CLI/cmux.swift), for parity with existing browser commands, markdown.open intentionally normalizes --surface before resolving --workspace/--window, relying on server-side resolution to disambiguate final routing. Avoid one-off reordering in markdown; consider any change only as a coordinated, cross-command refactor.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2475
File: CLI/cmux.swift:1710-1712
Timestamp: 2026-04-03T07:19:50.892Z
Learning: Repo manaflow-ai/cmux — Server path: TerminalController workspace.create applies Workspace.setCustomDescription(_:) which trims/normalizes and treats whitespace-only descriptions as nil. Therefore, CLI new-workspace can safely forward raw --description; whitespace-only input will not persist. No client-side error is required for this case.

Learnt from: qkrwpdlr
Repo: manaflow-ai/cmux PR: 0
File: :0-0
Timestamp: 2026-03-23T07:12:42.553Z
Learning: Repo: manaflow-ai/cmux — In Sources/Panels/BrowserPanel.swift, BrowserPanel.updateWorkspaceId(_:) is a dedicated method (var workspaceId) that updates both BrowserPanel.workspaceId and pickerMessageHandler?.updateWorkspaceId(_:) atomically. It is called from BrowserPanel.reattachToWorkspace(_:) when a panel moves between workspaces. This ensures BrowserPickerMessageHandler always posts notifications with the current workspaceId, not a stale one from panel initialization.

Learnt from: atani
Repo: manaflow-ai/cmux PR: 819
File: Sources/AppDelegate.swift:3491-3493
Timestamp: 2026-03-04T14:06:16.241Z
Learning: For manaflow-ai/cmux PR `#819` (Japanese i18n), keep scope limited to localization changes; UX enhancements like preferring workspace.customTitle in workspaceDisplayName() or altering move-target labels should be handled in a separate follow-up issue.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2528
File: Sources/cmuxApp.swift:776-777
Timestamp: 2026-04-03T03:35:40.100Z
Learning: Repo: manaflow-ai/cmux — As of PR `#2528`, workspace digit mapping (Cmd+1–9 and sidebar digits) intentionally uses TabManager.tabs as the canonical ordering for both sidebar rendering and app shortcut handling. Do not suggest switching to orderedSidebarWorkspaceIds() unless the model changes to a sidebar-ordered, group/collapse-aware source of truth.

Learnt from: atani
Repo: manaflow-ai/cmux PR: 819
File: Sources/AppDelegate.swift:0-0
Timestamp: 2026-03-04T14:05:42.574Z
Learning: Guideline: In Swift files (cmux project), when handling pluralized strings, prefer using localization keys with the ICU-style plural forms .one and .other. For example, use keys like statusMenu.unreadCount.one for the singular case (1) and statusMenu.unreadCount.other for all other counts, and similarly for statusMenu.tooltip.unread.one/other. Rationale: ensures correct pluralization across locales and makes localization keys explicit. Review code to ensure any unread count strings and related tooltips follow this .one/.other key pattern and verify the correct value is chosen based on the count.

Learnt from: austinywang
Repo: manaflow-ai/cmux PR: 954
File: Sources/TerminalController.swift:0-0
Timestamp: 2026-03-05T22:04:34.712Z
Learning: Adopt the convention: for health/telemetry tri-state values in Swift, prefer Optionals (Bool?) over sentinel booleans. In TerminalController.swift, socketConnectable is Bool? and only set when socketProbePerformed is true; downstream logic must treat nil as 'not probed'. Ensure downstream code checks for nil before using a value and uses explicit non-nil checks to determine state, improving clarity and avoiding misinterpretation of default false.

Learnt from: moyashin63
Repo: manaflow-ai/cmux PR: 1074
File: Sources/AppDelegate.swift:7523-7545
Timestamp: 2026-03-09T01:38:24.337Z
Learning: When the command palette is visible (as in manaflow-ai/cmux Sources/AppDelegate.swift), ensure the shortcut handling consumes most Command shortcuts to protect the palette's text input. Specifically, do not allow UI zoom shortcuts (Cmd+Shift+= / Cmd+Shift+− / Cmd+Shift+0) to trigger while the palette is open. Do not reorder shortcut handlers (e.g., uiZoomShortcutAction(...)) to bypass this guard; users must close the palette before performing zoom actions. This guideline should apply to Swift source files handling global shortcuts within the app.

Learnt from: zlatkoc
Repo: manaflow-ai/cmux PR: 1368
File: Sources/Panels/BrowserPanel.swift:69-69
Timestamp: 2026-03-13T13:46:01.733Z
Learning: Do not wrap engine/brand name literals (e.g., displayName values such as Google, DuckDuckGo, Bing, Kagi, Startpage) in String(localized: ...). These are brand/product names that are not translatable UI text. Localization should apply to generic UI strings (labels, buttons, error messages, etc.). Apply this guideline across Swift source files under Sources/ (notably in BrowserPanel.swift and similar UI/engine-related strings) and flag only brand-name strings that are part of user-facing UI text appropriately for translation scope.

Learnt from: kjb0787
Repo: manaflow-ai/cmux PR: 1461
File: Sources/GhosttyTerminalView.swift:5904-5905
Timestamp: 2026-03-15T19:22:32.330Z
Learning: In Swift files under the Sources directory that manage terminal/scroll behavior, ensure the following: when preserving scroll across workspace switches, save savedScrollRow only if the scrollbar offset is greater than 0 (indicating the user has scrolled up). On restore, call scroll_to_row only if savedScrollRow is non-nil; if it is nil, rely on synchronizeScrollView() to keep bottom-pinned sessions following new output. This pattern should be applied wherever GhosttyTerminalView-like views implement setVisibleInUI(_:) to maintain consistent user scroll state across workspace switches.

Learnt from: pratikpakhale
Repo: manaflow-ai/cmux PR: 2011
File: Resources/Localizable.xcstrings:15256-15368
Timestamp: 2026-03-23T21:39:50.795Z
Learning: When reviewing this repo’s Swift localization usage, do not flag missing `String.localizedStringWithFormat` for calls that use the modern overload `String(localized: "key", defaultValue: "...\(variable)")` (where `defaultValue` is a `String.LocalizationValue` built with `\(…)`). That overload natively supports interpolation and the xcstrings/runtime substitution handles the resulting placeholders automatically. Only require `String.localizedStringWithFormat` when using the older `String(localized:)` overload that takes a plain `String` (i.e., where format arguments must be passed separately), such as for keys like `clipboard.sshError.single`.

Learnt from: thunter009
Repo: manaflow-ai/cmux PR: 1825
File: Sources/TerminalController.swift:3620-3622
Timestamp: 2026-03-25T00:32:54.735Z
Learning: When validating or reporting workspace/tab colors in this repo, only accept and use 6-digit hex colors in the form `#RRGGBB` (no alpha, i.e., do not allow `#RRGGBBAA`). Ensure validation logic matches the existing behavior (e.g., WorkspaceTabColorSettings.normalizedHex(...) and TabManager.setTabColor(tabId:color:) as well as CLI/cmux.swift). Update any error/help text for workspace color to reference only `#RRGGBB` (not `#RRGGBBAA`).

Learnt from: mrosnerr
Repo: manaflow-ai/cmux PR: 2545
File: Sources/GhosttyTerminalView.swift:3891-3903
Timestamp: 2026-04-02T21:37:21.463Z
Learning: In Swift source files like Sources/GhosttyTerminalView.swift, avoid logging raw startup commands or initialInput even in DEBUG (to prevent leaking sensitive paths/tokens and multiline content). If you need to diagnose startup/input, log only non-sensitive metadata such as (1) presence flags (e.g., hasStartupCommand/hasInitialInput), (2) byte counts, and (3) the relevant surface id (so issues can be correlated without exposing the underlying strings).

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2528
File: Sources/cmuxApp.swift:6439-6444
Timestamp: 2026-04-03T03:35:54.082Z
Learning: In this repo’s keyboard shortcut implementation, ensure `KeyboardShortcutSettings.setShortcut(...)` does nothing (no-op) when `KeyboardShortcutSettings.isManagedBySettingsFile(action)` returns `true` (i.e., the shortcut is managed via `settings.json`). This prevents writing back overrides into `UserDefaults` and keeps `settings.json` as the source of truth.

Comment thread Sources/ContentView.swift
Comment thread Sources/ContentView.swift Outdated
Comment thread Sources/ContentView.swift Outdated
Comment thread Sources/SessionPersistence.swift
Comment thread Sources/TabManager.swift
Comment thread Sources/TabManager.swift
- Remove || true debug code left in section menu guard
- Fix intra-section drag off-by-one: remove before insert so indices
  are stable when dragging forward
- Fix tabIndexById to use flat tabs order for shift-selection and
  shortcut badges (not section order)
- Fix dictionary mutation during iteration in panel close loop
- Make target:current fail closed when no workspace is selected
- Only auto-apply to single-pane workspaces to avoid tearing down
  restored or user-customized split configurations
- Validate destination section before removing workspace from all
  sections in moveWorkspaceToSection
- Add backwards-compat comment for optional SessionWorkspaceSnapshot.id
- Replace SwiftUI import with Foundation in SidebarSection (only needs
  Combine + Foundation)
- Remove redundant revision property and bumpRevision() calls from
  SidebarSection (mutations already trigger objectWillChange)
- Remove redundant tabManager.objectWillChange.send() in section header
- Remove redundant sectionRevision read in sidebar body (sidebarLayout
  already reads it internally, establishing the SwiftUI dependency)
- Fix autoApply silently skipping global-config commands: fall back to
  global config directory when no local config is present
- Fix auto-apply race condition on rapid tab switching: capture the
  emitted tab ID and verify it's still selected before applying, so a
  quick B→C switch doesn't apply B's config to C
Comment thread Sources/ContentView.swift
Comment thread Sources/ContentView.swift Outdated
- Move "New Section…" out from under the `!sections.isEmpty` guard so the
  context menu entry is reachable when no sections exist yet. The whole
  "Move to Section" submenu now always shows.
- Compute "Move to Section" disabled states from all selected target IDs
  instead of only the primary `tab.id`, so multi-selection menus correctly
  reflect the full selection.
- Funnel sidebar drops through a single section-aware path. Previously
  cross-section and ungrouped↔section drops fell back to a flat reorder
  and membership never updated. Now a drop onto any section target routes
  through `moveWorkspaceToSection`, and drops onto ungrouped rows first
  lift the workspace out of its source section.
- `TabManager.reorderWorkspace` now early-returns for grouped workspaces.
  Grouped ordering lives in the section's `workspaceIds`, so a flat
  reorder here was a silent no-op that looked like the drag snapped back
  on the next `sidebarLayout` recompute.
- `CmuxConfigExecutor`: snapshot `panels.keys` into an `Array` before
  iterating (closePanel mutates the dictionary), and fall back to the
  first panel key when `focusedPanelId` is nil instead of tearing every
  panel down.
- Remove unused `allOrdered` local that was left behind after switching
  to flat-tab indexing.
Comment thread Sources/ContentView.swift Outdated
Comment thread Sources/SidebarSection.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.

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/TabManager.swift (1)

2604-2616: ⚠️ Potential issue | 🟠 Major

Don't treat pinned section members as grouped for flat reorders.

sidebarLayout renders pinned workspaces from tabs, even if their UUID still lives in section.workspaceIds. The unconditional sectionForWorkspace(tabId) check makes a workspace that was pinned after being sectioned impossible to reorder among pinned tabs.

Suggested fix
 `@discardableResult`
 func reorderWorkspace(tabId: UUID, toIndex targetIndex: Int) -> Bool {
     guard let currentIndex = tabs.firstIndex(where: { $0.id == tabId }) else { return false }
     if tabs.count <= 1 { return true }
+    let workspace = tabs[currentIndex]
 
     // Grouped workspaces are ordered by their section's `workspaceIds`, not by
     // the flat `tabs` array. Reordering `tabs` for a grouped workspace would
     // silently no-op on the next `sidebarLayout` recompute and the UI would
     // snap back. Callers must route grouped drops through the section API.
-    if sectionForWorkspace(tabId) != nil { return false }
-
-    let workspace = tabs[currentIndex]
+    if !workspace.isPinned && sectionForWorkspace(tabId) != nil { return false }
+
     let clamped = clampedReorderIndex(for: workspace, targetIndex: targetIndex)
     if currentIndex == clamped { return true }

Based on learnings: reorderWorkspace(...) must reject grouped tabs (early return). Flat-array reorders are not allowed for workspaces inside a group; use the group-aware reorder APIs instead.

Also applies to: 2774-2777

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

In `@Sources/TabManager.swift` around lines 2604 - 2616, The current early-return
uses sectionForWorkspace(tabId) to block reorders for any workspace that appears
in a section, but pinned workspaces are rendered from tabs and should be
reorderable in the flat pinned area; update reorderWorkspace(tabId:toIndex:) to
ignore section membership when the workspace is pinned (e.g., check the
workspace's pinned flag on the Tabs element before returning false from
sectionForWorkspace), so only non-pinned section members are blocked and leave
clampedReorderIndex(...) logic intact; apply the same change to the similar
guard in the other reorder code paths noted around the 2774–2777 area.
🤖 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/CmuxConfigExecutor.swift`:
- Around line 96-99: The current code unconditionally applies workspaceName
(which falls back to command.name) when wsDef.target == .current, causing
auto-apply to rename every workspace; change the logic in CmuxConfigExecutor so
that you only call setCustomTitle on tabManager.selectedWorkspace when the
workspace definition explicitly provides a name (i.e., check wsDef.name or an
equivalent indicator rather than workspaceName fallback). Locate the block that
references wsDef.target == .current, workspaceName, tabManager.selectedWorkspace
and setCustomTitle, and wrap the setCustomTitle call in a conditional that
verifies wsDef.name is non-nil (preserving existing behavior for color and other
properties).

---

Outside diff comments:
In `@Sources/TabManager.swift`:
- Around line 2604-2616: The current early-return uses
sectionForWorkspace(tabId) to block reorders for any workspace that appears in a
section, but pinned workspaces are rendered from tabs and should be reorderable
in the flat pinned area; update reorderWorkspace(tabId:toIndex:) to ignore
section membership when the workspace is pinned (e.g., check the workspace's
pinned flag on the Tabs element before returning false from
sectionForWorkspace), so only non-pinned section members are blocked and leave
clampedReorderIndex(...) logic intact; apply the same change to the similar
guard in the other reorder code paths noted around the 2774–2777 area.
🪄 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: 0155c0b1-ed3e-43fb-8132-b090caa4d3ff

📥 Commits

Reviewing files that changed from the base of the PR and between 69e6439 and f19a328.

📒 Files selected for processing (3)
  • Sources/CmuxConfigExecutor.swift
  • Sources/ContentView.swift
  • Sources/TabManager.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • Sources/ContentView.swift

Comment thread Sources/CmuxConfigExecutor.swift
- Drop handler: check section membership alongside flat position before
  early-returning. A drop can legitimately change section membership
  without changing the flat index (e.g. moving an ungrouped workspace
  into a section whose first row sits at the same flat position).
- CmuxConfigExecutor: only apply `wsDef.name` to an existing current
  workspace when it was explicitly set. Previously autoApply overwrote
  a user-customized title with `command.name` on every session start.
- Remove unused `SidebarLayout.allWorkspacesInOrder` and
  `SidebarLayout.empty` — no remaining callers after switching row
  indexing to the flat tabs array.
@coderabbitai

coderabbitai Bot commented Apr 14, 2026

Copy link
Copy Markdown

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{}

@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 OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 0b40f07. Configure here.

Comment thread Sources/ContentView.swift
if !useLiquidGlass {
Color(nsColor: tintColor)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Sidebar tint color lost on fallback path

Medium Severity

In the #if compiler(>=6.2) else-branch (macOS < 26 fallback when useLiquidGlass is true), SidebarVisualEffectBackground is created with preferLiquidGlass: false and then the tint overlay check if !useLiquidGlass evaluates to false, so no tint is rendered. But since the native glassEffect API isn't available on this OS version, the sidebar is left without any tinting mechanism — preferLiquidGlass: false disables the NSVisualEffectView-based glass, and the tint overlay is skipped. Compare with the #else branch, which passes preferLiquidGlass: useLiquidGlass (true), letting the NSVisualEffectView attempt its own glass+tint path.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0b40f07. Configure here.

@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: 3

♻️ Duplicate comments (1)
Sources/CmuxConfigExecutor.swift (1)

114-116: ⚠️ Potential issue | 🟠 Major

Avoid force-closing panels during auto-apply.

Lines 115-116 still use force: true. Since checkAutoApply(...) in Sources/CmuxConfig.swift:421-451 routes autoApply: true through this branch, a tab switch can still kill running terminals or discard editor/browser state with no user intervention.

♻️ Minimal fix
                 let panelIdsToClose = Array(current.panels.keys).filter { $0 != keep }
                 for panelId in panelIdsToClose {
-                    _ = current.closePanel(panelId, force: true)
+                    guard current.closePanel(panelId, force: command.autoApply != true) else {
+                        return
+                    }
                 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/CmuxConfigExecutor.swift` around lines 114 - 116, The code is
force-closing panels during auto-apply via current.closePanel(panelId, force:
true); update this call so panels are closed non-forcibly (e.g., pass force:
false or call the closePanel overload that defaults to non-forced) so that
checkAutoApply(...) / autoApply: true paths do not kill running terminals or
discard state; locate the call to current.closePanel(panelId, force: true) and
change the argument to false (or remove the force flag to use the safe default)
to preserve panel state.
🤖 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 9927-9988: tabItemViewForWorkspace is calling TabItemView for
sectioned/grouped workspaces but not passing the section/group identity, so Move
Up/Down and accessibility reorder actions still call reorderWorkspace(...) which
must no-op for grouped tabs; update tabItemViewForWorkspace (and the same block
at the other occurrence) to pass the section/group identifier (e.g. sectionId or
groupContext) into TabItemView via a new prop (name it sectionContext or
groupId) so the row can disable or route reorders appropriately, and then update
reorderWorkspace(...) to early-return when the target workspace is part of a
group/section (reject grouped tabs). Ensure the new prop is threaded through
TabItemView consumer logic that controls Move Up/Down and accessibility reorder
actions.
- Around line 12321-12325: The header currently applies combined/button
accessibility modifiers which swallow the embedded TextField's semantics when
isEditing is true; update the code around the isEditing branch (the TextField
using editedName and focused via isTextFieldFocused) to skip or remove the
parent combined accessibility modifiers while isEditing is true, and give the
TextField an explicit localized accessibility label using String(localized:
"inlineRename.field.label", defaultValue: "Name") so the field exposes its own
editable semantics; apply the same change in the analogous block at the other
location (around lines referencing the same header at 12407-12412).
- Around line 15065-15080: The same-section no-op uses flat indices (fromIndex
and targetIndex from tabManager.tabs) which can be equal even when
section-relative order changes; change the guard in the drop handler to only
early-return for unsectioned moves or to compare section-relative positions: use
tabManager.sectionForWorkspace(draggedTabId) / sectionForWorkspace(targetTabId)
(sourceSection and destSection) and, if both are nil (ungrouped), keep the
existing fromIndex==targetIndex check, otherwise compute and compare the
positions inside the respective section's workspaceIds (or consult sidebarOrder)
to determine true no-op; ensure you still call syncSidebarSelection() before
returning and leave behavior unchanged for ungrouped drops.

---

Duplicate comments:
In `@Sources/CmuxConfigExecutor.swift`:
- Around line 114-116: The code is force-closing panels during auto-apply via
current.closePanel(panelId, force: true); update this call so panels are closed
non-forcibly (e.g., pass force: false or call the closePanel overload that
defaults to non-forced) so that checkAutoApply(...) / autoApply: true paths do
not kill running terminals or discard state; locate the call to
current.closePanel(panelId, force: true) and change the argument to false (or
remove the force flag to use the safe default) to preserve panel state.
🪄 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: 045a0499-8829-4f52-b245-eefbe83bb76d

📥 Commits

Reviewing files that changed from the base of the PR and between f19a328 and 0b40f07.

📒 Files selected for processing (3)
  • Sources/CmuxConfigExecutor.swift
  • Sources/ContentView.swift
  • Sources/SidebarSection.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • Sources/SidebarSection.swift

Comment thread Sources/ContentView.swift
Comment on lines +9927 to +9988
private func tabItemViewForWorkspace(
_ tab: Workspace,
index: Int,
workspaceCount: Int,
canCloseWorkspace: Bool,
workspaceNumberShortcut: StoredShortcut,
tabItemSettings: SidebarTabItemSettingsSnapshot,
selectedContextTargetIds: [UUID],
selectedRemoteContextMenuWorkspaceIds: [UUID],
allSelectedRemoteContextMenuTargetsConnecting: Bool,
allSelectedRemoteContextMenuTargetsDisconnected: Bool
) -> some View {
let usesSelectedContextMenuTargets = selectedTabIds.contains(tab.id)
let contextMenuWorkspaceIds = usesSelectedContextMenuTargets
? selectedContextTargetIds
: [tab.id]
let remoteContextMenuWorkspaceIds = usesSelectedContextMenuTargets
? selectedRemoteContextMenuWorkspaceIds
: (tab.isRemoteWorkspace ? [tab.id] : [])
let allRemoteContextMenuTargetsConnecting = usesSelectedContextMenuTargets
? allSelectedRemoteContextMenuTargetsConnecting
: (tab.isRemoteWorkspace && tab.remoteConnectionState == .connecting)
let allRemoteContextMenuTargetsDisconnected = usesSelectedContextMenuTargets
? allSelectedRemoteContextMenuTargetsDisconnected
: (tab.isRemoteWorkspace && tab.remoteConnectionState == .disconnected)
TabItemView(
tabManager: tabManager,
notificationStore: notificationStore,
tab: tab,
index: index,
isActive: tabManager.selectedTabId == tab.id,
workspaceShortcutDigit: WorkspaceShortcutMapper.digitForWorkspace(
at: index,
workspaceCount: workspaceCount
),
workspaceShortcutModifierSymbol: workspaceNumberShortcut.numberedDigitHintPrefix,
canCloseWorkspace: canCloseWorkspace,
accessibilityWorkspaceCount: workspaceCount,
unreadCount: notificationStore.unreadCount(forTabId: tab.id),
latestNotificationText: {
guard showsSidebarNotificationMessage,
let notification = notificationStore.latestNotification(forTabId: tab.id) else {
return nil
}
let text = notification.body.isEmpty ? notification.title : notification.body
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? nil : trimmed
}(),
rowSpacing: tabRowSpacing,
setSelectionToTabs: { selection = .tabs },
selectedTabIds: $selectedTabIds,
lastSidebarSelectionIndex: $lastSidebarSelectionIndex,
showsModifierShortcutHints: modifierKeyMonitor.isModifierPressed,
dragAutoScrollController: dragAutoScrollController,
draggedTabId: $draggedTabId,
dropIndicator: $dropIndicator,
contextMenuWorkspaceIds: contextMenuWorkspaceIds,
remoteContextMenuWorkspaceIds: remoteContextMenuWorkspaceIds,
allRemoteContextMenuTargetsConnecting: allRemoteContextMenuTargetsConnecting,
allRemoteContextMenuTargetsDisconnected: allRemoteContextMenuTargetsDisconnected,
settings: tabItemSettings
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Pass section context into grouped rows.

Section members now reuse TabItemView, but that row still exposes Move Up/Down and accessibility reorder actions that call reorderWorkspace(...). Because grouped tabs intentionally reject flat reorders, those affordances will just no-op for workspaces inside a section. Pass the section/group identity down here so the row can disable those actions or route them through the section-aware reorder APIs.

Based on learnings, reorderWorkspace(...) must reject grouped tabs (early return).

Also applies to: 10057-10080

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

In `@Sources/ContentView.swift` around lines 9927 - 9988, tabItemViewForWorkspace
is calling TabItemView for sectioned/grouped workspaces but not passing the
section/group identity, so Move Up/Down and accessibility reorder actions still
call reorderWorkspace(...) which must no-op for grouped tabs; update
tabItemViewForWorkspace (and the same block at the other occurrence) to pass the
section/group identifier (e.g. sectionId or groupContext) into TabItemView via a
new prop (name it sectionContext or groupId) so the row can disable or route
reorders appropriately, and then update reorderWorkspace(...) to early-return
when the target workspace is part of a group/section (reject grouped tabs).
Ensure the new prop is threaded through TabItemView consumer logic that controls
Move Up/Down and accessibility reorder actions.

Comment thread Sources/ContentView.swift
Comment on lines +12321 to +12325
if isEditing {
TextField("", text: $editedName)
.textFieldStyle(.plain)
.font(.system(size: 11, weight: .semibold))
.focused($isTextFieldFocused)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Keep the inline rename field accessible while editing.

In edit mode this header is still exposed as one combined button, so the embedded TextField loses its own editable accessibility semantics. The field also has no explicit label. Skip the combined/button accessibility modifiers while isEditing is true, and give the editor its own localized accessibility label.

As per coding guidelines, "All user-facing strings must be localized. Use String(localized: "key.name", defaultValue: "English text") for every string shown in the UI (labels, buttons, menus, dialogs, tooltips, error messages)."

Also applies to: 12407-12412

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

In `@Sources/ContentView.swift` around lines 12321 - 12325, The header currently
applies combined/button accessibility modifiers which swallow the embedded
TextField's semantics when isEditing is true; update the code around the
isEditing branch (the TextField using editedName and focused via
isTextFieldFocused) to skip or remove the parent combined accessibility
modifiers while isEditing is true, and give the TextField an explicit localized
accessibility label using String(localized: "inlineRename.field.label",
defaultValue: "Name") so the field exposes its own editable semantics; apply the
same change in the analogous block at the other location (around lines
referencing the same header at 12407-12412).

Comment thread Sources/ContentView.swift
Comment on lines +15065 to 15080
// Funnel every drop through one section-aware helper so cross-section
// and ungrouped↔section drops update membership (not just position).
// Check section membership BEFORE early-returning on position equality:
// a drop can change membership even when the flat index is unchanged
// (e.g. moving an ungrouped workspace into a section that lives at the
// same flat index).
let sourceSection = tabManager.sectionForWorkspace(draggedTabId)
let destSection: SidebarSection? = targetTabId.flatMap { tabManager.sectionForWorkspace($0) }
let insertAfter = dropIndicator?.edge == .bottom

if fromIndex == targetIndex && sourceSection?.id == destSection?.id {
#if DEBUG
dlog("sidebar.drop.noop from=\(fromIndex) to=\(targetIndex)")
#endif
syncSidebarSelection()
return true

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't use flat tabs indices for the same-section no-op guard.

fromIndex and targetIndex come from tabManager.tabs, but section reorders are rendered from section/sidebar order. Once a section's workspaceIds diverge from tabs, a real same-section reorder can still resolve to equal flat indices and hit this early return, so the drag is dropped on the floor. Restrict this guard to unsectioned moves, or compare section-relative positions instead.

Based on learnings, sidebarOrder is the source of truth for sidebar rendering, and the tabs array insertion index is unrelated to the user's drag-drop target position.

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

In `@Sources/ContentView.swift` around lines 15065 - 15080, The same-section no-op
uses flat indices (fromIndex and targetIndex from tabManager.tabs) which can be
equal even when section-relative order changes; change the guard in the drop
handler to only early-return for unsectioned moves or to compare
section-relative positions: use tabManager.sectionForWorkspace(draggedTabId) /
sectionForWorkspace(targetTabId) (sourceSection and destSection) and, if both
are nil (ungrouped), keep the existing fromIndex==targetIndex check, otherwise
compute and compare the positions inside the respective section's workspaceIds
(or consult sidebarOrder) to determine true no-op; ensure you still call
syncSidebarSelection() before returning and leave behavior unchanged for
ungrouped drops.

Introduces a single "Active" toggle that filters the sidebar down to
workspaces with an agent session (any tracked agent PID), regardless of
running/idle state. Filter is pinned above the workspace list and
auto-clears when the active count reaches zero. Keeps pinned, ungrouped,
and section groups all respecting the filter.

@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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/ContentView.swift`:
- Around line 12560-12572: The drop handler currently moves the workspace but
doesn't ensure the target section is expanded; modify the closure that handles
SidebarTabDragPayload (inside the .onDrop block) to call
section.setCollapsed(false) on the MainActor before/after invoking
tabManager.moveWorkspaceToSection(tabId:sectionId:) so the section is always
expanded (use setCollapsed(false) directly, not a toggle) ensuring the dropped
workspace is visible.
- Around line 15881-15903: The tint overlay is incorrectly gated by if
!useLiquidGlass causing no tint on macOS <26 when the user prefers Liquid Glass;
since SidebarVisualEffectBackground is created with preferLiquidGlass: false
(forcing NSVisualEffectView) you must always render the Color(nsColor:
tintColor) overlay after SidebarVisualEffectBackground regardless of
useLiquidGlass. Update the `#if` compiler(>=6.2) else branch to remove the
conditional around the Color overlay so that the Color(nsColor: tintColor) is
always present (leave SidebarVisualEffectBackground(..., preferLiquidGlass:
false) and its parameters as-is); this ensures updateNSView’s lack of
NSVisualEffectView tinting is compensated by the overlay while NSGlassEffectView
remains unaffected.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f5cc57a7-7d85-4d68-951d-22ae408a37a8

📥 Commits

Reviewing files that changed from the base of the PR and between 0b40f07 and 92f2a47.

📒 Files selected for processing (1)
  • Sources/ContentView.swift

Comment thread Sources/ContentView.swift
Comment on lines +12560 to +12572
.onDrop(of: SidebarTabDragPayload.dropContentTypes, isTargeted: nil) { providers in
guard let provider = providers.first else { return false }
provider.loadDataRepresentation(forTypeIdentifier: SidebarTabDragPayload.typeIdentifier) { data, _ in
guard let data, let str = String(data: data, encoding: .utf8) else { return }
let prefix = "cmux.sidebar-tab."
guard str.hasPrefix(prefix),
let tabId = UUID(uuidString: String(str.dropFirst(prefix.count))) else { return }
Task { @MainActor in
tabManager.moveWorkspaceToSection(tabId: tabId, sectionId: section.id)
}
}
return true
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -t f -e swift SidebarSection
rg -nP -C3 '\b(setCollapsed|toggleCollapsed|isCollapsed)\b' --type=swift

Repository: manaflow-ai/cmux

Length of output: 7042


Auto-expand the section on drop so the dropped workspace is visible.

When the user drags a tab onto a collapsed section header, moveWorkspaceToSection succeeds but the new member is hidden. The user gets no feedback that the drop landed where they intended. Expand the section as part of the drop so the move is observable.

♻️ Proposed change
                 Task { `@MainActor` in
+                    section.setCollapsed(false)
                     tabManager.moveWorkspaceToSection(tabId: tabId, sectionId: section.id)
                 }

Use setCollapsed(false) directly instead of a conditional toggle to ensure the section is always expanded, avoiding potential no-op races when multiple drops arrive concurrently.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/ContentView.swift` around lines 12560 - 12572, The drop handler
currently moves the workspace but doesn't ensure the target section is expanded;
modify the closure that handles SidebarTabDragPayload (inside the .onDrop block)
to call section.setCollapsed(false) on the MainActor before/after invoking
tabManager.moveWorkspaceToSection(tabId:sectionId:) so the section is always
expanded (use setCollapsed(false) directly, not a toggle) ensuring the dropped
workspace is visible.

Comment thread Sources/ContentView.swift
Comment on lines +15881 to +15903
#if compiler(>=6.2)
if #available(macOS 26.0, *), useLiquidGlass {
// Native SwiftUI Liquid Glass on macOS 26+
Color.clear
.glassEffect(
.regular.tint(Color(nsColor: tintColor)),
in: .rect(cornerRadius: cornerRadius)
)
.opacity(sidebarBlurOpacity)
} else {
SidebarVisualEffectBackground(
material: material,
blendingMode: blendingMode,
state: state,
opacity: sidebarBlurOpacity,
tintColor: tintColor,
cornerRadius: cornerRadius,
preferLiquidGlass: false
)
if !useLiquidGlass {
Color(nsColor: tintColor)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Locate SidebarVisualEffectBackground and verify how tintColor is applied in the non-NSGlassEffectView path.
fd -t f -e swift SidebarVisualEffectBackground
rg -nP -C5 '\bstruct\s+SidebarVisualEffectBackground\b'
rg -nP -C8 '\btintColor\b' --type=swift -g '*VisualEffect*'
rg -nP -C5 '\bliquidGlassAvailable\b' --type=swift

Repository: manaflow-ai/cmux

Length of output: 1757


🏁 Script executed:

sed -n '15655,15800p' Sources/ContentView.swift | cat -n

Repository: manaflow-ai/cmux

Length of output: 6948


🏁 Script executed:

sed -n '15870,15910p' Sources/ContentView.swift | cat -n

Repository: manaflow-ai/cmux

Length of output: 2472


Tint overlay is dropped on macOS < 26 when Liquid Glass is preferred.

In the #if compiler(>=6.2) else branch (lines 31–33), you gate the tint overlay on if !useLiquidGlass. When a user on macOS earlier than 26 with the Liquid Glass preference enabled reaches this branch, SidebarVisualEffectBackground is instantiated with preferLiquidGlass: false, which falls back to NSVisualEffectView. However, the updateNSView method of SidebarVisualEffectBackground does not apply tintColor to NSVisualEffectView—it only applies it to NSGlassEffectView via private API. The external Color overlay is the only mechanism that tints the NSVisualEffectView, yet it is skipped when useLiquidGlass == true, leaving the sidebar with no tint at all.

The overlay must remain in this branch because preferLiquidGlass: false guarantees that native Liquid Glass is never used. Always render it:

Proposed fix
                         } else {
                             SidebarVisualEffectBackground(
                                 material: material,
                                 blendingMode: blendingMode,
                                 state: state,
                                 opacity: sidebarBlurOpacity,
                                 tintColor: tintColor,
                                 cornerRadius: cornerRadius,
                                 preferLiquidGlass: false
                             )
-                            if !useLiquidGlass {
-                                Color(nsColor: tintColor)
-                            }
+                            // Native Liquid Glass is not used in this branch
+                            // (preferLiquidGlass: false), so always apply the
+                            // tint overlay to NSVisualEffectView.
+                            Color(nsColor: tintColor)
                         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/ContentView.swift` around lines 15881 - 15903, The tint overlay is
incorrectly gated by if !useLiquidGlass causing no tint on macOS <26 when the
user prefers Liquid Glass; since SidebarVisualEffectBackground is created with
preferLiquidGlass: false (forcing NSVisualEffectView) you must always render the
Color(nsColor: tintColor) overlay after SidebarVisualEffectBackground regardless
of useLiquidGlass. Update the `#if` compiler(>=6.2) else branch to remove the
conditional around the Color overlay so that the Color(nsColor: tintColor) is
always present (leave SidebarVisualEffectBackground(..., preferLiquidGlass:
false) and its parameters as-is); this ensures updateNSView’s lack of
NSVisualEffectView tinting is compensated by the overlay while NSGlassEffectView
remains unaffected.

@greptile-apps

greptile-apps Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Too many files changed for review (3000 files, 100 file limit).

@teamleaderleo teamleaderleo added area: sidebar The workspace sidebar: list, groups, status, reordering S2: major A crash, hang, lost state, broken connection, or a regression on a path people use labels Sep 30, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: sidebar The workspace sidebar: list, groups, status, reordering S2: major A crash, hang, lost state, broken connection, or a regression on a path people use

Projects

None yet

Development

Successfully merging this pull request may close these issues.