Skip to content

iOS: unified animated toast system (CmuxMobileToast) - #8376

Merged
azooz2003-bit merged 10 commits into
mainfrom
feat-ios-toast-ux
Jul 21, 2026
Merged

azooz2003-bit merged 10 commits into
mainfrom
feat-ios-toast-ux

Conversation

@azooz2003-bit

@azooz2003-bit azooz2003-bit commented Jul 17, 2026 •

Copy link
Copy Markdown
Collaborator

One toast system for the iOS app, replacing the two ad-hoc attempts (the bottom WorkspaceActionToast capsule in WorkspaceShellView and ChatScreen's Timer.publish error banner).

Architecture. New Packages/iOS/CmuxMobileToast package. 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, .never for persistent), interaction holds that pause auto-dismiss mid-touch, and a 260ms breath between consecutive toasts. All timing runs on an injected Clock, covered by 15 Swift Testing tests with a hand-advanced manual clock. Views present via @Environment(ToastCenter.self); one .toastHost(_:) at CMUXMobileRootScene mounts the presentation layer.

Presentation. On iOS toasts render in a passthrough UIWindow at .alert level, 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, .escape and 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.message key replaced by failure.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=1 exercises every style, placement, queueing, coalescing, persistent, and action-bearing composition.

🤖 Generated with Claude Code


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with 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

    • Added CmuxMobileToast with ToastCenter: FIFO queue (cap 3), coalescing by key, style-based dwell, interaction holds; polished motion and drag-to-dismiss; Liquid Glass with a .regularMaterial plate; haptics, VoiceOver, and Reduce Motion/Transparency support; one overlay host via .toastHost(_:) rendering in a passthrough alert-level UIWindow and mounted at the app root so every root host gets the toast window. Tests cover timing/queue/interaction (manual clock).
    • Adoption & gating: unified “Copied” confirmations across chat, artifact viewer/gallery (“Copy path”), and terminal text sheet (“Copy All”); feedback success and “Reconnected to your Mac.” now toast; workspace-action failures present as bold title + sentence reason with updated localization (en/ja). Beta flag ToastCenter.isEnabled is 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 via CMUX_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

    • Passthrough window now captures touches only inside the toast card via geometry-gated hit-testing; taps, drags, and action buttons work reliably.
    • Reliability/polish: handle initial delivery (pre-existing chat errors and already-connected mounts), make Toast immutable, 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.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Added a unified, app-wide toast notification system (info/success/warning/failure) with optional actions, queuing, coalescing, keyboard-aware placement, and dismiss/hold interactions.
    • Enabled toast hosting at the app root, including a DEBUG toast gallery and JSON-driven debug trigger.
    • Added “copied” and “path copied” toast confirmations, plus a “reconnected” success toast.
  • Bug Fixes
    • Conversation and workspace-action failures now use toast presentation, with improved retry behavior and updated failure copy.
  • Tests
    • Added comprehensive ToastCenter tests (timing, queuing, coalescing, interaction pauses, dismiss-all) using a manual clock.

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

cursor Bot commented Jul 17, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

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

@coderabbitai

coderabbitai Bot commented Jul 17, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The change adds CmuxMobileToast, centralizes transient notifications through ToastCenter, provides SwiftUI and iOS host rendering, migrates chat and workspace feedback, and wires the system into application roots, previews, tests, and debug tooling.

Changes

Mobile toast system

Layer / File(s) Summary
Toast package and data contract
Packages/iOS/CmuxMobileToast/Package.swift, Packages/iOS/CmuxMobileToast/Sources/CmuxMobileToast/Toast.swift
Defines the package and Toast model with styles, actions, dismissal policies, factories, identity, and coalescing keys.
ToastCenter lifecycle and validation
Packages/iOS/CmuxMobileToast/Sources/CmuxMobileToast/ToastCenter.swift, Packages/iOS/CmuxMobileToast/Tests/CmuxMobileToastTests/*
Adds presentation, queuing, coalescing, dismissal, interaction handling, timed advancement, and manual-clock tests.
Toast rendering and host window
Packages/iOS/CmuxMobileToast/Sources/CmuxMobileToast/ToastCardView.swift, ToastMotion.swift, ToastOverlayRoot.swift, ToastHost.swift
Adds card rendering, accessibility, animation, drag dismissal, keyboard adjustment, and an iOS passthrough overlay window.
Chat and workspace integration
Packages/iOS/CmuxAgentChatUI/..., Packages/iOS/CmuxMobileShellUI/..., ios/cmux/Resources/Localizable.xcstrings
Routes errors, reconnection events, feedback success, and copy confirmations through ToastCenter, removes obsolete overlays, and updates failure text.
Application host and debug tooling
ios/cmuxPackage/..., Packages/iOS/CmuxAgentChatUI/Previews/..., Packages/iOS/CmuxMobileToast/Sources/CmuxMobileToast/ToastGalleryView.swift, ios/scripts/toast-debug.sh
Links the package, installs root and preview hosts, exposes a debug gallery, and adds simulator-driven toast commands.

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
Loading

Possibly related PRs


Important

Pre-merge checks failed

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

❌ Failed checks (2 errors, 2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Cmux No Test Or Debug Seam In Production Source ❌ Error Packages/iOS/CmuxMobileToast/Sources/CmuxMobileToast/ToastCenter.swift adds prefersExtendedDwell, an injectable closure marked for tests and only mutated by ToastCenterTests via @testable import. Remove the test-only hook from production source; keep accessibility/dwell testing in Tests/ via @testable import, or isolate any unavoidable debug-only code in a dedicated debug file. See PR #6452.
Cmux No Ambient Global State ❌ Error Packages/iOS/CmuxMobileToast/Sources/CmuxMobileToast/ToastMotion.swift:9 adds a caseless ToastMotion enum that only namespaces static funcs, which the rule forbids. Move driver/transition into ToastOverlayRoot/ToastPresentationView as private methods or private file-scope helpers; avoid a static-only namespace type.
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The description covers the summary well, but it omits required Testing, Demo Video, Review Trigger, and Checklist sections. Add the missing Testing, Demo Video, Review Trigger, and Checklist sections, including how you tested the change and any demo link if available.
Cmux Swift Logging ❓ Inconclusive placeholder need investigation
✅ Passed checks (20 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed New mutable toast state is main-actor isolated; all toast call sites are UI/main-actor paths, with no new off-main store access or unsafe Sendable refs.
Cmux Swift Blocking Runtime ✅ Passed No new production blocking primitives; only test-only NSLock in ManualClock. Runtime toast timing uses injected Clock.sleep for visual dwell/gap, not thread-blocking sync.
Cmux Browser Automation Off-Main ✅ Passed Diff is toast-only; no browser.* routing, WebKit wait callbacks, or socket-worker/main-actor automation code changed.
Cmux Expensive Synchronous Load ✅ Passed No touched production path adds agent-history loads or broad scans; the only JSON decode is DEBUG-only ToastDebugTrigger reading a small defaults spec.
Cmux Cache Substitution Correctness ✅ Passed Only transient toast/UI state changed; no persistence/history/undo/snapshot read was replaced by a cache, and no stale-cache path appears.
Cmux No Hacky Sleeps ✅ Passed The only changed non-Swift runtime script, ios/scripts/toast-debug.sh, just writes defaults and posts notifications; no sleep, polling, or delayed dispatch was added.
Cmux Algorithmic Complexity ✅ Passed PASS: the only new scans are on a toast queue explicitly capped at 3, plus debug/test code; no scalable hot-path rescans were introduced.
Cmux Swift Concurrency ✅ Passed No new bg Dispatch/Combine/completion-handler APIs; toast Tasks are stored/cancelled or tied to SwiftUI/OS callback boundaries.
Cmux Swift @Concurrent ✅ Passed No changed Swift code introduces invalid @concurrent use; async work stays UI-bound or actor-isolated, and no nonisolated async helper needs @concurrent.
Cmux Swift Package Boundaries ✅ Passed PASS — the diff adds a reusable toast package and keeps app-target changes to view/composition glue; no independent domain logic was added to app root sources.
Cmux Swiftpm Lockfiles ✅ Passed Only local-path SwiftPM deps were added; no Package.resolved or .gitignore changes appear in the PR diff, and no xcodeproj package-ref edit exists.
Cmux User-Facing Error Privacy ✅ Passed PASS: The new toasts and workspace failure copy are generic (“Your Mac”, “Couldn't %@”); I found no vendor/internal names, raw upstream text, or secrets in changed user-facing strings.
Cmux Full Internationalization ✅ Passed All new user-facing Swift copy uses localized APIs, and the added/changed toast/workspace keys have matching en/ja entries in Localizable.xcstrings.
Cmux Swiftui State Layout ✅ Passed PASS: new toast code uses @Observable/@State, not ObservableObject/@published; geometry measurement is localized to overlay hit-testing, and the List screen has no row store refs.
Cmux Architecture Rethink ✅ Passed Unified ToastCenter keeps one source of truth; observers/locks are limited to required platform bridges and test-only sync, not symptom patches.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PASS: The PR adds an iOS UIWindow toast overlay, but no AppKit/SwiftUI Window/WindowGroup/NSWindow code, no cmux.* identifier assignment, and no cmuxAuxiliaryWindowIdentifiers change.
Cmux Source Artifacts ✅ Passed Changed paths are source code and a shell script; no logs, build output, scratch dirs, or dependency checkouts were added.
Title check ✅ Passed The title clearly summarizes the main change: a unified iOS toast system.
✨ 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-toast-ux

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.

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

📥 Commits

Reviewing files that changed from the base of the PR and between c9f2d8c and 7eebcee.

⛔ Files ignored due to path filters (1)
  • cmux.xcworkspace/contents.xcworkspacedata is excluded by !**/*.xcworkspace/contents.xcworkspacedata
📒 Files selected for processing (21)
  • Packages/iOS/CmuxAgentChatUI/Package.swift
  • Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Previews/ChatScreenPreviews.swift
  • Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Screen/ChatScreen.swift
  • Packages/iOS/CmuxMobileShellUI/Package.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceActionToast.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceActionToastContent.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView+WorkspaceActionFailure.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift
  • Packages/iOS/CmuxMobileToast/Package.swift
  • Packages/iOS/CmuxMobileToast/Sources/CmuxMobileToast/Toast.swift
  • Packages/iOS/CmuxMobileToast/Sources/CmuxMobileToast/ToastCardView.swift
  • Packages/iOS/CmuxMobileToast/Sources/CmuxMobileToast/ToastCenter.swift
  • Packages/iOS/CmuxMobileToast/Sources/CmuxMobileToast/ToastGalleryView.swift
  • Packages/iOS/CmuxMobileToast/Sources/CmuxMobileToast/ToastHost.swift
  • Packages/iOS/CmuxMobileToast/Sources/CmuxMobileToast/ToastMotion.swift
  • Packages/iOS/CmuxMobileToast/Sources/CmuxMobileToast/ToastOverlayRoot.swift
  • Packages/iOS/CmuxMobileToast/Tests/CmuxMobileToastTests/ManualClock.swift
  • Packages/iOS/CmuxMobileToast/Tests/CmuxMobileToastTests/ToastCenterTests.swift
  • ios/cmux/Resources/Localizable.xcstrings
  • ios/cmuxPackage/Package.swift
  • ios/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

Comment thread Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Screen/ChatScreen.swift Outdated
Comment on lines +49 to +84
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)"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 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: String

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

Suggested change
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)"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 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

Comment on lines +121 to +184
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
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 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

Comment on lines +116 to +136
/// 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 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-apps

greptile-apps Bot commented Jul 17, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces a unified CmuxMobileToast package that replaces two ad-hoc notification surfaces (the WorkspaceShellView bottom capsule and ChatScreen's Timer.publish error banner) with a single, polished, policy-driven toast system gated behind a Beta Features toggle (default off).

  • New CmuxMobileToast package: ToastCenter (@MainActor @Observable) owns all policy — FIFO queue (capped at 3), coalescing by key, style-derived dwell, interaction holds, and a 260 ms breath between consecutive toasts; all timing is tested with an injectable ManualClock (15 Swift Testing tests). On iOS, toasts render in a passthrough UIWindow at .alert level so they float above sheets and full-screen covers without blocking touches outside the card.
  • Presentation & motion: Liquid Glass on iOS 26 with a regularMaterial fallback, spring arrival with scale + de-blur, fully-damped departure, drag-to-dismiss, coalescing pulse, haptics via sensoryFeedback, VoiceOver announcements, and Reduce Motion/Transparency support.
  • Call-site adoption: Workspace-action failures, chat-conversation errors, "Copied"/"Path copied" confirmations, feedback confirmation, and reconnect success now route through ToastCenter; all call sites fall back to their legacy surfaces when the beta flag is off; Localizable.xcstrings updated with both en and ja entries for every new key.

Confidence Score: 5/5

Safe 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

Filename Overview
Packages/iOS/CmuxMobileToast/Sources/CmuxMobileToast/ToastCenter.swift Core policy engine: @mainactor @observable, FIFO queue with cap-3 drop-oldest, coalescing, interaction holds, injected clock for testability. All timing via clock.sleep on owned Tasks with proper cancellation and weak self.
Packages/iOS/CmuxMobileToast/Sources/CmuxMobileToast/ToastHost.swift Passthrough UIWindow at .alert level, geometry-gated hitTest, keyboard overlap via NotificationCenter, ToastDebugTrigger lifecycle in #if DEBUG. Clean separation of concerns.
Packages/iOS/CmuxMobileToast/Sources/CmuxMobileToast/Toast.swift Value-type Toast with semantic factories, Sendable, identity-adoption for coalescing. Default coalescing key built from style+title+message using a separator no UI string contains.
Packages/iOS/CmuxMobileToast/Sources/CmuxMobileToast/ToastOverlayRoot.swift Full-screen overlay, ToastMotion transitions, sensoryFeedback trigger, VoiceOver announcements via AccessibilityNotification. ToastHostChrome shared between window coordinator and overlay.
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView+WorkspaceActionFailure.swift Workspace-action failure routing: coalesces per-action-type key, falls back to legacy banner when beta flag is off. Reason/title strings correctly routed through L10n with en+ja xcstrings entries.
Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Screen/ChatScreen.swift Chat error bridge: onChange(initial:true) presents toast and calls dismissError() when flag is on; legacy inline banner shown only when flag is off. VoiceOver announcement gating correct.
ios/cmuxPackage/Sources/cmuxFeature/CMUXMobileRootScene.swift ToastCenter created as @State at the app root and injected via .toastHost(_:), ensuring a single instance for the full app lifetime with the alert-level window mounted once.
ios/cmux/Resources/Localizable.xcstrings All new user-facing keys (toast UI, settings, connection, feedback, copy confirmations, workspace action fragments) added with both en and ja translations; old failure.message key removed.
Packages/iOS/CmuxMobileToast/Tests/CmuxMobileToastTests/ToastCenterTests.swift 15 Swift Testing tests covering queue/dequeue, coalescing, interaction holds, dwell, VoiceOver dwell extension, and dismissAll using a hand-advanced ManualClock.
ios/scripts/toast-debug.sh Developer-only simulator helper using xcrun simctl + notifyutil to drive ToastDebugTrigger; simulator-only per the documentation, no production exposure.

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())
Loading
%%{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())
Loading

Reviews (10): Last reviewed commit: "Address review findings: initial-deliver..." | Re-trigger Greptile

Comment on lines +9 to +42
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)
}
}

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 7eebcee and fce2f32.

📒 Files selected for processing (1)
  • Packages/iOS/CmuxMobileToast/Sources/CmuxMobileToast/ToastGalleryView.swift

Comment on lines +92 to +140
.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()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Suggested change
.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>
@azooz2003-bit

Copy link
Copy Markdown
Collaborator Author

Verification on cmux-melba-sim (iPhone 17 Pro, iOS 26.5, Liquid Glass path), tag melba:

  • 15/15 ToastCenterTests pass (manual-clock dwell/queue/coalesce/interaction-hold/extended-dwell), package cross-builds clean for iOS sim + macOS host; CmuxMobileShellUI, CmuxAgentChatUI, and cmuxFeature cross-build clean on this head.
  • Recorded autorun demos light+dark (CMUX_TOAST_GALLERY=1 CMUX_TOAST_GALLERY_AUTORUN=1): success/info capsules, failure/warning cards, custom icon, bottom placement above safe area, FIFO queue advance with inter-toast gap, coalescing re-present extending dwell, action card with Retry. Frame grids show the arrival spring (faint+small+blurred → settled) and the faster damped departure.
  • The last commit fixes a real bug the on-device hitTest probe caught: SwiftUI hosts don't create per-view UIViews, so the classic "pass through unless hitTest returns a subview" window filter passed every touch through — tap/drag/action on the toast were dead while videos looked perfect. The card now publishes its window-space frame (onGeometryChange → ToastHostChrome.interactiveRegion) and ToastPassthroughWindow.hitTest only captures inside it. Probe verdicts on this head: empty overlay nil ✓, card region captured ✓, beside-card nil ✓ (written to Documents/toast-probe.txt by the autorun).

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.

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

♻️ Duplicate comments (1)
Packages/iOS/CmuxMobileToast/Sources/CmuxMobileToast/ToastGalleryView.swift (1)

99-190: 🎯 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, runAutodemoIfRequested, and recordPassthroughProbe throwing functions, and use try await so 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

📥 Commits

Reviewing files that changed from the base of the PR and between fce2f32 and ea223c3.

📒 Files selected for processing (3)
  • Packages/iOS/CmuxMobileToast/Sources/CmuxMobileToast/ToastGalleryView.swift
  • Packages/iOS/CmuxMobileToast/Sources/CmuxMobileToast/ToastHost.swift
  • Packages/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>

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

📥 Commits

Reviewing files that changed from the base of the PR and between ea223c3 and 79db4b6.

📒 Files selected for processing (14)
  • Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Artifacts/ChatArtifactViewerPager.swift
  • Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Screen/ChatScreen.swift
  • Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Transcript/ChatBlockDetailSheetView.swift
  • Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Transcript/ChatMessageRowView.swift
  • Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Transcript/ChatRowActions.swift
  • Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Transcript/Rows/ChatProseBubbleView.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalArtifactFilesSheet+Content.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalArtifactFilesSheet.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalArtifactGalleryItemView.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalTextSheetView.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift
  • Packages/iOS/CmuxMobileToast/Sources/CmuxMobileToast/Toast.swift
  • ios/cmux/Resources/Localizable.xcstrings

Comment on lines +94 to +106
.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
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 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 5

Repository: 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' || true

Repository: 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.

cmux reload-cloud and others added 6 commits July 20, 2026 12:21
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>
@azooz2003-bit

Copy link
Copy Markdown
Collaborator Author

Shipping dark per Aziz: the whole system now sits behind Settings → Beta Features → Toasts, off by default (ToastCenter.isEnabled, persisted). Off = the pre-toast surfaces, restored from main (workspace-action bottom banner, chat inline error banner, Copy-All button morph, copy/feedback haptics). On = the toast layer. Verified both states live on the sim; 17 unit tests including the disabled gate. Review findings addressed in the last commit (both initial-delivery majors, immutable Toast, collision-free coalescing key, id-scoped interaction holds, DocC, autorun cancellation); skipped the caseless-enum style nit as repo idiom.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant