Skip to content

Turn a single workspace into a group (promote in place) - #5633

Closed
lawrencecchen wants to merge 7 commits into
mainfrom
feat-workspace-to-group
Closed

lawrencecchen wants to merge 7 commits into
mainfrom
feat-workspace-to-group

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jun 8, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Add the ability to turn a single workspace into a group in place: an existing ungrouped workspace becomes the group's anchor (the sidebar header), with no phantom empty anchor workspace created. Inherits the workspace's title as the group name and its pinned state, so the row keeps its sidebar position.
  • ⌘⇧G turns the focused workspace into a group when there's no multi-selection. Right-click → New Group from Workspace does the same. CLI/socket too.
  • Ungroup converts the group back into a normal workspace at the same spot. So workspace → group → ungroup round-trips cleanly.
  • Before this, grouping always synthesized a fresh empty anchor, so "New Group from Workspace" on one workspace produced a junk empty anchor with the original as a child, and ungrouping left two workspaces.

What changed

  • TabManager.makeWorkspaceGroupFromWorkspace(anchorWorkspaceId:name:) — promote an existing ungrouped workspace to be the anchor of a new single-member group, in place. No-ops for a missing or already-grouped workspace.
  • Sidebar context menu: New Group from Workspace (shown only for a single ungrouped workspace) routes here. Multi-select New Group from Selection / 2+ ⌘⇧G keep the documented fresh-anchor create contract.
  • Keyboard: with no 2+ selection, ⌘⇧G promotes the focused workspace, deferring to React Grab when React Grab would act (browser focused, or a single browser reachable from the focused terminal).
  • Socket workspace.group.from_workspace + CLI cmux workspace group from-workspace --workspace <ws> [--name <name>].
  • Sidebar render fix (the bug that made it look broken): the sidebar list is a LazyVStack + ForEach where a top-level slot switches between a .workspace row and a .groupHeader (different view types). LazyVStack mis-diffs that in-place type change, so on the focused workspace the header never appeared on create and a stale header lingered after ungroup. The data model was always correct (verified via the socket/logs); only the view didn't update. Fixed by keying the rows on a signature of the group-anchor set, so the list rebuilds cleanly when a workspace starts/stops being a header. Sorted, so reorders/renames don't trigger a rebuild and drag-reorder is unaffected. Multi-select grouping never hit this because it inserts a brand-new anchor rather than transforming an existing row.

Mechanism / principled?

Mostly principled. The promote primitive reuses the existing anchor model instead of adding a parallel concept. The render fix is the one pragmatic part: it forces a clean list rebuild on group-structure change rather than fully reworking the heterogeneous-row identity, because LazyVStack's in-place view-type swap is unreliable. Known minor tradeoff: on a group create/ungroup the sidebar scroll resets to top (only visible when the sidebar is actually scrolled). Worth a follow-up to restore scroll-to-selection if it bothers anyone; correctness first.

Testing

  • cmuxTests/WorkspaceGroupTests.swift: promote-in-place + round-trip, pinned-tier stability, no-op for an already-grouped workspace, header-only rendering. (CI tests job is green.)
  • Verified end-to-end on the tagged build (w2grp) with real keyboard input + cua window screenshots: ⌘⇧G on the focused terminal workspace renders the group header in place, and ungroup reverts it to a plain row. The data-level round-trip (from_workspace → list → ungroup) is also confirmed via the socket.

Issues

  • Task: make it possible to turn a single workspace into a group; ungroup converts the "group workspace" back into a normal workspace; ⌘⇧G in a terminal triggers it.

Summary by CodeRabbit

  • New Features

    • CLI: new command to convert an existing workspace into a group (optional name); supports JSON or simple OK output.
    • New keyboard shortcut to rename the focused workspace's group (default Cmd+Shift+P).
  • Context Menu

    • Distinct "New Group from Workspace" (single) and "New Group from Selection" (multi).
  • Behavior

    • Single-workspace promotion preserves pin/position; sidebar updates correctly and workspace renames propagate to group headers.
  • Tests

    • Added tests for promote/ungroup, pinned behavior, no-op when already grouped, and rendering.
  • Documentation

    • Updated workspace-groups guide and CLI examples.
  • Localization

    • Added localized strings including "workspace already grouped" error.

Note

Medium Risk
Touches sidebar rendering, keyboard routing (⌘⇧G vs React Grab), and workspace/group state; changes are localized but affect daily navigation UX.

Overview
Adds in-place “promote workspace to group”: an ungrouped workspace becomes the group anchor with no extra anchor tab, inheriting its title and pin tier. ⌘⇧G still groups 2+ sidebar selections under a fresh anchor; with ≤1 selection it promotes the focused workspace when React Grab would not handle the chord, and toggles ungroup for a solo single-member group. Context menu New Group from Workspace and socket workspace.group.from_workspace / CLI from-workspace call the same primitive.

Sidebar fix: keys the workspace list on the set of group anchor IDs so LazyVStack rebuilds when a row flips between plain workspace and group header (fixes invisible/stale headers).

Rename: renaming a workspace that is a group anchor updates the group name; new renameFocusedWorkspaceGroup shortcut (⌘⇧P, palette-closed only) opens the group rename prompt. Docs, schema, and tests cover promote/ungroup round-trip, pinned tier, and already-grouped errors.

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

Group keyboard shortcuts (follow-up)

  • ⌘⇧G toggles for a single focused workspace: first press makes it a single-member group, pressing again on that solo group ungroups it. A workspace inside a multi-member group (anchor or child) is left untouched, so the toggle never dissolves a group with other members.
  • ⌘⇧P (new .renameFocusedWorkspaceGroup) renames the focused workspace's group. It shares the chord with commandPalettePrevious, which only acts while the command palette is open (in-palette navigation, dispatched earlier), so they don't collide in practice. Rebindable in Settings. Fully wired (KeyboardShortcutSettings, dispatch + handler, schema, localized label, docs).
  • ⌘⇧R (rename workspace) now also renames the group when the focused workspace is a group anchor — the group name is what the header displays. Hooked once in applyRenameFlow, so every rename entrypoint (palette, context menu, shortcut) renames both.
  • Verified live: ⌘⇧G create/ungroup toggle and multi-member no-op; ⌘⇧P opens the group rename dialog. The rename completion and ⌘⇧R dual-rename use the same model paths as the existing (working) header rename, but the modal/palette text entry wasn't automatable, so they're flagged for dogfood.

Grouping previously always synthesized a fresh empty anchor workspace, so
"New Group from Workspace" on a single workspace produced a junk empty
anchor with the original as a child, and ungrouping left two workspaces.

Add a promote path: an existing ungrouped workspace becomes the anchor of
a new single-member group in place (no extra workspace), inheriting its
title as the group name and its pinned state to keep its sidebar position.
The existing ungroup is the exact inverse, so workspace -> group -> ungroup
round-trips back to the same single normal workspace.

- TabManager.makeWorkspaceGroupFromWorkspace(anchorWorkspaceId:name:)
- Sidebar "New Group from Workspace" (single, ungrouped) routes here;
  multi-select "New Group from Selection" / Cmd-Shift-G keep the documented
  fresh-anchor create contract; grouped rows still show Move/Remove only.
- Socket workspace.group.from_workspace + CLI `workspace group from-workspace`
  (non-focus, mirrors create's payload).
- Round-trip + no-op + header-render unit tests; docs updated.

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

vercel Bot commented Jun 8, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jun 9, 2026 9:57am
cmux-staging Building Building Preview, Comment Jun 9, 2026 9:57am

@coderabbitai

coderabbitai Bot commented Jun 8, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Promotes an existing ungrouped workspace into a single-member workspace group anchored on that workspace. Adds TabManager API, Terminal v2 handler, cmux CLI subcommand, sidebar/context-menu and shortcut wiring, tests, docs, schema, and localization.

Changes

Workspace-to-Group Promotion

Layer / File(s) Summary
Core TabManager API for workspace-to-group promotion
Sources/TabManager.swift
TabManager.makeWorkspaceGroupFromWorkspace(anchorWorkspaceId:name:) promotes an ungrouped workspace into a new single-member group anchored on that workspace; derives group name, preserves pinned state, assigns groupId, normalizes contiguity, posts order change, and returns new group id or nil. Adds reactGrabShortcutRouteForCurrentFocus() and reactGrabWouldHandleCurrentFocus() and updates toggleReactGrabFromCurrentFocus().
Terminal Controller v2 command handler
Sources/TerminalController.swift
Wires workspace.group.from_workspace to v2WorkspaceGroupFromWorkspace, validates params and window state, distinguishes not-found vs already-grouped, calls TabManager to create the group, and returns a group payload or an error.
CLI from-workspace command & help
CLI/cmux.swift
Adds from-workspace --workspace <id> [--name <name>] parsing (skips --window when scanning), calls workspace.group.from_workspace via client.sendV2, supports --json structured output or OK/OK <group> text output, and updates missing-subcommand help to suggest from-workspace.
Sidebar context menu integration
Sources/TabItemView+WorkspaceGroups.swift
Refactors context menu to separate multi-selection ("New Group from Selection" routed to promptNewWorkspaceGroup) and single ungrouped workspace ("New Group from Workspace" routed to new promptWorkspaceGroupFromWorkspace), which delegates to TabManager.
Shortcut handlers and dispatch
Sources/AppDelegate.swift
Adds .renameFocusedWorkspaceGroup dispatch that consumes only when a focused grouped workspace exists; updates .groupSelectedWorkspaces handling to promote focused workspace into a single-member group when React Grab won't handle focus and workspace is ungrouped; adjusts related comments.
SwiftUI identity & rename propagation
Sources/ContentView.swift
Propagates workspace rename to anchored group headers via tabManager.renameWorkspaceGroup and adds groupAnchorSignature used as .id(...) to rebuild rows only when the set of anchors changes.
Shortcut action, schema, and localization
Sources/KeyboardShortcutSettings.swift, web/data/cmux.schema.json, Resources/Localizable.xcstrings
Adds renameFocusedWorkspaceGroup action case, default shortcut (Cmd+Shift+P), maps label localization key, allows the action id in schema, and adds localization entries including workspaceGroup.error.workspaceAlreadyGrouped.
Tests for promotion and rendering
cmuxTests/WorkspaceGroupTests.swift
Adds tests verifying in-place promotion, no-op when already grouped, pinned-tier preservation and restore, and rendering behavior where a promoted single-member group appears only as a group header.
Documentation and CLI examples
docs/workspace-groups.md
Documents anchor vs in-place promotion behavior, ⌘⇧G variations, context-menu differences, the new cmux workspace-group from-workspace CLI usage, return contract, and an example.

Sequence Diagram

sequenceDiagram
  participant User
  participant CLI
  participant Client
  participant TerminalController
  participant TabManager
  User->>CLI: cmux workspace-group from-workspace --workspace <id> [--name <name>]
  CLI->>Client: sendV2(workspace.group.from_workspace, params)
  Client->>TerminalController: Deliver v2 command
  TerminalController->>TabManager: makeWorkspaceGroupFromWorkspace(workspace_id, name)
  alt workspace missing
    TerminalController-->>Client: error not_found
  else already grouped
    TerminalController-->>Client: error workspace_already_grouped
  else success
    TerminalController->>Client: group payload
    Client-->>CLI: payload
    alt jsonOutput
      CLI-->>User: formatted JSON
    else
      CLI-->>User: OK <group_name> (if present) or OK
    end
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • Ari4ka

Poem

🐰 I nudged a lonely workspace near,
"Become a group — stay anchored here!"
A click, a CLI, a tiny hop,
Now headers sing — the rows won't drop.
Hooray for groups! 🥕


Important

Pre-merge checks failed

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

❌ Failed checks (3 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Swift Blocking Runtime ❌ Error PR introduces DispatchQueue.main.sync in v2MainSync, a blocking call used in socket command handler v2WorkspaceGroupFromWorkspace, violating rules that flag main-queue sync in socket paths. Replace v2MainSync's DispatchQueue.main.sync with async continuation, actor re-entrancy, or other non-blocking main-thread coordination for socket handlers.
Cmux Algorithmic Complexity ❌ Error AppDelegate.swift line 14600 violates batch-action rule: nested scan checking each candidateId against entire tabs array. Line 14588 similarly filters whole array for count. Line 14600: Create Set from tabManager.tabs IDs for O(1) lookup per target. Line 14588: Use count(where:) instead of filter().count.
Cmux Full Internationalization ❌ Error New localization keys lack complete translations: shortcut.renameFocusedWorkspaceGroup.label and workspaceGroup.error.workspaceAlreadyGrouped have only en/ja but need all 20 locales. Complete translations in Resources/Localizable.xcstrings for all 20 locales: ar, bs, da, de, en, es, fr, it, ja, km, ko, nb, pl, pt-BR, ru, th, tr, uk, zh-Hans, zh-Hant.
Docstring Coverage ⚠️ Warning Docstring coverage is 26.32% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (17 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main feature: promoting a single workspace into a workspace group in place, which is the central theme throughout the PR.
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 MainActor classes properly marked. New methods on MainActor. WorkspaceGroup is a pure Sendable struct. No background access violates isolation boundaries.
Cmux Expensive Synchronous Load ✅ Passed The PR does not add new expensive synchronous loaders to main actor/interactive paths. Existing RestorableAgentSessionIndex.load() calls use the allowed nil-cache fallback pattern.
Cmux Cache Substitution Correctness ✅ Passed PR introduces no new cache substitution issues. The review comment references a pre-existing cache pattern in closeWorkspace (not modified here) that already existed in main branch.
Cmux No Hacky Sleeps ✅ Passed PR contains only Swift code, localization, and documentation changes. No TypeScript, JavaScript, or shell runtime scripts modified. Custom check scope (non-Swift runtime code) does not apply.
Cmux Swift Concurrency ✅ Passed All new methods use synchronous patterns. No DispatchQueue.async, completion handlers, fire-and-forget Tasks, or Combine state were introduced.
Cmux Swift @Concurrent ✅ Passed All new Swift functions are synchronous and properly isolated on @MainActor. No @concurrent violations, missing annotations, or improper actor hopping detected in the workspace group changes.
Cmux Swift File And Package Boundaries ✅ Passed All file/package boundary rules satisfied: no oversized new files, no >250-line additions to 800+ line files, coherent responsibilities per file, proper budget updates reflecting line count changes.
Cmux Swift Logging ✅ Passed New Swift code follows logging rules: CLI uses print() for command output (allowed); no debugPrint, dump, NSLog, or file logging added to production code.
Cmux User-Facing Error Privacy ✅ Passed All user-facing error messages and API payloads comply with privacy rules: no vendor/provider names, credentials, tokens, database details, or sensitive material exposed.
Cmux Swiftui State Layout ✅ Passed WorkspaceListRenderContext is a value-type struct; rows receive immutable snapshots not stores; groupAnchorSignature from snapshot only; mutations in event handlers not render.
Cmux Architecture Rethink ✅ Passed Single shared action path via TabManager, clear invariants with guards, no timing/split ownership hacks, comprehensive tests, proper SwiftUI identity via computed signature.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR only adds test-only NSWindow in #if DEBUG block within debugInjectWindowContextKeyMismatch. No user-visible windows introduced; test-only fixtures are allowed.
Cmux Source Artifacts ✅ Passed All 11 changed files are legitimate hand-written source, tests, docs, or configs. No artifacts, logs, caches, or build files detected.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering all key aspects: what changed, why, how it was tested, and the mechanism/trade-offs involved.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-workspace-to-group

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 468315f489

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

return (true, tab.groupId != nil)
}
guard knownState.exists else {
return .err(code: "not_found", message: "Workspace not found in target window", data: [

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Localize the new socket error text

This new error path returns a bare English message to CLI/socket callers. The repository's AGENTS.md explicitly requires String(localized:defaultValue:) for every user-facing error message, so Japanese users will see this new from-workspace failure untranslated even though the adjacent workspaceAlreadyGrouped case was localized.

Useful? React with 👍 / 👎.

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

greptile-apps Bot commented Jun 8, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds promote-in-place workspace grouping: an existing ungrouped workspace becomes the group's anchor directly (no phantom empty workspace created), inheriting its title and pinned state. ⌘⇧G on a single focused terminal workspace now toggles it into/out of a solo group, and a new ⌘⇧P shortcut renames the focused workspace's group. A LazyVStack .id key based on the sorted group-anchor set fixes mis-diffing of in-place view-type changes (workspace row ↔ group header).

  • TabManager.makeWorkspaceGroupFromWorkspace implements promote-in-place; ungroupWorkspaceGroup is the exact inverse, making the round-trip clean.
  • CLI/socket gain from-workspace / workspace.group.from_workspace; context menu splits into "New Group from Workspace" (single ungrouped) and "New Group from Selection" (multi).
  • Four new WorkspaceGroupTests cover the round-trip, pinned-tier stability, no-op guard, and header-only rendering.

Confidence Score: 4/5

Safe to merge with one localization gap to address before shipping to non-English users.

The feature logic is well-structured and backed by tests. The one outstanding issue is that shortcut.renameFocusedWorkspaceGroup.label — a new Settings → Keyboard label visible to all users — ships with only English and Japanese translations while the catalog's adjacent shortcut keys already carry full coverage for 17+ locales including German, French, Korean, Chinese, and Arabic. Users on those locales will see English text in a localized UI until translations are added.

Resources/Localizable.xcstrings — the new shortcut.renameFocusedWorkspaceGroup.label key needs translations for the 17+ locales already covered by neighboring shortcut-label keys.

Important Files Changed

Filename Overview
Sources/TabManager.swift Adds makeWorkspaceGroupFromWorkspace — promotes an existing ungrouped workspace to group anchor in-place, inheriting pin state and title. Logic is sound: guards against missing/already-grouped workspace, delegates to existing normalizeWorkspaceGroupContiguity and postWorkspaceOrderDidChange.
Sources/TerminalController.swift Adds v2WorkspaceGroupFromWorkspace socket handler. Uses three separate v2MainSync calls after the initial state-check, leaving a TOCTOU window (flagged in prior review thread). Functional but the error codes can be wrong under concurrent mutation.
Sources/AppDelegate.swift Extends ⌘⇧G to promote single-focused workspace via makeWorkspaceGroupFromWorkspace, defers to React Grab when a browser is reachable, and adds handleRenameFocusedWorkspaceGroupShortcut for ⌘⇧P. Logic looks correct; chord conflicts with React Grab and command-palette-previous are both handled defensively.
Sources/ContentView.swift Adds one-way sync from workspace rename to group name when the renamed workspace is an anchor, and keys LazyVStack on sorted group-anchor UUIDs to force a clean rebuild on promote/ungroup (acknowledged scroll-to-top tradeoff). Both changes are correct and the behavior is documented.
Resources/Localizable.xcstrings Adds two new keys; shortcut.renameFocusedWorkspaceGroup.label ships with only en+ja, missing 17+ supported locales that the adjacent shortcut.toggleFocusedWorkspaceGroupCollapsed.label already covers.
Sources/TabItemView+WorkspaceGroups.swift Splits context menu into single-ungrouped ("New Group from Workspace") and multi-select ("New Group from Selection") branches. Clean refactor; new single-workspace path correctly hides the item for already-grouped workspaces.
Sources/KeyboardShortcutSettings.swift Adds renameFocusedWorkspaceGroup action with default ⌘⇧P binding; documents chord sharing with command-palette-previous and graceful no-op behavior.
CLI/cmux.swift Adds from-workspace subcommand with --workspace and optional --name flags; routes to workspace.group.from_workspace socket method and outputs group handle or JSON. Help text and error messaging are consistent with adjacent subcommands.
cmuxTests/WorkspaceGroupTests.swift Adds four new tests: promote-in-place round-trip, no-op for already-grouped workspace, pinned-tier stability, and header-only rendering. Good coverage of the main invariants.
web/data/cmux.schema.json Adds renameFocusedWorkspaceGroup to the keyboard-action enum; matches the new KeyboardShortcutSettings.Action case.

Sequence Diagram

sequenceDiagram
    participant User
    participant AppDelegate
    participant TabManager
    participant TerminalController

    Note over User,TabManager: ⌘⇧G on single terminal workspace
    User->>AppDelegate: ⌘⇧G keypress
    AppDelegate->>TabManager: reactGrabWouldHandleCurrentFocus()
    TabManager-->>AppDelegate: false (no browser focus)
    AppDelegate->>TabManager: makeWorkspaceGroupFromWorkspace(anchorWorkspaceId:)
    TabManager->>TabManager: "guard groupId == nil"
    TabManager->>TabManager: inherit title + isPinned
    TabManager->>TabManager: workspaceGroups.append(group)
    TabManager->>TabManager: assignGroup + normalizeContiguity
    TabManager-->>AppDelegate: groupId (UUID)
    AppDelegate-->>User: return true (chord consumed)

    Note over User,TerminalController: CLI / socket path
    User->>TerminalController: workspace.group.from_workspace
    TerminalController->>TerminalController: v2MainSync — check exists/grouped
    TerminalController->>TabManager: makeWorkspaceGroupFromWorkspace(anchorWorkspaceId:name:)
    TabManager-->>TerminalController: groupId
    TerminalController->>TerminalController: v2MainSync — read group payload
    TerminalController-->>User: "{ group: { ... } }"

    Note over User,TabManager: ⌘⇧G toggle-off (solo group)
    User->>AppDelegate: ⌘⇧G keypress
    AppDelegate->>TabManager: "focused.groupId != nil, anchorWorkspaceId == focusedId, count == 1"
    AppDelegate->>TabManager: ungroupWorkspaceGroup(groupId:)
    TabManager-->>AppDelegate: dissolved
    AppDelegate-->>User: return true (chord consumed)
Loading

Reviews (5): Last reviewed commit: "Group shortcuts: ⌘⇧G toggle, ⌘⇧P rename ..." | Re-trigger Greptile

Comment on lines +167001 to +167017
"workspaceGroup.error.workspaceAlreadyGrouped": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Workspace is already in a group; remove it first"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "このワークスペースはすでにグループに属しています。先に削除してください"
}
}
}
},

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 Missing translations for 17 supported locales

The new workspaceGroup.error.workspaceAlreadyGrouped key ships with only en and ja, but the catalog already carries translations for ar, bs, da, de, es, fr, it, km, ko, nb, pl, ru, th, tr, uk, zh-Hans, and zh-Hant. Users on any of those locales will fall back to the English default — surfacing raw English in an otherwise-localized UI. Adjacent keys like workspaceGroup.error.workspaceIsOtherGroupAnchor follow the same two-locale pattern, but those are pre-existing; this one is new to the PR.

Rule Used: Flag production user-facing text that is not fully... (source)

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

Comment on lines +4909 to +4933
let knownState: (exists: Bool, grouped: Bool) = v2MainSync {
guard let tab = tabManager.tabs.first(where: { $0.id == wsId }) else {
return (false, false)
}
return (true, tab.groupId != nil)
}
guard knownState.exists else {
return .err(code: "not_found", message: "Workspace not found in target window", data: [
"workspace_id": wsId.uuidString
])
}
guard !knownState.grouped else {
return .err(
code: "invalid_state",
message: String(
localized: "workspaceGroup.error.workspaceAlreadyGrouped",
defaultValue: "Workspace is already in a group; remove it first"
),
data: ["workspace_id": wsId.uuidString]
)
}
var createdGroupId: UUID?
v2MainSync {
createdGroupId = tabManager.makeWorkspaceGroupFromWorkspace(anchorWorkspaceId: wsId, name: name)
}

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 TOCTOU between state-check and mutation

The workspace's exists/grouped state is read in one v2MainSync block and acted on in a second block two calls later. If another operation (e.g. workspace.group.create or workspace.group.add) runs on the same workspace between the two blocks, makeWorkspaceGroupFromWorkspace will safely return nil (its own guard), but the caller will respond with the generic not_created error instead of the more actionable not_found or invalid_state the pre-check was meant to produce. The v2WorkspaceGroupUngroup handler avoids this by merging its check and mutate into a single v2MainSync closure — a similar consolidation here would keep the error codes correct under concurrent access.

@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/TerminalController.swift`:
- Around line 4900-4905: Replace raw English error messages and the TabManager
type leak with localized keys and user-facing wording: when
v2ResolveTabManager(params:) returns nil, return a localized error key (e.g.
"window_unavailable" or similar existing localization used around the
grouped-workspace case) and a user-facing message that uses "window" or
"workspace" rather than exposing "TabManager"; likewise when v2UUID(params,
"workspace_id") fails, return a localized key (e.g. "invalid_workspace_id") and
a localized user-facing message. Update the three matching sites that use the
same patterns (the v2ResolveTabManager nil checks and v2UUID workspace_id
failure checks) to use the same localization keys and avoid exposing internal
type names, matching the style used in the grouped-workspace error handling
already present in this file.
- Around line 4909-4936: The existence/grouped checks, creation call and group
lookup must be executed atomically on the main thread to avoid races; replace
the separate v2MainSync hops with a single v2MainSync closure that (using wsId)
finds the tab in tabManager, verifies it is not grouped, calls
tabManager.makeWorkspaceGroupFromWorkspace(anchorWorkspaceId:name:) to create
the group, and returns both the created group id and the created group (or an
explicit error state) so the subsequent guards can inspect a single, consistent
result instead of relying on knownState/createdGroupId across multiple
v2MainSync calls and workspaceGroups lookups.
🪄 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: 3e2618f5-ab29-4d59-a568-d591cd31df6b

📥 Commits

Reviewing files that changed from the base of the PR and between 66a4315 and 468315f.

📒 Files selected for processing (7)
  • CLI/cmux.swift
  • Resources/Localizable.xcstrings
  • Sources/TabItemView+WorkspaceGroups.swift
  • Sources/TabManager.swift
  • Sources/TerminalController.swift
  • cmuxTests/WorkspaceGroupTests.swift
  • docs/workspace-groups.md

Comment on lines +4900 to +4905
guard let tabManager = v2ResolveTabManager(params: params) else {
return .err(code: "unavailable", message: "TabManager not available", data: nil)
}
guard let wsId = v2UUID(params, "workspace_id") else {
return .err(code: "invalid_params", message: "Missing or invalid workspace_id", data: nil)
}

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

Localize these new API errors and remove the TabManager leak.

This route is surfaced to CLI/API users, but the new failure paths still return raw English literals, and "TabManager not available" exposes an internal type name. Please switch these messages to localized keys and user-facing window/workspace wording, the same way Line 4923 already does for the grouped-workspace case.

As per coding guidelines, “All user-facing strings must be localized” and “API error bodies ... must not expose ... internal provider names” or other implementation details.

Also applies to: 4915-4918, 4936-4936

🤖 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/TerminalController.swift` around lines 4900 - 4905, Replace raw
English error messages and the TabManager type leak with localized keys and
user-facing wording: when v2ResolveTabManager(params:) returns nil, return a
localized error key (e.g. "window_unavailable" or similar existing localization
used around the grouped-workspace case) and a user-facing message that uses
"window" or "workspace" rather than exposing "TabManager"; likewise when
v2UUID(params, "workspace_id") fails, return a localized key (e.g.
"invalid_workspace_id") and a localized user-facing message. Update the three
matching sites that use the same patterns (the v2ResolveTabManager nil checks
and v2UUID workspace_id failure checks) to use the same localization keys and
avoid exposing internal type names, matching the style used in the
grouped-workspace error handling already present in this file.

Source: Coding guidelines

Comment on lines +4909 to +4936
let knownState: (exists: Bool, grouped: Bool) = v2MainSync {
guard let tab = tabManager.tabs.first(where: { $0.id == wsId }) else {
return (false, false)
}
return (true, tab.groupId != nil)
}
guard knownState.exists else {
return .err(code: "not_found", message: "Workspace not found in target window", data: [
"workspace_id": wsId.uuidString
])
}
guard !knownState.grouped else {
return .err(
code: "invalid_state",
message: String(
localized: "workspaceGroup.error.workspaceAlreadyGrouped",
defaultValue: "Workspace is already in a group; remove it first"
),
data: ["workspace_id": wsId.uuidString]
)
}
var createdGroupId: UUID?
v2MainSync {
createdGroupId = tabManager.makeWorkspaceGroupFromWorkspace(anchorWorkspaceId: wsId, name: name)
}
guard let gid = createdGroupId,
let group = v2MainSync({ tabManager.workspaceGroups.first(where: { $0.id == gid }) }) else {
return .err(code: "not_created", message: "Group was not created", data: nil)

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

Make the preflight and promotion atomic on the main thread.

Lines 4909-4936 split existence/grouped checks, creation, and group lookup across separate v2MainSync hops. Another socket/UI mutation can change the workspace between those hops, so a request that should return not_found or invalid_state instead falls through to the generic not_created branch.

Suggested structure
-        let knownState: (exists: Bool, grouped: Bool) = v2MainSync {
-            guard let tab = tabManager.tabs.first(where: { $0.id == wsId }) else {
-                return (false, false)
-            }
-            return (true, tab.groupId != nil)
-        }
-        guard knownState.exists else {
-            return .err(code: "not_found", message: "Workspace not found in target window", data: [
-                "workspace_id": wsId.uuidString
-            ])
-        }
-        guard !knownState.grouped else {
-            return .err(...)
-        }
-        var createdGroupId: UUID?
-        v2MainSync {
-            createdGroupId = tabManager.makeWorkspaceGroupFromWorkspace(anchorWorkspaceId: wsId, name: name)
-        }
-        guard let gid = createdGroupId,
-              let group = v2MainSync({ tabManager.workspaceGroups.first(where: { $0.id == gid }) }) else {
-            return .err(code: "not_created", message: "Group was not created", data: nil)
-        }
+        enum PromotionResult {
+            case notFound
+            case alreadyGrouped
+            case notCreated
+            case created(WorkspaceGroup)
+        }
+
+        let result: PromotionResult = v2MainSync {
+            guard let tab = tabManager.tabs.first(where: { $0.id == wsId }) else {
+                return .notFound
+            }
+            guard tab.groupId == nil else {
+                return .alreadyGrouped
+            }
+            guard let gid = tabManager.makeWorkspaceGroupFromWorkspace(anchorWorkspaceId: wsId, name: name),
+                  let group = tabManager.workspaceGroups.first(where: { $0.id == gid }) else {
+                return .notCreated
+            }
+            return .created(group)
+        }
🤖 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/TerminalController.swift` around lines 4909 - 4936, The
existence/grouped checks, creation call and group lookup must be executed
atomically on the main thread to avoid races; replace the separate v2MainSync
hops with a single v2MainSync closure that (using wsId) finds the tab in
tabManager, verifies it is not grouped, calls
tabManager.makeWorkspaceGroupFromWorkspace(anchorWorkspaceId:name:) to create
the group, and returns both the created group id and the created group (or an
explicit error state) so the subsequent guards can inspect a single, consistent
result instead of relying on knownState/createdGroupId across multiple
v2MainSync calls and workspaceGroups lookups.

@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

🤖 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 `@cmuxTests/WorkspaceGroupTests.swift`:
- Around line 922-923: Replace the direct property mutations of
manager.tabs[0].isPinned and manager.tabs[1].isPinned with the public API
manager.setPinned(workspace, pinned: true) so the test triggers the same side
effects (reordering/notifications) as production; locate the workspace objects
corresponding to tabs[0] and tabs[1] and call manager.setPinned(theWorkspace,
pinned: true) for each instead of assigning isPinned directly.
🪄 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: 203cbc43-d931-4c43-a923-c74dfd8081b9

📥 Commits

Reviewing files that changed from the base of the PR and between 468315f and 84cdbbb.

📒 Files selected for processing (1)
  • cmuxTests/WorkspaceGroupTests.swift

Comment on lines +922 to +923
manager.tabs[0].isPinned = true
manager.tabs[1].isPinned = 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.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Use setPinned API instead of direct mutation.

Lines 922-923 directly mutate isPinned, but the rest of this file consistently uses manager.setPinned(workspace, pinned: true) (see lines 147, 316, 339, 370, 387, 396, 421, 712). The setPinned method likely handles reordering, notification posting, or other side effects that direct mutation bypasses, which could make the test's initial state inconsistent with real usage.

♻️ Proposed fix
-    manager.tabs[0].isPinned = true
-    manager.tabs[1].isPinned = true
+    let firstWorkspace = try `#require`(manager.tabs.first { $0.id == ids[0] })
+    let secondWorkspace = try `#require`(manager.tabs.first { $0.id == ids[1] })
+    manager.setPinned(firstWorkspace, pinned: true)
+    manager.setPinned(secondWorkspace, pinned: true)
🤖 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 `@cmuxTests/WorkspaceGroupTests.swift` around lines 922 - 923, Replace the
direct property mutations of manager.tabs[0].isPinned and
manager.tabs[1].isPinned with the public API manager.setPinned(workspace,
pinned: true) so the test triggers the same side effects
(reordering/notifications) as production; locate the workspace objects
corresponding to tabs[0] and tabs[1] and call manager.setPinned(theWorkspace,
pinned: true) for each instead of assigning isPinned directly.

When there's no 2+ sidebar selection, Cmd-Shift-G now turns the single
focused workspace into a group in place, instead of doing nothing. It
defers to React Grab whenever React Grab would act on the current focus
(browser focused, or a single browser reachable from the focused
terminal), so React Grab keeps the chord on browser-bearing workspaces;
in a plain terminal (where it previously just beeped) it promotes.

Extracts a shared reactGrabShortcutRouteForCurrentFocus() so the gate and
the React Grab action use one route-resolution path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…in place

Turning the focused workspace into a single-member group (and ungrouping it)
changed the data model correctly but the sidebar didn't update: the group
header never appeared on create, and a stale header lingered after ungroup.

The sidebar list is a LazyVStack + ForEach over render items where a top-level
slot switches between a `.workspace` row and a `.groupHeader` (different view
types). LazyVStack mis-diffs that in-place type change, so the row view wasn't
swapped. Multi-select grouping never hit this because it inserts a brand-new
anchor workspace rather than transforming an existing row.

Key the rows on a signature of the group-anchor SET so the list rebuilds cleanly
whenever a workspace starts or stops being a group header. It is sorted, so
reorders and renames (which don't change the anchor set) don't trigger a rebuild
and drag-reorder is unaffected.

Verified visually via cua screenshots: keyboard Cmd-Shift-G on the focused
workspace now renders the header, and ungroup reverts it to a plain row.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lawrencecchen and others added 2 commits June 9, 2026 01:50
Merge with main raised these mega-files; the feature's small additions
(socket handler, CLI subcommand, TabManager primitive, sidebar render fix,
tests) push them a little further. Splitting them is out of scope.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
⌘⇧G now toggles for a single focused workspace: first press makes it a
single-member group, pressing again on that solo group ungroups it. A
workspace inside a multi-member group (its anchor or a child) is left
untouched so the toggle never dissolves a group with other members.

⌘⇧P (new .renameFocusedWorkspaceGroup action) renames the focused
workspace's group. It shares the chord with commandPalettePrevious, which
only acts while the palette is open (in-palette nav, dispatched earlier),
so they don't collide in practice. Full shortcut wiring: KeyboardShortcut
Settings case/label/default, dispatch + handler, cmux.json schema, docs,
localized label.

⌘⇧R (rename workspace) now also renames the group when the focused
workspace is a group anchor, since the group name is what the header
displays — hooked once in applyRenameFlow so every rename entrypoint
(palette, context menu, shortcut) renames both.

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

@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 7bcf7fd. Configure here.

// in-palette navigation, dispatched earlier), so this fires
// only when the palette is closed. No-ops gracefully when the
// focused workspace isn't in a group. Rebind in Settings.
return StoredShortcut(key: "p", command: true, shift: true, option: false, control: false)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Rename group shortcut blocked

Medium Severity

The new renameFocusedWorkspaceGroup action uses the same default chord as commandPalette (⌘⇧P). Shortcut dispatch handles commandPalette first and always consumes that keypress, so the group-rename handler never runs with factory defaults despite docs describing ⌘⇧P for renaming a focused workspace’s group.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 7bcf7fd. Configure here.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7bcf7fdcdc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Sources/ContentView.swift
Comment on lines +9778 to +9780
if let normalizedName,
let anchoredGroup = tabManager.workspaceGroups.first(where: { $0.anchorWorkspaceId == workspaceId }) {
tabManager.renameWorkspaceGroup(groupId: anchoredGroup.id, name: normalizedName)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Use a shared rename path for group anchors

When a promoted workspace is the group anchor, this special-case only fixes the command-palette rename flow. Other rename entrypoints still call setCustomTitle directly (for example socket/CLI workspace.rename in Sources/TerminalController.swift:5446 and the workspace rename alert in Sources/AppDelegate.swift:12436), but anchor display is sourced from group.name (TabManager.resolvedWorkspaceDisplayTitle), so those renames report success while the sidebar header/window title keep the old group name. Please move this anchor-aware behavior into a shared TabManager action used by every workspace rename path.

Useful? React with 👍 / 👎.

@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

🤖 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 `@CLI/cmux.swift`:
- Around line 19234-19241: The catch block that rethrows validation failures
wraps the error with error.localizedDescription which can lose detail; update
the catch in the invocation of CodexTeamsApprovalBridge.validateWorkingDirectory
to construct CLIError using String(describing: error) instead of
error.localizedDescription so full error information is preserved when creating
the CLIError instance.

In `@Sources/KeyboardShortcutSettings.swift`:
- Around line 371-377: Update the misleading comment on the
.renameFocusedWorkspaceGroup shortcut: replace the reference to
commandPalettePrevious with commandPalette (Cmd+Shift+P is shared with
.commandPalette, not Ctrl+P), and mirror the same correction to the related
comment in AppDelegate where renameFocusedWorkspaceGroup is discussed so both
files consistently state the chord is shared with .commandPalette and explain
the palette-open vs closed dispatch behavior.

In `@Sources/TabManager.swift`:
- Around line 5336-5343: The snapshot path currently uses
SharedLiveAgentIndex.shared.currentIndexSchedulingRefresh() without checking
freshness, risking persisting stale resume metadata; update the logic used by
workspace.sessionSnapshot(includeScrollback:restorableAgentIndex:) so it only
supplies the cached index when the cache is both loaded and fresh (e.g., expose
or check a freshness/loaded flag on SharedLiveAgentIndex or compare a
version/timestamp), and otherwise perform the authoritative
RestorableAgentSessionIndex.load() before creating the snapshot (or skip
persisting until a fresh index is available); ensure you change the call site
that now passes currentIndexSchedulingRefresh() to validate freshness and fall
back to RestorableAgentSessionIndex.load() when stale or cold.
🪄 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: c30272a8-b1ee-4e35-850e-3cc950385cf1

📥 Commits

Reviewing files that changed from the base of the PR and between 5630220 and 7bcf7fd.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (9)
  • CLI/cmux.swift
  • Resources/Localizable.xcstrings
  • Sources/AppDelegate.swift
  • Sources/ContentView.swift
  • Sources/KeyboardShortcutSettings.swift
  • Sources/TabManager.swift
  • Sources/TerminalController.swift
  • docs/workspace-groups.md
  • web/data/cmux.schema.json

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 3

🤖 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 `@CLI/cmux.swift`:
- Around line 19234-19241: The catch block that rethrows validation failures
wraps the error with error.localizedDescription which can lose detail; update
the catch in the invocation of CodexTeamsApprovalBridge.validateWorkingDirectory
to construct CLIError using String(describing: error) instead of
error.localizedDescription so full error information is preserved when creating
the CLIError instance.

In `@Sources/KeyboardShortcutSettings.swift`:
- Around line 371-377: Update the misleading comment on the
.renameFocusedWorkspaceGroup shortcut: replace the reference to
commandPalettePrevious with commandPalette (Cmd+Shift+P is shared with
.commandPalette, not Ctrl+P), and mirror the same correction to the related
comment in AppDelegate where renameFocusedWorkspaceGroup is discussed so both
files consistently state the chord is shared with .commandPalette and explain
the palette-open vs closed dispatch behavior.

In `@Sources/TabManager.swift`:
- Around line 5336-5343: The snapshot path currently uses
SharedLiveAgentIndex.shared.currentIndexSchedulingRefresh() without checking
freshness, risking persisting stale resume metadata; update the logic used by
workspace.sessionSnapshot(includeScrollback:restorableAgentIndex:) so it only
supplies the cached index when the cache is both loaded and fresh (e.g., expose
or check a freshness/loaded flag on SharedLiveAgentIndex or compare a
version/timestamp), and otherwise perform the authoritative
RestorableAgentSessionIndex.load() before creating the snapshot (or skip
persisting until a fresh index is available); ensure you change the call site
that now passes currentIndexSchedulingRefresh() to validate freshness and fall
back to RestorableAgentSessionIndex.load() when stale or cold.
🪄 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: c30272a8-b1ee-4e35-850e-3cc950385cf1

📥 Commits

Reviewing files that changed from the base of the PR and between 5630220 and 7bcf7fd.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (9)
  • CLI/cmux.swift
  • Resources/Localizable.xcstrings
  • Sources/AppDelegate.swift
  • Sources/ContentView.swift
  • Sources/KeyboardShortcutSettings.swift
  • Sources/TabManager.swift
  • Sources/TerminalController.swift
  • docs/workspace-groups.md
  • web/data/cmux.schema.json
🛑 Comments failed to post (3)
CLI/cmux.swift (1)

19234-19241: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use String(describing:) when wrapping validation errors into CLIError.

At Line 19240, error.localizedDescription can collapse important details from non-LocalizedError failures. Wrap with String(describing: error) so CLI diagnostics keep full context.

Suggested patch
         do {
             try CodexTeamsApprovalBridge.validateWorkingDirectory(
                 commandArgs: commandArgs,
                 baseDirectory: baseDirectory
             )
         } catch {
-            throw CLIError(message: error.localizedDescription)
+            throw CLIError(message: String(describing: error))
         }

Based on learnings, CLI error formatting in this repo should use String(describing: error) instead of error.localizedDescription to preserve full error detail.

🤖 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 `@CLI/cmux.swift` around lines 19234 - 19241, The catch block that rethrows
validation failures wraps the error with error.localizedDescription which can
lose detail; update the catch in the invocation of
CodexTeamsApprovalBridge.validateWorkingDirectory to construct CLIError using
String(describing: error) instead of error.localizedDescription so full error
information is preserved when creating the CLIError instance.

Source: Learnings

Sources/KeyboardShortcutSettings.swift (1)

371-377: ⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify shortcut conflict handling for Cmd+Shift+P between commandPalette and renameFocusedWorkspaceGroup

echo "Searching for dispatch logic that handles Cmd+Shift+P priority between commandPalette and renameFocusedWorkspaceGroup..."

rg -n -C5 'renameFocusedWorkspaceGroup|commandPalette' Sources/AppDelegate.swift | head -100

Repository: manaflow-ai/cmux

Length of output: 4873


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# 1) Confirm bindings for commandPalettePrevious vs commandPalette and the renameFocusedWorkspaceGroup comment context
FILE="Sources/KeyboardShortcutSettings.swift"
echo "== Relevant shortcut cases in $FILE =="
rg -n "case \.(commandPalettePrevious|commandPalette|renameFocusedWorkspaceGroup)" "$FILE" -n
echo
echo "== Neighborhood around renameFocusedWorkspaceGroup (approx 320-420) =="
sed -n '330,410p' "$FILE" | nl -ba | sed -n '1,90p'
echo
echo "== Neighborhood around commandPalettePrevious/commandPalette (approx 260-340) =="
sed -n '260,340p' "$FILE" | nl -ba | sed -n '1,90p'

Repository: manaflow-ai/cmux

Length of output: 1037


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# 2) Find where AppDelegate routes keyboard shortcut actions for commandPalette vs renameFocusedWorkspaceGroup
FILE="Sources/AppDelegate.swift"
echo "== References to renameFocusedWorkspaceGroup in $FILE =="
rg -n "renameFocusedWorkspaceGroup" "$FILE" || true
echo
echo "== References to commandPalette (routing/shortcut handling) in $FILE =="
rg -n "commandPalette" "$FILE" | head -200
echo
echo "== Look for key/shortcut dispatch around the command palette rename workspace flows =="
rg -n -C6 "(renameFocusedWorkspaceGroup|commandPalette)(Opened|open|visible|isOpen|pending|dispatch|shortcut|route|performKeyEquivalent|keyEquivalent|handle)" "$FILE" | head -200

Repository: manaflow-ai/cmux

Length of output: 15801


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="Sources/KeyboardShortcutSettings.swift"
echo "== KeyboardShortcutSettings.swift around renameFocusedWorkspaceGroup (lines ~350-390) =="
sed -n '350,390p' "$FILE" | cat -n

echo
echo "== KeyboardShortcutSettings.swift around commandPalette/commandPalettePrevious (lines ~280-330) =="
sed -n '280,330p' "$FILE" | cat -n

Repository: manaflow-ai/cmux

Length of output: 7526


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="Sources/AppDelegate.swift"
echo "== AppDelegate.swift around renameFocusedWorkspaceGroup match (lines ~13180-13260) =="
sed -n '13160,13260p' "$FILE" | cat -n

echo
echo "== AppDelegate.swift around commandPalette handler (lines ~12730-12840) =="
sed -n '12720,12860p' "$FILE" | cat -n

Repository: manaflow-ai/cmux

Length of output: 13581


Fix misleading chord-conflict comment for Cmd+Shift+P (.renameFocusedWorkspaceGroup)

The comment says it shares the chord with commandPalettePrevious, but commandPalettePrevious is Ctrl+P; the actual shared Cmd+Shift+P binding is .commandPalette. The “palette open vs closed” behavior described matches how AppDelegate.swift routes .commandPalette when the palette is effective, so only the conflicting action name needs correction (also update the similar comment in Sources/AppDelegate.swift for renameFocusedWorkspaceGroup).

🔧 Proposed fix
             case .renameFocusedWorkspaceGroup:
-                // Cmd+Shift+P. Shares the chord with commandPalettePrevious,
+                // Cmd+Shift+P. Shares the chord with commandPalette,
                 // but that only acts while the command palette is OPEN (it's
                 // in-palette navigation, dispatched earlier), so this fires
                 // only when the palette is closed. No-ops gracefully when the
                 // focused workspace isn't in a group. Rebind in Settings.
                 return StoredShortcut(key: "p", command: true, shift: true, option: false, control: false)
🤖 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/KeyboardShortcutSettings.swift` around lines 371 - 377, Update the
misleading comment on the .renameFocusedWorkspaceGroup shortcut: replace the
reference to commandPalettePrevious with commandPalette (Cmd+Shift+P is shared
with .commandPalette, not Ctrl+P), and mirror the same correction to the related
comment in AppDelegate where renameFocusedWorkspaceGroup is discussed so both
files consistently state the chord is shared with .commandPalette and explain
the palette-open vs closed dispatch behavior.
Sources/TabManager.swift (1)

5336-5343: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Don't persist closed-workspace snapshots from an unchecked cache.

This is a history/snapshot path, but the new branch only distinguishes “cache missing” from “cache present.” If SharedLiveAgentIndex is warm but older than the authoritative restorable-agent index, workspace.sessionSnapshot(...) will persist stale resume metadata for the closed workspace. The nil fallback also means the original synchronous load() is still reachable on the main-actor close path, so the freeze is only avoided when the cache is both loaded and fresh.

As per coding guidelines, when a persistence/history/snapshot path substitutes a cached value for an authoritative read, it must explicitly handle both cold and stale caches or document a graceful-degradation rationale.

🤖 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/TabManager.swift` around lines 5336 - 5343, The snapshot path
currently uses SharedLiveAgentIndex.shared.currentIndexSchedulingRefresh()
without checking freshness, risking persisting stale resume metadata; update the
logic used by workspace.sessionSnapshot(includeScrollback:restorableAgentIndex:)
so it only supplies the cached index when the cache is both loaded and fresh
(e.g., expose or check a freshness/loaded flag on SharedLiveAgentIndex or
compare a version/timestamp), and otherwise perform the authoritative
RestorableAgentSessionIndex.load() before creating the snapshot (or skip
persisting until a fresh index is available); ensure you change the call site
that now passes currentIndexSchedulingRefresh() to validate freshness and fall
back to RestorableAgentSessionIndex.load() when stale or cold.

Source: Coding guidelines

This branch was successfully deployed

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

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants