agent-float-term 0.2.2

Harness Floating Terminal: a persistent F7 shell for unwrapped AI CLIs
Documentation
#!/usr/bin/env expect
# expect tests/theme_smoke.exp /path/to/agent-float-term
# Select a real tmux (>= 3.4) with PATH. PTY color semantics, NOT OS opacity.
set timeout 15
log_user 0
if {$argc != 1} { puts stderr "usage: expect tests/theme_smoke.exp /path/to/agent-float-term"; exit 1 }
set source_binary [file normalize [lindex $argv 0]]
set fixture [file join [file dirname [file normalize [info script]]] fixtures terminal.c]
foreach tool {tmux cc bash} {
    if {[auto_execok $tool] eq ""} { puts stderr "theme smoke requires $tool"; exit 1 }
}
set tmux [lindex [auto_execok tmux] 0]
set shell [lindex [auto_execok bash] 0]
set version [exec $tmux -V]
set root ""
set socket ""
set client ""
set stream ""
set checkpoint 0
proc check {condition message} {
    if {![uplevel 1 [list expr $condition]]} { error $message }
}
proc tm {args} { global tmux socket; return [exec $tmux -S $socket {*}$args] }
proc type {text} { global client; send -i $client -- $text }
proc f7 {} { type "\033\[18~" }
proc drain {} {
    global client stream
    expect -i $client -timeout 0 -re {.+} { append stream $expect_out(0,string) } timeout {} eof { error "tmux client exited" }
}
proc eventually {script message} {
    set deadline [expr {[clock milliseconds] + 15000}]
    while {[clock milliseconds] < $deadline} {
        drain
        if {[uplevel 1 $script]} { return }
        after 50
    }
    error "timed out: $message"
}
proc attached {count} { return [expr {[llength [split [tm list-clients -F {#{client_pid}}]]] == $count}] }
proc contains {pane text} { return [expr {[string first $text [tm capture-pane -p -t $pane]] >= 0}] }
proc styles {scope target} {
    set result {}
    foreach option {window-style window-active-style} { lappend result [tm show-options $scope -t $target $option] }
    return $result
}
proc globals {} {
    set result {}
    foreach option {popup-style popup-border-style window-style window-active-style} {
        lappend result [tm show-options -g $option]
    }
    return $result
}
# Only decode SGR and printable output, not a framebuffer. Ignore OSC, charset,
# and cursor controls; retain a color pair for EVERY character in each marker.
proc colors {marker foreground background {latest 0}} {
    global stream checkpoint
    set fg default; set bg default; set saved [list default default]; set text ""; set pairs {}; set seen 0
    set position 0
    set last_pairs {}
    foreach token [regexp -all -inline {\x1b\[[0-?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[()][0-9A-Za-z]|\x1b.|[^\x1b]} $stream] {
        incr position [string length $token]
        if {[regexp {^\x1b\[([0-9;:]*)m$} $token ignored parameters]} {
            set codes [split [string map {: ;} $parameters] {;}]
            # An empty SGR parameter list is reset (CSI m), not a no-op.
            if {$parameters eq ""} { set codes [list 0] }
            for {set i 0} {$i < [llength $codes]} {incr i} {
                set code [lindex $codes $i]
                if {$code eq "" || $code == 0} { set fg default; set bg default
                } elseif {$code == 39} { set fg default
                } elseif {$code == 49} { set bg default
                } elseif {$code == 38 || $code == 48} {
                    set channel [expr {$code == 38 ? "fg" : "bg"}]
                    set mode [lindex $codes [incr i]]
                    if {$mode == 2} {
                        if {[lindex $codes [expr {$i + 1}]] eq ""} { incr i }
                        set rgb [lrange $codes [expr {$i + 1}] [expr {$i + 3}]]
                        set $channel "rgb:[join $rgb ,]"; incr i 3
                    } elseif {$mode == 5} { set $channel "index:[lindex $codes [incr i]]"
                    } else { error "unsupported SGR color: $token" }
                } elseif {($code >= 30 && $code <= 37) || ($code >= 90 && $code <= 97)} { set fg "ansi:$code"
                } elseif {($code >= 40 && $code <= 47) || ($code >= 100 && $code <= 107)} { set bg "ansi:$code" }
            }
        } elseif {$token eq "\0337"} {
            set saved [list $fg $bg]
        } elseif {$token eq "\0338"} {
            lassign $saved fg bg
        } elseif {[string index $token 0] ne "\033"} {
            append text $token
            lappend pairs [list $fg $bg]
            set text [string range $text end-[expr {[string length $marker] - 1}] end]
            set pairs [lrange $pairs end-[expr {[string length $marker] - 1}] end]
            if {$text eq $marker && $position > $checkpoint} {
                set last_pairs $pairs
                foreach pair [expr {$latest ? {} : $pairs}] {
                    if {$pair ne [list $foreground $background]} {
                        set context [string map [list "\033" {<ESC>} "\r" {<CR>} "\n" {<LF>}] [string range $stream [expr {max(0, $position - 300)}] $position]]
                        error "$marker: expected $foreground/$background, got $pair; controls: $context"
                    }
                }
                incr seen
            }
        }
    }
    # Borders reuse the same glyphs across raw and owned popups. Assert the last
    # completed border draw, not a delayed earlier popup redraw in the PTY queue.
    if {$latest} {
        foreach pair $last_pairs { check {$pair eq [list $foreground $background]} "$marker: latest border expected $foreground/$background, got $pair" }
    }
    return [expr {$seen > 0}]
}
# Decoder regressions: preserve rendition across checkpoints, empty SGR resets,
# and DEC cursor save/restore (which also saves/restores rendition).
set stream "\033\[38;2;170;51;102;48;2;51;68;85mOLD"
set checkpoint [string length $stream]
append stream "\033\[mAFT_EMPTY_RESET"
check {[colors AFT_EMPTY_RESET default default]} "empty SGR reset decoder"
set stream "\033\[31m\0337\033\[32mGREEN\0338AFT_SAVED_RED"
set checkpoint 0
check {[colors AFT_SAVED_RED ansi:31 default]} "DEC rendition restore decoder"
set stream ""; set checkpoint 0
proc teardown {} {
    global root socket client tmux
    if {$socket ne "" && [file exists $socket]} { catch {exec $tmux -S $socket kill-server} }
    if {$client ne ""} { catch {close -i $client}; catch {wait -i $client} }
    if {$root ne ""} { file delete -force -- $root }
}
trap {teardown; exit 1} {SIGINT SIGTERM SIGHUP}
set failed [catch {
    set root [file normalize [exec mktemp -d /tmp/aft-theme.XXXXXXXX]]
    set inherited_path $env(PATH)
    foreach name [array names env] { unset env($name) }
    set env(PATH) "$root/bin:$inherited_path"
    foreach {name directory} {HOME home XDG_CONFIG_HOME config XDG_DATA_HOME data XDG_STATE_HOME state XDG_RUNTIME_DIR runtime TMUX_TMPDIR sockets} {
        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(PS1) {AFT_PROMPT> }
    file mkdir $root/bin $env(XDG_CONFIG_HOME)/agent-float-term
    set binary $root/bin/agent-float-term
    file copy $source_binary $binary
    file attributes $binary -permissions 0700
    set claude $root/bin/claude
    exec cc -std=c99 -Wall -Wextra -Werror $fixture -o $claude
    set out [open $env(XDG_CONFIG_HOME)/agent-float-term/config.json {WRONLY CREAT EXCL} 0600]
    puts $out [format {{"shell":"%s","harness_paths":[{"harness":"claude","path":"%s"}]}} \
        [string map {\\ \\\\ \" \\\" \n \\n \r \\r \t \\t} $shell] \
        [string map {\\ \\\\ \" \\\" \n \\n \r \\r \t \\t} $claude]]
    close $out
    set socket $env(TMUX_TMPDIR)/server
    tm -f /dev/null new-session -d -s outer -c $env(HOME) $shell --noprofile --norc -i
    tm set-option -g default-shell $shell
    tm set-option -g status off
    tm set-option -g escape-time 100
    tm set-option -g popup-border-lines simple
    tm set-option -g popup-style fg=#aabbcc,bg=#112233
    tm set-option -g popup-border-style fg=#aa3366,bg=#334455
    set sentinel bold,underscore,fg=#aabbcc,bg=#445566
    foreach option {window-style window-active-style} { tm set-option -g $option $sentinel }
    set outer [tm display-message -p -t outer: {#{pane_id}}]
    set parent [tm display-message -p -t $outer {#{pane_id}|#{pane_pid}|#{window_id}}]
    foreach option {window-style window-active-style} {
        tm set-option -w -t $outer $option $sentinel
        # Visible control stays terminal-default without changing window/global sentinels.
        tm set-option -p -t $outer $option none,fg=terminal,bg=terminal
    }
    set before [globals]
    set outer_window [styles -w $outer]; set outer_pane [styles -p $outer]
    exec $binary bind --socket $socket
    spawn -noecho $tmux -T RGB -S $socket attach-session -t outer
    set client $spawn_id
    match_max -i $client 1048576
    stty rows 40 columns 120 < $spawn_out(slave,name)
    eventually {attached 1} "outer client"
    type "printf '\\033\[0mAFT_OUTER_%s\\n' DEFAULT; claude\r"
    eventually {contains $outer "AFT_READY claude"} "no-argument compiled claude fixture"
    eventually {colors AFT_OUTER_DEFAULT default default} "outer visible control"
    # A raw popup without -s/-S must expose the inherited RGB sentinels.
    # Keep earlier SGR state: discarding a stream chunk can split a control
    # sequence or lose an inherited color before the next redraw.
    set checkpoint [string length $stream]
    set raw_pid [exec $tmux -S $socket display-popup -E -t $outer -w 80% -h 80% "printf '\\033\[0mAFT_RAW_%s\\n' DEFAULT; exec '$claude'" &]
    eventually {colors AFT_RAW_DEFAULT rgb:170,187,204 rgb:17,34,51} "raw popup negative control"
    check {[colors ------ rgb:170,51,102 rgb:51,68,85 1]} "raw border sentinel not observed"
    type q
    eventually {expr {[catch {exec kill -0 $raw_pid 2>@1}]}} "raw popup closed"
    drain
    set checkpoint [string length $stream]
    foreach opening {first reopen} {
        f7
        eventually {attached 2} "$opening popup"
        set float ""
        foreach line [split [tm list-panes -a -F {#{pane_id}|#{@aft_owner}}] \n] {
            lassign [split $line |] pane owner
            if {$owner eq $outer} { check {$float eq ""} "duplicate owned panes"; set float $pane }
        }
        check {$float ne ""} "no owned float"
        check {[llength [split [tm list-panes -a -F {#{pane_id}}]]] == 2} "extra nested pane"
        foreach scope {-w -p} {
            foreach option {window-style window-active-style} {
                set actual [tm show-options ${scope}v -t $float $option]
                check {$actual eq "fg=terminal,bg=terminal" || $actual eq "none,fg=terminal,bg=terminal"} "$opening $scope $option: $actual"
            }
        }
        type "printf '\\033\[0mAFT_${opening}_%s\\n\\033\[38;2;12;34;56;48;2;65;43;21mAFT_${opening}_%s\\033\[0m\\n' DEFAULT RGB\r"
        eventually [list colors AFT_${opening}_DEFAULT default default] "$opening default content"
        eventually [list colors AFT_${opening}_RGB rgb:12,34,56 rgb:65,43,21] "$opening explicit RGB content"
        check {[colors ------ default default 1]} "$opening terminal-default border not observed"
        check {[globals] eq $before} "$opening changed global styles"
        check {[styles -w $outer] eq $outer_window && [styles -p $outer] eq $outer_pane} "$opening changed outer overrides"
        check {[tm display-message -p -t $outer {#{pane_id}|#{pane_pid}|#{window_id}}] eq $parent} "parent identity changed"
        set identity [tm display-message -p -t $float {#{pane_id}|#{pane_pid}}]
        if {$opening eq "reopen"} { check {$identity eq $retained} "reopen replaced the owned shell" }
        set retained $identity
        f7
        eventually {attached 1} "hide $opening"
        # Reopening must clear attributes and colors at BOTH owned scopes, not just creation.
        foreach scope {-w -p} {
            foreach option {window-style window-active-style} { tm set-option $scope -t $float $option $sentinel }
        }
        drain
        set checkpoint [string length $stream]
    }
    # A foreign link makes window-level writes unsafe: refuse before any style mutation.
    tm new-session -d -s foreign $shell --noprofile --norc -i
    set window [tm display-message -p -t $float {#{window_id}}]
    tm link-window -s $window -t foreign:
    set linked_window [styles -w $float]; set linked_pane [styles -p $float]
    tm set-option -g @aft_last_error ""
    f7
    eventually {expr {[tm show-options -gqv @aft_last_error] ne ""}} "linked-window refusal"
    check {[regexp -nocase {link|shar|foreign} [tm show-options -gqv @aft_last_error]]} "popup refused for a reason other than the foreign link"
    check {[attached 1]} "linked float opened instead of refusing"
    check {[styles -w $float] eq $linked_window && [styles -p $float] eq $linked_pane} "refusal changed foreign-linked styles"
    check {$window in [split [tm list-windows -t foreign -F {#{window_id}}]]} "refusal removed the foreign link"
    check {[globals] eq $before} "global styles changed after refusal"
    check {[styles -w $outer] eq $outer_window && [styles -p $outer] eq $outer_pane} "outer overrides changed after refusal"
    check {[tm display-message -p -t $outer {#{pane_id}|#{pane_pid}|#{window_id}}] eq $parent} "refusal changed parent identity"
} message options]
if {$failed} {
    puts stderr "Theme smoke FAILED ($version, $tmux): $message"
    if {[dict exists $options -errorinfo]} { puts stderr [dict get $options -errorinfo] }
}
teardown
if {$failed} { exit 1 }
puts "Theme smoke passed ($version, $tmux): raw RGB negative control, default content/border, explicit RGB, per-open owned styles, exact global/outer preservation, stable parent, linked-window refusal; PTY colors only"