Repository navigation
Improve tmux notification attention routing #1898
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
Changes from all commits
d481165
656786f
a41bc97
ec6915b
a0485df
2a04ac3
c325e2b
5e9a58d
af5207d
5051123
f0fb098
5cef77e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1863,12 +1863,36 @@ 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 preferTTYFallback = windowId == nil && ProcessInfo.processInfo.environment["TMUX"] != nil | ||
| let callerWorkspaceArg = preferTTYFallback | ||
| ? nil | ||
| : (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 = explicitSurfaceArg == nil && preferTTYFallback == false && 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 | ||
| ) | ||
| }() | ||
|
Comment on lines
+1866
to
+1895
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't let These branches correctly clear env-derived caller IDs when Suggested fixApply the same change in both call sites, and gate the helper's TTY fallback behind a flag. - return try resolveWorkspaceIdAllowingFallback(workspaceArg, client: client)
+ return try resolveWorkspaceIdAllowingFallback(
+ workspaceArg,
+ client: client,
+ allowCallerTTYFallback: windowId == nil
+ )Also applies to: 2086-2110 🤖 Prompt for AI Agents |
||
| 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 +2086,34 @@ 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 targetWorkspace = try resolveWorkspaceId(workspaceArg, client: client) | ||
| let targetSurface = try resolveSurfaceId(surfaceArg, workspaceId: targetWorkspace, client: client) | ||
| let explicitWorkspaceArg = optionValue(commandArgs, name: "--workspace") | ||
| let preferTTYFallback = windowId == nil && ProcessInfo.processInfo.environment["TMUX"] != nil | ||
| let callerWorkspaceArg = preferTTYFallback | ||
| ? nil | ||
| : (windowId == nil ? ProcessInfo.processInfo.environment["CMUX_WORKSPACE_ID"] : nil) | ||
| let workspaceArg = explicitWorkspaceArg ?? callerWorkspaceArg | ||
| let explicitSurfaceArg = optionValue(commandArgs, name: "--surface") | ||
| let callerSurfaceArg = explicitSurfaceArg == nil && preferTTYFallback == false && windowId == nil | ||
| ? ProcessInfo.processInfo.environment["CMUX_SURFACE_ID"] | ||
| : nil | ||
| let surfaceArg = explicitSurfaceArg ?? callerSurfaceArg | ||
|
|
||
| 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,26 +10528,133 @@ 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( | ||
| _ raw: String?, | ||
| 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) | ||
| } | ||
|
Comment on lines
10534
to
+10540
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Route the The new TTY fallback only helps call sites that use this helper. Line 10171 still calls 🤖 Prompt for AI Agents |
||
|
|
||
| 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 | ||
|
Comment on lines
+10546
to
10550
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Useful? React with 👍 / 👎. |
||
| } | ||
| if let callerWorkspaceId = resolveCallerWorkspaceIdByTTY(client: client), | ||
|
cubic-dev-ai[bot] marked this conversation as resolved.
|
||
| (try? client.sendV2(method: "surface.list", params: ["workspace_id": callerWorkspaceId])) != nil { | ||
| return callerWorkspaceId | ||
|
Comment on lines
+10552
to
+10554
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎. |
||
| } | ||
| 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 | ||
| } | ||
|
Comment on lines
+10569
to
+10573
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This candidate-return path runs before the TTY fallback, so a stale-but-existing Useful? React with 👍 / 👎. |
||
| } | ||
| 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, | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
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.
P2: The new
TMUXgate clearsCMUX_WORKSPACE_ID/CMUX_SURFACE_IDdefaults, sonotify/trigger-flashcan misroute in tmux when tty fallback is unavailable.Prompt for AI agents