Skip to content

Improve iOS attachment previews and file delivery - #9907

Closed
azooz2003-bit wants to merge 74 commits into
mainfrom
feat-ios-attachment-ux
Closed

azooz2003-bit wants to merge 74 commits into
mainfrom
feat-ios-attachment-ux

Conversation

@azooz2003-bit

@azooz2003-bit azooz2003-bit commented Aug 10, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • Unify Photos and Files selection across Terminal, Agent Chat, and both New Task layouts.
  • Show ordered square image and file cards with preview, removal, preparation progress, and localized errors.
  • Stream exact staged bytes to the Mac with stable retry identities, validated host paths, and shell-safe terminal insertion.
  • Enforce 10-file, 32 MB per-file, and 64 MB per-draft limits without loading full files into composer state.

Verification

Closes #6643

Summary by CodeRabbit

  • New Features

    • Added photo and file attachments in iOS composers.
    • Added staged uploads with previews, thumbnails, progress indicators, removal controls, and accessibility support.
    • Added localized English and Japanese attachment workflows and error messages.
  • Bug Fixes

    • Improved size, count, readability, and upload validation.
    • Improved cleanup after cancellation, removal, failures, delivery, and sign-out.
    • Preserved attachment identity across retries and prevented stale preparation results.
    • Added support for empty files and safer transfer error messages.

Note

Medium Risk
Touches composer send routing, large-file I/O, and new RPC delivery paths across chat and terminal; mistakes could leak temp files or drop attachments mid-retry, but auth is not redesigned and errors are sanitized for display.

Overview
iOS composers (terminal, agent chat, new task) now stage Photos and Files as app-owned on-disk payloads with shared card UI (preview, remove, preparing state, localized limits/errors), instead of holding full image bytes in composer state.

Delivery moves from inline base64 to mobile.task.attachment.upload chunking with stable upload_id / operation_id (retries reuse identities; completed uploads can short-circuit re-upload), then host references via mobile.chat.send, mobile.terminal.paste_attachment, or the existing task-create path. CmuxMobileAttachmentTransfer centralizes upload + privacy-safe transfer errors.

Chat store assigns a shared operation per prompt, releases staged files when pending rows reconcile, discard, reset, or change event source. Terminal shell adds typed admission results, raises per-terminal total staging to 64 MB, and deletes staged files on sign-out, topology prune, and clear/remove.

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

@coderabbitai

coderabbitai Bot commented Aug 10, 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 PR replaces in-memory image attachments with file-backed image and file attachments. iOS stages and previews local files, uploads them in chunks, and sends validated attachment references through mobile terminal RPC methods. Cleanup, retry identity, authorization, and end-to-end coverage were added.

Changes

File-backed attachment contracts

Layer / File(s) Summary
Attachment models and staging
Packages/Shared/CmuxAgentChat/..., Packages/iOS/CmuxMobileSupport/..., Packages/iOS/CmuxMobileShellModel/...
Attachments store file URLs, byte counts, filenames, kinds, thumbnails, upload IDs, and operation IDs. Shared staging validates files, preserves bytes, sanitizes names, and creates thumbnails.
Pending-send lifecycle
Packages/Shared/CmuxAgentChat/..., Packages/iOS/CmuxMobileShellModel/...
Pending sends preserve attachment identities across retries. Discard and reconciliation paths remove released staged files.

Composer staging and UI

Layer / File(s) Summary
Photo and file import flow
Packages/iOS/CmuxAgentChatUI/..., Packages/iOS/CmuxMobileShellUI/...
Composers support Photos and Files, asynchronous staging, attachment limits, cancellation generations, localized errors, and cleanup.
Attachment cards and previews
Packages/iOS/CmuxAgentChatUI/..., Packages/iOS/CmuxMobileShellUI/...
Shared cards display thumbnails, file metadata, preparation progress, removal controls, accessibility labels, and Quick Look previews.
Composer validation and fixtures
Packages/iOS/CmuxMobileShellUI/..., ios/cmuxUITests/...
Composer state reflects attachment preparation. Fixtures and UI tests cover previews, removal, picker options, and card sizing.

Chunked upload and delivery

Layer / File(s) Summary
Mobile upload transport
Packages/iOS/CmuxMobileShell/...
MobileAttachmentRPCUploader streams staged files in 3 MiB chunks, validates acknowledgements and paths, reports progress, and supports cancellation.
Shell submission and cleanup
Packages/iOS/CmuxMobileShell/...
Mobile sends upload references instead of inline image data. Attachment files are removed after submission, rejection, removal, pruning, clearing, and sign-out.
Upload and routing tests
Packages/iOS/CmuxMobileShell/Tests/..., Packages/iOS/CmuxMobileShellModel/Tests/...
Tests cover empty files, chunk boundaries, upload metadata, terminal routing, retries, sanitized errors, and cleanup.

Host resolution and terminal routing

Layer / File(s) Summary
Completed attachment resolution
Packages/macOS/CmuxControlSocket/...
The host resolves completed uploads by operation and upload IDs. It validates completion state, regular-file status, directory boundaries, symlink containment, and ordered batches.
Paste-attachment RPC flow
Sources/TerminalController*, Sources/Mobile/..., Packages/iOS/CmuxMobileRPC/...
Paste-attachment methods receive authorization, ordered-input classification, terminal routing, path resolution, shell escaping, and response handling. Legacy base64 image attachments remain supported.
Host authorization tests
Packages/macOS/CmuxControlSocket/Tests/..., cmuxTests/...
Tests cover empty final uploads, exact file resolution, invalid references, batch failures, and terminal-scoped authorization.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Composer
  participant MobileAttachmentRPCUploader
  participant MobileTaskAttachmentStore
  participant TerminalController
  Composer->>MobileAttachmentRPCUploader: Upload staged file chunks
  MobileAttachmentRPCUploader->>MobileTaskAttachmentStore: Store upload chunks
  Composer->>TerminalController: Send attachment references
  TerminalController->>MobileTaskAttachmentStore: Resolve completed attachment
  MobileTaskAttachmentStore-->>TerminalController: Return validated file URL
  TerminalController->>TerminalController: Paste shell-escaped path
Loading

Suggested reviewers: lawrencecchen, austinywang


Important

Pre-merge checks failed

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

❌ Failed checks (5 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Algorithmic Complexity ❌ Error ChatConversationStore.swift lines 746/821 call releaseStagedFiles inside message/block loops, while lines 838-843 rescan all pending rows; batch reconciliation is O(batch×pending) with no pending b... Collect removed rows during reconciliation and release staged files once per batch, or maintain a URL reference-count/index so cleanup is O(removed attachments).
Cmux Swift Package Boundaries ❌ Error MobileAttachmentRPCUploader keeps standalone chunking, acknowledgement, retry, and protocol-error logic in the 223-file CmuxMobileShell target, although chat and task flows both use it. Extract the transfer boundary into a small CmuxMobileAttachmentTransfer target; expose MobileAttachmentRPCUploader (with an injectable RPC protocol) and keep shell/UI lifecycle composition in CmuxMobileShell.
Cmux User-Facing Error Privacy ❌ Error MobileAttachmentTransferError.sanitizing preserves raw messages for actionable rpcError codes, and chat stores error.localizedDescription for the user-facing banner. Replace preserved RPC and authorization messages with fixed localized product copy; retain only bounded diagnostic classifications in logs or telemetry.
Cmux Full Internationalization ❌ Error New host RPC errors return English human-readable text in Sources/TerminalController+MobileChat.swift and +MobileTaskAttachments.swift; no localized API or entries exist in the 20-locale Resources/... Route the new RPC error messages through String(localized:defaultValue:) and add translated entries to Resources/Localizable.xcstrings for all 20 supported host locales.
Cmux Architecture Rethink ❌ Error ChatComposerView, TerminalComposerView, and TaskComposerSheet each own picker callbacks, staging Tasks, UUID generations, loops, and cleanup; shared code covers only cards/picker UI, leaving duplic... Create one injected attachment-draft coordinator/store that owns picker results, cancellation, staging, admission, and cleanup; pass value snapshots and action closures to all three composers, then remove their @State staging owners.
Docstring Coverage ⚠️ Warning Docstring coverage is 29.36% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (19 passed)
Check name Status Explanation
Cmux Swift Actor Isolation ✅ Passed Changed stores have explicit @MainActor isolation, upload/staging services are actors, UI types are allowed, and Swift 6 package manifests do not set MainActor by default; no unsafe Sendable refere...
Cmux Swift Blocking Runtime ✅ Passed The PR adds no new semaphore, blocking wait, sleep, delayed dispatch, polling, main-queue sync, or lock. Its only new loop awaits each RPC response; existing timing primitives remain unchanged.
Cmux Browser Automation Off-Main ✅ Passed The PR diff adds only mobile attachment routing and a visibility change; it does not modify browser commands, worker policy, or policy tests. Existing browser worker routing remains intact.
Cmux Expensive Synchronous Load ✅ Passed Full PR diff adds no agent-history loader or corpus parse; new file I/O is bounded attachment metadata/chunking, with staging and upload isolated in actors.
Cmux Cache Substitution Correctness ✅ Passed The diff shows no fresh-read-to-cache substitution in persistence, history, undo, or snapshot paths; thumbnailData replaces only a transient UI thumbnail cache, and pending rows are local-only.
Cmux No Hacky Sleeps ✅ Passed The PR adds no covered production TypeScript, JavaScript, shell, or build/runtime delay code; the non-Swift diff only changes Rust/localization/workflow files and deletes test scripts.
Cmux Swift Concurrency ✅ Passed The PR adds no DispatchQueue, DispatchGroup, Combine, or completion-handler async APIs; its new staging Tasks are stored, cancelled, and generation-guarded, while callbacks are SwiftUI boundaries.
Cmux Swift @Concurrent ✅ Passed Changed UI async code explicitly awaits actor-isolated MobileAttachmentStager and MobileAttachmentRPCUploader work; no new nonisolated async or @concurrent misuse was found.
Cmux Swiftpm Lockfiles ✅ Passed The PR adds only a local CmuxMobileSupport dependency to CmuxMobileShellModel; the reachable packages have no external URLs, no Xcode package-reference changes exist, and no cmux package ignores Pa...
Cmux Swift Logging ✅ Passed PR diff adds no print, debugPrint, dump, NSLog, stdout, or diagnostic file logging; changed FileHandle calls handle attachment bytes, and the existing mobileShellLog declaration/calls are unchanged.
Cmux Swiftui State Layout ✅ Passed The PR adds no ObservableObject, @Published, GeometryReader, lazy/list store rows, or render-time state writes; attachment rows use value snapshots and closures, and mutations occur in callbacks or...
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed The PR adds no NSWindow, NSPanel, NSWindowController, Window, or WindowGroup; its new Quick Look UI is a SwiftUI sheet, and scripts/lint_auxiliary_window_close_shortcuts.py passes.
Cmux Source Artifacts ✅ Passed The 48 changed paths are Swift source/tests or localization catalogs. No forbidden artifact directory, log, screenshot, recording, cache, build output, or binary file enters the diff.
Cmux No Test Or Debug Seam In Production Source ✅ Passed No added production member matches the prohibited seam names or guards; the UI-test fixture is isolated under Sources/.../Debug, and applyMobileViewportReport has production callers.
Cmux No Ambient Global State ✅ Passed The production diff adds no new file-scope API functions, mutable globals, or singletons; new behavior is scoped to constructable actors, structs, and MobileShellComposite, with only private helper...
Linked Issues check ✅ Passed The description links issue #6643, and the changes address its attachment preview objective plus the stated delivery objectives.
Out of Scope Changes check ✅ Passed The broad file-backed attachment changes directly support the stated preview, delivery, limit, and cleanup objectives.
Title check ✅ Passed The title clearly summarizes the main changes: improved iOS attachment previews and file delivery.
Description check ✅ Passed The description covers the scope, rationale, verification runs, and linked issue, but omits the template's demo video, review trigger, and checklist sections.
✨ Finishing Touches 💡 1
📝 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-attachment-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: 15

🤖 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/Resources/Localizable.xcstrings`:
- Around line 2588-2683: Add "extractionState": "manual" to every new
mobile.attachment.* catalog entry, matching the metadata shape of the existing
entries. Apply this consistently to all 16 entries, including
mobile.attachment.add, mobile.attachment.done, mobile.attachment.files, and the
attachment error and preview keys, while preserving their existing
localizations.

In
`@Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileAttachmentRPCUploader.swift`:
- Around line 33-60: Update the upload loop in upload() to resume from the
host’s received_bytes offset when retrying an existing uploadID, seeking the
local file handle to that offset before reading the next chunk. Keep the local
offset and host acknowledgement synchronized after each response; if the staging
identity cannot be reused safely, reset it explicitly before restarting at
offset zero.

In
`@Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileChatEventSource.swift`:
- Around line 257-261: Remove the stagedFile deletion loop from
MobileChatEventSource, leaving temporaryURLs cleanup intact because the
transport owns those files. Ensure staged-file removal occurs only in
ChatConversationStore.discard(pendingID:) after delivery is confirmed,
preserving the pending attachment URLs until transcript reconciliation.

In
`@Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/ComposerSubmitRoutingTestSupport.swift`:
- Around line 385-399: Update the upload handling logic around uploadFileNames
and uploadOperationIDs to validate any existing filename and operation ID before
accepting a chunk, not only when offset is zero. Reject metadata changes for
contiguous later chunks with the existing upload identity error, and only assign
the metadata when no prior value exists.

In
`@Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobilePendingAttachment.swift`:
- Around line 28-31: Update MobilePendingAttachment.data to return optional Data
and preserve read failures as nil instead of converting them to empty data. Find
every caller of MobilePendingAttachment.data, require each to handle the
optional result explicitly, and verify no reads occur from view bodies or other
main-actor paths; move affected file reads off the main actor if necessary.
- Around line 38-50: Make MobilePendingAttachment.init throw or return nil when
data.write(to:options:) fails instead of using try?, so no invalid staged
attachment is created. Update
CMUXMobileShellStore.addPendingAttachment(_:format:forTerminalID:) to propagate
that initializer failure through its existing optional result.
- Around line 53-61: Update MobilePendingAttachment.init(_:) and the
addPendingAttachment(_:forTerminalID:) retry flow so re-adding the same staged
attachment preserves its original operationID, either by storing that identity
on MobileStagedAttachment or reusing the existing MobilePendingAttachment.
Derive format from attachment.kind using the validated clipboard-format mapping,
including a safe fallback for unsupported or extensionless files, instead of
directly using the filename extension.

In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TaskComposer/TaskComposerSheet.swift`:
- Line 69: Document the ownership contract for initialAttachments in the
TaskComposerSheet initializer: callers must provide attachment files whose
staged copies are owned by the sheet and may be deleted by
removeStagedAttachmentFiles() during dismiss/reset. If that ownership cannot be
guaranteed, fail closed by preventing cleanup of caller-owned or shared staged
files.

In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TaskComposer/TaskComposerSheet`+Attachments.swift:
- Around line 246-250: Update the mobile.taskComposer.attachments.fileTooLarge
entries in ios/cmux/Resources/Localizable.xcstrings for every locale, including
en and ja, so their localized value matches “Choose a file 32 MB or smaller.”

In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalComposerView.swift`:
- Around line 582-629: Extract the duplicated attachment-staging algorithm from
stagePickedItems and stagePickedFiles into one shared helper near
MobileAttachmentStager, parameterized by the source list and per-item staging
operation. Update stagePickedItems, stagePickedFiles, stageSelectedPhotos, and
stageSelectedFiles to use this helper while preserving generation/session
guards, cancellation, capacity limits, rejected-file cleanup, error mapping,
selection cleanup, overflow reporting, and final remeasurement consistently
across both surfaces.
- Around line 692-700: Unify attachment alert localization under the
mobile.attachment.error.* namespace: update TerminalComposerView’s
staging-failure messages and OK action to use that namespace, matching the alert
title. In
Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Resources/Localizable.xcstrings
lines 2648-2683, retain those entries only if CmuxAgentChatUI resolves them;
otherwise move them to the CmuxMobileShellUI catalog so each string has one
owning package.

In
`@Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/MobileTaskAttachments/MobileTaskAttachmentStore.swift`:
- Around line 255-261: Update the attachment path validation around candidate
and parent URL construction to resolve symlinks on both the candidate path and
operationURL before comparing containment. Keep the regular-file validation and
reject any path whose resolved parent is not the resolved operation directory,
while returning the resolved candidate for downstream use.

In
`@Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Model/ChatOutboundAttachment.swift`:
- Around line 28-29: Update the attachment construction path that assigns
`thumbnailData` so it never stores the complete encoded image payload as preview
data. Either generate a bounded downsampled preview or set `thumbnailData` to
nil and preserve the existing view-side payload access; keep the
`ChatOutboundAttachment` contract consistent with its bounded-preview
documentation.

In
`@Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Store/ChatConversationStore.swift`:
- Around line 346-351: Centralize staged-file deletion in a private helper in
the store, such as releaseStagedFiles(of:), and replace the inline cleanup in
the current pending-item removal method with that helper. Update every
pending-row removal site in resetTranscriptAnchorForSourceReplacement() and the
.reset branch to invoke the helper for each removed delivered item before
removing it, preserving existing removal behavior.

In `@Sources/TerminalController`+MobileTaskAttachments.swift:
- Around line 77-83: Extract the repeated MobileTaskAttachmentStore
initialization into one shared factory helper, preserving the existing default
root URL, current date, and default file manager configuration. Update
v2MobileTaskAttachmentUpload, this handler, and v2MobileChatSend to call the
helper so all attachment operations use the same store construction.
🪄 Autofix

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 Plus

Run ID: fa768bd2-e0ca-4bbb-b673-8880d2005379

📥 Commits

Reviewing files that changed from the base of the PR and between a35880b and b6a735d.

📒 Files selected for processing (42)
  • Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Model/ChatOutboundAttachment.swift
  • Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Store/ChatConversationStore.swift
  • Packages/Shared/CmuxAgentChat/Tests/CmuxAgentChatTests/ChatConversationStoreTests.swift
  • Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Composer/ChatComposerView.swift
  • Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Composer/MobileAttachmentComponents.swift
  • Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Resources/Localizable.xcstrings
  • Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Transcript/Rows/ChatPendingBubbleView.swift
  • Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCClient.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileAttachmentRPCUploader.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileChatEventSource.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TaskAttachments.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/ComposerPendingAttachmentTests.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/ComposerSubmitRoutingTestSupport.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/ComposerSubmitRoutingTests.swift
  • Packages/iOS/CmuxMobileShellModel/Package.swift
  • Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobilePendingAttachment.swift
  • Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/TaskComposerAttachment.swift
  • Packages/iOS/CmuxMobileShellModel/Tests/CmuxMobileShellModelTests/MobileTaskSubmissionSnapshotTests.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/Debug/TaskComposer/TaskComposerAccessibilityPreviewView.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/Resources/Localizable.xcstrings
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TaskComposer/TaskComposerAttachmentPickerMenu.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TaskComposer/TaskComposerAttachmentPickerModifier.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TaskComposer/TaskComposerAttachmentStager.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TaskComposer/TaskComposerAttachmentStrip.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TaskComposer/TaskComposerMinimalLayout.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TaskComposer/TaskComposerPromptCard.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TaskComposer/TaskComposerSheet+Attachments.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TaskComposer/TaskComposerSheet.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalComposerView.swift
  • Packages/iOS/CmuxMobileSupport/Sources/CmuxMobileSupport/MobileAttachmentStager.swift
  • Packages/iOS/CmuxMobileSupport/Sources/CmuxMobileSupport/MobileStagedAttachment.swift
  • Packages/iOS/CmuxMobileSupport/Tests/CmuxMobileSupportTests/MobileAttachmentStagerTests.swift
  • Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/MobileTaskAttachments/MobileTaskAttachmentStore.swift
  • Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/MobileTaskAttachmentStoreTests.swift
  • Sources/Mobile/MobileHostOrderedRequestQueue.swift
  • Sources/Mobile/MobileHostService+TicketAuthorization.swift
  • Sources/TerminalController+MobileChat.swift
  • Sources/TerminalController+MobileTaskAttachments.swift
  • Sources/TerminalController.swift
  • cmuxTests/MobileHostAuthorizationTests.swift
  • ios/cmuxUITests/cmuxUITests.swift

Comment thread Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileChatEventSource.swift Outdated
Comment thread Sources/TerminalController+MobileTaskAttachments.swift Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (4)
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalComposerView.swift (2)

656-672: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Limit the Files batch before staging.

urls has no selection bound. This loop stages every selected file, even after the draft reaches its attachment cap. A large selection can cause unnecessary disk I/O and parsing before the store rejects each excess file.

Snapshot the remaining capacity before starting. Stage only that prefix. Show one count-limit error for omitted selections.

Proposed fix
+        let remainingCapacity = max(
+            Self.maxAttachmentCount - pendingAttachments.count,
+            0
+        )
+        let urlsToStage = urls.prefix(remainingCapacity)
+        if urls.count > urlsToStage.count {
+            attachmentError = attachmentAdmissionErrorMessage(.perTerminalCountLimit)
+        }
         let sessionGeneration = store.currentSessionGeneration
         stagingTask.task?.cancel()
@@
-            for url in urls {
+            for url in urlsToStage {
🤖 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/TerminalComposerView.swift`
around lines 656 - 672, Update the Files batch flow around the urls staging loop
to snapshot the remaining attachment capacity before staging, process only the
permitted prefix, and avoid staging omitted selections. When URLs exceed
capacity, show a single count-limit error for the omitted files while preserving
the existing cancellation and generation checks for staged attachments.

596-630: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Prevent stale staging tasks from mutating the active composer.

Cancellation does not guarantee that an awaited import or staging call throws CancellationError. A stale task can still set attachmentError; the photo path can also clear a newer pickerSelection. Recheck the generation and session token before every UI-state write after an await.

  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalComposerView.swift#L596-L630: guard attachmentError, pickerSelection, and the final remeasure against the current generation and session.
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalComposerView.swift#L656-L680: guard attachmentError and the final remeasure against the current generation and session.
🤖 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/TerminalComposerView.swift`
around lines 596 - 630, Prevent stale staging tasks from updating the active
composer by rechecking the staging generation and session generation before
every post-await UI mutation. In TerminalComposerView.swift lines 596-630, guard
attachmentError assignments, pickerSelection clearing, and the final
requestHeightRemeasure call; in lines 656-680, guard attachmentError assignments
and the final requestHeightRemeasure call. Preserve cancellation and cleanup
behavior while ensuring stale tasks exit without mutating current state.
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift (1)

7412-7476: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Use the backing file as the byte-count source of truth.

Admission trusts caller-provided MobileStagedAttachment.byteCount. The test helper writes one byte for every attachment while declaring arbitrary sizes. A replaced or malformed staged file can therefore bypass draft quotas or later upload a different byte count than the admitted attachment.

  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift#L7412-L7476: validate that localFileURL is a regular file and derive the accepted byte count from its current file metadata before applying limits and storing the attachment.
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/ComposerPendingAttachmentTests.swift#L29-L44: create files with the declared size and add coverage that mismatched or missing backing files are rejected.
🤖 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 7412 - 7476, Update admitPendingAttachment to require
stagedAttachment.localFileURL to reference a regular file, read its current file
size, and use that value as the accepted attachment byteCount before applying
per-item, per-terminal, and global limits or storing the attachment. In
Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/ComposerPendingAttachmentTests.swift:29-44,
make helper files match declared sizes and add coverage rejecting mismatched and
missing backing files.
Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/MobileTaskAttachments/MobileTaskAttachmentStore.swift (1)

183-200: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Put the attachment store under upload-id serialization.

MobileTaskAttachmentStore.upload is shared filesystem state without an actor, queue, or documented serialized owner. Two concurrent valid requests for the same uploadID can read the same staged offset, truncate, and append conflicting chunks. Use one actor per upload identity for the size check/truncate/write/finalize path.

🤖 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/macOS/CmuxControlSocket/Sources/CmuxControlSocket/MobileTaskAttachments/MobileTaskAttachmentStore.swift`
around lines 183 - 200, Serialize the full upload mutation path in
MobileTaskAttachmentStore.upload by routing each uploadID through a dedicated
per-upload actor, including offset validation, truncation, append, and
finalization. Reuse the same actor for all requests with the same uploadID,
while allowing different upload IDs to proceed independently.
🤖 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.

Outside diff comments:
In
`@Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift`:
- Around line 7412-7476: Update admitPendingAttachment to require
stagedAttachment.localFileURL to reference a regular file, read its current file
size, and use that value as the accepted attachment byteCount before applying
per-item, per-terminal, and global limits or storing the attachment. In
Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/ComposerPendingAttachmentTests.swift:29-44,
make helper files match declared sizes and add coverage rejecting mismatched and
missing backing files.

In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalComposerView.swift`:
- Around line 656-672: Update the Files batch flow around the urls staging loop
to snapshot the remaining attachment capacity before staging, process only the
permitted prefix, and avoid staging omitted selections. When URLs exceed
capacity, show a single count-limit error for the omitted files while preserving
the existing cancellation and generation checks for staged attachments.
- Around line 596-630: Prevent stale staging tasks from updating the active
composer by rechecking the staging generation and session generation before
every post-await UI mutation. In TerminalComposerView.swift lines 596-630, guard
attachmentError assignments, pickerSelection clearing, and the final
requestHeightRemeasure call; in lines 656-680, guard attachmentError assignments
and the final requestHeightRemeasure call. Preserve cancellation and cleanup
behavior while ensuring stale tasks exit without mutating current state.

In
`@Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/MobileTaskAttachments/MobileTaskAttachmentStore.swift`:
- Around line 183-200: Serialize the full upload mutation path in
MobileTaskAttachmentStore.upload by routing each uploadID through a dedicated
per-upload actor, including offset validation, truncation, append, and
finalization. Reuse the same actor for all requests with the same uploadID,
while allowing different upload IDs to proceed independently.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 54df5ec6-e031-4e8b-9707-95f421f5f0c2

📥 Commits

Reviewing files that changed from the base of the PR and between aa80bca and db2a608.

📒 Files selected for processing (6)
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/ComposerPendingAttachmentTests.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/Resources/Localizable.xcstrings
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalComposerView.swift
  • Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/MobileTaskAttachments/MobileTaskAttachmentStore.swift
  • Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/MobileTaskAttachmentStoreTests.swift

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ab3159d. Configure here.

Comment thread ios/cmuxUITests/cmuxUITests.swift
Comment thread ios/cmuxUITests/cmuxUITests.swift
@lawrencecchen lawrencecchen added the stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening. label Sep 23, 2026
@github-project-automation github-project-automation Bot moved this from Todo to Done in cmux backlog Sep 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

iOS: attached image preview is misaligned in the composer — should match iMessage

3 participants