Fix tmux notification attention routing

This commit is contained in:
Lawrence Chen 2026-03-20 20:20:54 -07:00
parent d4811650d7
commit 656786fb71
No known key found for this signature in database
11 changed files with 1151 additions and 64 deletions

View file

@ -1863,12 +1863,33 @@ struct CMUXCLI {
case "trigger-flash":
let tfWsFlag = optionValue(commandArgs, name: "--workspace")
let workspaceArg = tfWsFlag ?? (windowId == nil ? ProcessInfo.processInfo.environment["CMUX_WORKSPACE_ID"] : nil)
let surfaceArg = optionValue(commandArgs, name: "--surface") ?? optionValue(commandArgs, name: "--panel") ?? (tfWsFlag == nil && windowId == nil ? ProcessInfo.processInfo.environment["CMUX_SURFACE_ID"] : nil)
let explicitWorkspaceArg = tfWsFlag
let callerWorkspaceArg = windowId == nil ? ProcessInfo.processInfo.environment["CMUX_WORKSPACE_ID"] : nil
let workspaceArg = explicitWorkspaceArg ?? callerWorkspaceArg
let explicitSurfaceArg = optionValue(commandArgs, name: "--surface") ?? optionValue(commandArgs, name: "--panel")
let callerSurfaceArg = explicitWorkspaceArg == nil && windowId == nil
? ProcessInfo.processInfo.environment["CMUX_SURFACE_ID"]
: nil
let surfaceArg = explicitSurfaceArg ?? callerSurfaceArg
var params: [String: Any] = [:]
let wsId = try normalizeWorkspaceHandle(workspaceArg, client: client)
let wsId = try {
if explicitWorkspaceArg != nil {
return try normalizeWorkspaceHandle(workspaceArg, client: client)
}
return try resolveWorkspaceIdAllowingFallback(workspaceArg, client: client)
}()
if let wsId { params["workspace_id"] = wsId }
let sfId = try normalizeSurfaceHandle(surfaceArg, client: client, workspaceHandle: wsId)
let sfId = try {
if explicitSurfaceArg != nil {
return try normalizeSurfaceHandle(surfaceArg, client: client, workspaceHandle: wsId)
}
guard let wsId else { return nil }
return try resolveSurfaceIdAllowingFallback(
surfaceArg,
workspaceId: wsId,
client: client
)
}()
if let sfId { params["surface_id"] = sfId }
let payload = try client.sendV2(method: "surface.trigger_flash", params: params)
printV2Payload(payload, jsonOutput: jsonOutput, idFormat: idFormat, fallbackText: v2OKSummary(payload, idFormat: idFormat))
@ -2062,12 +2083,31 @@ struct CMUXCLI {
let subtitle = optionValue(commandArgs, name: "--subtitle") ?? ""
let body = optionValue(commandArgs, name: "--body") ?? ""
let notifyWsFlag = optionValue(commandArgs, name: "--workspace")
let workspaceArg = notifyWsFlag ?? (windowId == nil ? ProcessInfo.processInfo.environment["CMUX_WORKSPACE_ID"] : nil)
let surfaceArg = optionValue(commandArgs, name: "--surface") ?? (notifyWsFlag == nil && windowId == nil ? ProcessInfo.processInfo.environment["CMUX_SURFACE_ID"] : nil)
let explicitWorkspaceArg = optionValue(commandArgs, name: "--workspace")
let callerWorkspaceArg = windowId == nil ? ProcessInfo.processInfo.environment["CMUX_WORKSPACE_ID"] : nil
let workspaceArg = explicitWorkspaceArg ?? callerWorkspaceArg
let explicitSurfaceArg = optionValue(commandArgs, name: "--surface")
let callerSurfaceArg = explicitWorkspaceArg == nil && windowId == nil
? ProcessInfo.processInfo.environment["CMUX_SURFACE_ID"]
: nil
let surfaceArg = explicitSurfaceArg ?? callerSurfaceArg
let targetWorkspace = try resolveWorkspaceId(workspaceArg, client: client)
let targetSurface = try resolveSurfaceId(surfaceArg, workspaceId: targetWorkspace, client: client)
let targetWorkspace = try {
if explicitWorkspaceArg != nil {
return try resolveWorkspaceId(workspaceArg, client: client)
}
return try resolveWorkspaceIdAllowingFallback(workspaceArg, client: client)
}()
let targetSurface = try {
if explicitSurfaceArg != nil {
return try resolveSurfaceId(surfaceArg, workspaceId: targetWorkspace, client: client)
}
return try resolveSurfaceIdAllowingFallback(
surfaceArg,
workspaceId: targetWorkspace,
client: client
)
}()
let payload = "\(title)|\(subtitle)|\(body)"
let response = try sendV1Command("notify_target \(targetWorkspace) \(targetSurface) \(payload)", client: client)
@ -10482,13 +10522,7 @@ struct CMUXCLI {
}
private func resolveWorkspaceIdForClaudeHook(_ raw: String?, client: SocketClient) throws -> String {
if let raw, !raw.isEmpty, let candidate = try? resolveWorkspaceId(raw, client: client) {
let probe = try? client.sendV2(method: "surface.list", params: ["workspace_id": candidate])
if probe != nil {
return candidate
}
}
return try resolveWorkspaceId(nil, client: client)
try resolveWorkspaceIdAllowingFallback(raw, client: client)
}
private func resolveSurfaceIdForClaudeHook(
@ -10496,12 +10530,125 @@ struct CMUXCLI {
workspaceId: String,
client: SocketClient
) throws -> String {
if let raw, !raw.isEmpty, let candidate = try? resolveSurfaceId(raw, workspaceId: workspaceId, client: client) {
try resolveSurfaceIdAllowingFallback(raw, workspaceId: workspaceId, client: client)
}
private func resolveWorkspaceIdAllowingFallback(
_ raw: String?,
client: SocketClient
) throws -> String {
if let raw,
!raw.isEmpty,
let candidate = try? resolveWorkspaceId(raw, client: client),
(try? client.sendV2(method: "surface.list", params: ["workspace_id": candidate])) != nil {
return candidate
}
if let callerWorkspaceId = resolveCallerWorkspaceIdByTTY(client: client),
(try? client.sendV2(method: "surface.list", params: ["workspace_id": callerWorkspaceId])) != nil {
return callerWorkspaceId
}
return try resolveWorkspaceId(nil, client: client)
}
private func resolveSurfaceIdAllowingFallback(
_ raw: String?,
workspaceId: String,
client: SocketClient
) throws -> String {
if let raw,
!raw.isEmpty,
let candidate = try? resolveSurfaceId(raw, workspaceId: workspaceId, client: client),
let listed = try? client.sendV2(method: "surface.list", params: ["workspace_id": workspaceId]) {
let items = listed["surfaces"] as? [[String: Any]] ?? []
if items.contains(where: {
($0["id"] as? String) == candidate || ($0["ref"] as? String) == candidate
}) {
return candidate
}
}
if let callerSurfaceId = resolveCallerSurfaceIdByTTY(workspaceId: workspaceId, client: client),
let listed = try? client.sendV2(method: "surface.list", params: ["workspace_id": workspaceId]) {
let items = listed["surfaces"] as? [[String: Any]] ?? []
if items.contains(where: {
($0["id"] as? String) == callerSurfaceId || ($0["ref"] as? String) == callerSurfaceId
}) {
return callerSurfaceId
}
}
return try resolveSurfaceId(nil, workspaceId: workspaceId, client: client)
}
private struct CallerTerminalBinding {
let workspaceId: String
let surfaceId: String
}
private func resolveCallerWorkspaceIdByTTY(client: SocketClient) -> String? {
resolveCallerTerminalBindingByTTY(client: client)?.workspaceId
}
private func resolveCallerSurfaceIdByTTY(workspaceId: String, client: SocketClient) -> String? {
guard let binding = resolveCallerTerminalBindingByTTY(client: client),
binding.workspaceId == workspaceId else {
return nil
}
return binding.surfaceId
}
private func resolveCallerTerminalBindingByTTY(client: SocketClient) -> CallerTerminalBinding? {
guard let ttyName = resolveCallerTTYName() else {
return nil
}
guard let payload = try? client.sendV2(method: "debug.terminals") else {
return nil
}
let terminals = payload["terminals"] as? [[String: Any]] ?? []
for terminal in terminals {
guard normalizedTTYName(terminal["tty"] as? String) == ttyName,
let workspaceId = normalizedHandleValue(terminal["workspace_id"] as? String),
let surfaceId = normalizedHandleValue(terminal["surface_id"] as? String) else {
continue
}
return CallerTerminalBinding(workspaceId: workspaceId, surfaceId: surfaceId)
}
return nil
}
private func resolveCallerTTYName() -> String? {
let env = ProcessInfo.processInfo.environment
for key in ["CMUX_CLI_TTY_NAME", "CMUX_TTY_NAME", "TTY", "SSH_TTY"] {
if let ttyName = normalizedTTYName(env[key]) {
return ttyName
}
}
for fileDescriptor in [STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO] {
if let rawTTYName = ttyname(fileDescriptor),
let ttyName = normalizedTTYName(String(cString: rawTTYName)) {
return ttyName
}
}
return nil
}
private func normalizedTTYName(_ raw: String?) -> String? {
guard let trimmed = normalizedHandleValue(raw == "not a tty" ? nil : raw) else {
return nil
}
let components = trimmed.split(separator: "/")
if let last = components.last, !last.isEmpty {
return String(last)
}
return trimmed
}
private func normalizedHandleValue(_ raw: String?) -> String? {
guard let raw = raw?.trimmingCharacters(in: .whitespacesAndNewlines),
!raw.isEmpty else {
return nil
}
return raw
}
private func parseClaudeHookInput(rawInput: String) -> ClaudeHookParsedInput {
let trimmed = rawInput.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty,