* fix(cli): stamp panes with the workspace that holds them, not the client's name
A pane's owner names the workspace allowed to attach to it. The CLI wrote
a literal "tty7-cli" there for every pane it made, so a window opening on
a CLI-built workspace found none of them attachable: it spawned a fresh
shell for each tab, orphaned the live ones, and — because the tree still
carried each pane's agent session — greeted the user with a failing
`claude --resume <id>` in every one of them.
Both spawn paths now pass the workspace id, and restore treats an owner
that parses as no workspace as no claim at all, so panes already stamped
by an older CLI attach instead of stranding.
* fix(cli): let the OWNER column speak only when it disagrees with WS
Now that a pane's owner is the id of the workspace holding it, printing
both spells the same id twice on every row of `pane ls --all` — and
buries the rows that matter. The column now shows a dash when the two
agree, so what is left is exactly what is worth reading: a pane its
holder may not attach to, and an orphan still naming where it belongs.
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
capture_plain_returns_text_not_escapes gated its byte-level asserts on
the marker reaching the rendered capture, then asserted the raw capture
already carried a CR. The two captures are separate snapshots taken in
sequence, and on Windows ConPTY re-emits the echoed command in
escape-laden bursts: the marker can render (from the typed input line)
while the slightly earlier raw snapshot has yet to see a single CR —
Enter's CRLF only arrives with the command's execution. CI hit exactly
that window on x86_64-pc-windows-msvc.
Make the CR part of the settle condition the loop polls for, alongside
the marker, and name both in the timeout message so a genuine
CR-stripping regression still reads as one.
Co-authored-by: l0ng-ai <ysdpk123@gmail.com>
The CLI's own --help calls it "built for coding agents", but `capture` handed
back the daemon's raw PTY bytes, which is the least readable thing it emits,
and every verb panicked when its reader hung up.
`capture --plain` replays those bytes through a terminal grid instead of
stripping escapes from them, using the same alacritty_terminal rev the GUI
renders panes with. The difference is not cosmetic: only the grid knows that a
break at the pane's width was a wrap rather than a newline, that a CR meant
"overwrite this line" rather than "end it", and which cell a wide char shares
with its spacer. A regex gets the easy 90% and then invents the rest — on one
real pane it turned 1193 lines into 2806.
The size each segment needs comes for free: the daemon already sends
DaemonMsg::Size right before every Snapshot, and the CLI was discarding it.
Panes here measure 249 and 86 columns, so the hardcoded 120 would have wrapped
both in the wrong places. Observing still resizes nothing.
The pipe fix is two mechanisms with one contract. On Unix SIGPIPE goes back to
its default disposition, which covers every write site at once and ends the
process the way it ends `cat` (141). Windows has no such signal, so stdio::out
recognizes the hung-up write and leaves quietly. Before this, 16 of 19 verbs
printed a panic and a backtrace note for `tty7 ls | head -1`; `run` instead
reported it as a failure with exit 1.
Also adds skills/tty7, the Claude skill for driving this CLI. It shipped with a
Python ANSI stripper, which is what prompted --plain; the script is gone.
alacritty_terminal moves to [workspace.dependencies] so the GUI and the CLI
cannot drift onto two revs of the fork.
Manual testing found `tty7 run`, `send`, `capture`, `procs` and `split` broken
against any normally-installed server — the CLI's entire hot path. Only the
control verbs worked.
Two endpoints, two rules. The pane socket came from the config dir; the control
socket ignored it and sat in $XDG_RUNTIME_DIR/tty7 or ~/.local/share/tty7 —
under the same basename, `daemon.sock`. So they were told apart by directory
alone, and the CLI, handed one path in TTY7_SOCKET, reconstructed the other with
with_file_name: on the default layout that returns the input unchanged. Pane
verbs dialed the control socket and the daemon hung up on them. A --config-dir
server was worse: it published the *default* control socket to the shells it
spawned, so a CLI inside an isolated instance drove a different server.
The e2e suite passed throughout because its harness set TTY7_CONTROL_SOCK
explicitly, placing both endpoints in one directory under different names — a
layout production never produces. It had removed the bug's precondition.
Now: the control socket is derived from the config dir like the pane socket
(control.sock beside daemon.sock, mirroring Windows' control.port/daemon.port,
with -control on the hashed fallback so the two cannot collide), and panes are
handed TTY7_CONFIG_DIR instead of a socket path. A CLI inherits it, so
ControlClient::connect and PaneClient::local resolve the same two sockets the
server opened, through the same functions. No second derivation to disagree.
remote_link's remote_control_socket was a third copy of the old rule, used to
locate a remote server's endpoint before connecting; it follows the config dir
too, and the env probe now reads $TTY7_CONFIG_DIR.
Drops the CLI's server-lifecycle guard: stop/start already follow the config dir
through transport::connect and --config-dir, so there is no longer a mismatch to
refuse. The e2e case that covered only `status` over a lone variable now also
runs a pane verb — the asymmetry it missed is exactly what broke.
Note: this moves the control socket for existing installs. A running pre-change
daemon will not be found at the new path, which is the honest outcome — its
control dialect is v3 against this build's v4, so reaching it only produced a
version error anyway.
An interrupted `tty7 run` leaves its pane running with nothing referencing it:
no workspace holds it, every listing walks the tree, and the orphan sweep only
logs. `pane ls --all` reads the server's registry instead and marks what nothing
holds; `pane close` falls back to hanging a pane up directly when no workspace
can route a PaneClose, so an orphan is stoppable.
--quiet silenced failures as well as successes, leaving a bare exit code and
nothing to debug; it now suppresses only output on success, and covers --json
too. `run` exited through a path that skipped the report entirely, so
`run --json` printed nothing at all; it now carries its report, with
exit_code_known distinguishing the command's own 1 from the stand-in 1.
The server lifecycle verbs can only drive the default endpoint — spawn::stop
dials transport::connect() — while every other verb follows $TTY7_SOCKET. They
now refuse when that names a different endpoint rather than acting on a
different server than `tty7 status` reports on.
Also: tables pad by display width, so a CJK name or path no longer skews every
column after it; --h/--v become --horizontal/--vertical with the short forms
kept as aliases; the verbs that are not implemented say so in --help instead of
only at runtime; capture's help admits it decodes as lossy UTF-8.
- workspace: tty7-cli joins default-members, so a bare root cargo test runs it
- run --keep files the pane into its workspace via TabCreate (and refuses to
keep a pane no workspace would list); --ws help says what it really does
- server start|stop|restart|logs refuse -m instead of silently acting locally
- server start kills the spawned process when it never opens its endpoints
- -m over a down link is refused instead of redialing with auto auth
- capture help tells the truth: raw ANSI bytes, last ring segment by default
- a missed exit-code probe exits 1 with a stderr note, not a fabricated code
- TTY7_SOCKET is honored: control dials it, the pane endpoint is its sibling
- attach's success JSON says attached, not detached_from
- e2e daemons ride a KILL_ON_JOB_CLOSE Job Object on Windows, so a hard-killed
harness cannot leak servers
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014JPaaZVK7rfQPKyrymzsYv
StubBackend is gone; RealBackend lazily opens a ControlClient for control
ops and a PaneClient for pane ops. Backend::spawn_shell now returns the
daemon-assigned pane id, so every creating verb (new, tab new, split)
spawns first and seeds the tree op with the real id — client-side pane-id
allocation is deleted. Live end to end: ls, ws/tab/pane verbs, new, send
(attach-input-detach), capture (observer replay; --scrollback = whole
ring, default = the ring's last segment), procs, run (streams output,
passes the child's exit code through, --keep leaves the pane), events
(human lines or NDJSON), agents, status, machine ls, and doctor's server
half (reachability, dialect, status, links). tty7 server start|stop|
restart|logs manage a sibling/PATH/TTY7_SERVER_EXE tty7-server. -m routes
both channels over the local server's link, resolved against Routes by
key or bare host; jump/proxy-chained keys are refused with the reason.
Interactive attach stays stubbed for the next slice. A harness-free e2e
suite drives the compiled tty7.exe against an isolated real server.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014JPaaZVK7rfQPKyrymzsYv