agent-float-term 0.3.4

Harness Floating Terminal: a persistent F7 shell for unwrapped AI CLIs
Documentation
#!/usr/bin/env expect
# Run from any directory; select the server executable using PATH.
# expect tests/mouse_smoke.exp /path/to/agent-float-term patched|stock
# Interactive input always traverses real attached PTYs, never send-keys.
set timeout 5
log_user 0
if {$argc != 2 || [lindex $argv 1] ni {patched stock}} {
    puts stderr "usage: expect tests/mouse_smoke.exp BINARY patched|stock"
    exit 1
}
set source_binary [file normalize [lindex $argv 0]]
set mode [lindex $argv 1]
set fixture [file join [file dirname [file normalize [info script]]] fixtures terminal.c]
foreach tool {tmux bash cc mktemp} {
    if {[auto_execok $tool] eq ""} { puts stderr "mouse smoke requires $tool"; exit 1 }
}
set tmux [file normalize [lindex [auto_execok tmux] 0]]
# file normalize does not resolve a symlink in the final path component.
while {[file type $tmux] eq "link"} {
    set tmux [file normalize [file join [file dirname $tmux] [file readlink $tmux]]]
}
set shell [file normalize [lindex [auto_execok bash] 0]]
set root ""
set socket ""
set client ""
set viewers {}
set watchdog ""
set output ""
set deadline [expr {[clock milliseconds] + 60000}]

proc check {condition message} {
    if {![uplevel 1 [list expr $condition]]} { error $message }
}
proc tm {args} {
    global tmux socket deadline
    check {[clock milliseconds] < $deadline} "overall 60s deadline exceeded"
    return [exec $tmux -S $socket -f /dev/null {*}$args]
}
proc drain {} {
    global viewers client output
    foreach viewer $viewers {
        expect -i $viewer -timeout 0 -re {.+} {
            if {$viewer eq $client} { append output $expect_out(0,string) }
        } timeout {} eof {error "attached PTY client exited"}
    }
}
proc eventually {script message} {
    global deadline
    set until [expr {min($deadline, [clock milliseconds] + 5000)}]
    while {[clock milliseconds] < $until} {
        drain
        if {[uplevel 1 $script]} { return }
        after 25
    }
    error "timed out: $message"
}
proc settle {{milliseconds 350}} {
    global deadline
    set until [expr {[clock milliseconds] + $milliseconds}]
    while {[clock milliseconds] < $until} {
        check {[clock milliseconds] < $deadline} "overall 60s deadline exceeded"
        drain
        after 25
    }
}
proc type {text} { global client; send -i $client -- $text }
proc f7 {} { type "\033\[18~" }
proc click {x y} { type "\033\[<0;${x};${y}M\033\[<0;${x};${y}m" }
proc client_session {name} {
    foreach line [split [tm list-clients -F {#{client_name}|#{session_name}}] \n] {
        lassign [split $line |] actual session
        if {$actual eq $name} { return $session }
    }
    return ""
}
proc float_for {owner} {
    set found {}
    foreach line [split [tm list-panes -a -F {#{pane_id}|#{@aft_owner}}] \n] {
        lassign [split $line |] pane parent
        if {$parent eq $owner} { lappend found $pane }
    }
    check {[llength $found] <= 1} "duplicate retained floats for $owner"
    return [lindex $found 0]
}
proc attached {} {
    global float
    return [tm display-message -p -t $float {#{session_attached}}]
}
proc contains {pane text} {
    return [expr {[string first $text [tm capture-pane -p -t $pane]] >= 0}]
}
proc status_binding {} {
    foreach line [split [tm list-keys -T root] \n] {
        if {[regexp {^bind-key +(?:-r +)?-T +root +MouseDown1Status +} $line]} { return $line }
    }
    error "default MouseDown1Status binding missing"
}
proc state_is {path expected} {
    if {![file exists $path]} { return 0 }
    set input [open $path r]
    set actual [read $input]
    close $input
    return [expr {$actual eq $expected}]
}
proc shell_state {number {initialize 0}} {
    global root float float_pid env
    if {$initialize} { type {mouse_state=kept_unexported; cd "$HOME/retained"; } }
    type "printf '%s|%s|%s' \"\$\$\" \"\$mouse_state\" \"\$PWD\" > '$root/state-$number'\r"
    eventually [list state_is "$root/state-$number" "$float_pid|kept_unexported|$env(HOME)/retained"] \
        "same shell PID, unexported variable and cwd (sample $number)"
    check {[tm display-message -p -t $float {#{pane_pid}|#{pane_dead}}] eq "$float_pid|0"} "retained shell replaced or dead"
}
proc bindings {{restore_table ""} {restore_key ""}} {
    global user_tables instance
    set result {}
    foreach table $user_tables {
        foreach line [split [tm list-keys -T $table] \n] {
            check {[regexp {^bind-key +(.*?) *-T +([^ ]+) +([^ ]+) +(.*)$} $line ignored flags actual key body]} "unparseable binding: $line"
            if {$actual eq $restore_table && $key eq $restore_key} {
                check {[regexp {^User[0-9]+$} $key] && [string first "AFT_RESTORE_INSTANCE=$instance" $body] >= 0} "excluded key is not the owned restore binding"
                continue
            }
            lappend result [list [string trim $flags] $actual $key $body]
        }
    }
    return [lsort $result]
}
proc unchanged {} {
    global baseline float
    set table [tm show-options -qv -t $float @aft_restore_table]
    set key [tm show-options -qv -t $float @aft_restore_key]
    check {$table ne "" && $key ne ""} "missing owned restore-key identity"
    check {[bindings $table $key] eq $baseline} "existing user bindings were wrapped or changed"
}
proc no_handoff {input label} {
    global outer observer
    set changes [tm show-options -gqv @session_changes]
    set custom [tm show-options -gqv @custom_clicks]
    type $input
    settle
    check {[client_session $outer] eq "alpha" && [attached] == 1} "$label dismissed popup or switched origin"
    check {[client_session $observer] eq "alpha"} "$label switched observer"
    check {[tm show-options -gqv @session_changes] eq $changes && [tm show-options -gqv @custom_clicks] eq $custom} "$label invoked a main mouse binding"
    puts "PASS negative: $label"
}
proc switched_once {x y} {
    global outer observer mode
    set count [expr {[tm show-options -gqv @session_changes] + 1}]
    click $x $y
    eventually {expr {[client_session $outer] eq "beta" && [attached] == 0 && [tm show-options -gqv @session_changes] == $count}} \
        "status click switches only origin once and dismisses popup"
    settle
    check {[client_session $outer] eq "beta" && [tm show-options -gqv @session_changes] == $count} "duplicate press/release transition"
    check {[client_session $observer] eq "alpha" && [attached] == 0} "observer switched or stole the retained popup"
    check {[tm show-options -gqv @aft_last_error] eq ""} "$mode mouse close recorded a popup error"
}
proc teardown {} {
    global root socket tmux viewers watchdog
    if {$watchdog ne ""} {
        catch {exec kill -TERM [exp_pid -i $watchdog]}
        catch {close -i $watchdog}
        catch {wait -i $watchdog}
        set watchdog ""
    }
    if {$socket ne "" && [file exists $socket]} { catch {exec $tmux -S $socket -f /dev/null kill-server} }
    foreach viewer $viewers { catch {close -i $viewer}; catch {wait -i $viewer} }
    if {$root ne ""} { file delete -force -- $root }
}
trap {puts stderr "FAIL mouse smoke: interrupted or overall 60s deadline"; teardown; exit 1} {SIGINT SIGTERM SIGHUP SIGUSR1}

set failed [catch {
    set root [file normalize [exec mktemp -d /tmp/aft-mouse.XXXXXXXX]]
    file attributes $root -permissions 0700
    set inherited_path $env(PATH)
    foreach name [array names env] { unset env($name) }
    set env(PATH) $inherited_path
    foreach {name directory} {HOME home XDG_CONFIG_HOME config XDG_DATA_HOME data XDG_STATE_HOME state XDG_RUNTIME_DIR runtime XDG_CACHE_HOME cache TMUX_TMPDIR sockets TMPDIR tmp} {
        set env($name) [file join $root $directory]
        file mkdir $env($name)
        file attributes $env($name) -permissions 0700
    }
    set env(SHELL) $shell
    set env(TERM) xterm-256color
    set env(LC_ALL) C
    set env(PS1) {MOUSE> }
    # Independent wall-clock bound also interrupts a blocked exec, not just polls.
    set seconds [expr {max(1, ($deadline - [clock milliseconds]) / 1000)}]
    spawn -noecho $shell --noprofile --norc -c {sleep "$2" & timer=$!; trap 'kill "$timer" 2>/dev/null; exit' TERM INT HUP; wait "$timer"; kill -USR1 "$1"} watchdog [pid] $seconds
    set watchdog $spawn_id
    file mkdir [file join $root bin] [file join $env(HOME) retained]
    set socket [file join $env(TMUX_TMPDIR) server]
    set binary [file join $root bin agent-float-term]
    file copy $source_binary $binary
    file attributes $binary -permissions 0700
    set claude [file join $root bin claude]
    exec cc -std=c99 -Wall -Wextra -Werror $fixture -o $claude
    # Pin the same actual tmux executable for the application and nested viewer.
    file link -symbolic [file join $root bin tmux] $tmux
    set env(PATH) "[file join $root bin]:$inherited_path"
    set config_dir [file join $env(XDG_CONFIG_HOME) agent-float-term]
    file mkdir $config_dir
    set out [open [file join $config_dir config.json] {WRONLY CREAT EXCL} 0600]
    puts $out [format {{"shortcut":"F7","width":60,"height":60,"shell":"%s","harness_paths":[{"harness":"claude","path":"%s"}]}} \
        [string map {\\ \\\\ \" \\\"} $shell] [string map {\\ \\\\ \" \\\"} $claude]]
    close $out
    foreach session {alpha beta} {
        tm new-session -d -s $session -x 120 -y 40 -c $env(HOME) $shell --noprofile --norc -i
    }
    set capability [tm display-message -p {#{aft_popup_status_mouse}}]
    check {($mode eq "patched" && $capability eq "1") || ($mode eq "stock" && $capability eq "")} \
        "explicit $mode mode disagrees with running server capability '$capability'"
    puts "Mouse smoke: mode=$mode; [exec $tmux -V]; executable=$tmux; private socket=$socket"
    tm set-option -g default-shell $shell
    tm set-option -g mouse on
    tm set-option -g status-position top
    tm set-option -g status-interval 1
    tm set-option -g escape-time 100
    set alpha [tm display-message -p -t alpha: {#{pane_id}}]
    set beta_session [tm display-message -p -t beta: {#{session_id}}]
    set main_pids [tm list-panes -a -F {#{pane_pid}}]
    tm set-option -g {status-format[0]} "#\[range=session|$beta_session\] BETA #\[norange\]"
    set default_mouse [status_binding]
    check {[string first {switch-client -t =} $default_mouse] >= 0} "default session-range binding missing: $default_mouse"
    tm set-option -g @custom_clicks 0
    tm bind-key -T root MouseDown1StatusLeft {set-option -gF @custom_clicks '#{e|+:#{@custom_clicks},1}' ; set-option -gF @mouse_record '#{client_name}|#{mouse_status_range}|#{mouse_status_line}' ; switch-client -t beta}
    tm bind-key -T prefix t switch-client -T fixture-modal
    tm bind-key -T fixture-modal x display-message FIXTURE_UNWRAPPED
    tm bind-key -T root User17 display-message FIXTURE_USER_KEY
    tm bind-key -T root F10 display-popup -E -w 60 -h 16 -x C -y C $claude
    set user_tables {}
    foreach line [split [tm list-keys] \n] {
        if {[regexp { -T +([^ ]+) } $line ignored table]} { lappend user_tables $table }
    }
    set user_tables [lsort -unique $user_tables]
    set before_bind [bindings]
    exec $binary bind --socket $socket
    set baseline [bindings]
    set without_f7 {}
    foreach entry $baseline {
        if {[lrange $entry 1 2] ne {root F7}} { lappend without_f7 $entry }
    }
    check {$without_f7 eq $before_bind} "bind changed a user binding other than installing F7"
    set doctor [exec $binary doctor --socket $socket]
    set expected [expr {$mode eq "patched" ? "supported" : "unavailable"}]
    check {[regexp -line "^Status-bar mouse: ${expected}(?:;| |$)" $doctor]} "doctor did not report $expected: $doctor"

    spawn -noecho $tmux -S $socket -f /dev/null attach-session -t alpha
    set client $spawn_id
    lappend viewers $client
    stty rows 40 columns 120 < $spawn_out(slave,name)
    eventually {expr {[tm list-clients -F {#{session_name}}] eq "alpha"}} "origin PTY attached"
    set outer [tm list-clients -F {#{client_name}}]
    spawn -noecho $tmux -S $socket -f /dev/null attach-session -t alpha
    lappend viewers $spawn_id
    stty rows 40 columns 120 < $spawn_out(slave,name)
    eventually {expr {[llength [split [tm list-clients -F {#{client_name}}] \n]] == 2}} "observer PTY attached"
    foreach name [split [tm list-clients -F {#{client_name}}] \n] { if {$name ne $outer} { set observer $name } }
    eventually {expr {[tm display-message -p -t $alpha {#{pane_width}|#{pane_height}}] eq "120|39"}} "120x40 terminal with exposed top status"
    tm set-option -g @session_changes 0
    tm set-hook -g client-session-changed "if-shell -F '#{==:#{hook_client},$outer}' { set-option -gF @session_changes '#{e|+:#{@session_changes},1}' }"

    # A foreign popup without -M must stay modal even on the patched server.
    set output ""
    type "\033\[21~"
    eventually {expr {[string first "AFT_READY claude" $output] >= 0}} "raw non-M popup ready"
    click 3 1
    settle
    check {[client_session $outer] eq "alpha" && [tm show-options -gqv @session_changes] eq "0"} "raw popup without -M handed off a status click"
    type q
    settle
    click 3 1
    eventually {expr {[client_session $outer] eq "beta" && [tm show-options -gqv @session_changes] eq "1"}} "raw popup exited and native click works"
    check {[client_session $observer] eq "alpha"} "raw-popup control switched observer"
    tm switch-client -c $outer -t alpha
    settle
    check {[bindings] eq $baseline} "raw popup changed user bindings"
    puts "PASS raw popup without -M blocks status click ($mode)"

    type "claude\r"
    eventually {contains $alpha "AFT_READY claude"} "synthetic AI foreground job ready"
    f7
    eventually {expr {[float_for $alpha] ne ""}} "F7 creates application float"
    set float [float_for $alpha]
    set float_pid [tm display-message -p -t $float {#{pane_pid}}]
    set instance [tm show-options -qv -t $float @aft_instance]
    eventually {expr {[attached] == 1}} "F7 popup viewer attached (stock must not receive unsupported -M)"
    shell_state 1 1
    unchanged
    check {[tm show-options -qv -t $float @aft_origin_name] eq $outer} "popup assigned to observer instead of origin"
    puts "PASS F7 opens app popup; shell state captured; existing user tables unchanged"

    no_handoff "\033\[<0;2;4M\033\[<0;2;4m" "outside popup, non-status pane click"
    no_handoff "\033\[<0;60;20M\033\[<0;60;20m" "inside popup click"
    no_handoff "\033\[<64;3;1M\033\[<65;3;1M" "status wheel up/down"
    no_handoff "\033\[<32;3;1M\033\[<0;3;1m" "status drag and release"
    no_handoff "\033\[<0;3;1m" "status release only"
    tm set-option -g mouse off
    settle
    no_handoff "\033\[<0;3;1M\033\[<0;3;1m" "mouse off"
    tm set-option -g mouse on
    tm set-option -g status off
    settle
    no_handoff "\033\[<0;3;1M\033\[<0;3;1m" "status off"
    tm set-option -g status on
    # Discard any literal escape bytes delivered while mouse reporting was off.
    type "\025"
    settle
    if {$mode eq "patched"} {
        switched_once 3 1
        check {[status_binding] eq $default_mouse} "default top status binding changed"
        unchanged
        tm switch-client -c $outer -t alpha
        eventually {expr {[attached] == 1}} "return to origin automatically restores float"
        check {[float_for $alpha] eq $float} "automatic return replaced float"
        shell_state 2
        puts "PASS top default session-range click switches origin once; observer unchanged; same float auto-restores"
    } else {
        no_handoff "\033\[<0;3;1M\033\[<0;3;1m" "stock app popup status click"
        f7
        eventually {expr {[attached] == 0}} "stock F7 hides popup"
        switched_once 3 1
        tm switch-client -c $outer -t alpha
        settle 1500
        check {[attached] == 0} "stock explicit hide failed to suppress automatic return"
        f7
        eventually {expr {[attached] == 1}} "stock manual reopen"
        shell_state 2
        puts "PASS stock fallback opens without -M, blocks click until F7 hide, retains shell"
    }

    tm set-option -g status-position bottom
    tm set-option -g status 2
    tm set-option -g {status-format[0]} "#\[range=session|$beta_session\] BETA #\[norange\]"
    tm set-option -g {status-format[1]} {#[range=left] CUSTOM #[norange]}
    settle
    if {$mode eq "stock"} {
        no_handoff "\033\[<0;3;40M\033\[<0;3;40m" "stock bottom multiline custom range"
        f7
        eventually {expr {[attached] == 0}} "hide stock popup for custom binding control"
    }
    switched_once 3 40
    check {[tm show-options -gqv @custom_clicks] eq "1"} "custom binding did not execute exactly once"
    check {[tm show-options -gqv @mouse_record] eq "$outer|left|1"} "custom mouse client/range/status_line lost: [tm show-options -gqv @mouse_record]"
    unchanged
    tm switch-client -c $outer -t alpha
    if {$mode eq "stock"} {
        settle 1500
        check {[attached] == 0} "stock custom control restored explicitly hidden float"
        f7
    }
    eventually {expr {[attached] == 1}} "restore after bottom multiline click"
    check {[float_for $alpha] eq $float} "bottom click replaced retained float"
    shell_state 3
    puts "PASS bottom multiline custom range: client=$outer range=left status_line=1 invocations=1"

    f7
    eventually {expr {[attached] == 0}} "explicit F7 hide"
    tm switch-client -c $outer -t beta
    tm switch-client -c $outer -t alpha
    settle 1500
    check {[attached] == 0} "explicit F7 hide did not suppress automatic return"
    unchanged
    check {[tm show-options -gqv @aft_last_error] eq ""} "application recorded popup failure"
    type q
    eventually {expr {[float_for $alpha] eq ""}} "AI exit removes retained shell"
    eventually {expr {[catch {exec kill -0 $float_pid 2>@1}]}} "retained shell process exits"
    check {[tm list-panes -a -F {#{pane_pid}}] eq $main_pids} "AI cleanup replaced main shells or leaked a pane"
    check {[client_session $observer] eq "alpha"} "observer moved during lifecycle checks"
    # The watcher removes its restore key after terminating the retained shell.
    eventually {expr {[bindings] eq $baseline}} "AI cleanup restores original user tables"
    puts "PASS explicit hide suppresses return; AI exit cleans shell and owned restore key; no popup error"
} message options]
if {$failed && $socket ne "" && [file exists $socket]} {
    catch {puts stderr "Clients: [tm list-clients -F {#{client_name}|#{session_name}|#{pane_id}}]"}
    catch {puts stderr "Popup error: [tm show-options -gqv @aft_last_error]"}
}
teardown
check {$root eq "" || ![file exists $root]} "private directory survived cleanup"
if {$failed} {
    puts stderr "FAIL $mode mouse smoke: $message"
    puts stderr [dict get $options -errorinfo]
    exit 1
}
puts "PASS $mode mouse smoke (private server and directory removed)"