Skip to content
Merged
Show file tree
Hide file tree
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 Jul 24, 2026
52a4d22
Add Codex-style composer layout to the New Task sheet
azooz2003-bit Jul 24, 2026
656cf07
Address review findings on the model picker
azooz2003-bit Jul 29, 2026
feff01d
Address round-2 review findings
azooz2003-bit Jul 29, 2026
4d862a1
Address round-3 review findings
azooz2003-bit Jul 29, 2026
ee5bfdf
Address round-4 review findings
azooz2003-bit Jul 29, 2026
69f26ce
Address round-5 review findings
azooz2003-bit Jul 29, 2026
d113c75
Address final review round: comments and delisted-model drafts
azooz2003-bit Jul 29, 2026
7180ed8
Merge remote-tracking branch 'origin/main' into feat-newtask-model-pi…
azooz2003-bit Jul 29, 2026
3183c45
Use the adjustments glyph for the composer options button
azooz2003-bit Jul 29, 2026
9f26733
Stop pill labels clipping when the selection gets longer
azooz2003-bit Jul 29, 2026
ba07773
Add mobile task attachments
azooz2003-bit Jul 29, 2026
18c57e5
Always show the model pill in the composer layout; fix iOS 26 pill cl…
azooz2003-bit Jul 30, 2026
5373cd5
Fade the composer pill scroller into the bar at both edges
azooz2003-bit Jul 30, 2026
36152ab
Discover task models from connected Macs
azooz2003-bit Jul 30, 2026
ba74b1b
Merge remote-tracking branch 'origin/main' into feat-newtask-model-pi…
azooz2003-bit Jul 31, 2026
26fd7d4
Adopt MacPairingKey lookups in task capability checks
azooz2003-bit Jul 31, 2026
0d13a83
Scroll the composer pills under the bar buttons with a real edge effect
azooz2003-bit Jul 31, 2026
d648307
Merge remote-tracking branch 'origin/main' into feat-newtask-model-pi…
azooz2003-bit Jul 31, 2026
85715df
Render the pill scroller edge effect through UIKit's container intera…
azooz2003-bit Jul 31, 2026
479f93d
Attach the scroll edge effect via probes instead of rehosting the bar
azooz2003-bit Jul 31, 2026
08689c1
Reproduce the scroll edge effect deterministically in SwiftUI
azooz2003-bit Jul 31, 2026
06ff413
Use native scroll edge effects in task composer
azooz2003-bit Jul 31, 2026
9f5b1ca
Fix task composer scroll edge blur
azooz2003-bit Aug 1, 2026
ff4d7ae
Use native shaped scroll edge effects
azooz2003-bit Aug 1, 2026
b341130
Test composer pill scroller hard edges
azooz2003-bit Aug 3, 2026
3ff7b71
Fix composer hard-edge UI test lookup
azooz2003-bit Aug 3, 2026
e3e0821
Restore hard edges to composer pill scroller
azooz2003-bit Aug 3, 2026
5d5326d
Exercise overflowing composer pills in hard-edge test
azooz2003-bit Aug 3, 2026
c3007e7
Test composer prompt scroll gesture ownership
azooz2003-bit Aug 3, 2026
86aac4b
Prioritize prompt scrolling over sheet drag
azooz2003-bit Aug 3, 2026
8e47963
Test composer prompt scroll position stability
azooz2003-bit Aug 3, 2026
ecc0550
Keep composer prompt at manual scroll position
azooz2003-bit Aug 3, 2026
e08cb75
Merge remote-tracking branch 'origin/main' into feat-newtask-model-pi…
azooz2003-bit Aug 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -682,9 +682,10 @@ public final class MobileCoreRPCClient: MobileSyncing, Sendable {

switch method {
case "mobile.workspace.list", "workspace.list",
"mobile.task.models.list",
"mobile.directory.list", "mobile.directory.search":
return false
case "workspace.create":
case "workspace.create", "mobile.task.attachment.upload":
return false
case "workspace.action", "workspace.close":
return !ticketCoverage.ticketCoversWorkspaceRequest(
Expand Down
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,

Copy link
Copy Markdown

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
Check if this issue is valid — if so, understand the root cause and fix it. At Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TaskAttachments.swift, line 117:

<comment>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.</comment>

<file context>
@@ -0,0 +1,187 @@
+                    "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,
</file context>

"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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ extension MobileShellComposite {
)
}

private func taskComposerTargetName(macDeviceID: String, instanceTag: String?) -> String {
func taskComposerTargetName(macDeviceID: String, instanceTag: String?) -> String {
displayPairedMacs.first {
$0.macDeviceID == macDeviceID
&& (instanceTag == nil || $0.instanceTag == instanceTag)
Expand Down
Loading