A `machine.json` that does not parse is copied to `machine.json.corrupt`
and the machine comes up with no workspaces at all — every tab and every
pane layout on it. `MachineStore::open` calls that "recoverable by hand",
and it is, but only for a hand that knows where to look.
Nothing told it. The quarantine announces itself with a `log::warn!`, and
per `docs/reference/privacy.mdx` there is no log at all unless `TTY7_LOG`
or `RUST_LOG` is set — so on a default install the whole thing is silent.
What the user sees is `tty7 ws ls` saying "no workspaces — `tty7 new
<path>` starts one", which reads as an empty machine rather than as a
lost one, and gives no reason to look in the config directory.
`doctor` already makes this argument for `config.json`: a file that does
not parse is exactly the state someone runs `doctor` in, and none of it
is visible from the rows around it. The tree is the same case with more
at stake — settings are still on screen when `config.json` is quarantined;
workspaces are not.
The row only appears when a copy is really there. One that said "no tree
was set aside" beside every intact machine would be noise on every
install, and this has to read as news.
Verified end to end against a running server rather than only in a test:
corrupt the tree, restart, `doctor` names the copy; restore it, the
workspaces come back and the row goes away.
* fix(windows,scm,daemon): quote paths per shell, wire Checkout to, bound Spawn
Five fixes from a whole-codebase audit, in one sweep because they share
the paths they touch.
Path quoting had two implementations. file_tree::shell_quote_for wrapped
the path in quotes and picked the right ones per shell (#593);
view::shell_escape_path escaped with backslashes, which is POSIX-only
and collides head-on with the Windows path separator, so a dropped file,
a pasted path, a staged image path and an accepted completion candidate
all lost their separators there. completion::complete_path stripped the
same backslashes back off before looking a path up, so inline path
completion could never resolve a directory on Windows either. Both now
go through one core::shell_quote module, and shell_word_start tracks
quoting across the word so a second Tab still finds the word it just
inserted.
"Checkout to..." was registered, listed in the palette, bindable, and
handled by an empty match arm — invoking it did nothing at all. It now
opens an inline input row in the SCM panel, the twin of the existing
"create branch" one.
RemoteTerminal's Spawn read the daemon's reply with no deadline, while
Attach in the same file and PaneSession::spawn_over in core both bound
theirs. A daemon caught mid-restart accepts the connection and never
serves it, and the local route spawns synchronously on the UI thread, so
the silence froze the window on "new tab".
Two Windows papercuts: client_hostname spawned a console program from a
GUI process (a visible console flash) where COMPUTERNAME already has the
answer, and completion generators were a silent no-op with no way to
tell "produced nothing" from "never ran".
Three duplicated implementations merged: proc_name existed twice in the
daemon with a different fallback in each, the GUI's control link was the
one client socket that skipped transport::tune, and fps.rs and perf.rs
were the same windowed meter copied twice.
* refactor(completion): stop declaring spec fields nothing reads
The Fig spec structs mirrored seven keys the completer never looks at,
each held up by its own #[allow(dead_code)]. Serde ignores unknown
fields by default, so dropping the declarations parses the same specs
and drops the attributes with them.
* refactor(daemon): delete the loopback-forward management pipeline
Two protocol messages, their kind codes, encode and decode arms, two
daemon dispatch arms, two wire structs and two GUI client wrappers all
existed to reach SshManager::list_loopback_forwards and
close_loopback_forward, which were hardcoded to Vec::new() and false.
Nothing called the client wrappers either.
The kind codes are left as holes rather than renumbered, the way 13
already is, so the wire format is unchanged for every other message.
known-hosts management looks like the same shape but is not: its backend
parses the real file, fingerprints keys and rewrites through a 0600 temp
file. That one keeps its client half and gains a comment saying it is an
interface waiting for a screen.
* test(ssh): cover the host-key policy table and both proxy handshakes
The host-key decision is lifted out of check_server_key into
host_key_action, so what to do about Known/Unknown/Changed/
ChangedAlgorithm/Revoked can be read and tested without a server, a
broker or a known_hosts file. Eight tests pin it, including the two
subtleties the comments already claimed: verify_host_keys=false still
rejects a revoked key, and a new algorithm asks the unknown-host prompt
rather than a new variant older peers cannot decode.
socks5_connect and http_connect are split into connect + handshake, the
handshake generic over the stream, so nine tests drive them from an
in-memory duplex: length-prefix framing, the variable-length bound
address, auth refusal, reply codes, and the header terminator.
* test(cli,daemon): cover server binary resolution and the procargs parser
server_exe is split into environment lookup and resolve_server_exe, the
latter taking its three sources and an is_exe predicate so seven tests
can pin the precedence without touching the filesystem. Holding the
sibling to is_file rather than exists fixes a directory named
tty7-server shadowing the real binary on PATH.
parse_macos_procargs gets six tests over the KERN_PROCARGS2 layout:
exec-path skipping, however many bytes of alignment padding follow it,
argc bounding argv so the environment stays out, truncation, and a short
buffer.
* test(ui): cover the host-op pool decisions and the local reconnect schedule
The pool's retire condition moves into should_retire with the reason
named: a worker must not retire on the timeout alone, because submit
counted it as idle and so did not spawn a replacement for the job that
landed meanwhile.
LocalLink::tick's schedule moves into due(), taking the clock and the
link's state as arguments. The first attempt going out immediately, the
backoff only applying from the second, and a pending deadline not being
pushed further out by later ticks are now pinned. The identical
scheduler in remote_workspace had TestAppContext coverage; this one,
which every launch depends on, had none.
* fix(completion): unquote across the whole word, not just its first character
The round-trip test caught two things the first cut got wrong. A quote
can open partway into a word — quote_for_shell emits ~/'My Documents' so
the shell still expands the tilde — and a single-quoted body is literal
all through, so unescaping backslashes inside one took the separators
out of 'C:\Users\me'. Scanning with a quote state handles both, and
makes the '\'' seam fall out of the state changes rather than needing a
case of its own.
The GPUI test for accepting a candidate follows the insertion from
backslash escaping to quoting.
* fix(windows): unbreak the Windows build and quote for PowerShell's own dialect
`Instant` was moved behind `#[cfg(unix)]` while the generator cache still
uses it unconditionally, so the Windows target stopped compiling.
The quoting module treated every shell but cmd.exe as POSIX, including
PowerShell. PowerShell does not join a quoted string to the bare word beside
it, so the `'\''` seam is not a seam there — `C:\Users\O'Brien` came out as
three tokens, and the completion un-quoter turned the apostrophe back into a
backslash. Quoting is now a three-way dialect (cmd / PowerShell / POSIX)
chosen once and threaded through completion in place of the escapes flag.
* test(file-tree): name the shell where the quoting rule is the POSIX one
`shell_quote_for(_, None)` answers from the platform, so an assertion about
the `'\''` seam has to say which shell it means or it fails on Windows,
where the unnamed shell is PowerShell.
* fix(cli): address a tab by the bare id --json hands back
`parse_tab` required the `@` sigil, so the tab id from `tty7 tab new --json`
— the one id a caller is certain of — was the one shape the CLI refused.
`parse_pane` already made `%` optional for exactly this reason (#538); this
aligns tabs with it, keeping the digits-only guard so a leading `+` cannot
read as an ordinal now that the sigil is gone.
* docs(skill): hand a pane worker its interactive mode
The worked example passed the task with `-p`, which draws nothing: the pane
stays blank until the turn ends, `capture --plain` reads back empty, and the
user watching their tty7 window sees a worker that looks hung. Putting a
piped worker in a pane discards the only reason it is in one.
Also documents three things that cost real debugging time: a fresh pane can
swallow the Enter while its shell is still running startup files, `tty7 procs`
reports nothing running for a pane with a live agent in it, and the OSC 777
event stream in a raw `capture` is what actually answers "is it moving".
* feat(cli): restart the server in place by default, keeping sessions
`tty7 server restart` used to be stop + start, killing every pane, while
the GUI's Restart Server hands the daemon off to a new image via execve
and keeps everything running. Same verb, opposite side effects.
The CLI now probes the daemon for the handoff feature and asks it to
exec the tty7-server binary in place: same pid, same ptys, sessions
survive. Success is judged by the version endpoint answering with a new
per-process instance id, not by build strings, since the CLI and server
binaries can be on different versions.
A refused or stalled handoff leaves the daemon untouched and reports an
error suggesting `--hard` instead of silently killing sessions. The
stop + start path remains for `--hard` and for daemons that cannot
exec themselves (Windows, pre-handoff builds).
* fix(cli): leave a slow handoff's seat holder alive, and let a hard restart say sessions ended
After a taken handoff, the poll timing out does not mean the daemon died:
the singleton lock survives the exec, so a held seat is the new image
still coming up with every session aboard. Falling back to start() there
would grant it one second of grace and then reap it — bail with the seat
still held instead, and only start over a genuinely free seat.
The stop-and-start fallback (--hard, Windows, pre-handoff builds) now
reports that sessions ended instead of relaying start()'s plain report,
since the default restart's promise is sessions kept.
A daemon can survive quit-and-stop with its endpoint unlinked and its
pidfile gone while still holding the singleton seat (#667). Every later
launch then spawns a daemon that stands down against the lock and times
out red, and nothing on the machine can recover: stop answers "not
running", ensure_running reaps only through the pidfile, and flock
cannot say who the holder is.
Two roads led there, and both are closed:
- The reap identified a daemon by proc_pidpath alone, which fails
outright for a live process whose binary was deleted — every nightly
update replacing the installation. The identity check now falls back
to the kernel's comm name (proc_name on macOS, /proc/pid/comm on
Linux, both recorded at exec and immune to deletion), strips Linux's
" (deleted)" marker, and — decisively — no longer deletes the
pidfile of a live process it cannot identify: the record was the only
handle left on the survivor.
- When the pidfile is gone entirely, the pid the claimant now writes
into daemon.lock at claim time is the handle of last resort. The lock
file is never deleted and holding the flock is the definition of
being the server, so while the seat is held its content names the
holder; stop() and the reap fall back to it, and a confirmed reap
clears the record (only under a momentarily-free seat) so a stale
number cannot outlive its process. Unix-only: the Windows seat is
share_mode(0), unreadable while held.
Every road back now clears a stranded seat, not just the GUI's:
ensure_running's stale cleanup is factored into spawn::reap_stranded,
tty7 server start runs it too, and tty7 server stop no longer takes
"nobody answered" for "nothing to stop" when the seat is still held.
A short grace keeps the reap away from a daemon that is merely
mid-handoff or mid-startup — where health is an answered handshake,
never a bare connect: a wedged daemon's listener still completes
connections out of the kernel's backlog. The startup-timeout errors
name the seat-holding pid, with the kill advice identity-gated so a
stale record never tells anyone to kill an innocent process.
Two liveness corrections round it out: a zombie now reads as dead — it
answers kill(pid, 0) like the living but holds no lock and no image,
and no signal can end it, so counting it alive spent both reap timeouts
on a corpse (the GUI never waits on the daemons it spawns, so crashed
daemons are zombies as a rule) — and stop() only pays the
process-exit wait for a shutdown it actually delivered, instead of
watching an unreached survivor not move for five seconds.
The guard tests were each verified to fail against the behavior they
pin (fallbacks, the handshake criterion, the grace, and the wait gate
removed by mutation) before being trusted green; the zombie probe
semantics (proc_pidinfo failing for a zombie that still answers signal
0) were measured, not assumed.
The AGENT column came from a lowercased `{:?}`, which matched the agent's
slug for most variants by luck rather than by rule. Where it did not, the
table invented a third name for something that already had two: `OhMyPi`
printed as `ohmypi`, against a slug of `omp` — the spelling the hook rows,
`doctor` and the docs all use. It prints the slug now.
STATUS came the same way. The four variants are single words, so
lowercasing Debug happened to produce the documented `idle`/`working`/
`waiting`/`done`, but a variant named in two words would have printed
`needsinput` in the table against `needs-input` in the JSON for the same
pane. It goes through serde now, so the column cannot drift from what
`--json` says.
Neither the wire format nor `--json` changes: `CLIAgent` is serialised
across the control protocol, and that is a dialect not to touch in passing.
The test walks `CLIAgent::ALL` rather than listing variants, so an agent
added later is checked the moment it exists.
`tty7 events` blocks forever by contract, so returning at all means the
control connection went away with the server — and it returned 0. A reader
whose server stopped mid-run was told nothing had happened; the loop
consuming its lines simply stopped receiving any, with no exit code to
branch on. Verified against an isolated daemon: stopping the server ended
the stream and the CLI exited 0.
It exits 1 now and says so on stderr. Nothing extra goes to stdout, so a
reader parsing NDJSON is never handed a line of a shape it has not seen —
the same split the rest of the verbs use. An interrupted run is unaffected:
a signal takes the process rather than this path.
`tty7 events` rendered a layout delta with `{delta:?}`, so a line the docs
describe as an event arrived as a struct literal — internal field names,
`Some(..)` and all:
workspace f5e0c172… layout: PaneFacts { pane: PaneRecord { id: 1,
cwd: Some("/tmp"), title: "", osc_title: None, … } }
Debug output is not a format anyone should be reading, and it is certainly
not one to keep stable: renaming a field would silently rewrite what this
verb prints for whoever had started parsing it. All thirteen deltas now
have a line — `pane %1 is gone`, `tab 70e8e933 created at 0`, `renamed to
demo` — and the catch-all arm that dumped every remaining variant is gone
with them.
That arm covered `GitChunk`, whose `{:?}` prints a `Vec<u8>` one decimal
number per byte: a single chunk of a large diff would have arrived as pages
of them. It reports a byte count now.
The docs say the prose is for reading and `--json` is for parsing.
Found by running `tty7 events` against an isolated daemon while creating
and renaming a workspace.
`doctor` reported `$TTY7_WS` and `$TTY7_PANE` as "set (id)" without ever
asking whether the id resolves. A shell outlives the workspace it was
opened in, and one opened against another machine names an id this server
never had — a state the rest of the CLI already guards against, `run
--keep` in as many words. Doctor is the verb people reach for when the
address-taking verbs have started failing, and on that exact input it gave
the reassuring answer.
The rows now say GONE when the id names nothing on the server that
answered, and `--json` carries `workspace_gone`/`pane_gone` beside the
existing booleans rather than changing them. Both are absent when no
server answered: with no tree to check against, "not gone" would be a
claim rather than an answer.
Found by running `tty7 doctor` against an isolated daemon while the shell
still carried the real server's variables.
Bounding every cell cost `doctor` the thing it exists to report: the
config directory came out with its middle replaced by an ellipsis, and a
path a reader cannot copy is not a diagnostic. Caught by running `tty7
doctor` against the isolated daemon right after making the change.
Nothing is padded against the last column — `render_row` trims the end of
every line — so its width cannot push anything out of line, and cutting it
buys nothing. `ws ls` still bounds NAME, which has four columns to its
right; `doctor` and the agent listing, whose findings sit last, print
whole again.
`tab_label` clamps a talkative OSC title already, and says why: "so one
talkative tab cannot widen every column in the table". Nothing applied
that to the columns beside it. A 300-character workspace name — which
`tty7 ws new` accepts — padded every row of `ws ls` out past the edge of
the terminal, so a five-row listing arrived as a wall of spaces. The same
was true of any long cwd in `pane ls` and of an agent's status message,
which nobody types at all.
The bound moves into `table`, where it covers every column and every table
added later, at a full classic terminal width so no realistic name or path
is touched. `--json` remains the output for anything that must not be cut.
Found by running `tty7 ws new` with hostile names against an isolated
daemon; the wide-character padding held up in the same listing, checked by
measuring each row in display columns rather than by eye.
$ tty7 send %1 "$(printf 'echo ONE\necho TWO')"
... echo ONE
ONE
... echo TWO <- typed, waiting
Correct for a verb whose first line is "types TEXT into the pane exactly
as a keyboard would" -- typing a newline is pressing Enter. But `--enter`
is documented right next to it as the way to submit, which reads as
though TEXT alone cannot, and an orchestrator passing along text it did
not write runs it a line at a time.
So `--help` and the reference now say it, with the shape it takes: the
text before the newline runs, and what follows is left typed at the
prompt it asked for. A test pins the bytes -- neither stripped nor split
into two writes -- so the pages and the behaviour cannot drift apart.
Found while feeding a pane input it does not expect. Three neighbours
came through that unchanged and are worth recording: invalid UTF-8 in
output becomes U+FFFD with correct recovery (`\xc3\x28` is one
replacement and a literal `(`), `capture --json` stays valid JSON over
it, and an OSC title carrying bad bytes is folded before storage so
`machine.json` stays parseable and the tab table stays aligned.
Two claims in yesterday's comment were worth checking, and one was wrong.
A path really is a live vector. I made a directory called `evil<ESC>[31mdir`,
opened a pane in it, and it lands in the CWD column of `pane ls` -- now as
`evil?[31mdir`, zero escape bytes. That is a hostile name arriving from the
filesystem rather than from a rename the reader typed, which is the case
worth defending.
A tab's OSC title is not, or not any more: the daemon already folds control
characters to spaces before storing one, because gpui paints a label's tail
over whatever sits below it when the title contains a newline. So titles
reach the CLI clean, and the comment overstated the case by naming them.
The sanitiser still covers them and deliberately does not lean on the
folding -- that exists for the GUI and could reasonably move -- but the
comment now says which is which.
Also stops a nearby test asserting `chars().count() == 40` on a clamp that
bounds columns. Its title is ASCII so the two agree and it passed either
way, but read on its own it states the contract the previous commit just
finished correcting.
`clamp` is there so "one talkative tab cannot widen every column in the
table" -- its own comment. It counted characters, so a CJK title went
through at two columns each:
@1 工作工作...工作… tty7 1 <- 40 chars, 79 columns
and `tab ls` came out 98 columns wide, past an 80-column terminal, doing
the exact thing the clamp exists to prevent. Every locale this app ships
a UI for is affected, and none of the ASCII tests could see it.
The rest of the file was already careful here -- `width()` exists, and its
comment says padding by `len()` would push later columns out of line --
so this was one function measuring in different units from its caller.
Now it fills a column budget, dropping a wide character whole when it
would straddle the end: half of one is not a narrower character, it is a
different one. `saturating_sub` on the ellipsis's own column also retires
an underflow that `clamp(s, 0)` would have panicked on, unreachable as it
was with a single caller passing 40.
Measured on a live server before and after: 98 columns down to 58, with
every row still starting its fields at the same place.
The previous commit sanitised the tables. Errors quote what was typed and
did not:
$ tty7 ws rm "$(printf 'X\033[31mY\033[0m')"
tty7: no workspace named 'X<ESC>[31mY<ESC>[0m' -- `tty7 ls` lists them
Measured across eight verbs with a hostile argument, every leak was on
stderr and none on stdout -- the tables cover the stdout side already, and
nothing else there embeds free-form input. So this is one boundary, not
one call per message: `main` sanitises whatever an error carries, and a
message written later is covered by having been written at all.
The sanitiser is the one from the tables rather than a second copy, which
is the mistake the daemon's two disconnect logs already made once.
stdout is deliberately left alone. `tty7 capture` prints a pane's stored
bytes with their escapes intact -- that is the point of the verb -- and
sanitising the report path would have quietly gutted it. Checked after
the change: 46 escape bytes still come back raw, `--plain` still returns
none, and the text is unchanged.
The e2e test runs the real binary, because the unit test covers the
sanitiser while only a spawned process covers the single call in `main`
that puts it on the error path -- the part an edit could drop with every
other test still green. It also asserts the name is still readable: a
message that hides which name was refused has traded one failure for
another.
$ tty7 ws rename <id> "$(printf 'evil\033[31mRED\033[0m')"
$ tty7 ls | xxd | grep 1b
... 65 76 69 6c 1b 5b 33 31 6d ...
The escape went straight through to the terminal. `tty7 ls`, `pane ls`,
`ws tree` and the rest print names the CLI did not choose: a workspace or
tab name, a path -- a directory can be named with an escape in it, which
is the old `ls` trick -- and, through `tab_label`, a tab's OSC title,
which is set by whatever program is running in the pane. With `-m` the
tree comes from another machine entirely.
It threw the columns out as well, which is how I found it. The table
measures in display columns and already had CJK right, but an escape is
bytes with no width, so the padding counted characters the reader never
sees and that row's later columns sat nine over.
Control characters become `?`, the way `ls` has always done it, at the
two places human output is built: every table cell, and the tree, which
formats its names directly. `--json` is untouched -- it has to round-trip
the real name, and an encoder already writes the escape as six safe
characters.
Bidi overrides are deliberately left: they reorder a name without the
terminal obeying anything, and the same codepoints carry ordinary
right-to-left text.
Verified on a live server (zero escape bytes, columns level again across
ASCII, CJK and the hostile name) and the test fails without the fix.
`tty7 wait %1 --until free --timeout 2` against a pane running `sleep 30`
answered:
pane %1: still no-agent — timed out
nothing is reporting agent status in this pane — for a plain command
wait `--until free`, and for an agent check `tty7 agents` ...
Advice to pass the flag that was just passed. A plain shell reports no
agent status, so a `--until free` wait that runs out lands on the
`no-agent` hint, which was written for someone waiting on agent states
and never checked whether `free` was already in the until-set.
Worse than useless, because it displaced the answer: `free` is polled
every cycle when asked for, so still being here means the foreground
command has not exited. That is now what it says, with `tty7 procs` to
find out what is holding the pane — which names `sleep` on the pane above.
The other branch is untouched: someone waiting on agent states in a
plain shell still needs pointing at `--until free`, and the test holds
both sides so neither hint drifts onto the other's case.
`--scrollback`'s help said the flag makes no difference "for a
never-resized pane", and the reference said the same. Both are wrong for
the panes where the difference matters most.
I killed a daemon under a running GUI, watched it come back, and captured
a pane that had `RESTORE-MARKER-7788` in it. Plain `capture` answered with
two lines — the restore banner and a prompt — while `--scrollback` had the
marker and everything around it. Nothing had been resized.
`ReplayRing::seeded` ends with `resize(size)`, deliberately: the restored
screen is replayed at the size it was recorded at, and the new shell
writes at the size the pane came back as. When those differ the restored
screen is sealed into an earlier segment, and the default capture cannot
see it.
That is the agent-facing primitive answering "almost nothing" for a pane
that kept its screen, with the help explaining that this only happens
after a resize. Both now say restores count, and say what a plain capture
looks like when it does.
`resize` returns early on an unchanged size, so a pane that comes back the
same shape really does keep one segment — the test covers both sides of
that, which is the part the wording turns on.
`tty7 tab new --help` printed:
Arguments:
[WORKSPACE]
and nothing else. Every flag in this CLI carries help — I checked that
in a previous pass — and every positional carried none. Twenty-two of
them, the main argument of each verb. The gap is invisible in the source,
where a `value_name` sits where documentation would go and looks like it.
`--help` is where a verb is learned, so the blanks were hiding the things
a reader most needs, and three of them are genuine surprises:
tab move INDEX counts from 0 while tabs are addressed from 1, so
`@3 0` makes a tab first. Past the end it lands last.
new PATH omitted, the shell starts where the *server* was
started, not where you are.
ws new NAME omitted, the workspace has no name at all — the
codenames come from the GUI, not from here.
Each of those was established by running the commands against a live
server and reading the result, not by reading the code: I had guessed
the codename one the other way round.
The guard walks clap's own tree, so it covers a verb added later without
being told about it, and it refuses to pass on a walk that visits almost
nothing — a sweep that reads nothing looks exactly like one that finds
nothing wrong.
`tty7 tab new` in a shell whose workspace has been removed answered:
no workspace with id 65b90fd4-421f-4cfb-9848-d263c9a80959 on this machine
Nothing in what was typed contains a uuid. `workspace_or_context` falls
back to `$TTY7_WS`, so the id came from the shell — and bare like that it
reads as an internal error rather than something to act on. Every sibling
message in this file carries a hint; this one had none.
Found by running the CLI against a daemon that was not the one my shell
belonged to, which is the same shape as the two cases the comment above
`run --keep` already names: a workspace since removed, or a shell opened
against another machine. That insight was in the source and never reached
the reader.
The id stays whole rather than shortened the way an ambiguity is — it is
the exact string to compare against `echo $TTY7_WS`.
Checked the reference against the CLI it describes, in both directions.
It holds today: every flag it names is real — `--h` and `--v` are the
visible aliases of `--horizontal` and `--vertical`, `--all` and
`--orphans` belong to the `pane` subcommands — and every flag of every
verb it covers is on the page. So are the JSON keys: workspaces, agents,
build, control_version, protocol_version, socket, uptime_secs, ports,
procs.
Adding a flag is one line in this file. The page is somewhere else, and
an agent that cannot see a flag will not use it, so the drift is silent
and lands on the readers the page exists for.
The test asks clap for the flags rather than keeping a list beside it —
a list would be a third thing to drift — and skips the `ws`, `tab`,
`pane`, `machine` and `server` groups the page now says it leaves to
`--help`.
It also counts what it looked at. A cross-file test whose filter matches
nothing passes just as quietly as one that finds nothing wrong, and this
one would have: ten verbs and five flags are the floor.
tty7 ws rm 0c4c9162 # exits 0, says nothing
tty7 ls # 0c4c9162 is still there
Both are correct and together they read as a failed delete. The GUI logs
what it did — "deleted on its machine while a window still had it open;
putting it back under the same id" — rather than leave a window pointing
at nothing. The panes really are gone; what returns is empty, with a
fresh shell and a new name, which is why the codename changed under an
unchanged id.
Nothing said so on the side the reader typed. `ws rm`'s help now does,
including that a workspace with no window on it simply goes.
Measured twice against a live GUI: rc 0, panes to zero, no orphans, the
same id back with a different name, and the GUI still running.
The reconciliation itself is sound and was what I set out to test: a
workspace deleted underneath an open window produces no warning and no
error, and leaves the tree and the pane registry agreeing.
`tty7 doctor` is described as checking "socket, dialect, config,
versions, agent hooks, links, context". With a `config.json` that does
not parse it printed every row green: server ok, dialect ok, status ok.
Meanwhile every setting in that file was being ignored, a copy had been
kept beside it, and saving was suppressed.
The rows that mention config report `TTY7_CONFIG_DIR` — where the file
should be, which is a different question from whether it is being read.
There is now a row for the answer, from the same `LoadOutcome` the
loader already returns: ok, none yet, not valid JSON, unreadable.
It also exits 1 for the two broken states, for the reason written above
the server check: doctor is the verb people run when something is not
working, and `tty7 doctor || alert` has to fire. A config nothing reads
is that, as much as an unreachable server is.
All four states run against a live server: ok, none yet, NOT VALID JSON
(rc 1), and back to ok after repairing the file with no restart.
In 335d982 I gave `tty7 wait %3 --until free` as the form to use for a
plain command. `--changed` is what makes that safe after a `send`, and I
left it out.
The states are levels, not edges. A pane that has not yet started what
you just sent it is still `free`, so a wait that gets there first is
answered by the state the pane was already in — the flag's own comment
says exactly this, and it is why `--changed` exists. The example now
sends and waits the way a delegation loop actually runs.
Being straight about the evidence: I did not reproduce the race. Sending
a six-second command and waiting without `--changed` blocked the full
six seconds, because the shell had gone busy before the second process
started. The window is small and the failure is intermittent, which is
the worst kind to leave in an example — not a reason to call it fine.
`--changed` itself is correct, and now checked: `--until free` on an
idle pane returns at once, while `send` then `--until free --changed`
holds for the five seconds the command takes and the output is there
afterwards.
`stop` is described as "Stop the server; sessions end". `restart` was
"Stop, then start" — the same destruction, undisclosed, on the verb
people reach for casually when something seems stuck.
Measured rather than assumed: two panes before, zero after, workspaces
still there. So the layout survives, because the tree is on disk, and
what those shells were running does not.
The long form also separates this from the thing it will be confused
with. The server can hand panes to a new build of itself with their pids
and ptys kept — that is a real wire request (`ClientMsg::Handoff`, into
`hand_over` and `handoff::take_over`), and it is how an update keeps
sessions. `restart` is not that path, and someone who has read about one
should not assume the other.
`tty7 wait %N` on an idle shell blocks for as long as you let it. The
default states are `waiting`, `done` and `exit`; a plain shell with no
agent is `no-agent` and reaches none of them, so the wait has nothing to
wake for. Verified with a bounded run: still blocked at twelve seconds
against a freshly created pane.
Blocking is the contract — the verb is `wait` — so what was missing is
that choosing the wrong state is a hang rather than an error, and that
`--timeout` has no default to fall back on. Both are now said where
they are read: the trap in the command's help, the absence of a default
on the flag itself.
This is the same shape as the stdin note in the last commit. An agent
orchestrating panes has two ways to stop forever with no diagnostic, and
in both the software is doing exactly what it was asked to.
`tty7 run -- /no/such/binary` answered "no such shell on this machine".
The check in front of a spawn serves both the configured shell and a
command handed to `run`, and its three sentences all said "shell" —
so someone who typed `tty7 run -- ./build.sh` was told tty7 had
misunderstood what they asked for. They say "program" now, which is
true of both callers; the CLI already prefixes "spawning `…`" with the
context.
The other half is worse and is documented rather than changed:
`echo hi | tty7 run -- cat` never returns. The command reads the pane's
terminal, which nothing is typing into, so it waits for input that
cannot arrive — and there is no error, because an idle terminal is not
a failure. Output streams back; input does not go the other way.
Measured, after a first attempt sat for ten minutes: a bounded re-run
with `head -1` was still running at ten seconds with the pipe unread.
For a CLI whose stated audience is agents, `cmd | tty7 run -- …` is a
natural thing to try and an unbounded wait is the worst way to answer
it. Forwarding stdin would be a new capability; saying so is not, and
`sh -c 'cat < input.txt'` gets the input there today.
A pane running an editor or a pager answers `capture` with that
program's screen, and the scrollback behind it is not in the reply.
Measured: with a pane on the alternate screen the capture holds the
alt-screen marker and not the main-screen one; after leaving it, the
main scrollback is back and the alt content is gone, with only the
echoed command left behind — which was typed on the main screen and
belongs there.
That is right, and it is the sort of right that reads as a bug from the
outside. An agent that captures a pane mid-`less` gets a screenful and
no history, and the obvious conclusion — output was lost — is wrong. So
the help says which of the two you are holding, and that an
empty-looking capture is usually a TUI in front of the scrollback.
Also noted there: long output is trimmed from the oldest end, so a
capture after a big build gives the end of it. `seq 1 200000` through a
pane comes back as the last ~70 KB, ending at the prompt.
Checked while here, and correct: `send` carries an 8000-character
payload with nothing truncated.
`pane ls` reports PANE/WS/TAB/CWD/LIVE and `pane ls --all` reports
PANE/WS/OWNER/CWD/LIVE. The swap is right — a pane no workspace holds is
in no tab, so a TAB column would be empty for exactly the panes `--all`
exists to show — but nothing said so, and it is the sort of difference a
reader assumes they misread.
It also costs something specific: the @ numbers. A "no tab @7" error
sends the reader to `tty7 pane ls`, and reaching for `--all` there, as
one does when a thing seems to be missing, takes the column away.
Verified while here, and all correct: `--key C-c` interrupts a running
command (the `sleep` is gone and `procs` shows the shell back in the
foreground); `send` delivers quotes, backslashes, `$`, backticks, braces
and pipes verbatim; `-q` silences all twelve listing and creating verbs,
still prints errors, still exits 1, and wins over `--json` on success.
Same shape as the working directory, and the same surprise:
RALPH_MARKER=hello tty7 run -- sh -c 'echo [$RALPH_MARKER]'
[]
The pane is the server's child, so it gets the server's environment.
`FOO=bar tty7 run -- …` is a universal idiom and it silently does
nothing here, with no `--env` to reach for instead.
Documented beside the `--cwd` note, with the workaround that does work
— set the variable inside the command, where the shell running it can
see it. Adding `--env` would be a new flag, not a fix.
Checked while here, and correct: a pane does get a usable environment
(PATH, HOME) from the server; `$TTY7_PANE`, `$TTY7_WS` and
`$TTY7_CONFIG_DIR` are all set in a `run` pane as the top-level help
promises; and the payoff holds — `tty7 procs` with no address, run
inside a pane, resolves `$TTY7_PANE` and reports that pane's own
processes.
It does not run where you typed it. The pane is the server's child, so
it starts in the server's working directory — whatever that process was
launched from. Measured, not assumed: `tty7 run -- /bin/pwd` from /usr
and again from /tmp both answered with the directory the server had been
started in.
The help's own example is `tty7 run -- cargo test`, and a server the app
started is not sitting in your project. So the example as written builds
somewhere the reader did not choose. It fails loudly for a build — no
Cargo.toml — but not for anything that would happily run in the wrong
tree.
Documenting rather than changing it. Defaulting a local run to the
caller's directory is the behaviour most callers expect and would make
the example true, but it is a different result for every existing
invocation, and `-m` routes to a machine where the local path means
nothing. That is a decision to take deliberately, not a side effect of a
docs pass.
Checked while here, and correct: `split` inherits the split pane's cwd,
and a pane's reported cwd follows the shell through `cd`.
Twelve `tab new` at once against one workspace, then check that every
tab landed exactly once and that the tree's pane count still matches the
registry's.
Both invariants this leans on are invisible to a single-threaded test.
The store takes `notify_order` before its state lock and holds it across
delivery, so subscribers see mutations in the order they happened; and
each `tab new` spawns its pane before the tree is asked to hold it, so a
refusal in between leaves a pane running that nothing references. That
second one is quiet — it shows up only as these two counts disagreeing,
which is what `pane close --orphans` exists to mop up.
Verified by hand first: twelve racing creates, and a mixed race of
splits, tab creates, renames and closes, both left the counts equal with
no orphans; the same held with a GUI mirroring the changes live, whose
log recorded no resync or divergence. This is that check, kept.
Also measured while here, and sound, so it is not re-run: 180 pane
create/close cycles move the daemon from 10 fds and 5 threads to 13 and
7, and then stay there across two further rounds — one-time overhead,
not a leak.
`tty7 ws attach <ws>` answers `{"attached": "<id>", "took_over_from":
null}`, and `tty7 ws ls` shows the workspace unattached a moment later.
Both are correct: an attachment belongs to the connection that made it,
and this command's connection ends with the command.
A caller has no way to tell that from the reply. An agent reading
`attached` reasonably believes it now holds the workspace, and the next
`ws ls` says otherwise with nothing to explain the difference.
So say it. The claim does not outlive the process, that is not a
failure, and what does last is the displacement: the previous holder has
been told, and a dedicated one has been hung up. `took_over_from` is the
part of the reply worth acting on.
Behaviour unchanged — this is the same reasoning the `Attachment` type
already carries ("an attachment belongs to a live connection, and one
read back at boot would name a holder that no longer exists"), said
where the person running the command can see it.
Also checked while auditing, and sound: `tty7 <PATH>` already stats the
directory and refuses a file (`resolve_gui_path`), `ws stop` says "(not
implemented yet)" in its own help rather than only when run, and repeat
`ws detach` is idempotent on purpose for wire-compatibility reasons the
handler documents.
Auditing my own earlier fix, the way the last commit's lesson says to.
`run --cwd` and `new <path>` were fixed together and `tab new --cwd` was
missed, so it went on starting the tab's shell in whatever directory the
CLI happened to be run from and answering with a pane id:
tty7 tab new <ws> --cwd /nonexistent-xyz
%2
with `pane ls` then showing %2 rooted in the CLI's own directory.
The check is one function now, and the three verbs call it, which is
what should have happened when there were two of them. It refuses before
anything is spawned, and only judges paths on this machine — a routed
`--cwd` belongs to the far side's filesystem.
Pulling it into one place dropped the path from the sentence for a
moment ("--cwd: no such directory", naming the flag but not which of the
reader's paths was refused). The flag is optional in the message and the
path never is, since `new <path>` has no flag to name.
Three lines still wrote `pane(s)`: the partial-close report, the
hang-up failure, and the orphan note. All three are reachable with a
count of one, and all three are what a reader meets when something has
already gone wrong — a poor moment to look unfinished.
`pane(s)` is the form a codebase uses when it has not decided, and this
one has: the GUI counts through `t_plural` with one/other branches, and
`pane close` narrates a batch only when there is a batch. These were
what was left.
I said in the doctor fix that "1 panes" was the last of these. It was
not — I had swept the success paths and the format strings that spell
the word out, and these spell it `pane(s)`, so the search missed them.
Checked against a live server: "closed 1 pane; 1 could not be closed"
for one, "closed 0 panes" and "2 could not be closed" for the rest.
Following the shape found in the split fix to the other two verbs that
have it. Every verb adding a pane spawns it first and files it second,
because the seed carries the daemon's own pane id — there is no other
order. A refusal in that gap ended the command with a shell running that
no tree referenced: absent from `tty7 ls`, present only in `pane ls
--all`, and collectable only with `pane close --orphans`.
`tab new` and `new <path>` both had it. Confirmed rather than assumed:
the mock answers `TabCreate` with a reply `tab new` cannot read, and the
command left `spawned=1, killed=[]`.
One guard for all three sites now, since the ordering is forced and so
the window is permanent. `new <path>` also takes back the workspace it
made on the way in: it holds nothing, and leaving it adds a row to
`tty7 ls` the caller never asked for.
The mock grows a way to fail a chosen control call. Both cleanup paths
are reachable only after something has already been created, so there
was no way to test them from outside.
Verified against a live server that the ordinary paths are unchanged:
`new`, `tab new` and `split` leave the tree and the registry agreeing at
three panes, and `pane close --orphans` finds nothing.
`tty7 split` spawns the shell and only then asks the tree to hold it, so
any refusal in between leaves a pane running that nothing references —
`pane ls --all` shows it, the tree does not, and `pane close --orphans`
is the only way to be rid of it. Two attempts at `--ratio nan` left two.
NaN is the case that gets there, because it cannot be serialised onto
the control connection at all. The link drops mid-request, so the
daemon's own "a split ratio must be a finite number" never comes back:
the caller sees "control connection lost (request 2)" instead, which
says nothing about the ratio, and the pane is already running.
Refuse a non-finite ratio before the spawn, in the daemon's own terms.
Its clamp to a usable range is deliberate and stays its business — 0,
1 and 5.0 still land on the clamp, as intended; this only refuses what
the daemon would refuse anyway.
Then take the pane back down if the split fails for any other reason
too. Nobody asked for a pane that no tree holds, and leaving it for the
user to find with `--orphans` is not a refusal, it is a mess.
Verified against a live server: two refused splits now leave the pane
count where it was, and a valid `--ratio 0.3` split still works.
Swept the rest of the value-taking flags while here, all sound: `-m` on
an unknown machine, `--key` on an unknown key (which lists the real
ones), `--timeout 0` (exits 124, the conventional code) and a negative
timeout are each refused with the right message.
tty7 run --cwd /nonexistent -- /bin/pwd
/Users/thomas/repo/wt/ralph-wc
Exit 0, no warning, and the command ran wherever the CLI happened to be
started from. `tty7 run --cwd ~/porj -- make` builds the wrong tree and
reports success — and the caller most likely to typo a path is a script,
which has nothing to notice it by.
The daemon falls back to a directory that does resolve when the one it
is handed does not. That is right for a cwd inherited from a pane's OSC
7, which is only as good as the shell that reported it, and wrong for
one typed on the command line: an explicit `--cwd` is an instruction,
and one that cannot be carried out has to stop the run rather than be
quietly replaced. Same for `tty7 new <path>`, which rooted the workspace
in the CLI's directory instead and handed back an id as if it had not.
Both refuse before anything is spawned or created — a refusal after the
spawn leaves an orphan pane, and after `WorkspaceCreate` an empty
workspace to clean up. "No such directory" and "not a directory" are
told apart, because they read differently to whoever typed the path.
Only judged for this machine. A `-m` path belongs to the far side's
filesystem, which this process cannot stat — the reason `agent_hooks_state`
already gives for refusing to answer config questions when routed. Asking
the backend, so the two stay consistent.
The mock now defaults to *not* being this machine. It models a Windows
box (`C:\proj`), so leaving it on made two unrelated spawn-ordering
tests judge Windows paths against the host running the suite. A test
about local paths turns it on and uses paths that exist.
Six `tty7 server start` at once reported six different pids, five of
them gone by the time they were printed, and every one of them claimed
`"started": true`.
The singleton admits one server and the losers exit immediately — but
they all spawned a child first, and they all then see `running()` go
true, because the winner is up. Each reported the child it had spawned.
`pid` is the field a script keeps in order to watch or stop the server,
so five of six callers were handed a dead one.
Report the pid that is actually serving, read from the pidfile, and say
`started: false` when this call was not the one that started it — the
same answer `start` already gives when a server was up before it ran.
Readable by then: the daemon writes the pidfile after `bind`, and
`running()` needs a request answered, which is later still.
The ordinary path is untouched: a start that wins reports its own child,
because that is the serving pid.
Verified by racing six starts against an isolated config dir — all six
now report the one live pid, exactly one says it started it — and by a
single start, which reports the pid `ps` shows for that config dir.
Also checked while looking for this, and sound: a truncated, garbage,
empty or wrong-schema machine.json is quarantined (`.corrupt`, then
`.corrupt.N`) and the server starts clean; a killed server's pane
children do not leak, since closing the pty master hangs them up.
Found by running every `tty7 …` command the help and README recommend.
`tab ls @7` answered "no workspace named '@7'". `@7` is a well-formed
tab address typed into a *tab* command, and its own siblings `tab close
@7` and `tab rename @7` both answer "no tab @7" — only `ls` takes a
workspace in that slot, so the sigil looked like the problem instead of
the slot. `parse_workspace` accepts any word at all, unlike `parse_pane`
and `parse_tab` which each validate their own sigil, so nothing noticed.
Name it for what it is, `%` included. The hint is only reachable after
the lookup has already failed, so a workspace someone really did name
`@7` is found first and never sees it.
Then, checking my own new message did not recommend a dead end — the
mistake this same sweep caught last time — it did: `tty7 ls` renders
WORKSPACE/NAME/TABS/PANES/ATTACHED and has no @ column at all, so it
cannot answer "which workspace holds @7". The pre-existing "no tab @7 —
`tty7 ls` shows the @ numbers" was wrong the same way, and has been
since before this branch.
`tty7 pane ls` is the listing that actually carries panes, their tabs'
@ numbers and the workspace holding them, so all three now point there.
Verified against a live server: every message, and the command each one
recommends.
Also checked and found sound, so it is not re-swept: --json is a single
valid object for all ten listing verbs, errors are empty stdout plus a
message on stderr with rc=1, and a mistyped subcommand is named as one
rather than offered to the GUI (deliberate, and tested).
`tty7 wait`'s headline example is
tty7 wait %3 && tty7 capture %3 --plain
and `exit` is one of the three states `--until` waits for by default. Run
it that way and the pane is gone by the time capture runs: wait reports
`exit` and succeeds, `&&` proceeds, and capture answers that nothing is
running. An agent following the documented pattern gets no output for a
command that produced some.
The example is right for an agent — `waiting` and `done` leave the pane
alive. It is wrong for a plain command, which finishes by the shell
exiting. Spell both out in `wait`'s long help, and say plainly that
`exit` is the end state with nothing left to read.
Verified against a live server: `wait --until free` blocks for the
command (5s for a 4s sleep), and the capture after it finds the marker.
Also: a server running one pane had `doctor` say "1 panes" on its first
line. It is the first thing a new user runs. Every other count in the
tree is already plural-aware — the GUI routes these through `t_plural`
with "one"/"other" branches, and the CLI's other count (`closed N panes`)
handles the single case separately — so this was the last one.
Found by running the CLI rather than reading it. `tty7 run --keep` files
its pane into a workspace and the tree keeps that leaf after the command
exits — `tab ls --json` goes on reporting `panes: [31]`. But every
`%PANE` verb answered:
no pane %31 on this machine — `tty7 pane ls --all` lists them
which is wrong twice. The pane *is* on this machine. And `pane ls --all`
is documented as "every pane the server runs", so the command it sends
you to is the one guaranteed never to show this pane — the user follows
the advice, sees nothing, and has no next move.
`or_no_such_pane` consulted only the running registry, which cannot tell
"never existed" from "not running". It now asks the tree once it already
knows the pane is not running, and answers accordingly:
pane %31 is not running — ivory-heron still holds it, so there is
nothing to read or type into. `tty7 tab ls 1790a260` shows it;
opening the workspace in the GUI revives it.
The tree is fetched only on a path that has already failed, so no
ordinary call pays for it, and an unreadable tree falls back to the
plain sentence. A genuinely absent pane is unchanged.
Verified against a live daemon on an isolated config dir: both messages,
both exit 1, and the recommended `tab ls` really does list the pane —
having just fixed one dead-end recommendation, I checked the new one.
`run --keep` resolves `--ws` against the machine tree before it spawns
anything, and takes `$TTY7_WS` on trust — it only parses the id. Filing
the pane happens after the spawn, so a shell whose workspace has since
been removed, or one opened against another machine, started the pane and
then failed to file it. The pane kept running with nothing holding it.
Measured against a live daemon: with a `$TTY7_WS` this machine does not
have, `run --keep` exited 1 with a clear message and left a `sleep`
behind, `pane ls --all` reporting `"orphans":1`. With `--ws` spelled out,
the same bad id spawned nothing.
Recoverable — `pane ls --all` finds it and `pane close` ends it, both
documented — but nobody asked for the pane, and `$TTY7_WS` is the
*documented default* for `--keep`, so this is the ordinary path rather
than an exotic one.
Resolving the inherited id is now the same check the explicit one gets,
and only when `--keep` needs it: a plain `run` uses the workspace as an
ownership stamp, where a stale id costs nothing and the round trip to
find out would.
The spawn still precedes the filing — `TabCreate` names the pane the
daemon assigned, so it has to, and
`run_keep_spawns_first_then_files_the_pane_into_the_workspace` pins that.
What moved is only where the workspace is checked.
Three verbs, one condition, three sentences:
procs %99 no pane %99 on this machine — `tty7 pane ls --all` lists them
capture %99 observing pane %99: daemon refused Observe: no such pane 99
send %99 hi sending input to pane %99: no such pane 99
The daemon's own refusal makes a fine diagnostic and a poor sentence: it
names the wire request rather than the verb that was typed, so `capture`
told the user about "Observe", a word that appears nowhere else they can
see. This is the CLI agents read stderr from, and it was reporting one state
of the world three ways.
They now all answer with the line `procs` already used. The registry is
consulted only once something has failed, so the ordinary path still costs a
single round trip, and a failure with the pane present keeps its own words --
this must not swallow a connection error and call it a missing pane.
`resolve::no_such_pane` is now that sentence's one definition; the copy in
`workspace_of_pane` had drifted to suggesting plain `pane ls`, which does not
list the orphaned pane most likely to be asked about.
Found by running the verbs against a live daemon rather than by reading:
every one of these paths looks right in isolation.
The warning on `pane close --orphans` said an orphan "can still be doing real
work — an interrupted `run` leaves the command running". True, and it stops one
step short of the case that costs someone work.
A `run` that is executing *right now* is an orphan too. `run --ws W` stamps its
pane with `W` as the owner and files it into no tab until `--keep` does, so for
the whole length of the command the pane reads exactly like a leftover.
Confirmed against a live server: `tty7 run --ws W -- sleep 45` shows up as
`orphan=true, owner=<W>`, the same shape a leaked pane has, and
`pane close --orphans` duly reported `closed 2 panes` — one of which was the
running command.
So the old advice — "look at `pane ls --all` first" — cannot be followed:
nothing in that listing tells the two apart. The docs now say that, and say
what to do instead (close by id when anything might be running).
This also rules out the reaper people will reach for when they meet the pane
leak in `ui::tree_sync`: the daemon's own `spawn_orphan_sweep` already computes
this exact set and deliberately only reports it, and an in-flight `run` is why
acting on it would be wrong. `orphan_panes` now carries that reasoning.
The distinction that would work is whether a client is still attached — an
interrupted `run` has none, a running one does. The daemon knows and
`PaneInfo` does not say; adding the field is backward compatible, since every
other field on it is already `#[serde(default)]`, but it is a wire change and
wants more than a doc pass.
2951 tests pass.
The daemon answers a pane it is not running exactly as it answers an idle one:
`registry.get(pane_id)` misses and the reply is an empty `PaneProcs`. So
tty7 procs %999
printed `nothing running in this pane` and exited 0 for a pane that has never
existed. Every other verb taking a `%PANE` says when the pane is not there, and
an agent reading this one could not tell the two apart — which is the whole
point of the machine-readable half.
Checked against the registry rather than the workspace tree, because a pane no
workspace holds is still a pane the server runs and still worth reporting on;
that is exactly what `pane ls --all` surfaces it for. Verified live: a real
pane and an orphaned pane both still answer with exit 0, and %999 now exits 1
with "no pane %999 on this machine — `tty7 pane ls --all` lists them".
Asked only when the answer came back empty, so a pane with anything running in
it still costs one request.
One existing test needed the mock's registry seeded alongside its machine tree.
That is the fixture becoming faithful rather than the check being loosened: a
server running the pane its tree names is what the real pair look like, and the
mock had the tree without the registry.
2951 tests pass.
With no daemon running, most verbs give the message that helps:
tty7 ls could not reach the tty7 server on this machine —
`tty7 server start` brings one up
The three that go to a pane instead of to the control socket did not:
tty7 send %1 hi sending input to pane %1: No such file or directory (os error 2)
tty7 capture %1 observing pane %1: No such file or directory (os error 2)
tty7 procs %1 No such file or directory (os error 2)
All three read as if the *pane* were missing, or some file — and `procs` gave
no context at all. Panes are reached over the daemon's own socket, and with no
daemon there is no socket, so the connect fails with a bare `NotFound` that
each caller then wrapped in its own words.
`local_control` has contexted its connect all along; this is the same courtesy
one socket over. The local pane client is probed once when it is built, so all
three now say what `ls` says.
Local only, deliberately: the message names *this* machine, and a routed client
would pay a round trip across the link to be told something untrue of the far
end. Cached with the client, so it costs one round trip per process.
Nothing real is swallowed — a daemon that is running answers a bad pane id with
its own message, and still does:
send %999 sending input to pane %999: no such pane 999
capture %999 observing pane %999: daemon refused Observe: no such pane 999
Checked against a live instance both ways, plus a full send/capture round trip
on a real pane.
2949 tests pass.
The table synthesized the `local` row itself while `--json` serialized the
server's routes alone, and a route is a link to some *other* machine. So on a
machine with no remotes — which is every machine until someone connects one —
`tty7 machine ls` printed a row for itself and `tty7 machine ls --json`
answered `{"machines":[]}`. An agent enumerating machines read that as "there
are none", including the one it was running on.
The docs already said what it should be: "the local machine plus every link",
with `{"machines":[{"key","kind","connected"}]}`.
The list is now assembled once in `machine_ls` and handed to both renderings,
so the two cannot disagree again; `routes_table` renders what it is given
instead of adding a machine to it. Pinned by a test that asserts the table and
the JSON agree, with and without a link.
Found by running the command against a dev instance rather than by reading it —
the human output looked right, and it was the half nothing prints by default
that was wrong.
2934 tests pass.
Nothing had ever run `cargo doc`, so 16 warnings had collected. Four were
links to items that do not exist, and two of those were worse than a dead
link: `git_badge` and `info_chip` documented their sizes in terms of
`PANEL_TEXT` and `PANEL_TEXT_META`, px constants deleted when the interface
font scale landed. The module comment twenty lines up already says they went;
the prose downstream still derived pixel arithmetic from them, so a reader was
being told the pill is 20px tall against a 19px neighbour when both are now
rems that move with `ui_font_size`.
Rewritten against the ladder that exists (`META_MONO` beside `TEXT_MONO`), and
`info_chip`'s comment now records what its own numbers imply: its padding and
radius are pixels wrapped around rem-sized text, so the two stop agreeing once
the interface scale leaves 100% — the same trap `PIP_SIZE` right below it is
written in rems to avoid. Left as a note rather than changed, because that is a
visual decision and this cannot see the result.
The other ten are `private_intra_doc_links`, and that lint does not apply here:
it exists so a *published* crate does not ship docs whose links dead-end, and
all four crates are `publish = false`. Allowed at the crate root with that
reason, because a public item explaining how it relates to a private one is the
useful half of these comments.
The `clippy` job becomes `lint` and runs rustdoc too, on the same warm cache.
Still non-required.
2932 tests pass.