`--enter` is documented as sugar for `--key enter`, but the send dispatch
counted only `args.keys`, so `tty7 send %42 --enter` answered "needs TEXT
... or a --key to press" and pressed nothing. The key list is now built
before the dispatch and the dispatch counts it, so a marked address with
`--enter` and nothing else runs what the pane already has typed, and a
bare `send --enter` presses Enter where the caller sits.
An unmarked id is deliberately left out of that promotion. #567 made the
address slot take bare ids, and `send 83 --key C-c` addressing pane 83 is
fine because `--key` says "press this" and nothing else. `--enter` does
not: `send 2 --enter` reads at least as much like typing 2 into your own
pane and running it, and turning it into a keystroke at pane 2 would be
the silent retarget #567 spent its diff closing. It stays a loud error,
now naming both spellings (`send %83 --enter`, `send %PANE 83 --enter`)
rather than only the typing one.
The reference, the bundled skill reference, `send --help` and the
`--enter` help all said the old thing in slightly different words; they
now say the same thing as each other and as the code.
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
* fix(cli): refuse a broken send address instead of typing it into your pane (#538)
A lone positional that starts with `%` but fails parse_pane (`%3x`) used
to fall through to the text branch: the typo was typed into the caller's
own pane and any --key followed it there, so one wrong character
redirected an interrupt to whatever the caller was looking at. The guard
now propagates the parse error when the `%` is followed by a digit —
"clearly tried to write an address" — and leaves `%`-led text whose
second character is not a digit (`%s/foo/bar/`, `%!sort`) on the text
path it always was, per the review's narrowing.
The explicit address slot also accepts bare ids now: `pane ls --json`
prints `83`, not `%83`, and refusing the bare form made the workaround
for the typo hole (`"%${TTY7_PANE#%}"`) uglier than the hole. This
matches what pane_from_env already accepted and closes the missing-`%`
variant of the same mistake.
Tests cover the branch with a `Context { pane: Some("5") }` — every
existing send test used `Context::default()`, where the fallback errors
OUTSIDE_SHELL before the guard is reachable, which is why the hole had
no test.
Also correct the `ws rm` docs (#539): the reference claimed its panes
become orphans found via `pane ls --all`, but the code has hung them up
since #319; only a hang-up failure (reported by pane id) leaves
orphans. The site reference, the skill reference, and `ws rm --help`
now say so.
* fix(cli): keep the send guard to what actually looks like an address
The narrowing was described more widely than it works: a digit-led token
that fails to parse (`3x`) still types, only `%` then a digit refuses, so
the reference and the skill both promised an error that never comes. Say
what the code does and point at the two-argument form as the way to type
an address-shaped string anyway.
Now that the `%` is optional, `parse_pane` also has to be stricter than
`u64::from_str`, which accepts a leading `+`: a bare `+5` meant as text
would otherwise address pane 5. An address is digits and nothing else,
and `pane_from_env` delegates rather than repeating the read.
The broken-address arm parsed twice and ended in an `unreachable!` that
a future edit could walk into; one match on the parse result carries the
error out directly. A lone bare id is the one behaviour this takes away,
so it says how to type the number instead, and a test pins that it never
quietly presses a key at the pane the id names.
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
A window names its own tabs from its live terminals' OSC titles; every
other workspace it lists it reads out of the machine tree, which recorded
each pane's foreground process name and never its title. So the naming
fell through to the agent, and switching workspaces — which happens in
place and drops the terminals the window was reading — turned the tabs of
the workspace just left into a column of identical "Claude Code" rows.
The daemon now sniffs OSC 0/2 and records the title beside the pane's cwd,
capped at 256 characters, and `TabLabel` ranks it second only to a name
someone gave the tab. The switcher puts it through the same abbreviation
the tab strip uses, so a shell's `user@host:~/dir` reads `…/dir` in both
places, and in a split the pane running an agent names the tab rather than
whichever shell happens to be first. `tty7 tab ls` and workspaces on a
remote machine were reading the same missing field and are named the same
way now.
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
* feat(right-panel): give the Info tab rows that do what they show
The panel's Session table rendered every fact the same inert way, and its
two actions sat in a strip of their own under the whole list — unlabelled,
four rows below the path they acted on. Rows now carry their own shape:
`changes` is the sidebar's green-and-red `+N −M` and opens the same diff
overlay under the same setting, an agent wears the same status dot its tab
does, and what a row can do appears at the end of it on hover, in the strip
Source Control rows already use. A port row hands over its address instead
of leaving it to be retyped, and the lit panel tile closes the panel the way
every other activity bar does.
* fix(right-panel): answer for the row the pointer is actually on
Review of #531 found the new rows promising more than they could keep.
Port rows keyed their element id on the port alone, but a port is only
unique with its pid — a pre-forking server puts one row per worker on
screen, and gpui handed them a single interactive state, so a click on
one lit the tooltip and the pressed fill on all of them.
The `changes` counts were read off `Tab::git_status`, which resolves a
split tab to its *first* leaf, while the click target came from
`detail_pane`, which resolves it to the *last focused* one. Inert text
could disagree harmlessly; a button could not, and clicking `+2 −0`
opened another pane's repository. Both now come from the pane the rest
of the rows describe.
A port is only `localhost` if localhost reaches it. `lsof`'s bind
address was parsed and dropped, so a server on `172.17.0.1:8080` was
offered as `localhost:8080` — a refused connection, or somebody else's
service. `PortEntry` carries the address (`serde(default)`, so an older
daemon still answers), and the wildcard and loopback binds keep the
`localhost` spelling anyone would type.
The browser tile hung off `remote_context()` — where the *shell* is —
though the ports come from the pane's own process tree either way. It
hid the tile on the one pane where it works, a `ssh -L` forward listening
on this machine. It is about the host now.
The action strip is opaque and pinned to the row's right edge, so on the
working-directory row it covered the leaf that the head-first elision
exists to preserve. The value holds that width back for good rather than
on hover: taking it on hover would re-elide the path under the pointer,
which is the pixel-shifting the strip is absolutely positioned to avoid.
Also: the counts were `flex_1`, so the whole rest of the line was the
button and empty space underlined numbers it was nowhere near; the agent
pip was pinned in pixels inside rem-sized text and slid off its line at
any interface scale but 100%, and drew Waiting as a thin ring where the
tab strip punches a hole in a filled dot — one rule, two dialects; the
panel-toggle chrome tile, which on macOS lives inside the panel it
closes, still dropped focus into the destroyed element and left ⌘J
dead; and `scm/detail.rs` kept a third copy of `ROW_INSET`.
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
* feat(cli): wait on commands, press keys, and reap orphan panes
`tty7 wait` was the orchestration primitive for agents only. A pane with
nothing reporting agent status read as `idle`, so `--until idle` returned
success instantly about a shell that was midway through a build, and there
was no state that meant "the command finished" at all.
Panes with no agent now report `no-agent`, and `free` ends the wait when the
foreground command has exited — the process-tree question `procs` could
already answer but nothing could block on. `send --key` covers the keystrokes
text cannot express, which is what a worker stopped at `waiting` is usually
asking for. `pane close` takes several panes and `--orphans` clears what an
interrupted `run` leaves behind. `doctor` finally performs the hooks check
its own help has advertised.
The skill shipped in this repo predated `wait` entirely and taught a
hand-rolled `procs` polling loop with no notion of delegation; it now covers
the loop, and its agent statuses, `ws rm` orphan claim and not-implemented
list are corrected against the code.
* fix(cli): close the gaps review found in wait, --key and pane close
Five things the first pass got wrong, in the order they bite.
`--until free --changed` waited on a command it had already missed: the
"something ran" edge is only set by a poll that catches the pane busy, and a
command that starts and finishes inside one 500ms interval never is. That is
indistinguishable from a command that never ran, so the timeout now names both
doors instead of letting a finished build read as a hang.
`free` also outranked the agent ladder, which is backwards. A pane whose depth-0
process *is* the agent — the tree cannot tell that apart from a shell at its
prompt — reads free for its whole turn, so a `waiting` the caller explicitly
asked for could be overwritten by a process-tree fact and then withheld by the
`--changed` rule that comes with it. `free` is now consulted only when none of
the requested agent states answered, which is both cheaper and what the docs
already claimed. An empty process tree is "we could not see in" rather than
"free" for the same reason `no-agent` exists.
`--key M-X` sent `ESC x`: the whole spelling was folded to lowercase, which is
free for Ctrl (the C0 rule clears the case anyway) and wrong for Alt, where the
character rides through as itself.
`send --help` listed the key vocabulary by hand next to the table it is a list
of; it had already drifted by one alias. It is generated now.
And a `pane close` batch that could not close everything raised an error, which
left `--json` holding prose exactly when a cleanup script needs to know which
panes are still its problem. It exits 1 with `{"closed":[…],"failed":[…]}`, with
the complaint still on stderr so `-q` reports it.
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
Two things a pane lost when the background service stopped and started,
both of them things the tree was the only possible place to keep.
**The shell.** `PaneRecord` and `PaneSeed` carried a pane's cwd, its ssh
spec and its agent, but never what it was running. A window rebuilding a
dead pane from the tree therefore had nothing to pass and spawned on
whatever the default shell is now — so a restart turned a bash pane into
a PowerShell one, quietly and in place. The daemon resolves the override
against the config at spawn time and is the only party that knows the
answer, so it keeps it and reports it; the seed carries it too, for the
panes a window spawned itself. A handoff carries it in the blob, because
nothing on the far side of an `execve` can work out the command line of
a child it never spawned.
**The screen.** The startup sweep ran before the endpoint was listening,
which is the one moment nothing can answer the question it asks: the
registry is empty and the windows that know which screens are still
wanted cannot say so yet. A tree that failed to parse made it worse —
`read_machine` quarantines it and returns an empty `Machine`, so one bad
file took every pane's stored screen with it. The sweep now happens only
on the periodic pass, a tick later, with the registry filled in and the
tree caught up; nothing is serving a request in between. Turning the
setting *off* still clears the directory at once, because there the
promptness is the whole promise.
Two smaller ones alongside it: `restorable_pane_ids` now counts the
tree's pane list and not only the panes some tab currently stands on —
the two disagree while a window is between layouts, and being wrong
costs a file swept a tick late in one direction and somebody's terminal
in the other. And `restored_screen` drops the snapshot file *after*
deciding it was not empty, so a snapshot holding nothing is no longer
consumed by the request it could not answer.
The restore path had no end-to-end test, which is how this shipped: the
unit tests cover the file, not whether a window that reattaches is shown
anything. The new one runs a real daemon, puts a marker on a real pane,
stops the daemon, starts another, and reads the wire.
The daemon was created with CREATE_NEW_PROCESS_GROUP, which disables
Ctrl+C for the whole new group — and Windows hands that "ignore Ctrl+C"
state down to every descendant. Every ConPTY shell a pane spawned
inherited it, and so did everything those shells ran: the pane wrote 0x03
and conhost turned it into a keypress, but the CTRL_C_EVENT never came,
so `go run` and `npm install` carried on. Git Bash looked fine only
because MSYS synthesises SIGINT from the byte itself and never waits for
a console event.
DETACHED_PROCESS already leaves the daemon without a console for a
control event to arrive on, so the group flag bought nothing to begin
with. Both daemon spawn paths and tty7-cli's headless server — which
spawns panes too — now share one constant without it, and
DaemonPane::spawn clears any inherited ignore before it opens the pty, so
a tty7 launched from a shell that already had the bit set is covered as
well.
The regression test has to inherit the state rather than switch it on in
place, since that is the shape the daemon was in: an intermediate process
created exactly as the daemon used to be runs both arms, and a pane must
be interruptible only after the clear. Its observable is the shell rather
than the interrupted command — after the ^C, cmd gets its prompt back and
acts on the `exit` typed behind it — so it reads no message and holds on
a non-English Windows.
Fixes#451Fixes#314
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
* 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>
Five places where a workspace, a tab or an attachment was real on one side of the socket and invisible on the other. They share a root: the GUI kept its own list of which workspaces exist (WindowViews on disk) and consulted the machine tree only for the ones already in that list, so anything created by another client was unreachable by construction.
- The switcher lists workspaces the machine holds but this client has never opened, and opening one keeps its id instead of claiming a fresh one.
- for_workspace_at hydrates whenever the machine holds tabs, so opening a workspace no longer saves an empty session over them.
- finish_hydration writes a full window back over an empty tree, which is what puts a ws rm'd workspace back under the same id.
- A deletion nothing has open is forgotten here too, instead of haunting the switcher until a restart.
- Workspace::attachment travels over the wire (minus the token that proves the hold, which stays on the connection that owns it) and is stripped in persist, so tty7 ls can name the host holding a workspace.
- tab ls / ws tree fall back through name -> agent -> cwd leaf -> process name, and tab ls grew a read-only GROUP column.
- tty7 new --open raises a window on the workspace it just made.
* add CLI support for opening directories in new tabs
f
* feat(windows): add optional windows explorer context menus
f
* fix(gui): restore missing windows and reject lossy paths
* fix(windows): harden explorer menu registration and native path handling
* fix(cli): preserve native GUI paths on Windows
---------
Co-authored-by: thomas <thomas@gmail.com>
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>
Three holes in the wait primitive, all of which make a delegation loop
answer with something other than what it asked for.
The agent status the server keeps is a level, not an event: `done` stands
until the next turn begins, `waiting` until the agent moves again. A wait
issued right after a `send` therefore returned last turn's state before
the worker had even read the input, and a second task in the same pane
matched `--until done` instantly. `--changed` snapshots the position the
wait arrived at — status plus the activity counter, which ticks even when
the status letter does not — and refuses to match it; the JSON carries
`stale` so a plain wait can tell whether the answer might be a leftover.
`exit` was unreachable for any pane that had ever had an agent: the
snapshot has no liveness in it, and the daemon keeps a dead pane
registered until it is closed. A worker that crashed mid-turn reported
`working` until the timeout. Liveness is now re-checked from the tree
every few polls; the fast path where the first poll already answers still
costs exactly one request.
The "pane exited before reaching the awaited state" branch built its JSON
and then threw it away on an anyhow error, leaving `--json` with nothing
to read and the exit indistinguishable from an unreachable daemon. It now
exits 1 with its report.
Also: the wire spelling of a state is written out instead of derived from
the variant name, the sleep no longer overshoots a near deadline, an
absurd `--timeout` cannot overflow, and `--interval` is range-checked at
parse time rather than silently clamped.
* feat(cli): `tty7 wait` + the agent-coordination note
The two pieces of the original session-CLI PR that main's own CLI
doesn't cover, rebuilt as a minimal delta against it.
`tty7 wait %N --until waiting,done --timeout 600` blocks until a pane's
agent reaches a requested state — the orchestration primitive that lets
one agent sleep until its peer blocks on a permission prompt or
finishes a turn, instead of screen-scraping. A poll of `AgentStates`
rather than an `events` subscription on purpose: a one-shot stateless
question composes into scripts, survives a server restart mid-wait, and
needs no cursor management. Agentless-but-live panes read as idle via
the machine tree; a dead or vanished pane reads as exit, which ends
every wait (matched only when asked for). Timeout exits 124, the
`timeout(1)` convention.
The coordination note is discovery for the whole CLI: a marked,
idempotent block describing the verbs, installed into
~/.claude/CLAUDE.md (always; CLAUDE_CONFIG_DIR honored) and
~/.codex/AGENTS.md (only when ~/.codex exists). A one-time "Let your
agents coordinate?" prompt fires the first time a pane detects a coding
agent; a Settings → Agents switch drives the same install/remove, with
state read from the files themselves. Uninstall strips exactly the
marked block; an unterminated block is left alone rather than truncated
at a guess.
* feat(agents): replace the global note with an orchestration skill
Per review: global instructions tax every session's context and hand
every agent — workers included — the ambient authority to orchestrate
its neighbours. The common shape is primary → workers: one agent owns
decomposition, dispatch, waiting and aggregation; workers just do
bounded tasks.
A Claude Code skill fits that exactly. `core::orchestration_skill`
installs ~/.claude/skills/tty7-orchestration/SKILL.md — only its
one-line description rides in context until the user or the primary
agent explicitly invokes it, and workers never see it. The body can
therefore afford the full delegation loop (tab new → send → wait →
answer-or-capture → pane close) instead of a token-starved cheat
sheet.
The file is wholly tty7-owned: install is a plain overwrite (also the
version-refresh path), and uninstall keys on an ownership marker so a
user's hand-written skill under the same name is refused, not deleted.
Gone with the global note: the first-agent-detected prompt, its config
flag, and the CLAUDE.md/AGENTS.md writers — the Settings → Agents
switch now drives the skill install instead.
---------
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.
`tty7 tree` answered "launching the GUI is not wired up yet (would open tree)".
Bare `tty7 [PATH]` opens the GUI, so any word clap has no subcommand for lands
in that positional — every typo came back as an offer to open a directory named
after it.
A word with no separator, no leading ./~, and no matching file on disk is a
mistyped verb, and is now reported as one. Real paths still reach the launcher
and fail there for the honest reason.
Also: `ws`'s help said "the named session trees", missed by the session -> shell
pass, and long_about still advertised $TTY7_SOCKET, which no longer exists —
it is $TTY7_CONFIG_DIR now.
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
The thin console CLI from docs/cli-design.md, transport-free for now:
- clap grammar for the full documented surface: ws/tab/pane/machine/server
nouns, the hot-path top-level verbs (ls/attach/run/new/split/send/capture/
procs/agents/events/status/doctor), global -m/--json/-q, `run -- <cmd...>`,
and bare `tty7 [path]` parsing as the GUI launcher stub.
- tmux-style addressing (%42 pane, @7 machine-wide tab ordinal or @<uuid>,
workspace by name / id / unique id prefix) with implicit-context fallback
to TTY7_PANE / TTY7_WS and a "not inside a tty7 shell" error naming the fix.
- Backend trait as the integration seam: control() speaks real
tty7_core::daemon::control ControlRequest/ReplyOk values, plus declared
pane-side entry points (spawn_shell/send_input/capture/procs/attach_pane/
run/events). StubBackend fails loudly until the transport client lands;
MockBackend asserts the exact request shapes every structural verb builds.
- Plain aligned tables and trees for ls/tree/pane ls, one JSON object per
command under --json. Exit codes: 0 ok, 1 failed, 2 usage (clap default).
agents/status/machine ls stay stubbed: they need ControlRequest::AgentStates/
Status/Routes, which another slice is adding; ws stop and server start/stop
likewise wait on their mechanisms rather than inventing protocol.
Build/test this package alone (cargo build -p tty7-cli): its bin is named
tty7 and collides with the GUI bin until that one is renamed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014JPaaZVK7rfQPKyrymzsYv