Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
27 changes: 23 additions & 4 deletions CLI/cmux.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5386,6 +5386,7 @@ struct CMUXCLI {
let title = optionValue(commandArgs, name: "--title") ?? "Notification"
let subtitle = optionValue(commandArgs, name: "--subtitle") ?? ""
let body = optionValue(commandArgs, name: "--body") ?? ""
let allowsReply = hasFlag(commandArgs, name: "--reply")
let explicitWorkspaceArg = optionValue(commandArgs, name: "--workspace")
let windowRaw = windowFromArgsOrOverride(commandArgs, windowOverride: windowId)
let windowHandle = try normalizeWindowHandle(windowRaw, client: client)
Expand All @@ -5412,12 +5413,25 @@ struct CMUXCLI {
targetSurface = try explicitSurfaceArg.map { try resolveSurfaceId($0, workspaceId: targetWorkspace, client: client) }
?? resolveSurfaceId(nil, workspaceId: targetWorkspace, client: client)
}
let payload = notificationPayload(title: title, subtitle: subtitle, body: body)
let response = try sendV1Command("notify_target \(targetWorkspace) \(targetSurface) \(payload)", client: client)
print(response)
if allowsReply {
let payload = try client.sendV2(method: "notification.create_for_target", params: [
"workspace_id": targetWorkspace,
"surface_id": targetSurface,
"title": title,
"subtitle": subtitle,
"body": body,
"reply_shape": "text",
])
printV2Payload(payload, jsonOutput: jsonOutput, idFormat: idFormat, fallbackText: "OK")
} else {
let payload = notificationPayload(title: title, subtitle: subtitle, body: body)
let response = try sendV1Command("notify_target \(targetWorkspace) \(targetSurface) \(payload)", client: client)
print(response)
}
return
}
var params: [String: Any] = ["title": title, "subtitle": subtitle, "body": body]
if allowsReply { params["reply_shape"] = "text" }
let method: String
if explicitSurfaceArg != nil {
method = "notification.create"
Expand Down Expand Up @@ -17218,6 +17232,10 @@ struct CMUXCLI {
cmux send-key-panel --panel surface:2 ctrl+c
"""
case "notify":
let replyHelp = String(
localized: "cli.help.notify.reply",
defaultValue: "--reply Allow a free-text inline reply"
)
return """
Usage: cmux notify [flags]

Expand All @@ -17227,6 +17245,7 @@ struct CMUXCLI {
--title <text> Notification title (default: "Notification")
--subtitle <text> Notification subtitle
--body <text> Notification body
\(replyHelp)
--workspace <id|ref|index> Target workspace, except explicit surface UUIDs resolve globally
--surface <id|ref|index> Target surface (refs/indexes use workspace/window context)
--window <id|ref|index> Window context for workspace/surface refs and indexes
Expand Down Expand Up @@ -36539,7 +36558,7 @@ export default CMUXSessionRestore;
send-key [--workspace <id|ref|index>] [--surface <id|ref|index>] [--window <id|ref|index>] <key>
send-panel --panel <id|ref|index> [--workspace <id|ref|index>] [--window <id|ref|index>] <text>
send-key-panel --panel <id|ref|index> [--workspace <id|ref|index>] [--window <id|ref|index>] <key>
notify --title <text> [--subtitle <text>] [--body <text>] [--workspace <id|ref|index>] [--surface <id|ref|index>] [--window <id|ref|index>]
notify --title <text> [--subtitle <text>] [--body <text>] [--reply] [--workspace <id|ref|index>] [--surface <id|ref|index>] [--window <id|ref|index>]
list-notifications
dismiss-notification (--id <uuid> | --all-read)
mark-notification-read (--id <uuid> | --workspace <id|ref|index> [--surface <id|ref|index>] [--window <id|ref|index>] | --all)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
internal import CmuxMobileRPC
public import CmuxMobileShellModel
internal import Foundation
internal import OSLog

private let explicitTerminalInputLog = Logger(
subsystem: Bundle.main.bundleIdentifier ?? "dev.cmux.ios",
category: "mobile-shell"
)

extension MobileShellComposite {
/// Whether the Mac that owns `workspaceID` currently has an RPC channel able to accept terminal input.
///
/// This check does not change the selected Mac, workspace, terminal, or navigation state.
///
/// - Parameter workspaceID: The current UI row identifier for the target workspace.
/// - Returns: `true` when the workspace exists and its owning Mac has a live RPC client.
public func canSendTerminalInput(to workspaceID: MobileWorkspacePreview.ID) -> Bool {
guard workspaces.contains(where: { $0.id == workspaceID }) else { return false }
return workspaceMutationTarget(for: workspaceID).client != nil
}

/// Sends text to an explicit workspace and terminal without changing UI selection or navigation.
///
/// The workspace row selects the owning Mac's live RPC client, including a connected secondary Mac
/// in the aggregated workspace list, while the Mac-local workspace identifier is sent on the wire.
///
/// - Parameters:
/// - text: The exact terminal input bytes represented as text.
/// - workspaceID: The current UI row identifier for the target workspace.
/// - terminalID: The terminal that must still belong to `workspaceID`.
/// - Returns: `true` after the owning Mac acknowledges the input; otherwise `false`.
@discardableResult
public func sendTerminalInput(
_ text: String,
workspaceID: MobileWorkspacePreview.ID,
terminalID: MobileTerminalPreview.ID
) async -> Bool {
guard !text.isEmpty,
workspace(workspaceID, containsSurfaceID: terminalID.rawValue) else {
return false
}
let target = workspaceMutationTarget(for: workspaceID)
guard let client = target.client else { return false }

do {
_ = try await client.sendRequest(
MobileCoreRPCClient.requestData(
method: "terminal.input",
params: [
"workspace_id": remoteWorkspaceID(for: workspaceID).rawValue,
"surface_id": terminalID.rawValue,
"text": text,
"client_id": clientID,
]
)
)
return true
} catch {
explicitTerminalInputLog.error(
"explicit terminal input failed workspace=\(workspaceID.rawValue, privacy: .private) surface=\(terminalID.rawValue, privacy: .private) error=\(String(describing: error), privacy: .private)"
)
return false
}
}
}
Loading