Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -12,6 +12,27 @@ extension MobileShellComposite {
)
}

/// Build-channel labels for the computer pickers, keyed by pairing entry
/// id, resolved with the same priority as the Computers sheet badge: live
/// presence first, then the stored instance tag while offline.
public func pairedMacBuildLabelsByEntryID() -> [String: String] {
Self.buildLabelsByEntryID(for: displayPairedMacs) { macDeviceID, instanceTag in
presenceSummary(for: macDeviceID, instanceTag: instanceTag)?.buildLabel
}
}

/// Shared label derivation for store-backed pickers and store-free
/// DEBUG fixtures (which pass a lookup that always returns `nil`).
public static func buildLabelsByEntryID(
for macs: [MobilePairedMac],
presenceBuildLabel: (String, String?) -> String?
) -> [String: String] {
macs.reduce(into: [String: String]()) { result, mac in
result[mac.id] = presenceBuildLabel(mac.macDeviceID, mac.instanceTag)
?? MacBuildChannel().label(bundleID: nil, tag: mac.instanceTag)
}
}

/// Stored ids represented by a visible paired-Mac row.
public func pairedMacAliasIDs(
for macDeviceID: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,15 @@ public enum MobileTaskDirectorySearchFailure: Error, Equatable, Sendable {

extension MobileShellComposite {
/// Returns matching Mac directories with explicit index and filesystem coverage.
/// - Parameters:
/// - macDeviceID: Physical Mac that owns the filesystem.
/// - instanceTag: Exact paired app instance to query, or `nil` for
/// legacy device-level routing.
/// - rawQuery: User-entered directory search text.
/// - Returns: Matching directories or a user-actionable failure.
public func searchTaskDirectories(
macDeviceID: String,
instanceTag: String? = nil,
query rawQuery: String
) async -> Result<MobileTaskDirectorySearchResponse, MobileTaskDirectorySearchFailure> {
let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines)
Expand All @@ -32,10 +39,14 @@ extension MobileShellComposite {
searchScope: .contextualCandidatesOnly
))
}
if macDeviceID != foregroundMacDeviceID || remoteClient == nil {
guard await switchToMac(macDeviceID: macDeviceID) else { return .failure(.unavailable) }
if !matchesForegroundPairing(macDeviceID: macDeviceID, instanceTag: instanceTag)
|| remoteClient == nil {
guard await switchToMac(macDeviceID: macDeviceID, instanceTag: instanceTag) else {
return .failure(.unavailable)
}
}
guard !Task.isCancelled, foregroundMacDeviceID == macDeviceID,
guard !Task.isCancelled,
matchesForegroundPairing(macDeviceID: macDeviceID, instanceTag: instanceTag),
let client = remoteClient else { return .failure(.cancelled) }
let generation = connectionGeneration
// The last learned capability set can be stale after a tagged Mac
Expand All @@ -47,7 +58,8 @@ extension MobileShellComposite {
params: ["query": query]
)
let data = try await client.sendRequest(request, timeoutNanoseconds: 4_000_000_000)
guard !Task.isCancelled, foregroundMacDeviceID == macDeviceID else {
guard !Task.isCancelled,
matchesForegroundPairing(macDeviceID: macDeviceID, instanceTag: instanceTag) else {
return .failure(.cancelled)
}
return .success(try MobileTaskDirectorySearchResponse.decode(data))
Expand Down Expand Up @@ -168,13 +180,16 @@ extension MobileShellComposite {
/// Submit a task-composer workspace create request to the selected Mac.
/// - Parameters:
/// - macDeviceID: Target Mac device id.
/// - instanceTag: Exact paired app instance to target, or `nil` for
/// legacy device-level routing.
/// - spec: Workspace-create parameters derived from the selected template.
/// - willStartCreate: Optional main-actor callback invoked after the target
/// Mac and capability are resolved, immediately before the create begins.
/// - Returns: `success` when the workspace was created; otherwise the failure to display.
@discardableResult
public func submitTaskComposer(
macDeviceID: String,
instanceTag: String? = nil,
spec: MobileWorkspaceCreateSpec,
willStartCreate: (@MainActor () -> Void)? = nil
) async -> Result<Void, MobileWorkspaceMutationFailure> {
Expand All @@ -183,20 +198,42 @@ extension MobileShellComposite {
// must not skip the switch, or the create fails as not-connected without
// ever attempting a re-dial. `switchToMac` short-circuits when the
// foreground connection to this Mac is genuinely live.
if macDeviceID != foregroundMacDeviceID || remoteClient == nil {
guard await switchToMac(macDeviceID: macDeviceID) else {
return .failure(.notConnected(hostDisplayName: taskComposerTargetName(macDeviceID: macDeviceID)))
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 else {
return .failure(.notConnected(hostDisplayName: taskComposerTargetName(macDeviceID: macDeviceID)))
return .failure(.notConnected(
hostDisplayName: taskComposerTargetName(
macDeviceID: macDeviceID,
instanceTag: instanceTag
)
))
}
guard let pinnedContext = captureWorkspaceCreateContext(),
pinnedContext.macDeviceID == macDeviceID else {
return .failure(.notConnected(hostDisplayName: taskComposerTargetName(macDeviceID: macDeviceID)))
pinnedContext.macDeviceID == macDeviceID,
instanceTag == nil || pinnedContext.instanceTag == instanceTag else {
return .failure(.notConnected(
hostDisplayName: taskComposerTargetName(
macDeviceID: macDeviceID,
instanceTag: instanceTag
)
))
}
guard pinnedContext.supportedHostCapabilities.contains(Self.taskCreateCapability) else {
return .failure(.unsupported(hostDisplayName: taskComposerTargetName(macDeviceID: macDeviceID)))
return .failure(.unsupported(
hostDisplayName: taskComposerTargetName(
macDeviceID: macDeviceID,
instanceTag: instanceTag
)
))
}
guard !Task.isCancelled else {
return .failure(.notConnected(hostDisplayName: pinnedContext.hostDisplayName))
Expand All @@ -208,9 +245,22 @@ extension MobileShellComposite {
)
}

private func taskComposerTargetName(macDeviceID: String) -> String {
displayPairedMacs.first { $0.macDeviceID == macDeviceID }?.resolvedName
?? pairedMacs.first { $0.macDeviceID == macDeviceID }?.resolvedName
private func taskComposerTargetName(macDeviceID: String, instanceTag: String?) -> String {
displayPairedMacs.first {
$0.macDeviceID == macDeviceID
&& (instanceTag == nil || $0.instanceTag == instanceTag)
}?.resolvedName
?? pairedMacs.first {
$0.macDeviceID == macDeviceID
&& (instanceTag == nil || $0.instanceTag == instanceTag)
}?.resolvedName
?? macDeviceID
}

/// Whether the foreground connection already targets this Mac pairing.
/// A `nil` `instanceTag` keeps legacy device-level matching.
func matchesForegroundPairing(macDeviceID: String, instanceTag: String?) -> Bool {
foregroundMacDeviceID == macDeviceID
&& (instanceTag == nil || activeMacInstanceTag == instanceTag)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@ extension MobileShellComposite {
///
/// - Parameters:
/// - macDeviceID: The paired Mac that owns the filesystem.
/// - instanceTag: Exact paired app instance to query, or `nil` for
/// legacy device-level routing.
/// - path: An absolute path, `~`, or a path beginning with `~/`.
/// - offset: A nonnegative entry offset in the directory's stable order.
/// - limit: A page size from `1` through
/// ``MobileTaskDirectoryListRequest/maximumPageSize``.
/// - Returns: A validated page, or a typed user-actionable failure.
public func listTaskDirectories(
macDeviceID: String,
instanceTag: String? = nil,
path: String,
offset: Int = 0,
limit: Int = MobileTaskDirectoryListRequest.defaultPageSize
Expand All @@ -29,12 +32,14 @@ extension MobileShellComposite {
return .failure(.invalidPath)
}

if macDeviceID != foregroundMacDeviceID || remoteClient == nil {
guard await switchToMac(macDeviceID: macDeviceID) else {
if !matchesForegroundPairing(macDeviceID: macDeviceID, instanceTag: instanceTag)
|| remoteClient == nil {
guard await switchToMac(macDeviceID: macDeviceID, instanceTag: instanceTag) else {
return .failure(Task.isCancelled ? .cancelled : .unavailable)
}
}
guard !Task.isCancelled, foregroundMacDeviceID == macDeviceID,
guard !Task.isCancelled,
matchesForegroundPairing(macDeviceID: macDeviceID, instanceTag: instanceTag),
let client = remoteClient else {
return .failure(.cancelled)
}
Expand All @@ -53,7 +58,8 @@ extension MobileShellComposite {
requestData,
timeoutNanoseconds: 4_000_000_000
)
guard !Task.isCancelled, foregroundMacDeviceID == macDeviceID else {
guard !Task.isCancelled,
matchesForegroundPairing(macDeviceID: macDeviceID, instanceTag: instanceTag) else {
return .failure(.cancelled)
}
return .success(try MobileTaskDirectoryListResponse.decode(data))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ extension MobileShellComposite {
guard connectionState == .connected, let remoteClient else { return nil }
return WorkspaceCreatePinnedContext(
macDeviceID: foregroundMacDeviceID,
instanceTag: activeMacInstanceTag,
client: remoteClient,
generation: connectionGeneration,
supportedHostCapabilities: supportedHostCapabilities,
Expand All @@ -202,6 +203,7 @@ extension MobileShellComposite {
private func isCurrentWorkspaceCreateContext(_ context: WorkspaceCreatePinnedContext) -> Bool {
context.isCurrent(
macDeviceID: foregroundMacDeviceID,
instanceTag: activeMacInstanceTag,
client: remoteClient,
generation: connectionGeneration
) && isSignedIn && connectionState == .connected
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2471,7 +2471,10 @@ public final class MobileShellComposite: MobileTerminalOutputSinking {
/// To avoid stranding the user, the store's active row is only updated on a
/// successful connect, and on failure the previously-active Mac (still the
/// active row) is reconnected. A no-op when already connected to that Mac.
/// - Parameter macDeviceID: The stored Mac to switch to.
/// - Parameters:
/// - macDeviceID: The stored physical Mac to switch to.
/// - instanceTag: Exact saved app instance to switch to, or `nil` for
/// legacy device-level routing.
/// - Returns: `true` if the foreground connection now targets that Mac (or
/// already did), `false` if the switch could not connect — so callers like
/// `openWorkspace` can avoid selecting a workspace whose Mac is not live.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ extension MobileShellComposite {
/// Exact remote target captured before a workspace-create request suspends.
struct WorkspaceCreatePinnedContext {
let macDeviceID: String?
let instanceTag: String?
let client: MobileCoreRPCClient
let generation: UUID
let supportedHostCapabilities: Set<String>
Expand All @@ -25,10 +26,12 @@ extension MobileShellComposite {
/// Whether the caller still exposes the same Mac, client, and generation.
func isCurrent(
macDeviceID currentMacDeviceID: String?,
instanceTag currentInstanceTag: String?,
client currentClient: MobileCoreRPCClient?,
generation currentGeneration: UUID
) -> Bool {
macDeviceID == currentMacDeviceID
&& instanceTag == currentInstanceTag
&& client === currentClient
&& generation == currentGeneration
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,7 @@ import Testing

#expect(pinnedContext.isCurrent(
macDeviceID: store.foregroundMacDeviceID,
instanceTag: store.activeMacInstanceTag,
client: store.remoteClient,
generation: store.connectionGeneration
))
Expand All @@ -326,6 +327,7 @@ import Testing

#expect(!pinnedContext.isCurrent(
macDeviceID: store.foregroundMacDeviceID,
instanceTag: store.activeMacInstanceTag,
client: store.remoteClient,
generation: store.connectionGeneration
))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ public struct MobileTaskComposerDraft: Codable, Equatable, Sendable {
public var templateID: MobileTaskTemplate.ID?
/// Selected Mac, validated against current paired Macs when restored.
public var macDeviceID: String?
/// Paired app instance selected when the draft was saved, or `nil` for a
/// legacy draft or device-level selection. Missing keys decode as `nil`.
public var macInstanceTag: String?
/// Working directory exactly as entered by the user.
public var directory: String
/// Whether the user replaced the suggested directory.
Expand All @@ -25,6 +28,7 @@ public struct MobileTaskComposerDraft: Codable, Equatable, Sendable {
prompt: String,
templateID: MobileTaskTemplate.ID?,
macDeviceID: String?,
macInstanceTag: String? = nil,
directory: String,
didEditDirectory: Bool,
workspaceName: String? = nil,
Expand All @@ -34,6 +38,7 @@ public struct MobileTaskComposerDraft: Codable, Equatable, Sendable {
self.prompt = prompt
self.templateID = templateID
self.macDeviceID = macDeviceID
self.macInstanceTag = macInstanceTag
self.directory = directory
self.didEditDirectory = didEditDirectory
self.workspaceName = workspaceName
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ public struct MobileTaskSubmissionSnapshot: Equatable, Sendable {
public let templateID: MobileTaskTemplate.ID
/// Identifier of the Mac targeted by the captured submission.
public let macDeviceID: String
/// Paired app instance targeted by the captured submission, or `nil` for
/// legacy device-level routing.
public let macInstanceTag: String?
/// Unmodified prompt text captured from the composer.
public let prompt: String
/// Optional workspace name exactly as entered in the composer.
Expand Down Expand Up @@ -38,6 +41,7 @@ public struct MobileTaskSubmissionSnapshot: Equatable, Sendable {
/// - template: Task template selected when submission begins.
/// - prompt: Prompt text to compose into the template command.
/// - macDeviceID: Identifier of the Mac that should create the task.
/// - macInstanceTag: Exact paired app instance to target, or `nil`.
/// - directory: Working-directory text shown in the composer.
/// - workspaceName: Optional workspace name shown in the composer.
/// - didEditDirectory: Whether the user changed the suggested directory.
Expand All @@ -46,13 +50,15 @@ public struct MobileTaskSubmissionSnapshot: Equatable, Sendable {
template: MobileTaskTemplate,
prompt: String,
macDeviceID: String,
macInstanceTag: String? = nil,
directory: String,
workspaceName: String = "",
didEditDirectory: Bool,
operationID: UUID
) {
self.templateID = template.id
self.macDeviceID = macDeviceID
self.macInstanceTag = macInstanceTag
self.prompt = prompt
self.workspaceName = workspaceName
self.trimmedWorkspaceName = workspaceName.trimmingCharacters(in: .whitespacesAndNewlines)
Expand All @@ -71,6 +77,7 @@ public struct MobileTaskSubmissionSnapshot: Equatable, Sendable {
/// environment, and trimmed effective working directory.
public func isRequestEquivalent(to other: MobileTaskSubmissionSnapshot) -> Bool {
Self.hasEqualUTF8(macDeviceID, other.macDeviceID)
&& Self.hasEqualUTF8(macInstanceTag, other.macInstanceTag)
&& Self.hasEqualUTF8(composition.initialCommand, other.composition.initialCommand)
&& Self.hasEqualUTF8(composition.initialEnv, other.composition.initialEnv)
&& Self.hasEqualUTF8(workspaceTitle, other.workspaceTitle)
Expand All @@ -84,6 +91,7 @@ public struct MobileTaskSubmissionSnapshot: Equatable, Sendable {
MobileTaskSubmissionSnapshot(
templateID: templateID,
macDeviceID: macDeviceID,
macInstanceTag: macInstanceTag,
prompt: prompt,
workspaceName: workspaceName,
directory: directory,
Expand Down Expand Up @@ -129,6 +137,7 @@ public struct MobileTaskSubmissionSnapshot: Equatable, Sendable {
prompt: prompt,
templateID: templateID,
macDeviceID: macDeviceID.isEmpty ? nil : macDeviceID,
macInstanceTag: macDeviceID.isEmpty ? nil : macInstanceTag,
directory: directory,
didEditDirectory: didEditDirectory,
workspaceName: workspaceName.isEmpty ? nil : workspaceName,
Expand All @@ -139,6 +148,7 @@ public struct MobileTaskSubmissionSnapshot: Equatable, Sendable {
private init(
templateID: MobileTaskTemplate.ID,
macDeviceID: String,
macInstanceTag: String?,
prompt: String,
workspaceName: String,
directory: String,
Expand All @@ -150,6 +160,7 @@ public struct MobileTaskSubmissionSnapshot: Equatable, Sendable {
) {
self.templateID = templateID
self.macDeviceID = macDeviceID
self.macInstanceTag = macInstanceTag
self.prompt = prompt
self.workspaceName = workspaceName
self.trimmedWorkspaceName = trimmedWorkspaceName
Expand Down
Loading