Skip to content

Keep mobile chat GUI cached during reconnect - #7064

Merged
azooz2003-bit merged 31 commits into
mainfrom
feat-gui-cache-reconnect
Jun 30, 2026
Merged

azooz2003-bit merged 31 commits into
mainfrom
feat-gui-cache-reconnect

Conversation

@azooz2003-bit

@azooz2003-bit azooz2003-bit commented Jun 29, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • preserve the mobile chat-session cache when the Mac connection is unavailable or a refresh fails
  • move chat-session snapshots into the shell store so leaving and re-entering a workspace does not discard the evidence that GUI history exists
  • render the GUI toggle from cached GUI-history snapshots immediately on workspace re-entry, with authoritative session refreshes replacing or clearing that cache
  • keep the selected terminal/pinned chat conversation store warm while terminal rendering is visible, so opening GUI chat reuses already-loaded history
  • reserve fixed toolbar slots for GUI chat + terminal picker, so first GUI-history discovery fades/scales the chat glyph without pushing the terminal picker or recentering the title
  • add a focused refresh-outcome policy test

Verification

  • inspected /Users/abdulazizalbahar/Downloads/ScreenRecording_06-29-2026 15-53-13_1.mov frame-by-frame; the remaining jump was the chat button appearing alone before the terminal picker slot arrived
  • python3 scripts/swift_file_length_budget.py --budget .github/swift-file-length-budget.tsv
  • xcodebuild -workspace ios/cmux.xcworkspace -scheme cmux-ios -configuration Debug -destination "platform=iOS Simulator,name=iPhone 17" -derivedDataPath "$HOME/Library/Developer/Xcode/DerivedData/cmux-ios-guic" PRODUCT_BUNDLE_IDENTIFIER=dev.cmux.ios.guic "PRODUCT_DISPLAY_NAME=cmux DEV guic" CMUX_GIT_SHA=$(git rev-parse --short=10 HEAD)+ CMUX_DEV_TAG=guic CMUX_PRESENCE_BASE_URL= EXCLUDED_SOURCE_FILE_NAMES=Info.plist CODE_SIGNING_ALLOWED=NO SWIFT_OPTIMIZATION_LEVEL=-O SWIFT_COMPILATION_MODE=wholemodule GCC_OPTIMIZATION_LEVEL=s build
  • IOS_DEVELOPMENT_TEAM=7WLXT3NR37 ./ios/scripts/reload.sh --tag guic --device --device-id 4A52829D-6427-599F-A166-4058881D2DF4 (simulator build succeeded; physical iPhone build/install/auth launch succeeded)
  • ./scripts/reload-cloud.sh --tag guic (cloud unavailable, local fallback build succeeded)
  • curl -fsS http://127.0.0.1:17320/guic
  • CMUX_TAG=guic scripts/cmux-debug-cli.sh ping
  • CMUX_TAG=guic scripts/cmux-debug-cli.sh window display "LG HDR 4K"

Notes

  • verify-video skill was not present in the available session or local skill tree, so the recording was inspected with ffprobe/ffmpeg frame extraction instead.
  • swift test --package-path Packages/iOS/CmuxMobileShellUI is not usable for this iOS-only package locally because SwiftPM evaluates it as macOS and hits existing platform floor mismatches.
  • xcodebuild test -workspace ios/cmux.xcworkspace -scheme CmuxMobileShellUI is not configured for the test action.

Summary by CodeRabbit

  • New Features
    • Added workspace-scoped caching for chat-capable session descriptors and retrieval/recall APIs.
    • Added a refresh-outcome model to control how chat session lists update and whether selection can be invalidated.
    • Enabled chat conversation warming and refreshed session descriptor streams in the workspace detail UI.
    • Added an optional “run store task” mode for the chat screen.
  • Bug Fixes
    • Improved in-memory chat descriptor reconciliation to prevent stale/out-of-order updates.
    • Improved reconnect/backoff behavior to resume promptly when the transport source changes.
    • Better cleanup and fallback behavior when sessions/terminals temporarily disappear.
  • Tests
    • Added tests for refresh outcome behavior.

@coderabbitai

coderabbitai Bot commented Jun 29, 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

Adds chat-session snapshot caching, refresh-outcome handling, descriptor reconciliation, and extracted iOS workspace chat rendering and lifecycle logic.

Changes

iOS Agent Chat session management & detail view refactor

Layer / File(s) Summary
Store reconciliation and screen task control
Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Store/ChatConversationStore.swift, Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Screen/ChatScreen.swift
ChatConversationStore adds descriptor snapshot reconciliation and interruptible reconnect backoff. ChatScreen can now skip store.run() via runsStoreTask: Bool = true.
Workspace chat snapshot cache
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift, Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+AgentChat.swift
Adds per-workspace chat-session snapshot storage and accessors, plus chat event source identity generation and clearing on sign-out, stale foreground drop, and remote-client replacement.
Refresh outcome and pane wiring
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceChatSessionRefreshOutcome.swift, Packages/iOS/CmuxMobileShellUI/Tests/CmuxMobileShellUITests/WorkspaceChatSessionRefreshOutcomeTests.swift, Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceChatPane.swift
Defines the refresh outcome enum, verifies its behavior in tests, and makes WorkspaceChatPane require a conversation store directly.
WorkspaceDetailView state and toolbar rewiring
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView.swift
Widens access on rename and chat state, removes the inline chat block, routes trailing toolbar items through toolbarTrailingCluster, and fixes the iOS glass title chat-toggle flag.
WorkspaceDetailView agent chat flow
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView+AgentChat.swift
Adds the iOS-only agent-chat extension with session selection, toggle rendering, refresh streaming, conversation-store creation, warmup, pruning, repinning, and fallback reconciliation.

Sequence Diagram(s)

sequenceDiagram
  participant WorkspaceDetailView
  participant ChatEventSource
  participant MobileShellComposite
  participant ChatConversationStore

  WorkspaceDetailView->>ChatEventSource: refreshChatSessions()
  ChatEventSource-->>WorkspaceDetailView: authoritative sessions or unavailable
  WorkspaceDetailView->>MobileShellComposite: rememberChatSessions(workspaceID:)
  ChatEventSource-->>WorkspaceDetailView: streamed session frames
  WorkspaceDetailView->>WorkspaceDetailView: reconcileChatSessionSnapshot(...)
  WorkspaceDetailView->>ChatConversationStore: applyDescriptorSnapshot(_:)
  WorkspaceDetailView->>ChatConversationStore: replaceSource(_, descriptor:)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • manaflow-ai/cmux#5984: Introduced the CmuxAgentChat conversation store and chat UI surfaces extended here with reconciliation and task control.
  • manaflow-ai/cmux#6655: Introduced ChatSessionDescriptor.version and version-gated update logic used by the new snapshot reconciliation.
  • manaflow-ai/cmux#6300: Modified WorkspaceDetailView.swift chat-mode and toolbar entry points that this PR restructures for the extracted agent-chat flow.

Poem

🐇 Hop, hop, the chat sessions flow,
Cached by workspace, row by row.
No optional pane left bare and cold—
The store is passed, the session told.
Repin, refresh, then fallback right,
A rabbit wired the chat tonight! 🌙


Important

Pre-merge checks failed

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

❌ Failed checks (5 errors, 2 warnings)

Check name Status Explanation Resolution
Cmux Swift Blocking Runtime ❌ Error ChatConversationStore adds production reconnect backoff via ContinuousClock().sleep in waitForBackoffOrSourceReplacement, which is timing-based sync. Replace the sleep with a real cancellation-aware signal/timer from the transport, or restrict it to test-only/UI animation scaffolding.
Cmux Cache Substitution Correctness ❌ Error applyDescriptorSnapshot still accepts any version change, so an older ChatSessionDescriptor can overwrite newer live state in the snapshot path. Gate versioned snapshots with descriptor.version > self.descriptor.version (keeping the 0/0 case) so stale cache snapshots cannot replace newer state.
Cmux Algorithmic Complexity ❌ Error WorkspaceDetailView+AgentChat.swift rescans visibleChatSessions and filters chatConversationStores on every stream refresh, making the hot chat path repeated O(n) filtering without a bound. Keep a cached id->session/terminal index and update it incrementally; don’t rebuild liveIDs or filter the full store map on every refresh frame.
Cmux User-Facing Error Privacy ❌ Error ChatScreen renders store.lastErrorDescription directly, and ChatConversationStore sets it from error.localizedDescription, so user-visible toasts/VoiceOver can expose raw upstream errors. Map failures to sanitized product copy (e.g. unavailable/retry), and keep raw localizedDescription in logs/internal telemetry instead of the banner/announcement.
Cmux Architecture Rethink ❌ Error applyDescriptorSnapshot still accepts any non-equal nonzero version, so older snapshots can overwrite newer live session state. Change reconciliation to admit only strictly newer versions (or both-zero bootstrap) so the live stream remains the single source of truth.
Description check ⚠️ Warning The description covers the summary and verification, but omits the required Testing, Demo Video, Review Trigger, and Checklist sections. Add the template's Testing, Demo Video, Review Trigger, and Checklist sections, or explain why any are not applicable.
Docstring Coverage ⚠️ Warning Docstring coverage is 71.88% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (18 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the main change: preserving mobile chat GUI cache during reconnects.
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 chat state stays on @MainActor UI/store types; the new enum is pure value data, and no unisolated shared mutable Sendable type or background UI access was introduced.
Cmux Browser Automation Off-Main ✅ Passed PR diff doesn’t touch browser-automation routing files or any browser/WebKit paths, so the off-main rule isn’t implicated.
Cmux Expensive Synchronous Load ✅ Passed Diff adds async RPC-backed chat refresh/cache updates and body-launched tasks; no synchronous agent-history/disk loader was moved onto main-actor or interactive paths.
Cmux No Hacky Sleeps ✅ Passed No non-Swift runtime code adds sleeps/timers; the new delay/backoff logic is in Swift and out of scope for this rule.
Cmux Swift Concurrency ✅ Passed No disallowed legacy async patterns were introduced; new work uses SwiftUI .task, async funcs, and structured withTaskGroup/AsyncStream, with no DispatchQueue/Combine/completion APIs or detached ta...
Cmux Swift @Concurrent ✅ Passed No changed Swift code introduced missing/invalid @concurrent usage; heavy chat I/O already hops to actor-isolated sources, and UI tasks remain intentionally main-actor bound.
Cmux Swift File And Package Boundaries ✅ Passed Touched files stay under the rule’s new-file threshold; oversized files only get small, coherent updates while chat/session logic is extracted into package-target extensions.
Cmux Swiftpm Lockfiles ✅ Passed CmuxMobileShellUI adds only a local package dep; its .gitignore doesn't ignore Package.resolved, and no new external pins or Xcode ref changes appear.
Cmux Swift Logging ✅ Passed No new print/NSLog/Logger usage was added; the only NSLog and file-scoped Logger were already on origin/main.
Cmux Full Internationalization ✅ Passed Only new user-facing copy is the Agent Chat accessibility label, and it already has en/ja xcstrings translations; no other locale assets changed.
Cmux Swiftui State Layout ✅ Passed PASS: the touched SwiftUI views stay snapshot/action-based; no new ObservableObject/@published, list-row store refs, GeometryReader, or render-time state writes.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed The diff only changes in-view chat UI/store code; no NSWindow/NSPanel/NSWindowController/WindowGroup additions or cmux.* identifier changes appear, so the auxiliary-window rule isn’t triggered.
Cmux Source Artifacts ✅ Passed All changed paths are intentional source, test, or config files; no logs, screenshots, build output, caches, or temp artifacts appear in the PR set.
Cmux No Test Or Debug Seam In Production Source ✅ Passed PASS: the PR adds production cache/reconnect APIs and same-module access widening, but no new #if DEBUG/ForTesting/test-hook seam was added under Sources/.
Cmux No Ambient Global State ✅ Passed All new behavior stays on existing types/extensions; no new top-level free funcs, mutable globals, or singletons were added.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-gui-cache-reconnect

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.

@vercel

vercel Bot commented Jun 29, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jun 30, 2026 3:19am
cmux-staging Building Building Preview, Comment Jun 30, 2026 3:19am

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

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

Inline comments:
In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView`+AgentChat.swift:
- Around line 76-85: The warm-task identity in chatConversationWarmKey is too
granular because it includes session.version, which causes
ChatConversationStore.run() to restart on descriptor-only refreshes. Update the
key in WorkspaceDetailView+AgentChat to identify the task by workspace, session
id, and connectivity/foreground state only, so the long-lived conversation
runner stays subscribed while reconcileChatSessionSnapshot() and
ensureChatConversationStore(for:) handle metadata reconciliation.
- Around line 17-22: The visibleChatSessions logic currently prefers any
non-empty local chatSessions snapshot without verifying it belongs to the active
workspace, which can let stale sessions from another workspace drive
chatToggleSession and chosenChatSession. Update WorkspaceDetailView+AgentChat so
the local snapshot is scoped by workspace id (or ignored when it does not match
workspace.id.rawValue), and make the cache fallback in visibleChatSessions and
the related chat session selection flow use a single workspace-consistent source
of truth. Apply the same workspace identity check to the chat session state
handling referenced by the other affected section so stale local/session state
cannot substitute for the current workspace.

In
`@Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Store/ChatConversationStore.swift`:
- Around line 388-393: Prevent equal-version descriptor snapshots from
overwriting newer live state in applyDescriptorSnapshot(_:). The current version
guard allows descriptor.version == self.descriptor.version, which can replace
agentState with stale cached state. Tighten the check in ChatConversationStore
so only strictly newer versions update self.descriptor and agentState, or
explicitly preserve the existing agentState when versions are equal. Keep the
fix localized to applyDescriptorSnapshot(_:) and align it with the versioning
behavior used by apply(_:) and .stateChanged.
🪄 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: 435f87e9-1bda-4c02-95ed-5472db7ca732

📥 Commits

Reviewing files that changed from the base of the PR and between a70fcdd and ae96779.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (10)
  • Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Store/ChatConversationStore.swift
  • Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Screen/ChatScreen.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+AgentChat.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/iOS/CmuxMobileShellUI/Package.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceChatPane.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceChatSessionRefreshOutcome.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView+AgentChat.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView.swift
  • Packages/iOS/CmuxMobileShellUI/Tests/CmuxMobileShellUITests/WorkspaceChatSessionRefreshOutcomeTests.swift

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift (1)

3627-3630: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Only clear chat snapshots for workspaces that truly disappeared.

Lines 3627-3630 purge every snapshot that belonged to previousKey, but this helper also runs during promotion / anonymous→real reconnect where the same workspace can immediately survive under a different workspacesByMac key. In that case we delete the cache for a still-live workspace and the mobile GUI toggle/history disappears until a later refresh rebuilds it.

Proposed fix
         let removedWorkspaceIDs = Set(workspacesByMac[previousKey]?.workspaces.map { $0.id.rawValue } ?? [])
         workspacesByMac[previousKey] = nil
-        for workspaceID in removedWorkspaceIDs {
+        let survivingWorkspaceIDs = Set(
+            workspacesByMac.values.flatMap { state in
+                state.workspaces.map { $0.id.rawValue }
+            }
+        )
+        for workspaceID in removedWorkspaceIDs.subtracting(survivingWorkspaceIDs) {
             chatSessionSnapshotsByWorkspaceID[workspaceID] = nil
         }
🤖 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/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift`
around lines 3627 - 3630, The snapshot purge in MobileShellComposite’s workspace
migration flow is too aggressive because it deletes all entries from
chatSessionSnapshotsByWorkspaceID for every workspace ID under previousKey, even
when a workspace survives the reconnect under a new workspacesByMac key. Update
the cleanup logic around the removedWorkspaceIDs handling so it only clears
snapshots for workspace IDs that are no longer present anywhere in
workspacesByMac, and keep the existing workspacesByMac/previousKey transition
logic intact. Use the surrounding workspace promotion / anonymous-to-real
reconnect path in MobileShellComposite to locate the fix.
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView+AgentChat.swift (1)

123-127: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Retry chat-store creation when the source reconnects.

If ensureChatConversationStore(for:) returns nil while the Mac is unavailable, this branch stays keyed only by session.id; reconnect/source-identity changes will not rerun the task, leaving cached chat mode as a blank Color.clear.

Suggested fix
         } else {
             Color.clear
-                .task(id: session.id) {
+                .task(id: chatConversationWarmKey) {
                     _ = ensureChatConversationStore(for: session)
                 }
+                .task(id: chatRefreshKey) { await refreshChatSessions() }
         }
🤖 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/WorkspaceDetailView`+AgentChat.swift
around lines 123 - 127, The fallback branch in WorkspaceDetailView+AgentChat is
only keyed by session.id, so the .task that calls
ensureChatConversationStore(for:) won’t rerun when the source reconnects and the
Mac becomes available again. Update the task trigger in this Color.clear branch
to also depend on a reconnect/source-identity signal from the relevant
chat/session state, so a nil store result can be retried instead of leaving the
view stuck in blank mode.
🤖 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/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Store/ChatConversationStore.swift`:
- Around line 397-400: `ChatConversationStore.replaceSource(_:descriptor:)` only
swaps the `source` and reapplies the snapshot, but it leaves an already parked
`run()` loop sleeping in the old reconnect backoff. Update `replaceSource` to
immediately wake the store by cancelling/restarting the current run or otherwise
breaking the pending backoff so it resubscribes to the new source right away.
Keep the fix in `ChatConversationStore` and make sure
`ensureChatConversationStore(...)` rebinding cannot leave the store serving
stale cached state.

---

Outside diff comments:
In
`@Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift`:
- Around line 3627-3630: The snapshot purge in MobileShellComposite’s workspace
migration flow is too aggressive because it deletes all entries from
chatSessionSnapshotsByWorkspaceID for every workspace ID under previousKey, even
when a workspace survives the reconnect under a new workspacesByMac key. Update
the cleanup logic around the removedWorkspaceIDs handling so it only clears
snapshots for workspace IDs that are no longer present anywhere in
workspacesByMac, and keep the existing workspacesByMac/previousKey transition
logic intact. Use the surrounding workspace promotion / anonymous-to-real
reconnect path in MobileShellComposite to locate the fix.

In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView`+AgentChat.swift:
- Around line 123-127: The fallback branch in WorkspaceDetailView+AgentChat is
only keyed by session.id, so the .task that calls
ensureChatConversationStore(for:) won’t rerun when the source reconnects and the
Mac becomes available again. Update the task trigger in this Color.clear branch
to also depend on a reconnect/source-identity signal from the relevant
chat/session state, so a nil store result can be retried instead of leaving the
view stuck in blank mode.
🪄 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: 426b27ad-0c10-43b6-96df-81fe2dac6421

📥 Commits

Reviewing files that changed from the base of the PR and between d04fd04 and 8bc77c6.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (4)
  • Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Store/ChatConversationStore.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView+AgentChat.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView.swift

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView+AgentChat.swift (1)

171-194: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Return on cancelled refreshes before mutating chat-session state.

A previous .task(id: chatRefreshKey) can still resume after sessionEvents() or sessions(workspaceID:) when the connection/foreground epoch changes. The current catch also treats cancellation as .unavailable, then writes chatSessions, chatSessionsWorkspaceID, and store.rememberChatSessions(...), so an older refresh can overwrite a newer authoritative snapshot for the same workspace.

Suggested fix
         let reducer = ChatSessionListReducer(workspaceID: workspaceID)
         let stream = await source.sessionEvents()
+        guard !Task.isCancelled else { return }
         let seedOutcome: WorkspaceChatSessionRefreshOutcome
         do {
-            seedOutcome = .authoritative(try await source.sessions(workspaceID: workspaceID))
+            let seededSessions = try await source.sessions(workspaceID: workspaceID)
+            guard !Task.isCancelled else { return }
+            seedOutcome = .authoritative(seededSessions)
+        } catch is CancellationError {
+            return
         } catch {
             seedOutcome = .unavailable
         }
+        guard !Task.isCancelled else { return }
         let nextSessions = seedOutcome.applying(to: visibleChatSessions)
         withAnimation(.snappy(duration: 0.25)) {
             chatSessionsWorkspaceID = workspaceID
             chatSessions = nextSessions
@@
         reconcileChatSessionSnapshot(seedOutcomeCanInvalidateSelection: seedOutcome.canInvalidateSelection)
         for await frame in stream {
+            guard !Task.isCancelled else { return }
             let next = reducer.applying(frame, to: visibleChatSessions)
             withAnimation(.snappy(duration: 0.25)) {
                 chatSessionsWorkspaceID = workspaceID

As per path instructions, correctness-critical cached GUI state must not become visibly stale or use competing cached/live truths.

🤖 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/WorkspaceDetailView`+AgentChat.swift
around lines 171 - 194, The refresh flow in WorkspaceDetailView+AgentChat should
stop immediately when the async task is cancelled so an older .task(id:
chatRefreshKey) cannot overwrite newer state. In the session-refresh logic that
awaits source.sessionEvents() and source.sessions(workspaceID:), handle
CancellationError separately and return before setting chatSessionsWorkspaceID,
chatSessions, or calling store.rememberChatSessions(...). Keep the existing
authoritative/unavailable handling for non-cancellation failures, and ensure
reconcileChatSessionSnapshot only runs for non-cancelled refreshes.

Source: Path instructions

🤖 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/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Store/ChatConversationStore.swift`:
- Around line 390-394: The reconciliation in ChatConversationStore’s descriptor
update path is letting unversioned snapshots overwrite newer live agent state.
Update the logic around the descriptor.version check and state assignment so
version == 0 snapshots do not replace a currently active agentState when recency
cannot be proven; either preserve the existing state in this case or ensure the
snapshot gets a real monotonic version before reaching this branch. Keep the fix
localized to the descriptor/state reconciliation code that sets self.descriptor,
agentState, and didFlushThisIdleWindow.

---

Outside diff comments:
In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView`+AgentChat.swift:
- Around line 171-194: The refresh flow in WorkspaceDetailView+AgentChat should
stop immediately when the async task is cancelled so an older .task(id:
chatRefreshKey) cannot overwrite newer state. In the session-refresh logic that
awaits source.sessionEvents() and source.sessions(workspaceID:), handle
CancellationError separately and return before setting chatSessionsWorkspaceID,
chatSessions, or calling store.rememberChatSessions(...). Keep the existing
authoritative/unavailable handling for non-cancellation failures, and ensure
reconcileChatSessionSnapshot only runs for non-cancelled refreshes.
🪄 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: dfbaca3a-fece-43eb-aa59-ad232668111a

📥 Commits

Reviewing files that changed from the base of the PR and between 8bc77c6 and b1a88d0.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (2)
  • Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Store/ChatConversationStore.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView+AgentChat.swift

@greptile-apps

greptile-apps Bot commented Jun 29, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR preserves the mobile chat GUI during Mac reconnects by moving chat-session snapshots into the shell store, warming conversation stores while in terminal mode, and replacing a per-view conversation construction with a parent-managed store that is passed into WorkspaceChatPane. A new WorkspaceChatSessionRefreshOutcome enum prevents transport-loss events from clearing the cached session list, and ChatConversationStore gains generation-based staleness guards and a backoff-wake mechanism so source replacement cancels in-flight history fetches immediately.

  • Shell-store chat-session cache: MobileShellComposite gains chatSessionSnapshotsByWorkspaceID, pruned on workspace-list changes and cleared on sign-out, so visibleChatSessions can seed from the shell cache before the per-view refresh task reconnects.
  • ChatConversationStore source replacement: replaceSource bumps sourceGeneration, guarded on every await gap in run(), and calls wakeBackoff() to interrupt any in-progress backoff sleep so reconnects begin immediately.
  • Toolbar layout fix: the trailing cluster reserves a fixed slot for chat toggle + terminal picker so the first GUI-session discovery animates the chat glyph in without shifting the terminal picker or recentering the title.

Confidence Score: 4/5

Safe to merge with attention to the unsupported-Mac detection path.

The generation-based staleness guards in ChatConversationStore, the backoff-wake mechanism, and the unavailable/authoritative outcome enum are all correctly implemented. The one concrete defect is in chatSessionListFailureMeansUnsupported: when the Mac returns an RPC error whose code does not match the known set, the function falls back to scanning the free-text error message for substrings like "unknown method". A transient error whose message incidentally contains one of those phrases will be misclassified as "Mac does not support chat", causing the session list to be authoritatively cleared and potentially exiting chat mode.

MobileShellComposite+AgentChat.swift — the chatSessionListFailureMeansUnsupported message-string fallback should be removed in favour of relying solely on the structured error code.

Important Files Changed

Filename Overview
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+AgentChat.swift Adds workspace-scoped session-list cache (cachedChatSessions/rememberChatSessions) and a chatSessionListFailureMeansUnsupported helper; the helper's message-string fallback is an unreliable heuristic for a correctness-critical branch that can incorrectly clear the chat session cache.
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift Adds chatSessionSnapshotsByWorkspaceID store state, pruneChatSessionSnapshots on workspace list changes, dropStalePreviousForeground cleanup, and chatEventSourceGeneration UUID bumped on RPC client replacement; all looks correct.
Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Store/ChatConversationStore.swift Adds source-replacement (replaceSource/applyDescriptorSnapshot), generation-based staleness guards on every async gap in run(), a backoff-wake mechanism (waitForBackoffOrSourceReplacement/wakeBackoff), and @sendable fix for idleSleep; the generation checks are thorough and the backoff logic is correct.
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView+AgentChat.swift New extension splitting chat logic out of WorkspaceDetailView; implements refreshChatSessions with proper unavailable/authoritative outcome handling, chatConversationWarmKey-driven store warming, pruneCachedChatConversations, and push-stream reconciliation with seedOutcomeCanInvalidateSelection:true.
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView.swift Adds new @State vars (chatSessionsWorkspaceID, cachedChatToggleTerminalID, chatConversationStores) and widens several properties from private to internal for the new extension; widening is for production extension use, not test seams.
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceChatPane.swift Removes local @State conversation store creation in favor of a parent-supplied ChatConversationStore; passes runsStoreTask:false so the pane renders an already-warm store without launching a second run() task.
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceChatSessionRefreshOutcome.swift New enum cleanly models unavailable vs authoritative refresh outcomes; applying(to:) and canInvalidateSelection correctly prevent transport-loss events from clearing the cached session list.
Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Screen/ChatScreen.swift Adds runsStoreTask parameter so a parent-managed store can be passed in without ChatScreen launching its own run() task loop.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant SV as WorkspaceDetailView
    participant WDC as WorkspaceDetailView+AgentChat
    participant SS as MobileShellComposite
    participant CCS as ChatConversationStore
    participant Mac as Mac RPC Source

    SV->>WDC: body → chatConversationWarmKey task
    WDC->>WDC: runWarmChatConversation()
    WDC->>SS: makeChatEventSource()
    SS-->>WDC: MobileChatEventSource
    WDC->>CCS: ensureChatConversationStore(for:)
    CCS-->>WDC: ChatConversationStore (new or existing)
    WDC->>CCS: run()
    CCS->>Mac: source.events(sessionID:)
    Mac-->>CCS: AsyncStream

    note over SV,Mac: Connection drops
    Mac-->>CCS: stream finishes
    CCS->>CCS: waitForBackoffOrSourceReplacement()

    note over SV,Mac: Mac reconnects → agentChatEventSourceIdentity changes
    SV->>WDC: chatConversationWarmKey changes → task restarts
    WDC->>SS: makeChatEventSource() → new source
    WDC->>CCS: replaceSource(_:descriptor:sourceIdentity:)
    CCS->>CCS: "sourceGeneration += 1, wakeBackoff()"
    CCS->>Mac: source.events(sessionID:) [new source]

    note over SV,Mac: Parallel: session list refresh
    SV->>WDC: chatRefreshKey task → refreshChatSessions()
    WDC->>Mac: source.sessions(workspaceID:)
    Mac-->>WDC: [ChatSessionDescriptor] (authoritative)
    WDC->>SS: rememberChatSessions(_, workspaceID:)
    WDC->>WDC: reconcileChatSessionSnapshot(seedOutcomeCanInvalidateSelection: true)
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 SV as WorkspaceDetailView
    participant WDC as WorkspaceDetailView+AgentChat
    participant SS as MobileShellComposite
    participant CCS as ChatConversationStore
    participant Mac as Mac RPC Source

    SV->>WDC: body → chatConversationWarmKey task
    WDC->>WDC: runWarmChatConversation()
    WDC->>SS: makeChatEventSource()
    SS-->>WDC: MobileChatEventSource
    WDC->>CCS: ensureChatConversationStore(for:)
    CCS-->>WDC: ChatConversationStore (new or existing)
    WDC->>CCS: run()
    CCS->>Mac: source.events(sessionID:)
    Mac-->>CCS: AsyncStream

    note over SV,Mac: Connection drops
    Mac-->>CCS: stream finishes
    CCS->>CCS: waitForBackoffOrSourceReplacement()

    note over SV,Mac: Mac reconnects → agentChatEventSourceIdentity changes
    SV->>WDC: chatConversationWarmKey changes → task restarts
    WDC->>SS: makeChatEventSource() → new source
    WDC->>CCS: replaceSource(_:descriptor:sourceIdentity:)
    CCS->>CCS: "sourceGeneration += 1, wakeBackoff()"
    CCS->>Mac: source.events(sessionID:) [new source]

    note over SV,Mac: Parallel: session list refresh
    SV->>WDC: chatRefreshKey task → refreshChatSessions()
    WDC->>Mac: source.sessions(workspaceID:)
    Mac-->>WDC: [ChatSessionDescriptor] (authoritative)
    WDC->>SS: rememberChatSessions(_, workspaceID:)
    WDC->>WDC: reconcileChatSessionSnapshot(seedOutcomeCanInvalidateSelection: true)
Loading

Reviews (12): Last reviewed commit: "Ignore stale chat session refreshes" | Re-trigger Greptile

@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/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Store/ChatConversationStore.swift`:
- Around line 416-419: The version check in ChatConversationStore’s descriptor
update path is too permissive and can let older cached snapshots overwrite newer
live state. Update the guard around the descriptor assignment so versioned
snapshots are only accepted when their version is strictly greater than the
current descriptor version, while still allowing the unversioned case handled by
isUnversioned. Keep the actual state replacement in the same update block that
assigns self.descriptor and agentState so stale snapshots fail closed.
🪄 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: cd3f6d27-6209-47fc-9672-03fb115a40cb

📥 Commits

Reviewing files that changed from the base of the PR and between b1a88d0 and 7714488.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (3)
  • Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Store/ChatConversationStore.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView+AgentChat.swift
💤 Files with no reviewable changes (1)
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView+AgentChat.swift

Comment on lines +173 to +177
do {
seedOutcome = .authoritative(try await source.sessions(workspaceID: workspaceID))
} catch {
seedOutcome = store.chatSessionListFailureMeansUnsupported(error)
? .authoritative([])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 RPC failure in seed path incorrectly clears the session cache

When source.sessions() throws (e.g. a timeout while the Mac is technically connected), seedOutcome is set to .authoritative([]). Because canInvalidateSelection is true, this path (1) persists an empty list to the shell store via store.rememberChatSessions([], ...) and (2) allows applyChatModeFallback to exit chat mode. The whole point of WorkspaceChatSessionRefreshOutcome.unavailable was to prevent exactly this: transport or RPC failures should not clobber the cached snapshot. Using .unavailable here preserves the cached sessions and prevents spurious chat-mode exit on a transient RPC failure.

Suggested change
do {
seedOutcome = .authoritative(try await source.sessions(workspaceID: workspaceID))
} catch {
seedOutcome = store.chatSessionListFailureMeansUnsupported(error)
? .authoritative([])
do {
seedOutcome = .authoritative(try await source.sessions(workspaceID: workspaceID))
} catch {
seedOutcome = .unavailable
}

# Conflicts:
#	Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceChatPane.swift
#	Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView.swift
@azooz2003-bit
azooz2003-bit force-pushed the feat-gui-cache-reconnect branch from e5bef85 to b185e49 Compare June 30, 2026 00:50
Comment on lines +44 to +57
public func chatSessionListFailureMeansUnsupported(_ error: any Error) -> Bool {
guard case let MobileShellConnectionError.rpcError(code, message) = error else {
return false
}
let normalizedCode = code?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
let normalizedMessage = message.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
if let normalizedCode,
["method_not_found", "not_found", "unknown_method", "unsupported_method"].contains(normalizedCode) {
return true
}
return normalizedMessage.contains("unknown method")
|| normalizedMessage.contains("method not found")
|| normalizedMessage.contains("unsupported method")
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Unreliable message-string fallback drives a correctness-critical classification

When the Mac returns an rpcError whose code does not match the known set, the function falls back to scanning the free-text message for substrings like "unknown method". Any localized, reformatted, or vendor-extended error message that happens to contain one of those phrases will be misclassified as "unsupported", causing refreshChatSessions to use .authoritative([]) — which clears the cached session list and allows applyChatModeFallback to exit chat mode. Conversely, a genuinely unsupported Mac without a matching message string is silently treated as a transient failure, preserving stale sessions and leaving a chat toggle that can never open a session.

The code-based branch (normalizedCode) is a reliable structured signal; it should be the only detection path. If the Mac omits an error code for unsupported methods, the right fix is to add one on the Mac side and remove the message fallback here rather than guess on opaque prose.

@azooz2003-bit
azooz2003-bit merged commit 1123a9c into main Jun 30, 2026
40 checks passed
@azooz2003-bit
azooz2003-bit deleted the feat-gui-cache-reconnect branch June 30, 2026 03:42

This branch was successfully deployed

1 active deployment
Preview – cmux — 7516c107 Deployed Jun 30, 2026 by vercel[bot]
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