Repository navigation
Add model selection lab to the New Task composer #8800
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
34 commits
Select commit
Hold shift + click to select a range
b93b394
Add model selection lab to the New Task composer
azooz2003-bit 52a4d22
Add Codex-style composer layout to the New Task sheet
azooz2003-bit 656cf07
Address review findings on the model picker
azooz2003-bit feff01d
Address round-2 review findings
azooz2003-bit 4d862a1
Address round-3 review findings
azooz2003-bit ee5bfdf
Address round-4 review findings
azooz2003-bit 69f26ce
Address round-5 review findings
azooz2003-bit d113c75
Address final review round: comments and delisted-model drafts
azooz2003-bit 7180ed8
Merge remote-tracking branch 'origin/main' into feat-newtask-model-pi…
azooz2003-bit 3183c45
Use the adjustments glyph for the composer options button
azooz2003-bit 9f26733
Stop pill labels clipping when the selection gets longer
azooz2003-bit ba07773
Add mobile task attachments
azooz2003-bit 18c57e5
Always show the model pill in the composer layout; fix iOS 26 pill cl…
azooz2003-bit 5373cd5
Fade the composer pill scroller into the bar at both edges
azooz2003-bit 36152ab
Discover task models from connected Macs
azooz2003-bit ba74b1b
Merge remote-tracking branch 'origin/main' into feat-newtask-model-pi…
azooz2003-bit 26fd7d4
Adopt MacPairingKey lookups in task capability checks
azooz2003-bit 0d13a83
Scroll the composer pills under the bar buttons with a real edge effect
azooz2003-bit d648307
Merge remote-tracking branch 'origin/main' into feat-newtask-model-pi…
azooz2003-bit 85715df
Render the pill scroller edge effect through UIKit's container intera…
azooz2003-bit 479f93d
Attach the scroll edge effect via probes instead of rehosting the bar
azooz2003-bit 08689c1
Reproduce the scroll edge effect deterministically in SwiftUI
azooz2003-bit 06ff413
Use native scroll edge effects in task composer
azooz2003-bit 9f5b1ca
Fix task composer scroll edge blur
azooz2003-bit ff4d7ae
Use native shaped scroll edge effects
azooz2003-bit b341130
Test composer pill scroller hard edges
azooz2003-bit 3ff7b71
Fix composer hard-edge UI test lookup
azooz2003-bit e3e0821
Restore hard edges to composer pill scroller
azooz2003-bit 5d5326d
Exercise overflowing composer pills in hard-edge test
azooz2003-bit c3007e7
Test composer prompt scroll gesture ownership
azooz2003-bit 86aac4b
Prioritize prompt scrolling over sheet drag
azooz2003-bit 8e47963
Test composer prompt scroll position stability
azooz2003-bit ecc0550
Keep composer prompt at manual scroll position
azooz2003-bit e08cb75
Merge remote-tracking branch 'origin/main' into feat-newtask-model-pi…
azooz2003-bit File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
187 changes: 187 additions & 0 deletions
187
...es/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TaskAttachments.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,187 @@ | ||
| internal import CmuxMobileRPC | ||
| public import CmuxMobileShellModel | ||
| public import Foundation | ||
|
|
||
| extension MobileShellComposite { | ||
| /// Whether the selected Mac instance currently advertises task attachments. | ||
| /// | ||
| /// - Parameters: | ||
| /// - macDeviceID: Physical Mac selected in the task composer. | ||
| /// - instanceTag: Exact paired app instance, when known. | ||
| /// - Returns: `true` only for a matching host capability announcement. | ||
| public func supportsTaskAttachments( | ||
| macDeviceID: String, | ||
| instanceTag: String? | ||
| ) -> Bool { | ||
| if matchesForegroundPairing( | ||
| macDeviceID: macDeviceID, | ||
| instanceTag: instanceTag | ||
| ) { | ||
| return supportedHostCapabilities.contains(Self.taskAttachmentCapability) | ||
| } | ||
| if let subscription = controlSubscriptionMatching( | ||
| macDeviceID: macDeviceID, | ||
| instanceTag: instanceTag | ||
| ) { | ||
| return subscription.supportedHostCapabilities.contains( | ||
| Self.taskAttachmentCapability | ||
| ) | ||
| } | ||
| let aliases = pairedMacAliasIDs( | ||
| for: macDeviceID, | ||
| instanceTag: instanceTag | ||
| ) | ||
| if let instanceTag { | ||
| return aliases.contains { | ||
| presenceMap.instance(deviceId: $0, tag: instanceTag)? | ||
| .capabilities.contains(Self.taskAttachmentCapability) == true | ||
| } | ||
| } | ||
| return aliases.contains { | ||
| presenceMap.soleRouteAdvertisingInstance(deviceId: $0)? | ||
| .capabilities.contains(Self.taskAttachmentCapability) == true | ||
| } | ||
| } | ||
|
|
||
| /// Uploads one staged task attachment to the selected Mac in 3 MiB chunks. | ||
| /// | ||
| /// - Parameters: | ||
| /// - attachment: App-owned staged attachment file. | ||
| /// - operationID: Task submission idempotency key. | ||
| /// - macDeviceID: Target Mac device id. | ||
| /// - instanceTag: Exact paired app instance, when known. | ||
| /// - Returns: The final absolute Mac path, or a user-actionable failure. | ||
| public func uploadTaskAttachment( | ||
| _ attachment: TaskComposerAttachment, | ||
| operationID: UUID, | ||
| macDeviceID: String, | ||
| instanceTag: String? | ||
| ) async -> Result<String, MobileWorkspaceMutationFailure> { | ||
| if !matchesForegroundPairing( | ||
| macDeviceID: macDeviceID, | ||
| instanceTag: instanceTag | ||
| ) || remoteClient == nil { | ||
| guard await switchToMac( | ||
| macDeviceID: macDeviceID, | ||
| instanceTag: instanceTag | ||
| ) else { | ||
| return .failure(.notConnected( | ||
| hostDisplayName: taskComposerTargetName( | ||
| macDeviceID: macDeviceID, | ||
| instanceTag: instanceTag | ||
| ) | ||
| )) | ||
| } | ||
| } | ||
| guard !Task.isCancelled, | ||
| let context = captureWorkspaceCreateContext(), | ||
| context.macDeviceID == macDeviceID, | ||
| instanceTag == nil || context.instanceTag == instanceTag else { | ||
| return .failure(.notConnected( | ||
| hostDisplayName: taskComposerTargetName( | ||
| macDeviceID: macDeviceID, | ||
| instanceTag: instanceTag | ||
| ) | ||
| )) | ||
| } | ||
| guard context.supportedHostCapabilities.contains( | ||
| Self.taskAttachmentCapability | ||
| ) else { | ||
| return .failure(.unsupported(hostDisplayName: context.hostDisplayName)) | ||
| } | ||
|
|
||
| let data: Data | ||
| do { | ||
| data = try await loadTaskAttachmentData( | ||
| from: attachment.localStagedFileURL | ||
| ) | ||
| } catch { | ||
| return .failure(.rejected(hostDisplayName: context.hostDisplayName)) | ||
| } | ||
| guard data.count == attachment.byteCount, | ||
| data.count <= TaskComposerAttachment.maximumFileBytes else { | ||
| return .failure(.rejected(hostDisplayName: context.hostDisplayName)) | ||
| } | ||
|
|
||
| let plan = MobileTaskAttachmentChunkPlan(totalByteCount: data.count) | ||
| do { | ||
| var finalPath: String? | ||
| for (index, range) in plan.ranges.enumerated() { | ||
| try Task.checkCancellation() | ||
| let isLast = index == plan.ranges.count - 1 | ||
| let params: [String: Any] = [ | ||
| "operation_id": operationID.uuidString, | ||
| "upload_id": attachment.id.uuidString, | ||
| "file_name": attachment.displayName, | ||
| "total_bytes": data.count, | ||
| "offset": range.lowerBound, | ||
| "data_b64": data.subdata(in: range).base64EncodedString(), | ||
| "last": isLast, | ||
| ] | ||
| let response = try await context.client.sendRequest( | ||
| MobileCoreRPCClient.requestData( | ||
| method: "mobile.task.attachment.upload", | ||
| params: params | ||
| ) | ||
| ) | ||
| guard context.isCurrent( | ||
| macDeviceID: foregroundMacDeviceID, | ||
| instanceTag: activeMacInstanceTag, | ||
| client: remoteClient, | ||
| generation: connectionGeneration | ||
| ), isSignedIn else { | ||
| return .failure(.notConnected( | ||
| hostDisplayName: context.hostDisplayName | ||
| )) | ||
| } | ||
| guard let object = try JSONSerialization.jsonObject(with: response) | ||
| as? [String: Any] else { | ||
| throw MobileShellConnectionError.invalidResponse | ||
| } | ||
| if isLast { | ||
| guard let path = object["path"] as? String, | ||
| path.hasPrefix("/") else { | ||
| throw MobileShellConnectionError.invalidResponse | ||
| } | ||
| finalPath = path | ||
| } | ||
| } | ||
| guard let finalPath else { | ||
| return .failure(.rejected(hostDisplayName: context.hostDisplayName)) | ||
| } | ||
| return .success(finalPath) | ||
| } catch { | ||
| if context.isCurrent( | ||
| macDeviceID: foregroundMacDeviceID, | ||
| instanceTag: activeMacInstanceTag, | ||
| client: remoteClient, | ||
| generation: connectionGeneration | ||
| ) { | ||
| handleMacAvailabilityFailureIfCurrent( | ||
| after: error, | ||
| expectedClient: context.client, | ||
| expectedGeneration: context.generation | ||
| ) | ||
| } | ||
| return .failure( | ||
| workspaceMutationFailure( | ||
| error, | ||
| hostDisplayName: context.hostDisplayName | ||
| ) | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| private func loadTaskAttachmentData(from url: URL) async throws -> Data { | ||
| try await withThrowingTaskGroup(of: Data.self) { group in | ||
| group.addTask(priority: .utility) { | ||
| try Task.checkCancellation() | ||
| return try Data(contentsOf: url, options: .mappedIfSafe) | ||
| } | ||
| guard let data = try await group.next() else { | ||
| throw CancellationError() | ||
| } | ||
| return data | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P1: A timeout or connection loss after the Mac accepts any non-final chunk makes the attachment impossible to retry: the next attempt resends offset 0, but the host requires the current staged byte count as the contiguous offset. Making upload retries resume from the host’s acknowledged byte count (or adding a resumable upload/progress query) would preserve the advertised idempotent retry behavior.
Prompt for AI agents