Self-heal tmux attention routing for cmux panes

This commit is contained in:
Lawrence Chen 2026-03-21 03:26:11 -07:00
parent f0fb098d3b
commit 5cef77e456
No known key found for this signature in database
4 changed files with 67 additions and 45 deletions

View file

@ -1864,10 +1864,13 @@ struct CMUXCLI {
case "trigger-flash":
let tfWsFlag = optionValue(commandArgs, name: "--workspace")
let explicitWorkspaceArg = tfWsFlag
let callerWorkspaceArg = windowId == nil ? ProcessInfo.processInfo.environment["CMUX_WORKSPACE_ID"] : nil
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 = explicitWorkspaceArg == nil && windowId == nil
let callerSurfaceArg = explicitSurfaceArg == nil && preferTTYFallback == false && windowId == nil
? ProcessInfo.processInfo.environment["CMUX_SURFACE_ID"]
: nil
let surfaceArg = explicitSurfaceArg ?? callerSurfaceArg
@ -2084,10 +2087,13 @@ struct CMUXCLI {
let body = optionValue(commandArgs, name: "--body") ?? ""
let explicitWorkspaceArg = optionValue(commandArgs, name: "--workspace")
let callerWorkspaceArg = windowId == nil ? ProcessInfo.processInfo.environment["CMUX_WORKSPACE_ID"] : nil
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 = explicitWorkspaceArg == nil && windowId == nil
let callerSurfaceArg = explicitSurfaceArg == nil && preferTTYFallback == false && windowId == nil
? ProcessInfo.processInfo.environment["CMUX_SURFACE_ID"]
: nil
let surfaceArg = explicitSurfaceArg ?? callerSurfaceArg

View file

@ -76,6 +76,10 @@ _CMUX_TMUX_SYNC_KEYS=(
CMUX_TAG
CMUX_WORKSPACE_ID
)
_CMUX_TMUX_SURFACE_SCOPED_KEYS=(
CMUX_PANEL_ID
CMUX_SURFACE_ID
)
_cmux_tmux_sync_key_is_managed() {
local candidate="$1"
@ -116,6 +120,10 @@ _cmux_tmux_publish_cmux_environment() {
tmux set-environment -g "$key" "$value" >/dev/null 2>&1 || return 0
done
for key in "${_CMUX_TMUX_SURFACE_SCOPED_KEYS[@]}"; do
tmux set-environment -gu "$key" >/dev/null 2>&1 || return 0
done
_CMUX_TMUX_PUSH_SIGNATURE="$signature"
}
@ -205,17 +213,32 @@ _cmux_git_head_signature() {
printf '%s\n' "$line"
}
_cmux_report_tty_payload() {
[[ -n "$CMUX_TAB_ID" ]] || return 0
[[ -n "$_CMUX_TTY_NAME" ]] || return 0
local payload="report_tty $_CMUX_TTY_NAME --tab=$CMUX_TAB_ID"
if [[ -z "$TMUX" ]]; then
[[ -n "$CMUX_PANEL_ID" ]] || return 0
payload+=" --panel=$CMUX_PANEL_ID"
fi
printf '%s\n' "$payload"
}
_cmux_report_tty_once() {
# Send the TTY name to the app once per session so the batched port scanner
# knows which TTY belongs to this panel.
(( _CMUX_TTY_REPORTED )) && return 0
[[ -S "$CMUX_SOCKET_PATH" ]] || return 0
[[ -n "$CMUX_TAB_ID" ]] || return 0
[[ -n "$CMUX_PANEL_ID" ]] || return 0
[[ -n "$_CMUX_TTY_NAME" ]] || return 0
local payload=""
payload="$(_cmux_report_tty_payload)"
[[ -n "$payload" ]] || return 0
_CMUX_TTY_REPORTED=1
{
_cmux_send "report_tty $_CMUX_TTY_NAME --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
_cmux_send "$payload"
} >/dev/null 2>&1 & disown
}

View file

@ -82,6 +82,10 @@ typeset -ga _CMUX_TMUX_SYNC_KEYS=(
CMUX_TAG
CMUX_WORKSPACE_ID
)
typeset -ga _CMUX_TMUX_SURFACE_SCOPED_KEYS=(
CMUX_PANEL_ID
CMUX_SURFACE_ID
)
_cmux_tmux_sync_key_is_managed() {
local candidate="$1"
@ -119,6 +123,10 @@ _cmux_tmux_publish_cmux_environment() {
tmux set-environment -g "$key" "$value" >/dev/null 2>&1 || return 0
done
for key in "${_CMUX_TMUX_SURFACE_SCOPED_KEYS[@]}"; do
tmux set-environment -gu "$key" >/dev/null 2>&1 || return 0
done
_CMUX_TMUX_PUSH_SIGNATURE="$signature"
}
@ -305,17 +313,32 @@ _cmux_git_head_signature() {
return 1
}
_cmux_report_tty_payload() {
[[ -n "$CMUX_TAB_ID" ]] || return 0
[[ -n "$_CMUX_TTY_NAME" ]] || return 0
local payload="report_tty $_CMUX_TTY_NAME --tab=$CMUX_TAB_ID"
if [[ -z "$TMUX" ]]; then
[[ -n "$CMUX_PANEL_ID" ]] || return 0
payload+=" --panel=$CMUX_PANEL_ID"
fi
print -r -- "$payload"
}
_cmux_report_tty_once() {
# Send the TTY name to the app once per session so the batched port scanner
# knows which TTY belongs to this panel.
(( _CMUX_TTY_REPORTED )) && return 0
[[ -S "$CMUX_SOCKET_PATH" ]] || return 0
[[ -n "$CMUX_TAB_ID" ]] || return 0
[[ -n "$CMUX_PANEL_ID" ]] || return 0
[[ -n "$_CMUX_TTY_NAME" ]] || return 0
local payload=""
payload="$(_cmux_report_tty_payload)"
[[ -n "$payload" ]] || return 0
_CMUX_TTY_REPORTED=1
{
_cmux_send "report_tty $_CMUX_TTY_NAME --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
_cmux_send "$payload"
} >/dev/null 2>&1 &!
}

View file

@ -2526,51 +2526,21 @@ final class ZshShellIntegrationHandoffTests: XCTestCase {
}
func testShellIntegrationReportsTTYFromTmuxWithoutUsingPanelScope() throws {
let fileManager = FileManager.default
let root = fileManager.temporaryDirectory
.appendingPathComponent("cmux-zsh-tmux-report-tty-\(UUID().uuidString)")
let binDir = root.appendingPathComponent("bin", isDirectory: true)
let socketPath = root.appendingPathComponent("cmux-test.sock", isDirectory: false)
let logPath = root.appendingPathComponent("tty.log", isDirectory: false)
try fileManager.createDirectory(at: root, withIntermediateDirectories: true)
try fileManager.createDirectory(at: binDir, withIntermediateDirectories: true)
let listenerFD = try bindUnixSocket(at: socketPath.path)
defer {
Darwin.close(listenerFD)
unlink(socketPath.path)
try? fileManager.removeItem(at: root)
}
try writeExecutableScript(
at: binDir.appendingPathComponent("ncat", isDirectory: false),
contents: """
#!/bin/sh
cat > "\(logPath.path)"
exit 0
"""
)
_ = try runInteractiveZsh(
let output = try runInteractiveZsh(
cmuxLoadGhosttyIntegration: false,
cmuxLoadShellIntegration: true,
command: """
_CMUX_TTY_NAME=ttys999
_cmux_report_tty_once
sleep 0.05
print -r -- READY
print -r -- "$(_cmux_report_tty_payload)"
""",
extraEnvironment: [
"PATH": "\(binDir.path):/usr/bin:/bin:/usr/sbin:/sbin",
"TMUX": "/tmp/tmux-current,123,0",
"CMUX_SOCKET_PATH": socketPath.path,
"CMUX_TAB_ID": "11111111-1111-1111-1111-111111111111",
"CMUX_PANEL_ID": "99999999-9999-9999-9999-999999999999",
]
)
let log = (try? String(contentsOf: logPath, encoding: .utf8)) ?? ""
XCTAssertEqual(log, "report_tty ttys999 --tab=11111111-1111-1111-1111-111111111111\n")
XCTAssertEqual(output, "report_tty ttys999 --tab=11111111-1111-1111-1111-111111111111")
}
private func runInteractiveZsh(cmuxLoadGhosttyIntegration: Bool) throws -> String {