Repository navigation
iOS: unified animated toast system (CmuxMobileToast) - #8376
Conversation
New CmuxMobileToast package: ToastCenter (@observable, clock-injected dwell/queue/coalescing policy, fully unit-tested) + one toastHost overlay mounted at the root scene. Toasts render in a passthrough UIWindow above sheets, with Liquid Glass (material fallback), spring+blur arrival, interactive drag-to-dismiss with velocity projection, coalescing bump pulse, haptics, VoiceOver announcements, and Reduce Motion/Transparency fallbacks. Replaces WorkspaceActionToast (bottom capsule in WorkspaceShellView) and ChatScreen's Timer-based error banner; workspace-action failures now read as title + sentence reason (localization keys reworked en+ja). DEBUG gallery behind CMUX_TOAST_GALLERY=1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThe change adds ChangesMobile toast system
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant CMUXMobileRootScene
participant ToastCenter
participant ToastOverlayRoot
participant ToastCardView
CMUXMobileRootScene->>ToastCenter: initialize app-wide presenter
CMUXMobileRootScene->>ToastOverlayRoot: attach toastHost(toastCenter)
ToastCenter->>ToastOverlayRoot: publish presented toast
ToastOverlayRoot->>ToastCardView: render toast content
ToastCardView->>ToastCenter: dismiss or execute action
Possibly related PRs
Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (2 errors, 2 warnings, 1 inconclusive)
✅ Passed checks (20 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 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
`@Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Screen/ChatScreen.swift`:
- Around line 102-109: Update the onChange handler observing
store.lastErrorDescription to use SwiftUI’s initial-delivery overload, so the
current error is handled when ChatScreen first appears as well as on later
changes. Preserve the existing toast presentation and store.dismissError()
behavior.
In `@Packages/iOS/CmuxMobileToast/Sources/CmuxMobileToast/Toast.swift`:
- Line 84: Update the default coalescing key construction in the Toast
initializer to use a collision-free structured or length-prefixed encoding of
style, title, and message instead of delimiter concatenation. Ensure this single
authoritative coalescingKey value continues to control both refresh and queue
behavior, while preserving explicitly supplied coalescingKey values.
- Around line 121-184: Add DocC triple-slash comments documenting the public
Toast factory methods info, success, warning, and failure, describing the toast
style each creates and its message/configuration parameters. Keep the existing
factory signatures and behavior unchanged.
- Around line 49-84: Make the toast policy inputs style, title, message, and
action immutable after initialization by changing their declarations in Toast to
read-only properties. Preserve initializer assignment and the existing derived
autoDismiss and coalescingKey behavior, ensuring callers cannot mutate inputs
without recomputing those policy values.
In `@Packages/iOS/CmuxMobileToast/Sources/CmuxMobileToast/ToastCenter.swift`:
- Around line 116-136: Scope interaction ownership to the presented toast rather
than relying on the mutable interactionHolds reset in show. Update
beginInteraction and endInteraction to receive and validate the current Toast.ID
or an interaction token, ignoring stale callbacks from prior presentations
before changing holds or restarting auto-dismiss; add a regression test covering
an old release after the next toast begins interaction.
🪄 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: 21e882a7-2781-4b26-b607-8bf0b149564f
⛔ Files ignored due to path filters (1)
cmux.xcworkspace/contents.xcworkspacedatais excluded by!**/*.xcworkspace/contents.xcworkspacedata
📒 Files selected for processing (21)
Packages/iOS/CmuxAgentChatUI/Package.swiftPackages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Previews/ChatScreenPreviews.swiftPackages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Screen/ChatScreen.swiftPackages/iOS/CmuxMobileShellUI/Package.swiftPackages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceActionToast.swiftPackages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceActionToastContent.swiftPackages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView+WorkspaceActionFailure.swiftPackages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swiftPackages/iOS/CmuxMobileToast/Package.swiftPackages/iOS/CmuxMobileToast/Sources/CmuxMobileToast/Toast.swiftPackages/iOS/CmuxMobileToast/Sources/CmuxMobileToast/ToastCardView.swiftPackages/iOS/CmuxMobileToast/Sources/CmuxMobileToast/ToastCenter.swiftPackages/iOS/CmuxMobileToast/Sources/CmuxMobileToast/ToastGalleryView.swiftPackages/iOS/CmuxMobileToast/Sources/CmuxMobileToast/ToastHost.swiftPackages/iOS/CmuxMobileToast/Sources/CmuxMobileToast/ToastMotion.swiftPackages/iOS/CmuxMobileToast/Sources/CmuxMobileToast/ToastOverlayRoot.swiftPackages/iOS/CmuxMobileToast/Tests/CmuxMobileToastTests/ManualClock.swiftPackages/iOS/CmuxMobileToast/Tests/CmuxMobileToastTests/ToastCenterTests.swiftios/cmux/Resources/Localizable.xcstringsios/cmuxPackage/Package.swiftios/cmuxPackage/Sources/cmuxFeature/CMUXMobileRootScene.swift
💤 Files with no reviewable changes (2)
- Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceActionToastContent.swift
- Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceActionToast.swift
| public private(set) var id: UUID | ||
| public var style: Style | ||
| public var title: String? | ||
| public var message: String | ||
| /// SF Symbol name overriding the style's default icon. `nil` uses the | ||
| /// style default (`info` has none, so plain info toasts read as a quiet | ||
| /// text capsule). | ||
| public var systemImage: String? | ||
| public var placement: Placement | ||
| public var autoDismiss: AutoDismiss | ||
| public var action: Action? | ||
| /// Toasts with equal keys coalesce: presenting one whose key matches the | ||
| /// visible toast refreshes its content in place and re-bumps it instead of | ||
| /// queueing a duplicate behind it. Defaults to style + title + message, so | ||
| /// identical notices never stack. | ||
| public var coalescingKey: String | ||
|
|
||
| public init( | ||
| style: Style, | ||
| title: String? = nil, | ||
| message: String, | ||
| systemImage: String? = nil, | ||
| placement: Placement = .top, | ||
| autoDismiss: AutoDismiss? = nil, | ||
| action: Action? = nil, | ||
| coalescingKey: String? = nil | ||
| ) { | ||
| self.id = UUID() | ||
| self.style = style | ||
| self.title = title | ||
| self.message = message | ||
| self.systemImage = systemImage | ||
| self.placement = placement | ||
| self.autoDismiss = autoDismiss ?? Self.defaultAutoDismiss(for: style, hasAction: action != nil) | ||
| self.action = action | ||
| self.coalescingKey = coalescingKey ?? "\(style.rawValue)|\(title ?? "")|\(message)" |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Make toast policy inputs immutable.
style, title, message, and action can be mutated after autoDismiss and coalescingKey are derived. This permits a failure toast to retain success dwell/key behavior, causing incorrect timing and coalescing.
Proposed fix
- public var style: Style
- public var title: String?
- public var message: String
+ public let style: Style
+ public let title: String?
+ public let message: String
...
- public var systemImage: String?
- public var placement: Placement
- public var autoDismiss: AutoDismiss
- public var action: Action?
+ public let systemImage: String?
+ public let placement: Placement
+ public let autoDismiss: AutoDismiss
+ public let action: Action?
...
- public var coalescingKey: String
+ public let coalescingKey: StringAs per coding guidelines, “Swift architecture changes must preserve clear ownership and invariants” and must not leave bad state representable.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| public private(set) var id: UUID | |
| public var style: Style | |
| public var title: String? | |
| public var message: String | |
| /// SF Symbol name overriding the style's default icon. `nil` uses the | |
| /// style default (`info` has none, so plain info toasts read as a quiet | |
| /// text capsule). | |
| public var systemImage: String? | |
| public var placement: Placement | |
| public var autoDismiss: AutoDismiss | |
| public var action: Action? | |
| /// Toasts with equal keys coalesce: presenting one whose key matches the | |
| /// visible toast refreshes its content in place and re-bumps it instead of | |
| /// queueing a duplicate behind it. Defaults to style + title + message, so | |
| /// identical notices never stack. | |
| public var coalescingKey: String | |
| public init( | |
| style: Style, | |
| title: String? = nil, | |
| message: String, | |
| systemImage: String? = nil, | |
| placement: Placement = .top, | |
| autoDismiss: AutoDismiss? = nil, | |
| action: Action? = nil, | |
| coalescingKey: String? = nil | |
| ) { | |
| self.id = UUID() | |
| self.style = style | |
| self.title = title | |
| self.message = message | |
| self.systemImage = systemImage | |
| self.placement = placement | |
| self.autoDismiss = autoDismiss ?? Self.defaultAutoDismiss(for: style, hasAction: action != nil) | |
| self.action = action | |
| self.coalescingKey = coalescingKey ?? "\(style.rawValue)|\(title ?? "")|\(message)" | |
| public private(set) var id: UUID | |
| public let style: Style | |
| public let title: String? | |
| public let message: String | |
| /// SF Symbol name overriding the style's default icon. `nil` uses the | |
| /// style default (`info` has none, so plain info toasts read as a quiet | |
| /// text capsule). | |
| public let systemImage: String? | |
| public let placement: Placement | |
| public let autoDismiss: AutoDismiss | |
| public let action: Action? | |
| /// Toasts with equal keys coalesce: presenting one whose key matches the | |
| /// visible toast refreshes its content in place and re-bumps it instead of | |
| /// queueing a duplicate behind it. Defaults to style + title + message, so | |
| /// identical notices never stack. | |
| public let coalescingKey: String | |
| public init( | |
| style: Style, | |
| title: String? = nil, | |
| message: String, | |
| systemImage: String? = nil, | |
| placement: Placement = .top, | |
| autoDismiss: AutoDismiss? = nil, | |
| action: Action? = nil, | |
| coalescingKey: String? = nil | |
| ) { | |
| self.id = UUID() | |
| self.style = style | |
| self.title = title | |
| self.message = message | |
| self.systemImage = systemImage | |
| self.placement = placement | |
| self.autoDismiss = autoDismiss ?? Self.defaultAutoDismiss(for: style, hasAction: action != nil) | |
| self.action = action | |
| self.coalescingKey = coalescingKey ?? "\(style.rawValue)|\(title ?? "")|\(message)" |
🤖 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 `@Packages/iOS/CmuxMobileToast/Sources/CmuxMobileToast/Toast.swift` around
lines 49 - 84, Make the toast policy inputs style, title, message, and action
immutable after initialization by changing their declarations in Toast to
read-only properties. Preserve initializer assignment and the existing derived
autoDismiss and coalescingKey behavior, ensuring callers cannot mutate inputs
without recomputing those policy values.
Source: Coding guidelines
| self.placement = placement | ||
| self.autoDismiss = autoDismiss ?? Self.defaultAutoDismiss(for: style, hasAction: action != nil) | ||
| self.action = action | ||
| self.coalescingKey = coalescingKey ?? "\(style.rawValue)|\(title ?? "")|\(message)" |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Use a collision-free default coalescing identity.
Delimiter concatenation aliases distinct content. For example, title "a|b"/message "c" and title "a"/message "b|c" produce the same key and incorrectly replace one another. Use a structured key type or unambiguous length-prefixed encoding.
As per path instructions, ensure exactly one authoritative coalescing key controls refresh versus queue behavior.
🤖 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 `@Packages/iOS/CmuxMobileToast/Sources/CmuxMobileToast/Toast.swift` at line 84,
Update the default coalescing key construction in the Toast initializer to use a
collision-free structured or length-prefixed encoding of style, title, and
message instead of delimiter concatenation. Ensure this single authoritative
coalescingKey value continues to control both refresh and queue behavior, while
preserving explicitly supplied coalescingKey values.
Source: Path instructions
| public extension Toast { | ||
| static func info( | ||
| _ message: String, | ||
| title: String? = nil, | ||
| systemImage: String? = nil, | ||
| placement: Placement = .top, | ||
| autoDismiss: AutoDismiss? = nil, | ||
| action: Action? = nil, | ||
| coalescingKey: String? = nil | ||
| ) -> Toast { | ||
| Toast( | ||
| style: .info, title: title, message: message, systemImage: systemImage, | ||
| placement: placement, autoDismiss: autoDismiss, action: action, | ||
| coalescingKey: coalescingKey | ||
| ) | ||
| } | ||
|
|
||
| static func success( | ||
| _ message: String, | ||
| title: String? = nil, | ||
| systemImage: String? = nil, | ||
| placement: Placement = .top, | ||
| autoDismiss: AutoDismiss? = nil, | ||
| action: Action? = nil, | ||
| coalescingKey: String? = nil | ||
| ) -> Toast { | ||
| Toast( | ||
| style: .success, title: title, message: message, systemImage: systemImage, | ||
| placement: placement, autoDismiss: autoDismiss, action: action, | ||
| coalescingKey: coalescingKey | ||
| ) | ||
| } | ||
|
|
||
| static func warning( | ||
| _ message: String, | ||
| title: String? = nil, | ||
| systemImage: String? = nil, | ||
| placement: Placement = .top, | ||
| autoDismiss: AutoDismiss? = nil, | ||
| action: Action? = nil, | ||
| coalescingKey: String? = nil | ||
| ) -> Toast { | ||
| Toast( | ||
| style: .warning, title: title, message: message, systemImage: systemImage, | ||
| placement: placement, autoDismiss: autoDismiss, action: action, | ||
| coalescingKey: coalescingKey | ||
| ) | ||
| } | ||
|
|
||
| static func failure( | ||
| _ message: String, | ||
| title: String? = nil, | ||
| systemImage: String? = nil, | ||
| placement: Placement = .top, | ||
| autoDismiss: AutoDismiss? = nil, | ||
| action: Action? = nil, | ||
| coalescingKey: String? = nil | ||
| ) -> Toast { | ||
| Toast( | ||
| style: .failure, title: title, message: message, systemImage: systemImage, | ||
| placement: placement, autoDismiss: autoDismiss, action: action, | ||
| coalescingKey: coalescingKey | ||
| ) | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Add DocC comments to the public factory methods.
info, success, warning, and failure are public package API but have no /// documentation.
As per path instructions, “document every public symbol in new Swift packages with DocC triple-slash comments.”
🤖 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 `@Packages/iOS/CmuxMobileToast/Sources/CmuxMobileToast/Toast.swift` around
lines 121 - 184, Add DocC triple-slash comments documenting the public Toast
factory methods info, success, warning, and failure, describing the toast style
each creates and its message/configuration parameters. Keep the existing factory
signatures and behavior unchanged.
Source: Path instructions
| /// The user started touching the toast; auto-dismiss holds until every | ||
| /// balanced ``endInteraction()`` lands. | ||
| public func beginInteraction() { | ||
| interactionHolds += 1 | ||
| cancelAutoDismiss() | ||
| } | ||
|
|
||
| /// Balances ``beginInteraction()``. When the last hold releases, the full | ||
| /// dwell restarts (forgiving: touching a toast means the user is reading it). | ||
| public func endInteraction() { | ||
| guard interactionHolds > 0 else { return } | ||
| interactionHolds -= 1 | ||
| if interactionHolds == 0, presented != nil { | ||
| restartAutoDismiss() | ||
| } | ||
| } | ||
|
|
||
| private func show(_ toast: Toast) { | ||
| // A cancelled drag on the previous toast can leak a hold; interaction | ||
| // is per-toast, so a fresh presentation always starts unheld. | ||
| interactionHolds = 0 |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Scope interaction holds to the presented toast.
beginInteraction() and endInteraction() carry no toast identity. After dismissal and a new presentation, a late callback from the old view can decrement the new toast’s hold and restart auto-dismiss while it is still being dragged. The reset in show does not prevent this stale-callback race.
Pass the presented Toast.ID or an interaction token through both methods and ignore releases that do not match the current toast; add a regression test for an old release arriving after the next toast begins interaction.
As per coding guidelines and path instructions, lifecycle races must preserve one explicit owner rather than patching leaked state with mutable counters or resets.
🤖 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 `@Packages/iOS/CmuxMobileToast/Sources/CmuxMobileToast/ToastCenter.swift`
around lines 116 - 136, Scope interaction ownership to the presented toast
rather than relying on the mutable interactionHolds reset in show. Update
beginInteraction and endInteraction to receive and validate the current Toast.ID
or an interaction token, ignoring stale callbacks from prior presentations
before changing holds or restarting auto-dismiss; add a regression test covering
an old release after the next toast begins interaction.
Sources: Coding guidelines, Path instructions
Greptile SummaryThis PR introduces a unified
Confidence Score: 5/5Safe to merge — the beta flag is off by default so no user-visible change ships until explicitly enabled, and all legacy surfaces are preserved as fallbacks. All new user-facing strings have en+ja translations in xcstrings. The ToastCenter policy (queue cap, coalescing, interaction holds, auto-dismiss) is covered by 15 Swift Testing tests against an injectable clock. The UIWindow passthrough is correctly geometry-gated so no touch regressions are expected. The beta flag drops toasts silently when off and calls dismissAll() on toggle, keeping legacy UI intact. Local SwiftPM path dependencies don't generate Package.resolved entries so the lockfile policy is satisfied. No actor-isolation escapes, no blocking primitives on interactive paths, and no test seams widened in production source. No files require special attention. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant CS as ChatScreen / WorkspaceShellView
participant TC as ToastCenter (@MainActor)
participant TH as ToastHost (.toastHost)
participant PW as ToastPassthroughWindow (UIWindow .alert)
participant OR as ToastOverlayRoot
CS->>TC: present(.failure("…", coalescingKey:))
TC-->>TC: guard isEnabled else drop
TC-->>TC: coalesce OR enqueue OR show(toast)
TC-->>TC: "Task { clock.sleep(dwell) } → autoDismissTask"
TH->>PW: install UIHostingController(ToastOverlayRoot)
OR-->>OR: "observe center.presented (SwiftUI @Observable)"
OR->>OR: .transition + .animation (spring / cross-fade)
OR->>OR: .sensoryFeedback → haptic
OR->>OR: AccessibilityNotification.Announcement
note over PW: hitTest: interactiveRegion?.contains(point) — falls through outside card
CS->>TC: beginInteraction(for: toastID)
TC-->>TC: cancelAutoDismiss()
CS->>TC: endInteraction(for: toastID)
TC-->>TC: restartAutoDismiss()
TC-->>TC: autoDismissFired → dismissCurrent()
TC-->>TC: scheduleAdvance (260 ms breath)
TC-->>TC: advanceFired → show(queue.removeFirst())
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant CS as ChatScreen / WorkspaceShellView
participant TC as ToastCenter (@MainActor)
participant TH as ToastHost (.toastHost)
participant PW as ToastPassthroughWindow (UIWindow .alert)
participant OR as ToastOverlayRoot
CS->>TC: present(.failure("…", coalescingKey:))
TC-->>TC: guard isEnabled else drop
TC-->>TC: coalesce OR enqueue OR show(toast)
TC-->>TC: "Task { clock.sleep(dwell) } → autoDismissTask"
TH->>PW: install UIHostingController(ToastOverlayRoot)
OR-->>OR: "observe center.presented (SwiftUI @Observable)"
OR->>OR: .transition + .animation (spring / cross-fade)
OR->>OR: .sensoryFeedback → haptic
OR->>OR: AccessibilityNotification.Announcement
note over PW: hitTest: interactiveRegion?.contains(point) — falls through outside card
CS->>TC: beginInteraction(for: toastID)
TC-->>TC: cancelAutoDismiss()
CS->>TC: endInteraction(for: toastID)
TC-->>TC: restartAutoDismiss()
TC-->>TC: autoDismissFired → dismissCurrent()
TC-->>TC: scheduleAdvance (260 ms breath)
TC-->>TC: advanceFired → show(queue.removeFirst())
Reviews (10): Last reviewed commit: "Address review findings: initial-deliver..." | Re-trigger Greptile |
| enum ToastMotion { | ||
| /// The ambient animation that drives presence changes on the overlay. | ||
| static func driver(reduceMotion: Bool) -> Animation { | ||
| reduceMotion | ||
| ? .easeInOut(duration: 0.18) | ||
| : .spring(response: 0.42, dampingFraction: 0.75) | ||
| } | ||
|
|
||
| static func transition(placement: Toast.Placement, reduceMotion: Bool) -> AnyTransition { | ||
| guard !reduceMotion else { | ||
| return .asymmetric( | ||
| insertion: AnyTransition.opacity.animation(.easeInOut(duration: 0.18)), | ||
| removal: AnyTransition.opacity.animation(.easeInOut(duration: 0.15)) | ||
| ) | ||
| } | ||
| let sign: CGFloat = placement == .top ? -1 : 1 | ||
| let anchor: UnitPoint = placement == .top ? .top : .bottom | ||
| let insertion = AnyTransition.modifier( | ||
| active: ToastTransitionModifier( | ||
| offset: 22 * sign, scale: 0.86, blur: 5, opacity: 0, anchor: anchor | ||
| ), | ||
| identity: ToastTransitionModifier.identity(anchor: anchor) | ||
| ) | ||
| .animation(.spring(response: 0.44, dampingFraction: 0.72)) | ||
| let removal = AnyTransition.modifier( | ||
| active: ToastTransitionModifier( | ||
| offset: 12 * sign, scale: 0.94, blur: 3, opacity: 0, anchor: anchor | ||
| ), | ||
| identity: ToastTransitionModifier.identity(anchor: anchor) | ||
| ) | ||
| .animation(.spring(response: 0.25, dampingFraction: 1)) | ||
| return .asymmetric(insertion: insertion, removal: removal) | ||
| } | ||
| } |
There was a problem hiding this comment.
Caseless enum used as static-function namespace
ToastMotion is a caseless enum whose entire API is two static funcs — exactly the "static-only namespace" shape the cmux-no-ambient-global-state rule flags. The rule's intent is that logic should live on a constructable/injectable owning type rather than floating as module-internal ambient surface. The canonical fix is to inline these as private helpers on the types that call them: driver(reduceMotion:) and transition(placement:reduceMotion:) are only called from ToastOverlayRoot and ToastPresentationView (both in ToastOverlayRoot.swift), so they could become private static funcs on ToastOverlayRoot, or fileprivate free functions within that file, either of which passes the rule. If they need to stay in their own file a fileprivate struct with static members works.
Rule Used: Flag new ambient global state in production Swift:... (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!
CMUX_TOAST_GALLERY_AUTORUN=1 walks every style, placement, coalescing bump, and queue advance on a fixed cadence so a simulator screen recording captures the full motion vocabulary without UI driving. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
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 `@Packages/iOS/CmuxMobileToast/Sources/CmuxMobileToast/ToastGalleryView.swift`:
- Around line 92-140: Update runAutodemoIfRequested and its nested pause helper
to be throwing async functions, replace the swallowed clock.sleep cancellation
with try await, and propagate errors through every pause call so task
cancellation aborts the autodemo sequence instead of continuing immediately.
🪄 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: 39a75d85-0866-4604-a905-c76046a3b53a
📒 Files selected for processing (1)
Packages/iOS/CmuxMobileToast/Sources/CmuxMobileToast/ToastGalleryView.swift
| .task { await runAutodemoIfRequested() } | ||
| } | ||
|
|
||
| /// With `CMUX_TOAST_GALLERY_AUTORUN=1`, walks the full toast vocabulary on | ||
| /// a fixed cadence so a screen recording captures every arrival, | ||
| /// departure, coalescing bump, and queue advance without UI driving. | ||
| /// Demo pacing only (DEBUG harness), hence the deliberate wall-clock sleeps. | ||
| private func runAutodemoIfRequested() async { | ||
| guard ProcessInfo.processInfo.environment["CMUX_TOAST_GALLERY_AUTORUN"] == "1" else { return } | ||
| let clock = ContinuousClock() | ||
| func pause(_ seconds: Double) async { | ||
| try? await clock.sleep(for: .seconds(seconds)) | ||
| } | ||
|
|
||
| await pause(2) | ||
| toasts.present(.success("Workspace created")) | ||
| await pause(5.5) | ||
|
|
||
| toasts.present(.failure( | ||
| "Not connected to your Mac.", | ||
| title: "Couldn't rename workspace", | ||
| coalescingKey: "demo.error" | ||
| )) | ||
| await pause(2) | ||
| toasts.present(.failure( | ||
| "Not connected to your Mac.", | ||
| title: "Couldn't rename workspace", | ||
| coalescingKey: "demo.error" | ||
| )) | ||
| await pause(7.5) | ||
|
|
||
| toasts.present(.success("First: workspace created")) | ||
| toasts.present(.info("Second: agent finished")) | ||
| toasts.present(.warning("Third: build is out of date")) | ||
| await pause(13) | ||
|
|
||
| toasts.present(.info("Copied to clipboard", systemImage: "doc.on.doc")) | ||
| await pause(4.5) | ||
| toasts.present(.success("Saved", placement: .bottom)) | ||
| await pause(5) | ||
|
|
||
| toasts.present(.failure( | ||
| "The request timed out.", | ||
| title: "Couldn't create workspace", | ||
| action: Toast.Action(label: "Retry") {} | ||
| )) | ||
| await pause(7.5) | ||
| toasts.dismissAll() | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Propagate task cancellation to prevent instantaneous demo execution.
By using try? to swallow the CancellationError from clock.sleep, the pause function will return immediately if the .task is cancelled (e.g., if the user dismisses the view). This causes the remainder of the autodemo script to execute instantaneously, spamming the ToastCenter queue in a single frame.
Make pause and runAutodemoIfRequested throwing functions, and use try await so that cancellation properly aborts the sequence.
🐛 Proposed fix
- .task { await runAutodemoIfRequested() }
+ .task { try? await runAutodemoIfRequested() }
}
/// With `CMUX_TOAST_GALLERY_AUTORUN=1`, walks the full toast vocabulary on
/// a fixed cadence so a screen recording captures every arrival,
/// departure, coalescing bump, and queue advance without UI driving.
/// Demo pacing only (DEBUG harness), hence the deliberate wall-clock sleeps.
- private func runAutodemoIfRequested() async {
+ private func runAutodemoIfRequested() async throws {
guard ProcessInfo.processInfo.environment["CMUX_TOAST_GALLERY_AUTORUN"] == "1" else { return }
let clock = ContinuousClock()
- func pause(_ seconds: Double) async {
- try? await clock.sleep(for: .seconds(seconds))
+ func pause(_ seconds: Double) async throws {
+ try await clock.sleep(for: .seconds(seconds))
}
- await pause(2)
+ try await pause(2)
toasts.present(.success("Workspace created"))
- await pause(5.5)
+ try await pause(5.5)
toasts.present(.failure(
"Not connected to your Mac.",
title: "Couldn't rename workspace",
coalescingKey: "demo.error"
))
- await pause(2)
+ try await pause(2)
toasts.present(.failure(
"Not connected to your Mac.",
title: "Couldn't rename workspace",
coalescingKey: "demo.error"
))
- await pause(7.5)
+ try await pause(7.5)
toasts.present(.success("First: workspace created"))
toasts.present(.info("Second: agent finished"))
toasts.present(.warning("Third: build is out of date"))
- await pause(13)
+ try await pause(13)
toasts.present(.info("Copied to clipboard", systemImage: "doc.on.doc"))
- await pause(4.5)
+ try await pause(4.5)
toasts.present(.success("Saved", placement: .bottom))
- await pause(5)
+ try await pause(5)
toasts.present(.failure(
"The request timed out.",
title: "Couldn't create workspace",
action: Toast.Action(label: "Retry") {}
))
- await pause(7.5)
+ try await pause(7.5)
toasts.dismissAll()
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| .task { await runAutodemoIfRequested() } | |
| } | |
| /// With `CMUX_TOAST_GALLERY_AUTORUN=1`, walks the full toast vocabulary on | |
| /// a fixed cadence so a screen recording captures every arrival, | |
| /// departure, coalescing bump, and queue advance without UI driving. | |
| /// Demo pacing only (DEBUG harness), hence the deliberate wall-clock sleeps. | |
| private func runAutodemoIfRequested() async { | |
| guard ProcessInfo.processInfo.environment["CMUX_TOAST_GALLERY_AUTORUN"] == "1" else { return } | |
| let clock = ContinuousClock() | |
| func pause(_ seconds: Double) async { | |
| try? await clock.sleep(for: .seconds(seconds)) | |
| } | |
| await pause(2) | |
| toasts.present(.success("Workspace created")) | |
| await pause(5.5) | |
| toasts.present(.failure( | |
| "Not connected to your Mac.", | |
| title: "Couldn't rename workspace", | |
| coalescingKey: "demo.error" | |
| )) | |
| await pause(2) | |
| toasts.present(.failure( | |
| "Not connected to your Mac.", | |
| title: "Couldn't rename workspace", | |
| coalescingKey: "demo.error" | |
| )) | |
| await pause(7.5) | |
| toasts.present(.success("First: workspace created")) | |
| toasts.present(.info("Second: agent finished")) | |
| toasts.present(.warning("Third: build is out of date")) | |
| await pause(13) | |
| toasts.present(.info("Copied to clipboard", systemImage: "doc.on.doc")) | |
| await pause(4.5) | |
| toasts.present(.success("Saved", placement: .bottom)) | |
| await pause(5) | |
| toasts.present(.failure( | |
| "The request timed out.", | |
| title: "Couldn't create workspace", | |
| action: Toast.Action(label: "Retry") {} | |
| )) | |
| await pause(7.5) | |
| toasts.dismissAll() | |
| } | |
| .task { try? await runAutodemoIfRequested() } | |
| } | |
| /// With `CMUX_TOAST_GALLERY_AUTORUN=1`, walks the full toast vocabulary on | |
| /// a fixed cadence so a screen recording captures every arrival, | |
| /// departure, coalescing bump, and queue advance without UI driving. | |
| /// Demo pacing only (DEBUG harness), hence the deliberate wall-clock sleeps. | |
| private func runAutodemoIfRequested() async throws { | |
| guard ProcessInfo.processInfo.environment["CMUX_TOAST_GALLERY_AUTORUN"] == "1" else { return } | |
| let clock = ContinuousClock() | |
| func pause(_ seconds: Double) async throws { | |
| try await clock.sleep(for: .seconds(seconds)) | |
| } | |
| try await pause(2) | |
| toasts.present(.success("Workspace created")) | |
| try await pause(5.5) | |
| toasts.present(.failure( | |
| "Not connected to your Mac.", | |
| title: "Couldn't rename workspace", | |
| coalescingKey: "demo.error" | |
| )) | |
| try await pause(2) | |
| toasts.present(.failure( | |
| "Not connected to your Mac.", | |
| title: "Couldn't rename workspace", | |
| coalescingKey: "demo.error" | |
| )) | |
| try await pause(7.5) | |
| toasts.present(.success("First: workspace created")) | |
| toasts.present(.info("Second: agent finished")) | |
| toasts.present(.warning("Third: build is out of date")) | |
| try await pause(13) | |
| toasts.present(.info("Copied to clipboard", systemImage: "doc.on.doc")) | |
| try await pause(4.5) | |
| toasts.present(.success("Saved", placement: .bottom)) | |
| try await pause(5) | |
| toasts.present(.failure( | |
| "The request timed out.", | |
| title: "Couldn't create workspace", | |
| action: Toast.Action(label: "Retry") {} | |
| )) | |
| try await pause(7.5) | |
| toasts.dismissAll() | |
| } |
🤖 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 `@Packages/iOS/CmuxMobileToast/Sources/CmuxMobileToast/ToastGalleryView.swift`
around lines 92 - 140, Update runAutodemoIfRequested and its nested pause helper
to be throwing async functions, replace the swallowed clock.sleep cancellation
with try await, and propagate errors through every pause call so task
cancellation aborts the autodemo sequence instead of continuing immediately.
SwiftUI renders the card without dedicated UIViews, so UIWindow.hitTest returns the hosting view for card and empty space alike — the previous view-identity filter passed EVERY touch through, leaving tap/drag/action dead. The card now publishes its window-space frame (onGeometryChange → ToastHostChrome.interactiveRegion) and the window captures only inside it. Verified on-device via the DEBUG hitTest probe (empty overlay nil / card region captured / beside-card nil), written to the app container by the gallery autorun. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Verification on cmux-melba-sim (iPhone 17 Pro, iOS 26.5, Liquid Glass path), tag
Not exercised with real touches (sim windows are offscreen on this Mac, no HID tooling): finger drag-to-dismiss and the action button tap — both ride standard SwiftUI gestures now that hit delivery is proven, and are the first dogfood checks. |
There was a problem hiding this comment.
♻️ Duplicate comments (1)
Packages/iOS/CmuxMobileToast/Sources/CmuxMobileToast/ToastGalleryView.swift (1)
99-190: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winPropagate task cancellation to prevent instantaneous demo execution.
By using
try?to swallow theCancellationErrorfromclock.sleep, thepausefunction will return immediately if the.taskis cancelled (e.g., if the user dismisses the view). This causes the remainder of the autodemo script to execute instantaneously, spamming theToastCenterqueue in a single frame.Make
pause,runAutodemoIfRequested, andrecordPassthroughProbethrowing functions, and usetry awaitso that cancellation properly aborts the sequence. Note that you will also need to update the caller (e.g.,.task { try? await runAutodemoIfRequested() }) which is outside of this diff block.🐛 Proposed fix
- private func runAutodemoIfRequested() async { + private func runAutodemoIfRequested() async throws { guard ProcessInfo.processInfo.environment["CMUX_TOAST_GALLERY_AUTORUN"] == "1" else { return } let clock = ContinuousClock() - func pause(_ seconds: Double) async { - try? await clock.sleep(for: .seconds(seconds)) + func pause(_ seconds: Double) async throws { + try await clock.sleep(for: .seconds(seconds)) } - await pause(2) + try await pause(2) `#if` os(iOS) - await recordPassthroughProbe(pause: pause) + try await recordPassthroughProbe(pause: pause) `#endif` toasts.present(.success("Workspace created")) - await pause(5.5) + try await pause(5.5) toasts.present(.failure( "Not connected to your Mac.", title: "Couldn't rename workspace", coalescingKey: "demo.error" )) - await pause(2) + try await pause(2) toasts.present(.failure( "Not connected to your Mac.", title: "Couldn't rename workspace", coalescingKey: "demo.error" )) - await pause(7.5) + try await pause(7.5) toasts.present(.success("First: workspace created")) toasts.present(.info("Second: agent finished")) toasts.present(.warning("Third: build is out of date")) - await pause(13) + try await pause(13) toasts.present(.info("Copied to clipboard", systemImage: "doc.on.doc")) - await pause(4.5) + try await pause(4.5) toasts.present(.success("Saved", placement: .bottom)) - await pause(5) + try await pause(5) toasts.present(.failure( "The request timed out.", title: "Couldn't create workspace", action: Toast.Action(label: "Retry") {} )) - await pause(7.5) + try await pause(7.5) toasts.dismissAll() } `#if` os(iOS) /// Exercises the passthrough window's `hitTest` with real on-device /// geometry and writes the verdicts to Documents/toast-probe.txt: with no /// toast the overlay must return nil everywhere (UIKit then routes the /// touch to the app's window below); with a toast visible, the card region /// must resolve to a live view while the rest still falls through. - private func recordPassthroughProbe(pause: (Double) async -> Void) async { + private func recordPassthroughProbe(pause: (Double) async throws -> Void) async throws { var lines: [String] = [] defer { let url = URL.documentsDirectory.appending(path: "toast-probe.txt") try? lines.joined(separator: "\n").write(to: url, atomically: true, encoding: .utf8) } guard let overlay = UIApplication.shared.connectedScenes .compactMap({ $0 as? UIWindowScene }) .flatMap(\.windows) .first(where: { $0 is ToastPassthroughWindow }) else { lines.append("FAIL overlay window not found") return } let center = CGPoint(x: overlay.bounds.midX, y: overlay.bounds.midY) let empty = overlay.hitTest(center, with: nil) lines.append(empty == nil ? "PASS empty overlay passes touches through (hitTest nil)" : "FAIL empty overlay captured touch: \(type(of: empty!))") toasts.present(.info("Passthrough probe", coalescingKey: "probe")) - await pause(0.8) + try await pause(0.8) lines.append("presented=\(String(describing: toasts.presented?.toast.message)) safeTop=\(overlay.safeAreaInsets.top)") var hitAny = false for y in stride(from: 4, through: 120, by: 8) { let point = CGPoint(x: overlay.bounds.midX, y: overlay.safeAreaInsets.top + CGFloat(y)) if let hit = overlay.hitTest(point, with: nil) { hitAny = true lines.append("hit y=+\(y): \(type(of: hit))") } } lines.append(hitAny ? "PASS toast region captures touch" : "FAIL toast region did not hit-test at any scanned point") let besideToast = overlay.hitTest(center, with: nil) lines.append(besideToast == nil ? "PASS area beside visible toast still passes through" : "FAIL area beside toast captured: \(type(of: besideToast!))") toasts.dismissAll() - await pause(0.8) + try await pause(0.8) } `#endif`🤖 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 `@Packages/iOS/CmuxMobileToast/Sources/CmuxMobileToast/ToastGalleryView.swift` around lines 99 - 190, Propagate cancellation through the autodemo flow: make runAutodemoIfRequested, its local pause helper, and recordPassthroughProbe throwing, and replace swallowed sleep calls with try await. Update the task caller to handle the thrown cancellation (for example, by using try?), while preserving the existing demo sequence and probe behavior when not cancelled.
🤖 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.
Duplicate comments:
In `@Packages/iOS/CmuxMobileToast/Sources/CmuxMobileToast/ToastGalleryView.swift`:
- Around line 99-190: Propagate cancellation through the autodemo flow: make
runAutodemoIfRequested, its local pause helper, and recordPassthroughProbe
throwing, and replace swallowed sleep calls with try await. Update the task
caller to handle the thrown cancellation (for example, by using try?), while
preserving the existing demo sequence and probe behavior when not cancelled.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 42575dee-ced7-4162-9f21-ccd291fae760
📒 Files selected for processing (3)
Packages/iOS/CmuxMobileToast/Sources/CmuxMobileToast/ToastGalleryView.swiftPackages/iOS/CmuxMobileToast/Sources/CmuxMobileToast/ToastHost.swiftPackages/iOS/CmuxMobileToast/Sources/CmuxMobileToast/ToastOverlayRoot.swift
One copy-confirmation vocabulary via Toast.copied(): chat bubble copy (threaded through ChatRowActions.notifyCopied to respect the snapshot boundary), chat detail Copy All, artifact copy contents/path, terminal text-sheet Copy All (replacing its button morph), and artifact gallery Copy path (closure-threaded through lazy rows). Feedback submit success now toasts after the composer dismisses; regaining a Mac connection toasts Reconnected. Ad-hoc UINotificationFeedbackGenerator calls at these sites removed: the toast supplies the haptic. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
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
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift`:
- Around line 94-106: Update the store.connectionState observer in
WorkspaceShellView to use the initial: true overload, so its handler runs
immediately for the current state and primes hasHeldConnection when the view
mounts already connected; preserve the existing reconnect toast behavior for
later connected transitions.
🪄 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: c88dfdb1-45b0-4c48-a998-2ec7ba5f74e4
📒 Files selected for processing (14)
Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Artifacts/ChatArtifactViewerPager.swiftPackages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Screen/ChatScreen.swiftPackages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Transcript/ChatBlockDetailSheetView.swiftPackages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Transcript/ChatMessageRowView.swiftPackages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Transcript/ChatRowActions.swiftPackages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Transcript/Rows/ChatProseBubbleView.swiftPackages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalArtifactFilesSheet+Content.swiftPackages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalArtifactFilesSheet.swiftPackages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalArtifactGalleryItemView.swiftPackages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalTextSheetView.swiftPackages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView.swiftPackages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swiftPackages/iOS/CmuxMobileToast/Sources/CmuxMobileToast/Toast.swiftios/cmux/Resources/Localizable.xcstrings
| .onChange(of: store.connectionState) { _, state in | ||
| guard state == .connected else { return } | ||
| if hasHeldConnection { | ||
| toasts.present(.success( | ||
| L10n.string( | ||
| "mobile.connection.reconnectedToast", | ||
| defaultValue: "Reconnected to your Mac." | ||
| ), | ||
| coalescingKey: "connection.reconnected" | ||
| )) | ||
| } | ||
| hasHeldConnection = true | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Verify the repository uses iOS 17+ SwiftUI onChange(of:initial:) signature.
# Test: Check for the initial-delivery overload usage in the codebase.
rg "\.onChange\(of:[^,]+, initial:\s*true\)" Packages/iOS --type swift | head -n 5Repository: manaflow-ai/cmux
Length of output: 154
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== file excerpt =="
sed -n '1,180p' Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift | cat -n
echo
echo "== search for onChange(initial:) usage =="
rg -n "\.onChange\(of:.*initial:\s*true" Packages/iOS -g '*.swift' || true
echo
echo "== search for connectionState handling =="
rg -n "connectionState|hasHeldConnection|reconnectedToast" Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI -g '*.swift' || trueRepository: manaflow-ai/cmux
Length of output: 12758
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== deployment-target clues =="
rg -n "IPHONEOS_DEPLOYMENT_TARGET|MARKETING_VERSION|platforms:|iOS\(" Packages iOS . -g 'Package.swift' -g '*.xcconfig' -g '*.pbxproj' -g '*.plist' -g '*.swift' | head -n 200
echo
echo "== package manifests =="
git ls-files '**/Package.swift' '**/*.xcworkspace/contents.xcworkspacedata' '**/*.pbxproj' | sed -n '1,120p'Repository: manaflow-ai/cmux
Length of output: 15222
Prime hasHeldConnection on mount
If this view appears while store.connectionState is already .connected, hasHeldConnection stays false until the next transition, so the first real reconnect in this lifecycle won’t toast. Use the initial: true overload here so the flag is primed as soon as the current state is read.
🤖 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
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift`
around lines 94 - 106, Update the store.connectionState observer in
WorkspaceShellView to use the initial: true overload, so its handler runs
immediately for the current state and primes hasHeldConnection when the view
mounts already connected; preserve the existing reconnect toast behavior for
later connected transitions.
ToastDebugTrigger (DEBUG builds only) listens for darwin notifications and presents a toast from a JSON spec in the app's defaults, so a shell can exercise the toast layer inside the running app without touch input: simctl spawn defaults write + notifyutil -p. ios/scripts/toast-debug.sh wraps both (style/title/message/icon/placement/persistent/action, plus --dismiss-all). Registered by the toast window host, torn down with it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bare Liquid Glass (.regular, untinted) is nearly transparent over busy content; over a terminal screen the toast text disappeared entirely. All cards now sit on a .regularMaterial plate (vibrancy-aware in both modes); on iOS 26 the glass layer rides on top purely for its rim and specular response. Verified over the dark workspace UI and in light mode via the debug trigger. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extracts the demo choreography into ToastDemo (DEBUG-only, shared by the gallery autorun, the remote trigger's new --demo verb, and Settings). Developer section gains Toast Gallery (sheet), Run Toast Demo, and a 0-30s delay stepper (persisted via AppStorage) so you can start the demo, navigate to any screen, and watch it play there. Strings localized en+ja. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
WorkspaceShellView: keep ToastCenter env in main's new tab-scaffold body; old WorkspaceActionToast mount in workspaceTabContent removed (failures go through the app-wide toast layer). Root scene: toastHost moves into main's new applyingRootEnvironment(to:) so every root host gets the toast window. Failure copy: adopt main's three new failure cases (invalidWorkingDirectory, persistenceUnavailable, alreadyCompleted) as sentence-form reasons (en+ja) and rewrite main's new WorkspaceActionFailureMessageTests against the title+reason API. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ToastCenter.isEnabled (persisted, default false) drops every present() while off and is surfaced as Settings > Beta Features > Toasts. Off-state falls back to the pre-toast surfaces, restored from main: the dismissible bottom workspace-action banner (title+reason joined), ChatScreen's inline error banner with its VoiceOver announcement, the text sheet's Copied button morph, and the success haptics at copy/feedback sites. The DEBUG gallery env force-enables the flag so harness runs stay meaningful. Verified live on the sim: trigger drops with flag off, presents with flag on; 16 ToastCenter tests incl. the disabled gate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…coped holds CodeRabbit majors: onChange(initial: true) so a pre-existing chat error and an already-connected mount are handled; Toast fields are lets; the default coalescing key joins on a unit separator instead of an aliasable delimiter; interaction holds are scoped to the presented toast id (a straggling gesture from a departed toast can no longer pause or resume its successor); DocC on the four factories; gallery autorun propagates cancellation. Skipped Greptile's caseless-enum-namespace nit: that is the repo's established idiom. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Shipping dark per Aziz: the whole system now sits behind Settings → Beta Features → Toasts, off by default ( |
One toast system for the iOS app, replacing the two ad-hoc attempts (the bottom
WorkspaceActionToastcapsule inWorkspaceShellViewand ChatScreen'sTimer.publisherror banner).Architecture. New
Packages/iOS/CmuxMobileToastpackage.ToastCenter(@MainActor @Observable) owns all policy: FIFO queue (capped at 3, drop-oldest), coalescing (same-key presents refresh the visible toast in place and pulse it instead of stacking duplicates), style-derived dwell (info/success 3.5s, warning/failure 6s, actions 6s,.neverfor persistent), interaction holds that pause auto-dismiss mid-touch, and a 260ms breath between consecutive toasts. All timing runs on an injectedClock, covered by 15 Swift Testing tests with a hand-advanced manual clock. Views present via@Environment(ToastCenter.self); one.toastHost(_:)atCMUXMobileRootScenemounts the presentation layer.Presentation. On iOS toasts render in a passthrough
UIWindowat.alertlevel, so they float above sheets and full-screen covers while every touch outside the card falls through to the app. Keyboard overlap feeds in through the host so bottom-placed toasts ride above the keyboard. Cards are Liquid Glass on iOS 26 (glassEffect) with a material fallback, capsule for plain messages, continuous rounded rect for title+message.Motion & feel. Arrival: spring drift from the resting edge + scale from 0.86 + de-blur, with a symbol bounce as the card lands. Departure: faster fully-damped settle. Interactive drag-to-dismiss follows the finger toward the edge, rubber-bands away from it, and uses velocity projection on release. Coalescing re-presents pulse the card. Haptics via
sensoryFeedback(success/warning/error/impact per style). VoiceOver announcements on present,.escapeand named dismiss actions, dwell doubles under VoiceOver/Switch Control, Reduce Motion collapses to cross-fades, Reduce Transparency gets an opaque card.Call sites. Workspace-action failures now read as a bold title ("Couldn't rename workspace") plus a sentence reason ("Not connected to your Mac."), with the localization keys reworked in en+ja (composed
failure.messagekey replaced byfailure.titleFormat; reason fragments became standalone sentences). Chat errors present through the same center with a per-store coalescing key so a newer error replaces the visible one instead of queueing stale ones.Dogfood. DEBUG gallery behind
CMUX_TOAST_GALLERY=1exercises every style, placement, queueing, coalescing, persistent, and action-bearing composition.🤖 Generated with Claude Code
Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.Summary by cubic
Unified toast system for iOS with
CmuxMobileToast, replacing ad‑hoc banners. Toasts are consistent, accessible, animated, and float above sheets and the keyboard; adds a developer gallery, demo, and remote trigger; now gated behind a Beta Features toggle (off by default) with safe fallbacks.New Features
CmuxMobileToastwithToastCenter: FIFO queue (cap 3), coalescing by key, style-based dwell, interaction holds; polished motion and drag-to-dismiss; Liquid Glass with a.regularMaterialplate; haptics, VoiceOver, and Reduce Motion/Transparency support; one overlay host via.toastHost(_:)rendering in a passthrough alert-levelUIWindowand mounted at the app root so every root host gets the toast window. Tests cover timing/queue/interaction (manual clock).ToastCenter.isEnabledis persisted and exposed in Settings > Beta Features > “Toasts” (default off). When off, we fall back to the legacy UI (ChatScreen inline error banner with VoiceOver, bottom workspace-action banner with joined title+reason, text sheet’s “Copied” checkmark, direct success haptics). DEBUG gallery (CMUX_TOAST_GALLERY=1, optional autorun viaCMUX_TOAST_GALLERY_AUTORUN=1), Settings Developer includes Toast Gallery + “Run Toast Demo” with a delay, remote trigger (ToastDebugTrigger) +ios/scripts/toast-debug.sh(present/dismiss/--demo).Bug Fixes
Toastimmutable, scope interaction holds to the presented toast, use a unit separator in the default coalescing key, propagate cancellation in the gallery autorun, and add DocC for the toast factories.Written for commit 47c0f8a. Summary will update on new commits.
Summary by CodeRabbit