Skip to content

iOS: render workspace groups as collapsible sections - #5625

Closed
lawrencecchen wants to merge 4 commits into
mainfrom
feat-ios-groups-mobile
Closed

lawrencecchen wants to merge 4 commits into
mainfrom
feat-ios-groups-mobile

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jun 8, 2026 •

Copy link
Copy Markdown
Contributor

The iOS app flattened workspaces and ignored the desktop's named, collapsible groups. This renders groups on the phone: collapsible named sections in group order, members nested under the group header, mirroring desktop semantics (the anchor workspace renders as the header, not a separate row; collapsing hides members but keeps the header).

P1 is display plus expand/collapse from the phone. Phone-side group create/rename/restructure is deferred.

Did the mobile payload already carry group info?

No. mobileWorkspacePayload emitted only id/title/current_directory/is_selected/is_pinned/terminals, and v2MobileWorkspaceList had no groups. The first step was to surface it on the Mac host, then render it.

Mac host (surface group structure)

  • mobileWorkspacePayload now emits group_id per workspace (nil for ungrouped). v2MobileWorkspaceList adds a top-level groups array (id, name, is_collapsed, is_pinned, anchor_workspace_id, member_workspace_ids). The all-windows branch aggregates each window's groups in window-iteration order (groups are per-TabManager).
  • MobileWorkspaceListObserver now subscribes to $workspaceGroups and folds group order/name/collapse/pin/anchor plus each workspace's groupId into its summary hash. Without this a phone collapse toggles isCollapsed on the Mac but the observer never re-emits workspace.updated, so the disclosure would look frozen.
  • workspace.group.collapse/workspace.group.expand are exposed to mobile (added to mobileHostHandleRPC and the ticket-auth model in MobileHostService), gated by the same same-account Stack auth as the rest of the data plane. Routing is by group_id, which v2ResolveTabManager already resolves to the owning window across all windows. New workspace.groups.v1 capability so iOS feature-detects.

iOS (decode + render + collapse)

  • MobileSyncWorkspaceListResponse decodes an optional group_id per workspace and an optional groups array (backward compatible: both absent on older Macs).
  • MobileWorkspaceGroupPreview value model; MobileWorkspaceListItem.items builds the ordered render items mirroring SidebarWorkspaceRenderItem (anchor as header, collapsed hides members, ungrouped interleave by position, unknown group degrades to an ungrouped row).
  • WorkspaceGroupHeaderRow: the chevron toggles collapse; tapping the name selects/opens the anchor workspace, so the anchor's terminals stay reachable (mirrors the desktop header, whose chevron collapses and whose body focuses the anchor).
  • Store carries workspaceGroups + supportsWorkspaceGroups and a fire-and-forget setWorkspaceGroupCollapsed RPC (authoritative re-fetch via the observer, no local optimistic state). The grouped section renders only when the Mac advertises the capability and the user is not searching; otherwise the list stays flat (pinned-first), preserving member contiguity. Search deliberately flattens so members are findable across groups.

Where the list renders groups

  • Packages/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileWorkspaceListItem.swift (render-item builder)
  • Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceListView.swift groupedRows (the grouped List section)
  • Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceGroupHeaderRow.swift (the collapsible header)

Verification

  • iOS simulator build (tagged derivedDataPath, build-only): BUILD SUCCEEDED.
  • macOS app build (build-only): BUILD SUCCEEDED.
  • 6 pure unit tests for the render-item grouping logic (MobileWorkspaceListItemTests) pass.
  • Collapse/expand round-trip is wired and the observer hash makes it sound, but the autonomous simulator cannot pair to a Mac, so the end-to-end collapse round-trip is pending on-device dogfood.

New user-facing strings localized (en + ja).

🤖 Generated with Claude Code


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


Note

Medium Risk
Touches mobile RPC payloads, multi-window list aggregation, and sync observer hashing; display-only mutations but incorrect merge/refresh logic could flatten or freeze group UI on iOS.

Overview
The iOS workspace list no longer ignores Mac sidebar groups: it can show collapsible named sections (anchor as header, indented members) and collapse/expand from the phone, with search still using a flat pinned-first list.

Mac host extends the mobile workspace.list payload with per-workspace group_id and a top-level groups array (including multi-window aggregation). It adds mobile RPC handlers for workspace.group.collapse / expand, advertises workspace.groups.v1, and moves list serialization into TerminalController+MobileWorkspaceList.swift. MobileWorkspaceListObserver now watches $workspaceGroups and each workspace’s groupId in its summary hash so collapse and membership changes push workspace.updated.

iOS decodes the new fields (backward compatible), maps them to MobileWorkspaceGroupPreview / groupID on previews, and builds rows via MobileWorkspaceListItem plus WorkspaceGroupHeaderRow. The shell store keeps workspaceGroups, gates on capability (or emitted groups), and sends fire-and-forget collapse RPCs without optimistic local state; full-list refreshes only update groups so merge paths do not clear sections.

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


Summary by cubic

Adds collapsible workspace groups to the iOS workspace list to mirror desktop: the anchor renders as the header, members nest beneath, and collapsing hides members. The Mac now emits group data in the mobile payload and supports collapse/expand over mobile RPC.

  • New Features

    • Mac host: adds group_id per workspace and a top-level groups array in v2MobileWorkspaceList (aggregated across windows in order); exposes workspace.group.collapse/workspace.group.expand; advertises workspace.groups.v1.
    • iOS: decodes optional group_id and groups (defaults groups to empty for older Macs); maps to MobileWorkspaceGroupPreview; MobileWorkspaceListItem builds ordered items (anchor-as-header, collapsed hides members, unknown groups render ungrouped); WorkspaceGroupHeaderRow toggles collapse and opens the anchor, and the chevron is passive when toggle isn’t available; grouped layout renders when the payload includes groups and search is empty, otherwise the list stays flat (pinned-first); groups update only on full-list refresh (merge responses omit groups); adds a11y labels (en, ja).
  • Bug Fixes

    • MobileWorkspaceListObserver now watches $workspaceGroups and per-workspace $groupId, so collapse/rename/membership moves re-emit workspace.updated; adds tests (including a pure membership-move hash check).

Written for commit 660cd47. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Mobile now supports Mac-synced workspace groups with collapsible sections, anchor headers, and host-capability detection; iOS can send collapse/expand requests.
  • UI Changes

    • Workspace list renders Mac-style grouped presentation (falls back to flat during search or when unsupported); headers toggle collapse and select anchor workspaces.
  • Accessibility

    • Added localized labels for expand/collapse.
  • Tests

    • Added unit tests validating grouped rendering and observer change detection.
  • Documentation

    • Added iOS design spec for mobile workspace groups.

@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 10, 2026 12:10am
cmux-staging Building Building Preview, Comment Jun 10, 2026 12:10am

@coderabbitai

coderabbitai Bot commented Jun 8, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds Mac-style collapsible workspace groups to mobile: RPC decoding and mapping, preview/list models, grouped UI and header row, shell capability/state and collapse RPC, observer hashing for updates, backend payload routing, tests, localization, and design docs.

Changes

iOS Workspace Groups Feature

Layer / File(s) Summary
RPC data models and remote mapping
Packages/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileSyncWorkspaceListResponse.swift, Packages/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileWorkspacePreview+RemoteMapping.swift
MobileSyncWorkspaceListResponse adds Group and top-level groups, and Workspace.groupID; remote mapping initializes preview groupID and constructs MobileWorkspaceGroupPreview.
iOS preview data models and list builder
Packages/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileWorkspaceGroupPreview.swift, Packages/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileWorkspacePreview.swift, Packages/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileWorkspaceListItem.swift
New MobileWorkspaceGroupPreview with ID/name/collapse/pin/anchor; MobileWorkspacePreview gains groupID; MobileWorkspaceListItem.items(workspaces:groups:) builds ordered grouped or flat snapshots honoring anchor/indent/collapse semantics.
List item rendering tests
Packages/CmuxMobileShellModel/Tests/CmuxMobileShellModelTests/MobileWorkspaceListItemTests.swift
Tests validate grouped/flat rendering, anchor-as-header behavior, collapsed member hiding, interleaving by workspace order, handling unknown group IDs, and header-only groups.
Shell state, capability, and workspace actions
Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift, Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+WorkspaceActions.swift
Adds workspaceGroups and supportsWorkspaceGroups, host capability parsing and reset logic, renameWorkspace/setWorkspacePinned/setWorkspaceGroupCollapsed fire-and-forget RPCs with logging, and avoids overwriting group state on merge updates.
Observer hashing and publishers
Sources/Mobile/MobileWorkspaceListObserver.swift
Observer subscribes to TabManager.$workspaceGroups, includes group order and render-relevant group fields plus per-workspace groupId in summaryHash, and updates test helper to accept groups.
UI components and list rendering
Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceGroupHeaderRow.swift, Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceListView.swift, Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift
WorkspaceGroupHeaderRow implements disclosure chevron and anchor navigation; WorkspaceListView conditionally renders groupedSections vs flatRows, gates grouped rendering on empty search, and accepts a toggleGroupCollapsed closure; WorkspaceShellView wires group data and collapse closure from shell state.
Mac backend RPC routing and mobile payloads
Sources/Mobile/MobileHostService+Capabilities.swift, Sources/Mobile/MobileHostService.swift, Sources/TerminalController+MobileWorkspaceList.swift, Sources/TerminalController.swift
Advertises workspace.groups.v1 capability, treats collapse/expand as display-only for ticket auth, routes workspace.group.collapse/expand RPCs, implements v2MobileWorkspaceList with groups + per-workspace group_id, validation, and serializers.
Project, tests, localization, and docs
cmux.xcodeproj/project.pbxproj, cmuxTests/MobileWorkspaceListFidelityTests.swift, ios/cmux/Resources/Localizable.xcstrings, plans/feat-ios-groups-mobile/DESIGN.md
Adds new source files to project, a fidelity test ensuring moving a workspace between groups changes observer hash, two accessibility localization strings for expand/collapse, and a design document describing end-to-end behavior and rollout.

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly Related PRs

Suggested Reviewers

  • Ari4ka

Poem

🐰 Groups on the glass,
Chevrons whisper, rows align—
Anchor leads the pass.
Collapsed, unfolded, fine;
Mobile and Mac entwine.


Important

Pre-merge checks failed

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

❌ Failed checks (5 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Swift Actor Isolation ❌ Error File-scoped Logger constants in @MainActor contexts lack nonisolated marking: MobileWorkspaceListObserver.swift line 5 and MobileShellComposite+WorkspaceActions.swift lines 6-8. Mark both loggers as nonisolated private let to match established patterns in SessionIndexStore and TerminalNotificationStore, avoiding unnecessary MainActor coupling.
Cmux Cache Substitution Correctness ❌ Error workspaceGroups cache is not cleared on disconnection; stale groups persist across Mac reconnects, causing the UI to render old group structure without staleness handling. Clear workspaceGroups in resetTerminalOutputTracking() (set to []), or gate rendering on supportsWorkspaceGroups to prevent stale group rendering.
Cmux Swift Concurrency ❌ Error PR introduces fire-and-forget Tasks in SwiftUI closures (WorkspaceShellView for setWorkspaceGroupCollapsed/renameWorkspace/setWorkspacePinned) without lifecycle management or cancellation support. Store returned Tasks with cancellation support or use structured concurrency tied to view lifecycle instead of discarding Tasks in closures.
Cmux Swift @Concurrent ❌ Error Network-heavy async functions in @MainActor called via Task { } closures lack explicit actor hop or @concurrent, violating the concurrency rules against network work on UI isolation. Change Task { } to Task.detached(priority: .userInitiated) { } in closures to execute network I/O off main thread per established codebase pattern.
Cmux Swift Logging ❌ Error MobileShellComposite+WorkspaceActions.swift adds a file-scoped Logger (line 6) in an extension of @MainActor class without 'nonisolated' keyword, violating swift-logging.md rule. Change line 6-9 from 'private let mobileShellLog' to 'nonisolated private let mobileShellLog' per preferred Logger pattern in swift-logging.md.
Docstring Coverage ⚠️ Warning Docstring coverage is 28.21% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (15 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: rendering workspace groups as collapsible sections on iOS, which aligns with the core functionality added across all files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Blocking Runtime ✅ Passed 6 new Swift files added; comprehensive scan found no blocking/timing primitives (semaphores, locks, sleep, main.sync). Async operations use fire-and-forget Task { await } pattern.
Cmux Expensive Synchronous Load ✅ Passed PR adds iOS workspace groups UI/RPC without expensive I/O. No RestorableAgentSessionIndex.load(), sysctl, or FileManager calls in new interactive paths; only safe dictionary/array operations.
Cmux No Hacky Sleeps ✅ Passed PR modifies only Swift, localization XML, documentation, and Xcode configuration. The rule scope explicitly covers TypeScript, JavaScript, shell, and non-Swift runtime scripts; Swift is out of scope.
Cmux Algorithmic Complexity ✅ Passed PR uses linear algorithms: items() O(W+G) with dicts; group payload single-passes tabs; merge rescans single entries; search throttled 80ms; observer throttled. No nested scans or batch rescans found.
Cmux Swift File And Package Boundaries ✅ Passed All new files under 400 lines with single responsibilities. Domain models in packages, UI in packages, RPC glue in app target per rules. TerminalController net -152 lines.
Cmux User-Facing Error Privacy ✅ Passed RPC error messages are generic without vendor/provider names or sensitive details. Localization strings are safe accessibility labels. iOS logging uses privacy markers.
Cmux Full Internationalization ✅ Passed New accessibility labels use L10n.string() with matching Localizable.xcstrings entries (en/ja). Group names from server data don't require localization. Design docs and models are exempt.
Cmux Swiftui State Layout ✅ Passed All SwiftUI state patterns are modern: @Observable store, value snapshots in list rows, proper closures for actions, no GeometryReader, @Published, or render-time mutations.
Cmux Architecture Rethink ✅ Passed PR uses fire-and-forget RPC with observer pattern, single state owners; no timing repairs, locks, polling, or split lifecycle issues found.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR adds SwiftUI Views and data models only; no NSWindow, NSPanel, NSWindowController, or Window/WindowGroup instances created. Passes rule exceptions for views in existing app.
Cmux Source Artifacts ✅ Passed All 20 changed files are hand-written source code, tests, configs, docs, or localization—none are build artifacts or generated files violating the source-control-artifacts rule.
Description check ✅ Passed PR description is comprehensive and addresses all template sections with substantive content about changes, testing, and verification.
✨ 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-ios-groups-mobile

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

❤️ Share

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

@greptile-apps

greptile-apps Bot commented Jun 8, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR surfaces Mac workspace groups on iOS: the Mac now emits group_id per workspace and a top-level groups array in v2MobileWorkspaceList, exposes workspace.group.collapse/expand to mobile, and advertises workspace.groups.v1; the iOS side decodes optional group fields (backward-compatible with older Macs), builds ordered render items via MobileWorkspaceListItem.items, and renders collapsible group headers through WorkspaceGroupHeaderRow, with a fire-and-forget collapse RPC and authoritative re-fetch via the observer.

  • Mac host: MobileWorkspaceListObserver now watches $workspaceGroups and per-workspace $groupId so collapse/rename/membership changes re-emit workspace.updated; TerminalController+MobileWorkspaceList.swift extracts mobile-list payload helpers from the main controller file; multi-window enumeration aggregates each window's groups in window order.
  • iOS model & rendering: MobileWorkspaceGroupPreview is a pure value model; MobileWorkspaceListItem.items mirrors SidebarWorkspaceRenderItem (anchor-as-header, collapse hides members, ungrouped rows interleave by position, unknown groupID degrades to ungrouped); grouped sections render only when the Mac emits groups and search is empty.
  • State & capability gating: workspaceGroups and supportsWorkspaceGroups added to MobileShellComposite; toggleGroupCollapsedClosure enabled when either the capability flag is confirmed or non-empty groups have already arrived (covers slow/failed status fetch without losing group interactivity).

Confidence Score: 4/5

Safe to merge with one known gap to watch: the end-to-end collapse round-trip (phone → Mac RPC → observer re-emit → phone refresh) requires a physical device paired to a Mac and is not yet covered by automated tests.

The Mac-side payload changes, observer subscriptions, and iOS rendering logic are all sound and well-tested in isolation (unit tests cover render-item grouping and observer hash fidelity). The multi-window group aggregation correctly appends groups in window order and derives membership from per-workspace group_id. The fire-and-forget + authoritative re-fetch design avoids optimistic state drift. The one open area is that a group header can be emitted with an anchorWorkspaceID absent from the workspace list during transient payload skew, potentially leaving a blank detail pane until the next refresh.

TerminalController+MobileWorkspaceList.swift (mobileWorkspaceGroupPayloads) — consider filtering out groups whose anchor workspace is not present in the workspace list to avoid a tappable header that navigates to a non-existent workspace view.

Important Files Changed

Filename Overview
Sources/TerminalController+MobileWorkspaceList.swift New file extracting mobile workspace-list payload helpers; mobileWorkspaceGroupPayloads is linear (single pass over tabs to build memberIDsByGroup); multi-window group aggregation appends in window order; v2MobileWorkspaceGroupSetCollapsed validates group_id before delegating to the shared v2WorkspaceGroupSetCollapsed path.
Sources/Mobile/MobileWorkspaceListObserver.swift Adds $workspaceGroups and per-workspace $groupId subscriptions so collapses/membership moves re-emit workspace.updated; hash correctly covers group order, name, collapse, pin, anchor, and membership; hash-based deduplication prevents double-emitting when both subscriptions fire in the same throttle window.
Packages/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileWorkspaceListItem.swift Pure O(n+g) render-item builder; correctly emits anchor-as-header and suppresses collapsed members; non-contiguous same-group members (noted in prior review thread) render indented without a nearby header but collapse is correctly applied via collapsedByGroupID set at first-header emit.
Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceGroupHeaderRow.swift New collapsible header view; chevron correctly gates .isButton trait on the interactive branch; accessibility label applied to outer Group covers both branches — passive chevron a11y behavior noted in prior review thread.
Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceListView.swift Adds grouped rendering path (groupedRows / groupedListItems) gated on !groups.isEmpty && trimmedQuery.isEmpty; flat path and search still use pinned-first filteredWorkspaces; value-type snapshots cross the List boundary.
Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift Adds workspaceGroups and supportsWorkspaceGroups; workspace actions extracted to MobileShellComposite+WorkspaceActions.swift; workspaceGroups updated only on full (non-merge) responses; supportsWorkspaceGroups reset in resetTerminalOutputTracking.
Packages/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileSyncWorkspaceListResponse.swift Adds Group struct and groups array with decodeIfPresent defaulting to [] for backward compat; member_workspace_ids intentionally omitted since iOS derives membership from per-workspace group_id; custom init correctly handles all optional fields.
Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift Passes workspaceGroups and toggleGroupCollapsedClosure to both split-view and navigation-stack WorkspaceListView instances; toggleGroupCollapsedClosure condition (supportsWorkspaceGroups
ios/cmux/Resources/Localizable.xcstrings Adds mobile.workspaceGroup.expand.a11y and mobile.workspaceGroup.collapse.a11y with both en and ja translations, covering the complete locale set in this catalog.
cmuxTests/MobileWorkspaceListFidelityTests.swift New test confirms a pure groupId change changes the observer hash, directly guarding the $groupId subscription added to MobileWorkspaceListObserver.

Sequence Diagram

sequenceDiagram
    participant iOS as iOS App
    participant Store as MobileShellComposite
    participant Mac as Mac TerminalController
    participant Obs as MobileWorkspaceListObserver

    iOS->>Mac: workspace.list (initial sync)
    Mac-->>iOS: workspaces + groups array
    Store->>Store: "workspaceGroups = response.groups"
    iOS->>Mac: mobile.host.status
    Mac-->>iOS: capabilities including workspace.groups.v1
    Store->>Store: "supportsWorkspaceGroups = true"

    Note over iOS: User taps chevron
    iOS->>Mac: workspace.group.collapse with group_id
    Mac->>Mac: v2WorkspaceGroupSetCollapsed
    Obs->>Obs: workspaceGroups fires, emitIfNeeded
    Mac-->>iOS: workspace.updated event
    iOS->>Mac: workspace.list re-fetch
    Mac-->>iOS: workspaces + groups with isCollapsed true
    Store->>Store: workspaceGroups updated
Loading

Reviews (4): Last reviewed commit: "Split mobile list payloads and actions o..." | Re-trigger Greptile

Comment on lines +33 to +51
return Group {
if let toggleCollapsed {
Button {
toggleCollapsed(group.id, !group.isCollapsed)
} label: {
image
}
.buttonStyle(.plain)
} else {
image
}
}
.accessibilityAddTraits(.isButton)
.accessibilityLabel(
group.isCollapsed
? L10n.string("mobile.workspaceGroup.expand.a11y", defaultValue: "Expand group")
: L10n.string("mobile.workspaceGroup.collapse.a11y", defaultValue: "Collapse group")
)
.accessibilityIdentifier("MobileWorkspaceGroupDisclosure-\(group.id.rawValue)")

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 .accessibilityAddTraits(.isButton) is applied unconditionally to the Group, so when toggleCollapsed is nil the chevron image carries the isButton trait but has no activation action — VoiceOver will announce "Expand/Collapse group, button" and then nothing happens. The trait (and the label) should only be present on the interactive branch; the non-interactive chevron should be hidden from accessibility entirely since it carries no actionable meaning.

Suggested change
return Group {
if let toggleCollapsed {
Button {
toggleCollapsed(group.id, !group.isCollapsed)
} label: {
image
}
.buttonStyle(.plain)
} else {
image
}
}
.accessibilityAddTraits(.isButton)
.accessibilityLabel(
group.isCollapsed
? L10n.string("mobile.workspaceGroup.expand.a11y", defaultValue: "Expand group")
: L10n.string("mobile.workspaceGroup.collapse.a11y", defaultValue: "Collapse group")
)
.accessibilityIdentifier("MobileWorkspaceGroupDisclosure-\(group.id.rawValue)")
return Group {
if let toggleCollapsed {
Button {
toggleCollapsed(group.id, !group.isCollapsed)
} label: {
image
}
.buttonStyle(.plain)
.accessibilityAddTraits(.isButton)
.accessibilityLabel(
group.isCollapsed
? L10n.string("mobile.workspaceGroup.expand.a11y", defaultValue: "Expand group")
: L10n.string("mobile.workspaceGroup.collapse.a11y", defaultValue: "Collapse group")
)
.accessibilityIdentifier("MobileWorkspaceGroupDisclosure-\(group.id.rawValue)")
} else {
image
.accessibilityHidden(true)
}
}

Comment on lines +55 to +82
var lastEmittedGroupID: MobileWorkspaceGroupPreview.ID?
var emittedHeaders: Set<MobileWorkspaceGroupPreview.ID> = []
var collapsedByGroupID: [MobileWorkspaceGroupPreview.ID: Bool] = [:]

for workspace in workspaces {
// Resolve the membership only when the referenced group actually
// exists; otherwise treat the workspace as ungrouped.
let groupID: MobileWorkspaceGroupPreview.ID? = workspace.groupID
.flatMap { groupsByID[$0] != nil ? $0 : nil }

if groupID != lastEmittedGroupID {
lastEmittedGroupID = groupID
if let groupID, let group = groupsByID[groupID], !emittedHeaders.contains(groupID) {
items.append(.groupHeader(group))
emittedHeaders.insert(groupID)
collapsedByGroupID[groupID] = group.isCollapsed
}
}

if let groupID, let group = groupsByID[groupID], group.anchorWorkspaceID == workspace.id {
// Anchor is represented exclusively by the group header.
continue
}

let isCollapsed = groupID.map { collapsedByGroupID[$0] ?? false } ?? false
if groupID == nil || !isCollapsed {
items.append(.workspace(workspace, indented: groupID != nil))
}

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 Non-contiguous same-group members render with a dangling indent

lastEmittedGroupID prevents emitting a second header for the same group, but the algorithm doesn't document what happens when group members are non-contiguous (e.g., [A(g1/anchor), B(ungrouped), C(g1)]). In that case C renders as indented: true after the ungrouped B, with no visible parent header nearby. Collapse still works correctly (because collapsedByGroupID was set at the first header), so this is a visual-only issue — but it's an undocumented edge case worth a comment since the graceful-degradation note in the doc-comment only covers the unknown-groupID case, not the non-contiguous same-group case. Mac groups are always contiguous today, but a brief payload skew could produce this ordering transiently.

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!

@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/Mobile/MobileHostService.swift (1)

304-317: 🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Extract new capability/auth policy logic out of this oversized file.

This production file is already far beyond the repo’s Swift size/responsibility budget; adding more feature branches here increases coupling and makes future changes riskier. Please move the mobile capability/ticket-policy surface touched by this PR into a focused helper/type and keep MobileHostService as orchestration glue.

As per coding guidelines: {Sources,CLI,Packages,cmuxTests,cmuxUITests}/**/*.swift should be flagged when production Swift files exceed the size thresholds and mix responsibilities.

🤖 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/Mobile/MobileHostService.swift` around lines 304 - 317, The
MobileHostService nonisolated static var mobileHostCapabilities is implementing
capability and auth policy logic inside a large production file; extract this
surface into a focused helper type (e.g., MobileCapabilities or
MobileHostPolicy) that owns the capability list and any ticket/policy logic,
then update MobileHostService to call that helper (replace direct access to
mobileHostCapabilities with MobileCapabilities.shared.capabilities or similar).
Move all capability strings and any related ticket-policy decision code out of
MobileHostService into the new type, keep MobileHostService as orchestration
glue only, and ensure the new helper is small, well-named, and covered by
existing tests or add lightweight unit tests for its policy decisions.

Source: Coding guidelines

🤖 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 21284-21286: mobileWorkspaceGroupPayload currently does a full
scan of the tabs array per group (memberIds = tabs.compactMap { $0.groupId ==
group.id ? ... }), causing O(groups × workspaces) work; instead build a single
index mapping groupId -> [workspaceId] once for the tabs list and reuse it when
serializing each group. Modify the call site that iterates groups to first
compute let workspacesByGroup = Dictionary(grouping: tabs, by: { $0.groupId })
(or a helper function that returns [UUID: [String]]), then change
mobileWorkspaceGroupPayload to accept that precomputed map (or add an overload)
and use workspacesByGroup[group.id] to create memberIds, avoiding repeated scans
of tabs.

---

Outside diff comments:
In `@Sources/Mobile/MobileHostService.swift`:
- Around line 304-317: The MobileHostService nonisolated static var
mobileHostCapabilities is implementing capability and auth policy logic inside a
large production file; extract this surface into a focused helper type (e.g.,
MobileCapabilities or MobileHostPolicy) that owns the capability list and any
ticket/policy logic, then update MobileHostService to call that helper (replace
direct access to mobileHostCapabilities with
MobileCapabilities.shared.capabilities or similar). Move all capability strings
and any related ticket-policy decision code out of MobileHostService into the
new type, keep MobileHostService as orchestration glue only, and ensure the new
helper is small, well-named, and covered by existing tests or add lightweight
unit tests for its policy decisions.
🪄 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: 8d84a773-57a6-4dad-9bab-7639dc021339

📥 Commits

Reviewing files that changed from the base of the PR and between 4424644 and bb79d1f.

📒 Files selected for processing (15)
  • Packages/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileSyncWorkspaceListResponse.swift
  • Packages/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileWorkspacePreview+RemoteMapping.swift
  • Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileWorkspaceGroupPreview.swift
  • Packages/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileWorkspaceListItem.swift
  • Packages/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileWorkspacePreview.swift
  • Packages/CmuxMobileShellModel/Tests/CmuxMobileShellModelTests/MobileWorkspaceListItemTests.swift
  • Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceGroupHeaderRow.swift
  • Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceListView.swift
  • Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift
  • Sources/Mobile/MobileHostService.swift
  • Sources/Mobile/MobileWorkspaceListObserver.swift
  • Sources/TerminalController.swift
  • ios/cmux/Resources/Localizable.xcstrings
  • plans/feat-ios-groups-mobile/DESIGN.md

Comment thread Sources/TerminalController.swift Outdated
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Refreshed this branch: rebased onto current main (was 33 commits behind), fixed a real bug autoreview caught, and added a regression test.

Why the user saw nothing: this PR was never merged, so the groups code is simply not in their build. There is no payload-gating or capability-check bug that would hide groups when the code is present. The iOS gate chain is sound (workspace.groups.v1 capability -> supportsWorkspaceGroups -> toggleGroupCollapsedClosure != nil -> rendersGroupedSections), the producer emits group_id + a groups array on the unscoped list, and the render-item builder folds members under their anchor header.

Bug fixed during refresh: the mobile workspace-list summary hash includes each workspace's groupId, but MobileWorkspaceListObserver never subscribed to workspace.$groupId. Moving a workspace into/out of an existing group mutates only that workspace's groupId (not the tab set, workspaceGroups, panels, title, or pin), so emitIfNeeded never fired and paired phones kept stale membership until an unrelated change woke the observer. Added workspace.$groupId to the per-workspace publisher set + a movingWorkspaceBetweenGroupsChangesObserverHash test.

Build-verified: iOS simulator (cmux-ios) and macOS (cmux) both BUILD SUCCEEDED. Autoreview clean (overall correct 0.82).

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

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Rebased onto current main and addressed all review feedback in two follow-up commits:

  • Grouped rendering is now gated on the groups payload itself, not the workspace.groups.v1 capability from mobile.host.status (Cursor finding): a slow or failed status fetch no longer flattens sections the list already has. The toggle closure derives from capability-or-payload.
  • The passive chevron (no toggle action) no longer carries an .isButton accessibility trait it cannot honor (Greptile finding).
  • The render-item builder documents the non-contiguous-member degradation: a stray member renders at its own position, still indented, still hidden while collapsed (Greptile finding).
  • Group membership is resolved with a single pass over tabs instead of one scan per group (CodeRabbit finding).
  • New fix: MobileWorkspaceListObserver now subscribes to workspace.$groupId. Moving a workspace into or out of an existing group mutates only that workspace's groupId, so the observer never re-emitted and paired phones kept stale membership. Covered by a new fidelity test.
  • Mobile list payload builders, workspace mutations, and the capability list moved into their own files to stay within the Swift file length budget.

Build-verified iOS simulator (arm64) and macOS, package tests pass, autoreview clean.

lawrencecchen and others added 4 commits June 9, 2026 16:45
The iOS workspace list flattened workspaces and ignored the desktop's
named, collapsible groups. The mobile workspace-list payload did not carry
any group structure, so surface it on the Mac host, then render it on iOS.

Mac host:
- mobileWorkspacePayload now emits group_id per workspace; the list adds a
  top-level groups array (id, name, is_collapsed, is_pinned,
  anchor_workspace_id, member_workspace_ids). The all-windows branch
  aggregates each window's groups in window-iteration order.
- MobileWorkspaceListObserver now subscribes to $workspaceGroups and folds
  group order/name/collapse/pin/anchor + per-workspace groupId into its
  summary hash, so a collapse/expand (or rename/move) re-emits
  workspace.updated to the phone. Without this the phone's disclosure would
  look frozen.
- Expose workspace.group.collapse/expand to mobile (mobileHostHandleRPC +
  the ticket-auth model), gated by the same same-account Stack auth as the
  rest of the data plane. Advertise a workspace.groups.v1 capability.

iOS:
- Decode optional group_id per workspace + an optional groups array
  (backward compatible with older Macs).
- MobileWorkspaceGroupPreview value model; MobileWorkspaceListItem builds the
  ordered render items mirroring SidebarWorkspaceRenderItem (anchor renders
  as the header, no separate row; collapsed hides members; ungrouped
  interleave by position).
- WorkspaceGroupHeaderRow: chevron toggles collapse; tapping the name
  selects/opens the anchor workspace (so the anchor stays reachable),
  mirroring the desktop header.
- Store carries workspaceGroups + supportsWorkspaceGroups and a
  fire-and-forget setWorkspaceGroupCollapsed RPC (authoritative re-fetch, no
  optimistic state). The grouped section renders only when the Mac
  advertises the capability and the user is not searching; otherwise the
  list stays flat (pinned-first), preserving member contiguity.

P1 is display + expand/collapse. Phone-side group create/rename/restructure
is deferred. New strings localized (en + ja).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The mobile workspace-list summary hash includes each workspace's groupId, but
the observer never subscribed to workspace.$groupId. Moving a workspace into or
out of an existing group mutates only that workspace's groupId (not the tab set,
workspaceGroups, panels, title, or pin state), so emitIfNeeded never fired and
paired phones kept rendering stale group membership until some unrelated change
woke the observer. Add workspace.$groupId to the per-workspace publisher set and
cover a pure membership move in MobileWorkspaceListFidelityTests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review feedback (Cursor, Greptile): grouped sections rendered only once
mobile.host.status returned and advertised workspace.groups.v1, so a slow
or failed status fetch kept the list flat even though the workspace list
already carried groups. A Mac that emits groups also handles
workspace.group.collapse/expand (they shipped together), so render from
the payload and derive the toggle closure from capability-or-payload.
The passive chevron (no toggle) no longer carries an .isButton
accessibility trait it cannot honor, and the render-item builder
documents the non-contiguous-member degradation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The groups payload pushed TerminalController.swift (+66),
MobileShellComposite.swift (+49), and MobileHostService.swift (+11) over
the Swift file length budget. Move the mobile workspace-list payload
builders and the mobile group collapse handler into
TerminalController+MobileWorkspaceList.swift, the fire-and-forget
workspace mutations into MobileShellComposite+WorkspaceActions.swift,
and the advertised capability list into
MobileHostService+Capabilities.swift, then tighten the three budget
entries to the new actuals.

Also resolves group membership with a single pass over tabs instead of
one scan per group (review feedback) via mobileWorkspaceGroupPayloads.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lawrencecchen
lawrencecchen force-pushed the feat-ios-groups-mobile branch from c1982e1 to 660cd47 Compare June 9, 2026 23:45
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Verdict on "groups don't show on the phone": both causes were real, and both are handled now.

  1. Unmerged producer (the dominant cause in dogfood). The groups payload is emitted by the Mac host in this same PR. Any pairing where the Mac is not running this branch returns no top-level groups array and no per-workspace group_id from workspace.list, so the phone renders the flat list by design (backward compatibility with older Macs). Dogfooding groups requires the Mac app AND the iOS app both built from this branch (or its stack).

  2. Real client bug, fixed in-branch. Grouped rendering was originally gated on mobile.host.status advertising workspace.groups.v1, which arrives on a separate call from workspace.list. A slow or failed status fetch kept the list flat even when the payload already carried groups. Fixed by gating on the payload itself (commit 1379b7b, originally 7ff117f before the rebase).

Branch refreshed onto current origin/main (72812f3) and force-pushed; the rebase was conflict-free.

@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 `@Sources/TerminalController.swift`:
- Around line 20826-20829: The collapse/expand RPC handlers call
v2MobileWorkspaceGroupSetCollapsed (which delegates to
v2WorkspaceGroupSetCollapsed) but neither function verifies that the caller's
MobileHostService.mobileHostCapabilities includes "workspace.groups.v1"; add a
server-side capability check at the start of v2MobileWorkspaceGroupSetCollapsed
(or in v2WorkspaceGroupSetCollapsed) that uses the request's
MobileHostService/authorization context and, if the capability is missing,
immediately return a standardized "missing required capability" error (same
style used elsewhere) before performing any authorization checks or mutating the
group collapsed state; keep MobileHostService.requiresAuthorization behavior
intact for same-account enforcement, but gate the feature first.
🪄 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: 6305efa2-5744-4284-bbe7-a4620bb202c3

📥 Commits

Reviewing files that changed from the base of the PR and between c1982e1 and 660cd47.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (20)
  • Packages/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileSyncWorkspaceListResponse.swift
  • Packages/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileWorkspacePreview+RemoteMapping.swift
  • Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+WorkspaceActions.swift
  • Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileWorkspaceGroupPreview.swift
  • Packages/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileWorkspaceListItem.swift
  • Packages/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileWorkspacePreview.swift
  • Packages/CmuxMobileShellModel/Tests/CmuxMobileShellModelTests/MobileWorkspaceListItemTests.swift
  • Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceGroupHeaderRow.swift
  • Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceListView.swift
  • Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift
  • Sources/Mobile/MobileHostService+Capabilities.swift
  • Sources/Mobile/MobileHostService.swift
  • Sources/Mobile/MobileWorkspaceListObserver.swift
  • Sources/TerminalController+MobileWorkspaceList.swift
  • Sources/TerminalController.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/MobileWorkspaceListFidelityTests.swift
  • ios/cmux/Resources/Localizable.xcstrings
  • plans/feat-ios-groups-mobile/DESIGN.md

Comment on lines +20826 to +20829
case "workspace.group.collapse":
result = v2MobileWorkspaceGroupSetCollapsed(params: request.params, isCollapsed: true)
case "workspace.group.expand":
result = v2MobileWorkspaceGroupSetCollapsed(params: request.params, isCollapsed: 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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Locate v2MobileWorkspaceGroupSetCollapsed and verify capability/auth gating

# Find the function definition
echo "=== Searching for v2MobileWorkspaceGroupSetCollapsed definition ==="
rg -n -A 30 'func v2MobileWorkspaceGroupSetCollapsed' Sources/

# Verify capability gating for workspace.groups.v1
echo ""
echo "=== Checking for workspace.groups.v1 capability validation ==="
rg -n -C 5 'workspace\.groups\.v1' Sources/TerminalController.swift Sources/Mobile/

# Verify auth validation in mobile RPC context
echo ""
echo "=== Checking for mobile auth validation pattern ==="
rg -n -C 3 'mobileHostTicketAccount|MobileHostService.*auth' Sources/Mobile/

Repository: manaflow-ai/cmux

Length of output: 5905


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== v2WorkspaceGroupSetCollapsed definition ==="
rg -n -A 80 'func v2WorkspaceGroupSetCollapsed' Sources/

echo
echo "=== Capability gating helpers usage in mobile workspace group handlers ==="
rg -n -C 3 'workspace\.groups\.v1|groups\.v1|v2HasCapability|hasCapability|capabilit' Sources/TerminalController+MobileWorkspaceList.swift Sources/TerminalController.swift Sources/Mobile/

echo
echo "=== Same-account auth / authorization usage near group collapse/expand ==="
rg -n -C 3 'same-account|sameAccount|account.*id|authorize|authorization|requiresAuthorization|authorized' Sources/TerminalController+MobileWorkspaceList.swift Sources/TerminalController.swift Sources/Mobile/

echo
echo "=== Dispatcher routes for workspace.group.collapse/expand ==="
rg -n -C 5 'workspace\.group\.collapse|workspace\.group\.expand' Sources/TerminalController.swift

Repository: manaflow-ai/cmux

Length of output: 38675


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== occurrences of workspace.group.collapse / expand in Sources/Mobile and TerminalController ==="
rg -n 'workspace\.group\.(collapse|expand)' Sources/Mobile Sources/TerminalController.swift Sources/TerminalController+*.swift

echo
echo "=== occurrences of workspace.groups.v1 across Sources ==="
rg -n 'workspace\.groups\.v1' Sources/

echo
echo
echo "=== check authorization gating: requiresAuthorization(method:) cases ==="
rg -n 'requiresAuthorization\(method:|switch method' Sources/Mobile/MobileHostService.swift
rg -n -A 200 'nonisolated private static func requiresAuthorization\(method: String\)' Sources/Mobile/MobileHostService.swift

echo
echo
echo "=== check if capability gating exists in mobile host request authorization path ==="
rg -n '(capabilit|capability|missing required capability|requiresCapability|required capability)' Sources/Mobile/MobileHostService.swift Sources/Mobile/

Repository: manaflow-ai/cmux

Length of output: 11139


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Search for server-side capability enforcement (missing required capability / requiresCapability) ==="
rg -n 'missing required capability|required capability|requiresCapability|hasCapability|capabilit' Sources/TerminalController.swift Sources/Mobile Sources/TerminalController+*.swift

echo
echo "=== Look specifically for capability checks around workspace.group.collapse/expand handlers ==="
rg -n -C 5 'workspace\.group\.(collapse|expand)' Sources/TerminalController.swift

Repository: manaflow-ai/cmux

Length of output: 3848


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== TerminalController+MobileWorkspaceList.swift: v2MobileWorkspaceGroupSetCollapsed context ==="
sed -n '1,120p' Sources/TerminalController+MobileWorkspaceList.swift

echo
echo "=== TerminalController.swift: mobile RPC dispatcher switch context around workspace.group.collapse/expand ==="
sed -n '20780,20860p' Sources/TerminalController.swift

echo
echo "=== TerminalController.swift: v2WorkspaceGroupSetCollapsed full context ==="
sed -n '5060,5130p' Sources/TerminalController.swift

Repository: manaflow-ai/cmux

Length of output: 14237


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Find any v2RequiresCapability / v2HasCapability helpers ==="
rg -n 'v2(Requires|Has)Capability|required capability|missing required capability' Sources/

echo
echo "=== Find where errors mentioning 'missing required capability' are constructed ==="
rg -n 'missing required capability' Sources/

echo
echo "=== Search for capability string 'workspace.groups.v1' usage beyond capability advertisement ==="
rg -n 'workspace\.groups\.v1' Sources/

echo
echo "=== Search for required-capability checks near RPC method dispatcher ==="
rg -n 'workspace\.group\.collapse|workspace\.group\.expand' -C 20 Sources/TerminalController.swift

Repository: manaflow-ai/cmux

Length of output: 8550


Enforce workspace.groups.v1 capability gating for workspace.group.collapse/expand RPCs

  • The routes call v2MobileWorkspaceGroupSetCollapsed(...) → v2WorkspaceGroupSetCollapsed(...), but neither function checks whether workspace.groups.v1 is supported; the capability only appears in MobileHostService.mobileHostCapabilities advertisement.
  • Same-account auth is enforced at the mobile RPC transport layer via MobileHostService.requiresAuthorization(...) + Stack same-account authorization; only mobile.host.status is exempt.
  • Add a server-side workspace.groups.v1 check (returning a “missing required capability” style error) before mutating group collapsed state to match the PR objective and advertised capability.
🤖 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 20826 - 20829, The
collapse/expand RPC handlers call v2MobileWorkspaceGroupSetCollapsed (which
delegates to v2WorkspaceGroupSetCollapsed) but neither function verifies that
the caller's MobileHostService.mobileHostCapabilities includes
"workspace.groups.v1"; add a server-side capability check at the start of
v2MobileWorkspaceGroupSetCollapsed (or in v2WorkspaceGroupSetCollapsed) that
uses the request's MobileHostService/authorization context and, if the
capability is missing, immediately return a standardized "missing required
capability" error (same style used elsewhere) before performing any
authorization checks or mutating the group collapsed state; keep
MobileHostService.requiresAuthorization behavior intact for same-account
enforcement, but gate the feature first.

lawrencecchen added a commit that referenced this pull request Jun 10, 2026
…ps, #5625)

Conflict resolutions:
- MobileShellComposite: union of dog (notifications store, paste/dogfood capabilities, feedback submitter) and groups (workspaceGroups, supportsWorkspaceGroups); clientID made internal for the new +WorkspaceActions extension; dropped dog's inline rename/pin copies (moved to MobileShellComposite+WorkspaceActions.swift); kept un-gated privileged Send Feedback
- MobileHostService: capability list now lives only in +Capabilities.swift, unioned dog entries (notification.dismiss.v1, terminal.paste.v1, unconditional dogfood.v1, DEBUG checklist/feedback) with workspace.groups.v1
- TerminalController: kept both notification and workspace.group RPC cases; preserved un-gated dogfood.feedback.submit
- WorkspaceListView/ShellView: grouped/flat row structure from groups branch with dog's unread counts, mute, device tree threaded through workspaceRow
lawrencecchen added a commit that referenced this pull request Jun 11, 2026
Reset to origin/main (composer #5876 landed), then merge old dog HEAD
a300868 to preserve every feature not yet on main: notifications
dismiss-sync (#5568), multi-Mac switcher hardening (#5545), foreground
repaint (#5571), image paste too-large toast (#5572), hidden native
input (#5596), workspace groups (#5625), wslist round-10 snapshot,
scroll-to-bottom hysteresis, DEV dogfood pane, attachments button,
arrow toolbar keys, terminal.paste capability gating.

Conflict policy: main's reviewed composer-land form wins for composer
core (keyed focus handshake, draft FIFO coalescing, paste submit
partial-success), dog wins for unlanded feature surface. ghostty pinned
to dog 34cbf18 (descendant of main's e5c962a).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lawrencecchen added a commit that referenced this pull request Jun 12, 2026
Carries unlanded: multi-Mac #5545, notif-sync #5568, foreground-repaint #5571,
image-paste toast #5572, hidden-input #5596, groups (iOS side) #5625,
scroll-hysteresis, dogfood pane, capabilities superset.
Main's reviewed forms win: TerminalController decomposition (Control*Context),
notif-tap-deeplink #5927, mobile.terminal.* routing.
lawrencecchen added a commit that referenced this pull request Jun 13, 2026
…#5596/#5625/#5628) over current main

Beta queue (#5876/#5872/#5869/#5875/#5927/#5912/#5726/#5776/#5916) is now on
main; conflicts resolved by taking main as authoritative for the merged
workspace-list/notifications/read-state/close surface, while preserving the
carry-set: terminal.paste capability (#5572), hidden-input strings (#5596),
smooth-scroll/scroll-to-bottom (#5628), and the live notifications feed
(notificationsStore + mobile.notifications.list/mark_read dispatch). Dropped
the superseded mute design. Capability flags unified onto main's computed
supportedHostCapabilities set (added computed supportsTerminalPaste +
DEBUG supportsDogfoodChecklist). xcstrings merged (HEAD-precedence union,
mute keys dropped). pbxproj took HEAD consistently; budget regenerated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@lawrencecchen lawrencecchen added the stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening. label Sep 23, 2026
@github-project-automation github-project-automation Bot moved this from Todo to Done in cmux backlog Sep 23, 2026

This branch was successfully deployed

1 active deployment
Preview – cmux — 660cd478 Deployed Jun 10, 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