mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 08:02:24 +00:00
3611fd39e084c94bfc75ed2a6f535b857fd9dd15
112
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c14f05829f |
fix(cli): flag a row as an orphan by the same test the reaper uses
Half a change, found by reading the docs rather than the code. `pane ls --all` learned to count strays by whether anybody is attached; the per-row `orphan` flag beside that count did not. The same JSON object contradicted itself, and the reference says `--orphans` "closes what `pane ls --all` lists as orphaned" — which had stopped being true for exactly the rows that matter, the panes a window is adopting during a restore. A script filtering `.panes[] | select(.orphan)` would have gone after panes the reaper deliberately spares. The row, the count, the doctor's row and the reaper now make one test, held by a test that walks all four. The reference states both halves of what an orphan is, since one of them is the half that keeps the command from taking a session that is still coming up. |
||
|
|
ac90f6c212 |
fix(doctor): say when the server is running panes nothing holds
An interrupted `tty7 run` strands a pane. So does a window reconciling its layout while something else edits the tree — measured at 17 from 160 operations, every one a live `zsh` holding a pty and its descriptors, and `tty7 pane close --orphans` ended all 17. None of that was news to tty7: `pane ls --all` names them, the switcher lists them, and the reaper works. What was missing is that nothing volunteered they existed. `doctor` printed `status pid …, up 6s, 2 panes` and counted the stray among them, so the one verb somebody runs when something feels wrong had the number and did not say what it meant. A row and a `server.orphans` count. Deliberately not an exit code: one stray after an interrupted `run` is ordinary and the reference documents it as such, so `tty7 doctor || alert` firing on it would cry wolf. What was missing was the sentence, not an alarm. Counted inside the arm that already has the machine tree in hand for the dangling-context row, so it costs no extra round trip, and under `server` because it takes a server to have panes at all. The first draft put the count at the top level of the JSON, and `the_doctor_json_sections_are_the_ones_the_reference_names` — added earlier today for exactly this — caught it as an undocumented section before it went anywhere. |
||
|
|
fa5b901d8d |
fix(doctor): check the shell every new tab is going to launch
$ cat config.json
{ "shell": { "program": "/nonexistent/shell" } }
$ tty7 new
tty7: spawning a shell: daemon refused Spawn:
no such program on this machine: /nonexistent/shell
$ tty7 doctor | grep config
config ok
Nothing can open a tab, and the verb that checks the install says the config
is fine — which it is, in the only sense that row means: the file parses. The
same table already reports an unusable `custom_shells` entry, and that costs a
menu row. This costs every tab, every `tty7 new`, and the GUI's new-tab button.
The check is the daemon's own `shell_program_problem`, moved from
`daemon::pane` to `core::shells` beside `unusable_custom_shells` so both
callers share one definition and the row cannot drift from the refusal it is
predicting — it prints the same sentence the spawn will. Missing, a directory,
and not executable are told apart, because they are three different fixes.
A program given as a bare name is still not reported: the OS resolves it
through PATH and guessing at that would be worse than silence — the moved
comment says so and the moved tests pin it. Skipped under `-m` for the same
reason the hooks row is: a path checked here would answer about the wrong
machine.
Verified against a live daemon across all five cases — missing, directory,
non-executable, bare name, and no `shell` set — with exit 1 for the three that
break and 0 for the two that do not.
|
||
|
|
4805cb1765 |
fix(doctor): notice a config directory that cannot be written to
Made the config directory read-only and asked doctor about it:
CHECK RESULT
TTY7_CONFIG_DIR set (/tmp/…)
config none yet — the defaults are the config
server ok (build 26.8.3)
…
rc=0
Meanwhile, on the same directory:
$ tty7 new
tty7: could not write the machine tree at /tmp/…/machine.json:
Permission denied (os error 13)
Every `new`, every `tab new`, and every settings save fails, and the verb
whose entire job is "check this install" called it healthy. The config row is
about *reading* — it is answering "does the file parse", and there is no file
— so nothing in the table was wrong, and nothing in it was the answer either.
`tty7 doctor || alert` is exactly the thing that should have fired.
Now: a row naming the directory and what stops working, a headline on stderr
so `-q` still says it, `config.dir_writable` in the JSON, and exit 1 alongside
the unparseable-config case it sits next to.
The check writes a probe file and removes it, rather than reading the mode
bits. A read-only mount, an ACL, an immutable flag or another user's directory
all leave `0700` on something that refuses every write, and what is being
diagnosed is whether the write succeeds. A directory that cannot be reached at
all counts as unwritable — an install nobody can open is not a healthy one.
Verified against a live daemon in all three states: healthy exits 0, read-only
exits 1 with the row and the headline, and fixing the permissions goes back to
0. The unit test covers the probe both ways and that it cleans up after
itself; replacing it with an `exists()` check fails it.
|
||
|
|
a67234266b |
docs(cli): name doctor's config section, and pin the shape
`tty7 doctor --json` has emitted a fourth top-level section, `config`, since it started answering for a config file that parses but carries keys tty7 does not read. The reference went on describing three. Checked the documented shape of every `--json` verb against a live daemon; this was the one that disagreed. It matters more here than elsewhere. `doctor` is one of the three verbs whose JSON is printed even when the verb fails — an unreachable server is the finding, and `tty7 doctor || alert` needs the rows as well as the code — so something is always parsing this. The page now names `config`, says what `ok` and `state` mean, and says which of the table's rows are prose rather than fields, so the next reader is not left looking for `config keys` in the JSON. The guard compares doctor's top-level sections against the ones the reference line names, both directions. Top level only: the fields inside vary with what a server could be asked — `context` gains `workspace_gone` and `pane_gone` only when one answered — and the page explains that in prose it would be wrong to pin to a fixed list. Verified by dropping the section from the page, and by dropping it from doctor. |
||
|
|
acd7d69815 |
fix(ssh-config): report a forward that cannot be imported, rather than dropping it
Differential-tested the ssh_config parser against `ssh -G`: `Host=name`, keyword case, tabs, multiple patterns on a line, negation, first-value-wins across blocks, `%h`, quoted ProxyCommand, a `#` mid-value not being a comment, glob includes, the `Host *` fallback. All agreed. Host patterns are case-sensitive in OpenSSH and case-sensitive here, which also agreed. Forwards did not. OpenSSH takes a Unix socket on either end and a service name wherever a port goes — `ssh -G` accepts `LocalForward /tmp/my.sock localhost:80`, `LocalForward 8080 /tmp/remote.sock`, and resolves `localhost:http` to port 80. A `ForwardRule` is a host and a `u16`, so `parse_forward_rule` returns `None` for every one of them and the rule never reaches the profile. Not being able to hold those is a limitation and is now written down. What made it a bug is that the import report answered by keyword alone: `localforward` was on the supported list, so the report claimed a forward it had thrown away. Someone whose tunnel never came up had nothing to read — the report said it was there. The supported-list check now takes the value and, for the three forwarding keywords, asks whether the rule actually parses. It stays one list, so the resolver and the report cannot drift apart. `docs/remote/ssh.mdx` lists the omission beside `Match` and GSSAPI, and the guard reads that line as well as checking that the forwards which do fit still come across untouched — otherwise honesty could have been bought by reporting everything. Checked against an injected regression. |
||
|
|
3064ce8a68 |
fix(history): stop per-pane history from silently disabling history search
`<config>/history` was two different things at once. The window keeps the input bar's command store there as a *file* (`terminal::history`), and the daemon put each pane's `HISTFILE` in a *directory* of the same name. A path is one or the other, and the daemon created its directory the moment `per_pane_history` was switched on, so the daemon won. After that the window's `append` opened a directory, got an error, and — the call site being `if let Ok(mut f) = open(..)` — dropped the command line without a word. Up and Ctrl-R went on offering whatever had been recorded before the setting was turned on and never grew again. Nothing in the UI or the log said why, and the two halves are in different crates, so neither side had any reason to notice the other's name. Per-pane files move to `<config>/pane-history/`, which leaves the old name to the file that should have had it. An install that already tripped over this is carried across on the next start: the directory sitting at the old name is exactly what belongs at the new one, so it is renamed rather than abandoned — but only when it *is* a directory, since a plain file there is the window's store and moving that would take the very thing this repairs, and only when the new name is free, so a second call cannot bury a directory in use. Verified end to end against a daemon: with the setting on it now creates `pane-history/` 0700 with the pane files in it, and an append to `<config>/history` succeeds where it previously failed. Seeding an old-layout install with `history/pane-9` and starting the daemon leaves the line intact at the new path. Guards: one reads the window's source for the names it passes to `config_path` and fails if the daemon's directory is among them, so moving either side onto the other trips it; one covers the three ways the move can lose data (directory moves, file stays, occupied target is not overwritten). Both were checked against injected regressions. The privacy page and the integration test that read the path follow it. |
||
|
|
0d82891cd1 |
docs(privacy): say where per-pane history is kept
The page said shell history is "your shell's own file, exactly as before
— unless you turned on per-pane history, which merges back into it". The
merge is real, but it is not the whole account: with per-pane history on,
tty7 points each pane's `HISTFILE` at `<config>/history/pane-<n>`, and
what accumulates there is the command lines someone typed.
That is the most sensitive thing tty7 causes to be written anywhere, and
the page named neither the location nor the mode. It enumerates
`<config>/scrollback/*.bin` down to its 0600 and its retention rules;
this belongs on the same footing.
Verified rather than read off the source: with the setting on and the
server restarted to pick it up, a fresh pane reports
HISTFILE=<config>/history/pane-1
and the directory is created `0700`. The files inside are the shell's own
writing, under the user's umask, which the page now says.
The guard asserts the path appears, not the prose around it — the path is
the part a reader needs in order to go and look.
|
||
|
|
56b4769d9c |
docs(privacy): update.log is a file tty7 writes, so list it
The privacy page accounts for every file tty7 leaves in the config directory, and `crash.rs` has a test holding it to that. The test named two files. There are three: the updater appends `<config>/update.log` while it installs a release — what it verified, what it replaced, and why it stopped if it did. It is written whenever an update runs, not only under `TTY7_LOG`. That makes it exactly what the test's own comment describes as belonging on the page: "a file written without being asked for". It is also the only account of a swap that happens after the window is gone, which is why it exists. The guard now names all three, and was checked against the page as it was. Also corrects a filename I got wrong in the previous commit: the doc comment on `install_crash_log` called this file `tty7-updater.log`. There is no such file — I invented the name while describing what a silent panic costs, and it went in unchecked. It is `update.log`, and finding that is what turned up the missing page entry. |
||
|
|
36404a6cbb |
fix(keymap): Restart Server is bindable, like every other menu item
Forty-five items in the app menu dispatch an action. Forty-four of them can be given a key; `RestartDaemon` could not, because it was missing from `default_bindings` and `make_binding` — so config.json dropped the name silently and the Keybindings page never listed it. Nothing else was missing. The gpui action is declared, the handler is wired, the app menu dispatches it and the palette runs it. Only the two table entries that make a name bindable were absent. This corrects a claim I made when I moved it out of the palette's chord lookup: I said then that making it bindable "means a gpui action and a handler, which is a feature rather than a fix". That was wrong — both already existed, and the app menu had been dispatching the action the whole time. Finding it took comparing the menu against the keymap rather than reading either alone. No default chord, like the sixty-odd others that ship unbound. The palette gets its chord lookup back, so once a key is on it the row shows it — which is the thing that lookup was doing wrong before and is now simply right. The shortcuts-page guard added earlier this session caught the last step without being asked: it failed on `RestartDaemon` the moment the action became bindable, which is what it is for. |
||
|
|
eb211d7fc1 |
docs(shortcuts): the document dock's actions are bindable, so name them
The shortcuts page has two halves: a table of default chords, and a list of the rest — "more actions you can bind". An action with no chord of its own appears only in that second list, so leaving it out makes the action unfindable: no key to be discovered by, and no row to be read on. The document dock (#625) shipped with four such actions — `ToggleDocumentFill`, `DocumentWidthThird`, `DocumentWidthHalf`, `DocumentWidthTwoThirds` — all bindable, all chordless, and none of them on the page. The CHANGELOG names them as palette commands; the page a reader goes to for "what can I bind" did not. The neighbouring guard already holds the other direction: the page names no action that has been renamed away. This one asks whether the page is *complete*, which is the half a rename cannot break but a new feature can. Two spellings count as named, because the page uses both. The rebinding lists write identifiers, and compactly — `ResizePaneLeft/Right/Up/Down`, `SelectWorkspace1`…`SelectWorkspace9` — so a trailing direction or digit is part of a family rather than an entry of its own, the same convention the neighbouring test already keeps. The chord table writes labels, which is how `CopyText` is covered: `per_platform("", "ctrl-shift-c")` leaves it chordless on macOS while it is Ctrl+Shift+C elsewhere, so it earns a table row and is written "Copy" there. The first draft of the guard missed that and called it undocumented. Checked against the page as it was: the guard names all four. |
||
|
|
020769b3bb |
docs(cli): server restart keeps sessions, and --hard is the one that does not
`Restart` became `Restart { hard: bool }` and the reference page did not
follow. It still read
| `server restart` | Stop, then start — same consequence |
which had been true before the flag split the two apart. So the page told
a reader that restarting the server kills every pane on the machine — it
does not — and said nothing whatever about the option that does.
Both halves measured against a running server with a `sleep 600` in a
pane, not taken from the help text:
server restart "restarted in place; sessions kept running"
shell 74647 still alive, pane still LIVE
server restart --hard "stopped and started; sessions ended"
shell 74647 gone, pane LIVE=no
Wrong in the cautious direction, which is the quiet kind: someone who
wants their server on a new build reads that line, believes it will cost
them every shell, and does not run it — while the destructive spelling
they were never told about sits one flag away.
The guard walks clap's own command tree and holds every long flag to
being named somewhere on the page. Names only: whether the prose around a
flag is right is not something a test can hold, but a flag missing from
the page entirely is, and that is the state that shipped. Global flags
are exempt — they repeat on all forty-odd verbs and the page documents
them once, in a table of their own.
Checked against the page as it was: the guard reports `restart: --hard`.
|
||
|
|
e7d04066d2 |
fix(capture): --plain no longer answers with a fraction of the pane
`capture` offers "two independent choices": how much (`--scrollback`) and
in what form (`--plain`). They were not independent. Every `--plain` grid
was built with `Config::default()`, whose 10,000 scrolling lines were
justified in a comment as "the daemon ring's order of magnitude".
The ring is capped in *bytes* — 8 MiB — which at ordinary line lengths is
nearer 100,000 lines. So the plain form silently dropped most of what the
raw form returned, and `--scrollback` could not bring it back. On a pane
that had printed 120,000 lines:
capture 8,388,609 bytes, from line 30,764 (the whole ring)
capture --plain 932,651 bytes, from line 109,973
capture --scrollback --plain identical to the above
Nine tenths of the pane missing, with nothing said. `--plain` is the form
the orchestration docs reach for (`tty7 wait %3 && tty7 capture %3
--plain`), so an agent reading the end of a long build log got the tail
and no reason to doubt it had the rest.
The grid is now sized from the segment it has to replay. Counting bytes
per column is not enough and the difference is the bug in miniature: a
line shorter than the pane is wide still costs a whole row, so dividing by
the width under-counts exactly when lines are short, which is most output
— that estimate alone recovered sixty thousand lines and still stopped
twenty thousand short. One row per newline plus one per screenful of wrap
is the bound that holds. Both forms now begin at line 30,764.
Over-estimating is free: alacritty grows its history as lines arrive
rather than allocating up front, and the daemon's ring bounds the whole
thing. Measured: an ordinary capture is unchanged at 8 MB resident and
0.3s; a full 8 MiB ring costs 343 MB for the 2.5s it takes.
The regression test uses short lines for the reason above, and fails
against the old fixed 10k grid.
|
||
|
|
449d38e538 |
docs(wait): say that an unknown pane answers exit, and pin it
`tty7 wait` is the one address-taking verb that does not refuse a pane the server has no record of. `capture`, `procs`, `send` and `pane close` all exit 1 on the same address; `wait` answers `exit`, `matched: true`, `stale: true`, and exits 0. That is the right behaviour and must not change. The server forgets a pane once it is reaped, so "the worker finished and was cleaned up" and "that id never existed" are one question to it — measured, not assumed: a pane that really ran and exited comes back byte-identical to `%9999`, and `pane ls --all` has forgotten both. Refusing would break the first case, which is the ordinary end of an orchestration: you wait on work that may already be over. What was missing is that nobody had written it down. Neither the CLI reference nor the orchestration page said what an unknown pane does, and both define `exit` as "the pane is gone" — true of a typo, but not what a reader takes from it when every neighbouring verb errors. An orchestrator that trusts a bare `wait` as proof the work happened gets an instant success from a stale id and reads an empty capture as "no output". So both pages say it, and a test pins it. Without the test this is an accident that reads like a bug, and the obvious "fix" — make it error like its siblings — would silently break waiting on finished work. |
||
|
|
791d0d0cfa |
Merge remote-tracking branch 'origin/main' into polish/ralph-wc
# Conflicts: # README.md # README.zh-CN.md # crates/tty7-cli/src/cli.rs # crates/tty7-cli/src/server.rs # crates/tty7-core/src/core/config.rs # crates/tty7-core/src/core/git/status.rs # crates/tty7-core/src/daemon/install/wsl.rs # crates/tty7-core/src/daemon/protocol.rs # crates/tty7-core/src/daemon/spawn.rs # crates/tty7-core/src/daemon/ssh/mod.rs # src/terminal/completion.rs # src/terminal/remote.rs # src/ui/app.rs # src/ui/i18n/en.rs # src/ui/i18n/ja.rs # src/ui/i18n/zh.rs # src/ui/tree_sync.rs |
||
|
|
024d368925 |
docs(skill): restructure the agent skill around a delegation playbook (#702)
* docs(skill): restructure around a delegation playbook SKILL.md becomes a slim routing layer: a what-are-you-here-to-do section up front, the pane/run/wait primitives, and four delegation rules that survive even when the reference is skipped. Everything specific to running another agent moves to references/delegation.md, which adds what the old text never had: per-worker git worktree isolation, a delivery contract collected through git instead of screen scraping, a launch-verification checklist, a babysit loop, and a fan-out harvest with short per-worker timeouts so one stuck worker cannot stall the round. Also replaces the last remaining 'claude -p' example (the fan-out one #699 missed) and keeps every snippet valid under both bash 3.2 and zsh. * docs(skill): un-deadlock the fan-out harvest loop Fresh read of SKILL.md and references/delegation.md. Every internal anchor resolves and the two files agree on the primitives; three things did not hold up: - The harvest loop passed `--changed`, which cannot work there. `wait` compares against the state standing when *that* wait began, so a worker that reached `done` while you were waiting on a different one is already in `done` when its own turn in the round comes up — refused, every round, forever. Each pane runs one turn, so a standing `done` is this turn's; drop the flag and note the one thing it was buying (a just-answered `waiting` worker needs to leave that state before it is requeued). - The same loop folded `wait`'s exit 1 into its 124 branch, so a pane that died got requeued instead of reported — and requeued at full speed, since a dead pane answers immediately. Split the three codes. - SKILL.md described `--plain` unwrapping "a line the shell wrapped at column 249" while two other passages state a pane is 120 columns. Say "at the pane's width", as references/commands.md already does. No typos or grammar slips found. Every bash block in both files parses under bash 3.2 and zsh. * docs(skill): two failure modes from the playbook's first live run Dogfooded the delegation playbook end to end (worker reviewing this very file). Two failures it hit that the text did not cover: - A turn aborted by an API error emits no turn boundary, so the status stands at 'working' forever and wait sleeps through it. Diagnose from the screen's error line; recover by telling the still-alive interactive session to continue. - A short capture tail cuts off the spinner line and shows only the TUI's always-present input box, which reads as idle. Tail 15+ lines and read for the spinner; 'bottom looks like a prompt' is only evidence on a shell pane. |
||
|
|
2cdc26f357 |
Wire hooks, resume and detection for Kimi Code CLI (#694)
* feat(agents): wire hooks, resume and detection for Kimi Code Kimi Code CLI takes its hooks as [[hooks]] entries in the same config.toml that holds the user's providers and models, so this adds a third install strategy — a format-preserving TOML merge on toml_edit — beside the JSON map merge and the owned files. Like Qwen it reports permission requests first-class, so it gets no Notification hook. Resume rides `kimi --session <id>`; fork stays unwired, Kimi documents none. Closes #693 Signed-off-by: Austin Spraggins <spragginsdesigns@gmail.com> * fix(agents): harden the Kimi Code TOML hook merge and its resume flags The TOML merge strategy the Kimi wiring introduces round-trips a shared config.toml cleanly, but three gaps sat behind it. `hooks_state` counted only the marked entries that still named an event, so a hand-edit that dropped the key off one of nine entries left the remaining eight matching the roster exactly and the file reported Installed with a broken entry in it. Every marked entry now counts, which is what the JSON merge already did and what `refresh_hooks` needs to see. A `hooks = []` spelled as an empty inline array made install fail outright -- toml_edit keeps an empty array and an array of tables apart, but the two say the same thing and neither carries any configuration. It is now promoted rather than refused. Every other wrong-shaped `hooks` key -- a string, a table, a non-empty inline array -- still refuses with the file left byte-for-byte alone. `Stop` is not the only way a Kimi turn ends: its own event reference says `Stop` does not fire on interrupts and `Interrupt` fires instead, and a turn that dies on an error reports `StopFailure`. Without those two an Esc or a failed turn left the pane on "working" for good and `tty7 wait` could only ever time out. Both are observation-only events and report the same end of turn `Stop` does. On resume, `--agent` and `--agent-file` join the stale flags: Kimi rejects either next to `--session` at startup, and resuming rebinds the session agent by itself, so replaying them turned a working resume into a launch error. Tests cover the wrong-shaped `hooks` keys, a config.toml that does not parse on both install and uninstall, a file that does not exist yet, a second install being byte-for-byte the first, mangled and surplus marked entries, an uninstall threading between the user's own entries and the tables after them, and the `--session=<id>`, bare `--session`, `--continue` and `--agent` spellings on the resume path. --------- Signed-off-by: Austin Spraggins <spragginsdesigns@gmail.com> Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
958d8b7442 |
feat(window): dock the code panel and the diff overlay beside the terminal (#625) (#685)
* feat(window): dock the code panel and the diff overlay beside the terminal (#625) Opening a file covered the workspace. The terminal underneath kept running and was neither visible nor typeable, so reading a file while an agent talked was a toggle loop: open it, close it to read the reply, open it again. The Files tree already docks; the two surfaces you go to *from* it did not. They dock now, as a flex sibling of the terminal column rather than a narrower overlay — that distinction is the feature. `set_grid_size` is driven by the terminal element's laid-out bounds, so a column takes width away from the grid and the PTY reflows into what is left; a card painted over half the workspace would have left the grid full width with half of it hidden. `overlay_top` stops ordering a pair and starts choosing between them: a column has one child, and two `flex_1` siblings would split it and fight. Fill mode keeps the old vector, the old opaque paint and the old platform hoist untouched, so nothing about today's overlay changes for anyone who picks it. - Half the terminal column by default; drag the divider, double-click it to cycle a third / half / two thirds, or use the palette commands. Two thirds deliberately runs past the half-window cap the side panels obey — only the terminal's floor binds it. - `DOCUMENT_MIN_W` joins the width budget: both side panels reserve it the way they already reserve each other, and the column is derived from the *live* sidebar and panel widths rather than their floors, so a panel someone dragged wider is width the terminal keeps. - A window too narrow to seat both fills for that frame. The fallback is derived at render time and never stored, so widening re-docks on the next frame with nothing to undo. - Fill or dock is per tab, on the header's context menu. Reading a long file over the whole window in one tab while an agent keeps half of another is the normal case, and one global switch made each of those flip the other. A tab that has not been told reads `document_layout` from the config, which is what a fresh tab starts as — and which the menu therefore does not write, since every untold tab is reading it. - Everywhere but macOS the title bar spans the workspace, which left a bar's height of nothing above the column. The header is drawn into it, and behaves like the title bar it now sits in. With the detail panel closed the column reaches the window's right edge, so the header stops short of the trailing chrome through a width the tab strip's own reservation shares. - The docked headers drop the traffic-light inset they never had to clear, and the diff header's branch name becomes the thing that yields so the view toggle and the close tile survive a column's width. New in `config.json`: `document_ratio`, and `document_layout` for what a fresh tab starts as. Four new actions, bindable and unbound by default. * fix(window): hold the docked column to widths the strip and the file agree on Three defects in the document column, each with a guard test that fails without its fix. The tab strip did not know a column had taken width off it. On macOS the strip lives inside the terminal column and sizes itself to the window less the detail panel, so a docked document left it 340 points wider than the column it sits in and the chips ran on under the column — the same overrun the panel's own reservation was added for. Everywhere else the strip spans the workspace and the column's hoisted header is drawn over its trailing end with no fill of its own, so a chip left under it showed through the file name and stayed clickable through it. The column's width now comes off `strip_w` on macOS and off `corner_w` elsewhere, which is where the panel's already goes. The divider wrote widths the file would not keep. `Config::sanitize` holds `document_ratio` to 0.2..=0.8; the drag clamped in pixels only, so a column pushed against either edge of a wide window was saved outside that band and reopened somewhere else — on a 2560-point body, 232 points from where it was dropped. The band is a pair of shared constants now and the drag clamps to it, the way the font size and its stepper were made to agree in #550. The palette named the config's layout rather than the tab's. Fill is per tab, so a tab told to fill was still offered "Document: Fill Window" — a row that named the state it was already in and did the opposite. It reads the active tab through `ChromeState` now. Also: `document_layout`'s doc comment still described the global switch an earlier draft had, three lines after the field became a per-tab default. |
||
|
|
7bcb91d8af |
fix(input): give the PTY back the Ctrl chords tty7 was eating (#684)
* fix(input): give the PTY back the Ctrl chords tty7 was eating Follow-up to #682, which handed Ctrl+V to a full-screen program but left three neighbouring holes of the same shape: a key the terminal answers without the keymap ever seeing it. The C0 table was half a table. `input.rs` mapped the alphabet, `[ \ ]` and Ctrl+2, and nothing else — so `Ctrl-^` (Ctrl+6, vim's alternate file), `Ctrl-_` (readline's undo, typed as Ctrl+/ or Ctrl+Shift+-) and Ctrl+3..8 produced no bytes at all. They were not mis-encoded, they were silent: gpui filters control characters out of `key_char` on all three backends, so the text fallback had nothing to offer either. The table is now the VT-220 one, each digit beside the punctuation that shares its key, because every platform hands Ctrl+Shift+6 over as `^` with the Shift already spent. Ctrl+/ is xterm's addition rather than VT-220's and is spelled out with the reason. The twenty-six letters fold to `& 0x1f`. `on_key_down` swallowed plain Ctrl+1..9 off macOS with a bare `return`, left over from when tabs lived on ctrl-digits — they have been on Alt+1..9 for a long time, so nothing claimed those chords and the block only deleted keys. It also sat before `keystroke_to_bytes`, so not even the kitty protocol got through it. Gone. Ctrl+V is now a binding. `AlternatePaste` carries `ctrl-v` off macOS in a `Terminal && !alt_screen` context, and the pane declares `alt_screen` whenever a full-screen program owns the grid, so the behaviour #682 settled on is unchanged — paste at a prompt, SYN inside vim — while the keymap can finally express it, the Keybindings page lists it, and the user gets a say: `"AlternatePaste": ""` hands Ctrl+V to the shell everywhere, including readline's `quoted-insert`, and `"PasteText": "ctrl-v"` pastes on every screen the way Windows Terminal does. That cohort is real — Warp keeps Ctrl+V pasting on Windows on purpose, as a removable binding, for exactly this reason. The hardcoded arm in `handle_cmd_shortcut` now answers Cmd+V alone, which is macOS's only paste chord and carries no control code to lose. Last, the rule about control codes is one function instead of an assertion buried in a test. `steals_a_control_code` plus a commented `control_code_binding_allowed` back both the defaults test and a new runtime warning, so a hand-edited config.json that takes EOF away from every shell says so in the log. It warns rather than refuses: a chord the user asked for by name is theirs to spend, the way the tmux preset spends Ctrl+B. The invariant that still fails a build is that no *default* spends one silently. Tests: `cargo test --bin tty7-app` 1360 passed, 1 known flake (`a_routed_auth_prompt_carries_the_machine_that_raised_it`, green on a rerun and on a clean tree). New: the whole VT-220 table asserted byte by byte, with Ctrl+- held out; `ctrl_6_reaches_the_pty_as_rs`, `ctrl_v_pastes_at_a_prompt` and `ctrl_v_reaches_a_full_screen_program_as_syn` drive the real keymap through `simulate_keystrokes` rather than calling into the view; the keymap tests cover both escape hatches and the context that withholds the binding. #682's two `handle_cmd_shortcut` tests are replaced by those three, which assert the same behaviour at the layer that now decides it; its end-to-end SYN test stands unchanged. The gpui tests are unix-only, so CI is what runs them. * fix(input): ask the grid, not the last frame, before Ctrl+V pastes `AlternatePaste` carries `Terminal && !alt_screen`, but gpui matches a keystroke against the frame it last painted, so the context outlives the switch: a full-screen program that took the screen after that paint is still "at a prompt" as far as the keymap is concerned, and the clipboard lands in it. In vim's normal mode that runs as commands. The action now re-reads the terminal mode and propagates instead, which hands the chord to `on_key_down` and encodes it as the SYN the program is waiting for. Also: - the two escape-hatch assertions in `paste_ships_both_terminal_chords_off_macos_and_retires_together` built a one-entry binding table instead of the default one, so both passed without the hatch working — an emptied `AlternatePaste` cannot dispatch anything when it is the only entry in the table. They now apply the config line on top of the whole default table, and the `PasteText: ctrl-v` case checks both screens; - the keyboard-shortcuts page claimed every other Ctrl chord reaches the program, which Ctrl+Tab and the Windows/Linux font-size chords do not; - `steals_a_control_code` documents `@` and the backtick, which are in the set it walks but were not in the list beside it. |
||
|
|
3c95995e82 |
fix(input): hand Ctrl+V to a full-screen program on the alternate screen (#677) (#682)
In vim or neovim on Windows and Linux, Ctrl+V pasted the clipboard where the editor expected blockwise Visual mode. Windows Terminal (with its ctrl+v binding removed), WezTerm and Alacritty all send the key; macOS was never affected, since Cmd+V is the paste chord there. Ctrl+V was not a keybinding at all. `on_key_down` hands plain Ctrl+C, V and X to `handle_cmd_shortcut` off macOS, and of the three the "v" arm was the only unconditional one: Ctrl+C copies with a selection and otherwise falls through to SIGINT, Ctrl+X falls through outside the editor, but Ctrl+V always consumed, so SYN never reached the PTY -- `input.rs` had the byte, unreachably -- and an empty clipboard turned the key into nothing at all. #270 set the rule that off macOS ctrl-<letter> belongs to the terminal and anything sitting on one must fall through; Ctrl+V was the exception that had escaped it. The arm is now contextual like its neighbours. On the alternate screen it falls through, and `keystroke_to_bytes` sends 0x16, or the CSI u form when the program has the kitty protocol on; off it Ctrl+V pastes exactly as before, and Cmd+V on macOS is untouched. The alternate screen is the gate rather than `input_active` because the editor is inactive whenever shell integration is missing or the prompt editor is off, and gating on that would take paste away from every such user; a program that has switched screens is precisely the case reported. Inside such a program paste is Ctrl+Shift+V, Shift+Insert or the right-click menu, all of which still stage a clipboard image for an agent. The same block did not exclude Shift, so Ctrl+Shift+C/V/X reached the hardcoded path whenever the keymap had nothing on them -- exactly the state rebinding Paste leaves behind, which #271 promised would retire Ctrl+Shift+V, but it went on pasting behind the user's back. Only unshifted chords enter the block now; the shifted ones are the keymap's alone. The right-click menu advertised Ctrl+C, Ctrl+X and Ctrl+V off macOS as though they were the bindings, next to a Select All row that already showed its hint on macOS only. The three rows take the same treatment, which is also what the command palette does. Three view tests pin the split -- Ctrl+V falls through on the alternate screen while Cmd+V still pastes there, Ctrl+V pastes off it, and a key down on the alternate screen arrives at the PTY as SYN and nothing else -- and the keymap's paste test now asserts that no default claims ctrl-v in the Terminal context. The shortcuts reference notes where plain Ctrl+V pastes and where it is the program's. Fixes #677. |
||
|
|
ef333bf055 |
feat(terminal): make the wheel-zoom modifier configurable (#676)
Cmd-scroll zoomed the font with no way to move it or switch it off, so a thumb left on Cmd resized the terminal mid-scroll (#668). The modifier is now a setting: the platform modifier by default, or Ctrl, Alt, or none. Stored as the choice rather than the resolved key, so one config file still means the same thing on a Mac and on a Linux box. Settings -> Terminal -> Mouse carries the picker; off macOS Ctrl and the platform modifier are the same key, so it shows one cell for them. |
||
|
|
9c2869a25f |
Trim the app's long-winded copy, add four dark themes (#663)
* refactor(i18n): drop the About page shell primer and trim the long copy The About page carried a "How shells work" section explaining that shells live in a background server. Nothing linked to it and the Updates and Server sections below already say what happens to those shells, so it was a paragraph of prose the page did not need. Remove it, its search index entry, and its three L10nKeys. Then cut the padding out of 48 strings across settings rows, dialogs and notices. Two patterns accounted for most of it: the restart-server dialogs stated "your shells keep running" up to four times each in different words, and the config.json failure notices packed three subordinate clauses into every sentence. Nothing is dropped but repetition and clauses the reader can infer — every consequence a dialog asks the user to weigh is still spelled out. en, zh and ja stay in sync. * feat(themes): add Catppuccin Mocha, Gruvbox Dark, Nord and Tokyo Night Four more dark built-ins, taking the set from nine to thirteen. The docs table and description are updated to match. * fix(themes): give Catppuccin Mocha its rosewater caret, refresh a stale builtin count |
||
|
|
0295a98915 |
feat(sftp): open remote text files in the built-in editor
A click on a file in the SSH Files panel used to start a download; the only way to change a remote file was download, edit, re-upload. Now a click opens it in the built-in editor and Cmd-S saves straight back over the pane's own SFTP channel, matching what the Files panel already does locally and over a remote workspace. - protocol: SftpOp::ReadFile/WriteFile and SftpOpResult::File, bytes as base64; the reply carries the body plus the stat it was read under - daemon: ReadFile enforces the caller's size ceiling before and during the read; WriteFile rewrites in place (truncate, not temp-and-rename) so the file keeps its mode and ownership - SftpHost: a Host over the pane's SFTP route, so the editor's existing open/save path works unchanged; git/search/watch honestly Unsupported - editor: an open buffer holds the host it was read from, and save/reload/dedup/watch key on (host, path) instead of the active host - panel: single click opens (dirs navigate, text files edit), the same gesture as the local tree; binary or oversized files get the local tree's toast, and Download moves to the context menu Review follow-ups, in this PR: the SFTP host stays out of HostRegistry, which means "a machine this window has a link to" and is swept as such — filing the pane's channel there made Cmd-S return silently once a workspace deletion took it back out. The cursor-jump lookup, the status bar's path, and the SCM panel's repository all key on the buffer's own host now. Closes #656. |
||
|
|
bcbdf51fed |
fix(cli): a server that closes the event stream is not a success
`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. |
||
|
|
c1b3a10561 |
fix(cli): print an event as a sentence, not a Rust debug dump
`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.
|
||
|
|
baeffd0274 |
fix(doctor): say when the inherited context names nothing here
`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. |
||
|
|
89426fb5c4 |
fix(links): keep the path when a template token holds an absent value
`link_file_command` dropped a whole token whose value was missing. That is
right for `--line={line}`, where the flag means nothing without it, but the
same rule threw the file away in `code --goto {path}:{line}:{column}` —
which is VS Code's own spelling, and one of the three examples the docs
offer, `zed {path}:{line}` being another. Clicking a link that carried no
line number ran `code --goto` with no file and opened nothing, silently.
A token that has already produced the path now keeps it and stops there,
taking the separator that introduced the absent value with it so the
argument ends at the path rather than at a bare `:`. Tokens with no path in
them still go entirely, so the documented flag behaviour is unchanged.
This edge was noted in a test that asserted it as a "sharp edge" rather
than fixing it; that test now asserts the file opens.
|
||
|
|
eaca1e50d8 |
fix(keymap): refuse a tmux prefix that cannot carry a sequence
Every preset binding is built as `<prefix> <key>`, and nothing checked the prefix. `"prefix": ""` in a hand-edited config therefore produced no sequence at all — it bound bare `c`, `x`, `z`, `n`, `o` and the digits directly onto NewTab, CloseActiveTab, ToggleMaximizePane and the rest, so typing an `x` in the terminal closed the tab. `"c"` and `"shift-c"` are the same trap one step removed: the letter starts a sequence and swallows the next keystroke, and shift-c is simply how a capital C is typed. `preset_prefix` now requires one chord carrying a non-shift modifier and falls back to the default otherwise, naming the refused prefix in the log. That also catches `"C-a"` — tmux's own spelling, which gpui does not parse — which previously slipped through to `action_bindings` and dismantled the preset one binding at a time, warning about each key rather than about the prefix that caused it. The GUI only ever offered Ctrl-B and Ctrl-A, so this is reachable through config.json, which the docs describe. Those docs also promised a **Prefix** field to type into; it is a two-option row, and now says so. |
||
|
|
5fc97d5d06 |
docs(shell): put nushell on the page that lists integrated shells
The "Which shells" table named zsh, bash, fish and PowerShell. Nushell has had the same treatment as the rest for as long as they have -- a throwaway `config.nu` passed with `--config`, sourcing the user's own back in -- and a nushell user reading that page concluded they got nothing. The row says how, including the part that makes it unlike the others: `source` is parse-time in Nushell, so the path to your config is resolved as the wrapper is written rather than checked when it runs. The test reads the shells out of the injection dispatch, so a sixth has to be written down before it passes; removing the new row fails it, naming Nushell. Found by checking a table against the thing it claims to describe, which also cleared two nearby ones: `PATH_PROBED_SHELLS` holds twelve shells against these five, and that difference is right -- probing PATH so a shell can be chosen is not the same as having hooks for it, and ksh or tcsh work fine without them. |
||
|
|
db275b7770 |
docs(cli): say that a newline inside send's TEXT presses Enter
$ 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.
|
||
|
|
cb07fd45e7 |
docs(cli): correct two claims about --json that scripts branch on
Both found by running the verbs rather than reading them, and one of them
is mine from four commits ago.
`--json` said two verbs still print JSON when they fail. There are three:
`doctor` prints its whole report on the exit-1 path, deliberately, because
an unreachable server is the finding and `tty7 doctor || alert` needs the
rows as well as the code. My earlier batch simply had not included it --
this time every verb that can fail was run with --json, and the other
seventeen do print nothing.
The `run` section was worse. It said a signal death reports
`exit_code_known: true` "because a status was read", and concluded that
`run` therefore cannot tell a command the OOM killer took from one that
exited 1 on its own. The flag is false, and it is exactly that
distinction:
$ tty7 run --json -- sh -c 'kill -9 $$'
{"pane":14,"exit":1,"exit_code_known":false,"kept":false}
`reported_exit_code` returns `None` for a status that spells itself
"Terminated by", `run` turns that into an exit 1 plus a note on stderr,
and an orchestrator can branch on the flag without reading the pane. The
page had been telling it to do the opposite.
So the daemon side now carries a comment saying why a signal is not
dressed up as `128 + signal` -- that convention belongs to the shell, and
inventing it here hands back a number the command never returned -- and a
test pins `None` for a signal against `Some(code)` for an exit, including
the "Terminated by" spelling the whole thing turns on.
|
||
|
|
74e8fdb5a7 |
docs: say what --json does when a verb fails
The flag's row promised "one JSON object on stdout" and stopped there. Running the error paths, most verbs print nothing on stdout at all — `capture %999`, `procs %999`, `tab close @99`, `ws rm nosuch`, `tab rename @99 x` each exit 1 with the message on stderr and an empty stdout. A reader that parses stdout without checking the exit code first gets an empty string where it expected an object. Two verbs deliberately do the opposite, because there the bad news is the answer: `pane close` prints `failed[]` and `wait` prints the state it gave up in. Both are already pinned by tests, so this only writes down the rule they are exceptions to. `run` looked like a third exception and is not: a child exiting nonzero is the verb succeeding. Checked rather than assumed — `run --json -- sh -c 'exit 3'` prints its object and exits 3 — and the row now says so, since that is the case most likely to be misread as a failure that kept its JSON. |
||
|
|
ed29cdce47 |
docs: a restored pane is a resized pane, as far as capture is concerned
`--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. |
||
|
|
9006e3ea17 |
docs: put the two files nobody opted into on the privacy page
"What is stored, and where" listed settings, keychain entries, scrollback
and shell history, and omitted both files that carry incidental personal
data:
crash.log written whenever tty7 panics — the hook is installed
unconditionally, so nobody opts in. Time, version, panic
message, backtrace. Capped at 256 KiB, never uploaded.
tty7.log only while TTY7_LOG or RUST_LOG is set, and genuinely absent
otherwise. At debug it carries the directories and workspace
names in each request.
A page that enumerates storage and leaves out the two files most likely
to end up attached to a bug report is answering the wrong question, so
both rows say what is in them and what to check before sending them on.
Noticed while reading a debug log for something else: a dependency had
written this machine's hostname and working directory into it.
"What leaves your machine" needed no change — nothing sends either file
anywhere, which is why each row says so.
The test lives next to the panic hook, where a change to what gets
written is a change someone is already making.
|
||
|
|
4ad5775fb4 |
docs(cli): the command reference does not cover every verb
Its description said "Every verb, its flags, and the JSON it emits". It documents twelve of seventeen: `ws`, `tab`, `pane`, `machine` and `server` are not on the page, not even in passing. Writing the missing twenty-odd sections is a documentation project, not a correction, and inventing them from the outside is how references start disagreeing with the program. So the page now says what it is — the verbs a script or an agent reaches for — and points at `tty7 ws --help` and its siblings for the rest. Those are generated from the definitions the CLI runs on, so unlike a second copy here they cannot drift. My first attempt at that paragraph claimed `ls`, `new` and `split` were all shortcuts into the groups. Two are: `ls` is `ws ls` and `split` is `pane split`, both said so in their own help. `new` is not — it makes a workspace *and* its first tab, where `ws new` leaves it empty — so the paragraph now says which is which. `tty7 pane there`, which a grep of the overview turned up, is not a verb either. It is the middle of "if you are not inside a tty7 pane there is nothing to split". |
||
|
|
418ebbf960 |
docs: the front page promised shells that survive a reboot
README, both languages, and the docs index said that after rebooting
the machine your shells are still running. They are not, and the
project's own agent documentation says so plainly:
When the server goes away — a reboot, a crash, a deliberate
restart — the shells go with it. Panes that were running an agent
relaunch the conversation on restore.
That is the accurate version, and it is what I measured: killing the
GUI leaves the server and every pane alive, and restarting the server
takes panes from two to zero while the workspaces survive. So quitting
the app really does keep shells running — the front page was right
about that half — and a reboot rebuilds the layout around fresh shells
with agent conversations resumed.
The headline is no weaker for being true; "resumes your agent
conversations after a reboot" is the thing that is hard to build, and
it is the thing that actually happens.
Left alone, because they were already exact: "a pane lost to a reboot
relaunches the conversation", prompt history that "carries across
sessions and reboots" (it is on disk), and "session resume after a
reboot".
|
||
|
|
07790f589d |
docs(config): say that agent_commands needs the server restarted
Checking whether any setting is written but never read turned up one
field named nowhere outside `config.rs` — `agent_commands`. It is read,
through `agent_commands_cached`, which is the point: that is the only
setting behind a `OnceLock`, so a running server keeps the map it built
at startup.
Every other key here is picked up by a reload. This one is not, and
nothing about editing the file says so: a user adding `{"cc": "claude"}`
sees the wrapper go on being unrecognised and has no reason to suspect
the server rather than the spelling.
The cache is right — the map is read on every pane spawn, and the
alternative is `Config::load()` off disk each time a pane starts. So the
row says `tty7 server restart` and the function says why it costs that,
each pointing at the other.
All 77 fields are read; this was the only one worth a word.
|
||
|
|
99994553ef |
fix(config): hold side-panel widths to the widths they can be drawn at
#550 settled the rule: one range, defined where the value is validated, so a config-legal number cannot be turned around by a widget's narrower clamp. `sanitize_clamps_to_the_same_bounds_the_gui_steps_within` states it and pins the font pair. The two side panels broke it. `sanitize` clamped both widths to 100–2000, and `docs/reference/configuration.mdx` published that range, while the sidebar floored itself at 180 and the right panel at 216. So a documented `sidebar_width: 120` was accepted by sanitize, kept in the file, and drawn at 180 — the file said one thing and the window showed another, with nothing to explain the difference. The floors move to `core::config` beside the font bounds, and the two widget constants are defined from them, which is the direction that cannot drift: the widget cannot be narrowed without moving the floor the file is validated against. Docs updated to the real numbers. The *ceiling* is deliberately not shared. Both panels also cap against the viewport, but a panel wider than its window is a different question from a panel wider than the setting allows, and only the second belongs in `sanitize`. |
||
|
|
9710fb23e6 |
docs(cli): say what exit_code_known means, and what run cannot tell you
The field was listed in `run`'s JSON and nowhere explained, on the surface agents parse. It says whether the server managed to read a status before it stopped waiting; when it did not, `run` exits 1 as a stand-in and says so on stderr, and the flag is the only way to tell that 1 from a real one. A command killed by a signal also comes back as `exit: 1` -- not the 128+N a shell reports -- and with `exit_code_known: true`, because a status really was read. The two are byte-identical in the JSON, so an agent cannot tell a command the OOM killer took from one that exited 1 by itself, and it should not be left to find that out the hard way. That last part is a limitation rather than a choice: the pty crate keeps the signal in a private field with no accessor, and hands back a placeholder code of 1 in its place. `success()` cannot separate the two either, since it is false for both. Reporting the real 128+N needs the child reaped directly -- which the daemon already does for adopted panes, where it gets this right. Found by running the commands, not by reading them. |
||
|
|
3edcc3884a |
docs(cli): agent_status is never sent either — correcting yesterday's table
The previous commit documented the `events` schema and listed `agent_status` as one of the kinds a listener sees. It is not: `ControlEvent::AgentStatus` has the same shape of problem as `PaneExited`, which is how it slipped through — the only two references anywhere are the round-trip test in its own file and the formatter in `tty7-cli`, with no emitter. Auditing all six protocol enums is what turned it up. `ControlRequest` (42 variants), `ClientMsg` (30) and `DaemonMsg` (28) are clean; every `LayoutDelta` and every other `ControlEvent` has a real emitter — `Preempted` and `LayoutResync` in `host::server`, `GuiOpen` beside them — which is exactly what made these two look supported. Agent status is reported the same way a pane exit is: a `layout` delta whose `pane_facts` carries the pane, with `agent.status` on it (`AgentFacts.status` rides on `PaneRecord`). The note now covers both, and points at `tty7 wait` and `tty7 agents` as the supported way to ask about an agent, since neither is built on this stream. Both variants now carry the comment; the recommendation is unchanged — emit them or remove them together with their `event_line` arms, but not in passing, because it is a dialect change. 2951 tests pass. |
||
|
|
a4d0a10a6e |
docs(cli): events had no schema, and promised a pane-exit event nothing sends
`events` is the streaming interface an agent builds on, and it was the one verb whose JSON shape the reference did not give — every other one lists its fields exactly. Now it does: the externally tagged envelope, the four event kinds a listener sees, and the thirteen `layout` delta kinds. The prose also listed "pane exits" as an event type. There is no such event. `ControlEvent::PaneExited` is defined, encodes, and has a line `tty7 events` would print for it — and nothing anywhere constructs one outside the round-trip tests in its own file. `AgentStatus`, `Preempted` and `LayoutResync` have 2, 10 and 6 emitters respectively; this one has none. An agent waiting for a pane-exit line waits for good. What actually happens is checked against a running server: exit the shell in a pane and exactly one event arrives — a `layout` delta carrying `pane_facts` for that pane with `"live": false`, and the pane is gone from `pane ls --all`. The docs now say to watch for that, in a note that says outright there is no pane-exit line whatever the wire protocol's shape suggests. The variant keeps a comment saying the same thing, and that resolving it means either emitting it where the pane is reaped or removing it along with the `event_line` arm — not something to do in passing, since taking a variant out of a wire enum is a dialect change. 2951 tests pass. |
||
|
|
27c0c6c399 |
docs(cli): --orphans kills a run that is still going, not just an interrupted one
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. |
||
|
|
50b1d4a455 |
fix(cli): procs on a pane that does not exist is an error, not an idle pane
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.
|
||
|
|
a5229e18fa |
docs(cli): say what attach, ATTACHED and tab move INDEX actually do
Found by driving a dev instance with the CLI rather than by reading, which is the only way any of these would have surfaced. `ws attach` is documented as "become its controlling client", and the CLI cannot be one: the claim belongs to the connection that made it and the connection ends when the command does, so `tty7 ls` reads unattached again the moment it returns. Its real and only lasting effect is displacing whoever held it — which is exactly what the human output says (`took over from HOST`) and what the docs never did. `ATTACHED` said it names "a GUI window, or another client". A GUI only claims a workspace it is showing as a *remote* one; a window on a workspace of its own machine claims nothing, because there is no second client to arbitrate against. So with a window sitting on it `tty7 ls` prints `-`, and the column read as "nothing has this open" when it means "no remote client holds this". `tab move @TAB INDEX` never said which end `INDEX` counts from. It is 0-based — beside an `@N` address that is 1-based, on the same command line — so `tab move @1 2` moves the first tab to the third slot. Verified against a running server: `@1 0` leaves it, `@1 1` puts it second, past the end clamps to last the way `split --ratio` already documents. `to` in the JSON echoes the number you asked for rather than where the tab landed, which is worth saying since the clamp makes those differ. Also records why `WorkspaceDetach` throws away the one thing it computes: the reply has always been `Unit`, and the dialect is spoken to whatever build was pushed to a remote machine, so widening it to `Bool` would break every server already out there. 2932 tests pass. |
||
|
|
9fc0f331e8 |
feat(tabs): drag a tab in as a pane, and a pane out as a tab (#651)
* feat(tabs): drag a tab in as a pane, and a pane out as a tab A tab dragged by its chip or its sidebar row can be dropped over the panes to become one of them, and a pane dragged by its grip can be dropped on the strip or the sidebar to become a tab of its own. Both carry the panes across as they are: nothing is spawned and nothing is killed, so a shell mid-command, an SSH session or an agent mid-turn keeps running. The landing is read the way a pane drag's already is, minus the middle: an arriving tab has nothing here to trade places with, so a pane's core means "split it the way it is longest". A tab that was itself split arrives with its own shape intact and takes one share of the row or column it joined. A pane on its way out is offered a caret between two tabs, and the last pane in a tab is offered nothing, being a tab of its own already. Picking a tab up no longer switches to it: the strip and the sidebar now activate on the click rather than on the press. Without that the merge cannot be expressed at all — pressing the tab to drag it would put it on screen, leaving no other tab to drop it into. Two things in the machine tree had to follow: * Panes that change tabs are told as PaneMove, one at a time, rather than as a tab closing and another being rebuilt around them. * The tabs the machine already has are reconciled before new ones are created, so a pane leaving for a tab of its own is given up by the old tab before the new one asks to register it. The machine refuses a pane that is in two tabs at once, and the refusal desynced the window. Closes #621 * test(tree-sync): a tab grafted above a whole layout still converges * fix(tabs): keep a click on the close button from switching tabs Switching on the release rather than the press means every click inside a chip or a sidebar row now reaches the row itself, and gpui-component's `Button` does not stop propagation on a click it handled. So one click on a tab's close button ran `close_tab(i)` and then `activate(i)` — with `i` by then naming whichever tab had slid into that slot, which moved the active tab somewhere nobody asked for. A click into the rename field did the same: it switched away from the tab whose name was being typed, and took the focus out of the field with it. Both now hold the click where they handled it, the way they already held the press. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
422808191d |
feat(sidebar): group a tab by its folder when its cwd is not a repo (#631)
`sidebar_grouping` gains a third, opt-in mode, `repo-or-directory`: group by repository home as before, and when the repo probe has landed and answered "not a repo", group under the cwd itself instead of filing every such tab under Scratch. A probe that has not run yet resolves to no decision, so a tab keeps the group it already has rather than bouncing through Scratch mid-probe. The decision lives in one `resolved_group` free function shared by the per-frame key derivation and spawn-time seeding. The default (`repo`) and flat modes behave exactly as before, and an unknown value in an existing config still degrades to `repo`. Knock-on: `machine_mirror::subject_path_of` names a window after its most common group, so in the new mode a window of plain shells takes its name from the most common directory rather than from the first pane's cwd. Closes #620. |
||
|
|
0346e35b40 |
fix(shell): stop injecting into a zsh or fish the user gave arguments to (#629)
The zsh and fish arms of `shell_integration::setup` never checked `has_custom_args`, so a shell the user launched with their own arguments was injected anyway — fish had `-C <script>` appended to its argv, zsh had its ZDOTDIR swapped. Both arms now sit behind the same gate bash, PowerShell and WSL already used, hoisted to a single early return ahead of the dispatch so a new ShellKind cannot silently reintroduce the bug.
Docs now describe what the code does: the `shell` row's own `{"program": "fish", "args": ["-l"]}` example loses integration under this rule, and the shell-integration note distinguishes user-written arguments from the ones detection supplies (Git Bash, WSL).
Part of #624; the native-input-mode half is separate.
|
||
|
|
72db26d15a |
feat(prompt): let the shell's own line editor own the prompt (#633)
Closes #624 tty7's inline editor takes the prompt the moment OSC 133 reports one, and until now the only way to keep it off was to hide the shell's own name from tty7 so integration never armed — which costs the prompt boundaries, cwd and exit codes as well. Someone who binds `history-beginning-search- backward-end` to Up in their zshrc had no way to reach it, and the local history the editor walks instead is per-view: a command run in one pane is not in another's list, so the shell's shared history looked broken too. The new `prompt_editor` switch (Settings -> Input -> Prompt, on by default) hands the line back. Off, every key at the prompt goes to the PTY, so ZLE / readline / fish do the editing and what the user bound behaves as written. Shell integration is untouched by it. The gate is one line in `input_inactive_reason`, which every path that could take the prompt from the shell already asks: keys, IME commits, paste, Tab, the completion and reverse-search menus, the input bar. That is what makes this a mode rather than a special case per key. `shell_owns_prompt` learns the flag too, and that half matters more than it looks: the gap hold and the typeahead record both exist to feed the local editor, and `flush_typeahead` sends ^U to erase the line before moving it there — on a line only ZLE is editing, that erases the user's work. Ctrl-R landing on the PTY also stops raising the missing-integration notice: the shell owning it is what was asked for. Turning it off mid-line hands what is typed to the shell the way an unknown chord does, so the text is still on the prompt to finish. Live panes follow the switch, including a hand edit of config.json in another window. Tab completion and history search are menus tty7 opens inside that editor, so the page greys them out and says why while it is off. Only their text dims — a switch already draws its thumb at 35% when disabled, and dimming the row on top of that leaves a pill with nothing visible in it. Their stored values are left alone and come back with the editor. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
1424da0891 |
fix: nine UX fixes across the diff overlay, layout, settings and pane spawn (#623)
Found by driving a dev instance and measuring what an idle window costs. Two of them were frame loops that never stopped. A `Head` diff overlay calls itself stale when the cached git status disagrees with the snapshot on screen, and every landed probe wakes that check by touching the cache — but the read published its counts and left the branch behind, so a branch switched outside tty7 made the disagreement permanent: two `git` processes a lap, forever, with `refreshing…` pinned to the header and an idle window at 7% of a core. And the home page asked for a frame sixty times a second to change one glyph's opacity twice, which made a window with nothing open in it eight times more expensive than one running a shell. Both now settle: the diff read publishes the branch it found, and the home cursor flips a bool on a timer the way the terminal's own does. The rest: - The tab sidebar and the right panel each capped themselves at half the window and knew nothing about the other, so together they could take all of it — 260 points of terminal on a 720-point window. Both now cap at whichever binds harder: half the window, or what is left after the terminal's floor and the other panel's floor. The same cap bounds the drag, so a panel dragged to its limit stays where it was dropped. - Only a HEAD diff may correct the sidebar's counts. Those numbers mean `git diff --numstat HEAD`; an unstaged or staged patch answers a smaller question, so opening an untracked file from the Source Control panel took the staged lines off the total on the click. - An untracked row in the diff overlay had no click target, and once focused could not be left — the breadcrumb looks the path up in `files`, where an untracked file has no entry. Both ends fixed. - A new pane keeps the name its directory was reached by. `cwd()` alone loses it: the shell falls back to `getcwd()`, so `/tmp/x` became `/private/tmp/x` in every tab opened from the first. `PWD` carries it, and POSIX has the shell discard a `PWD` that names the wrong directory, so this can correct the name and cannot invent one. - The settings search now sees into the Keybindings page, which is generated from the binding table rather than the static index — so searching for a feature finds its shortcut, and the page filters to the matches. Closes #444. - The settings reading column is centred rather than pinned to the nav: on a window as wide as the display it was made for, 640 points of settings sat beside 1600 points of nothing. - `New Workspace…` takes the ellipsis its three sibling actions already carry — it opens a form asking for a name and a host. Every fix has a test. The re-probe loop is pinned end-to-end with `render_probe::draws() == 0` against a real repository, confirmed to fail on the old behaviour before it was kept. |
||
|
|
a2d53a9597 |
fix: 19 项低危 UX 问题(#584–#602) (#615)
* fix(scm): say what "discard all" actually discards (#594) The group-level Discard prompt asked to "discard every change in this repository", but discard_all_ops has only ever swept unstaged edits and untracked files — staged changes survive, as the function's own comment notes. Users confirmed under one belief and the code kept another. Narrow the prompt to the operation's real footprint, in all three languages. * fix(scm): keep the amend toggle when its confirmation is cancelled (#595) scm_commit cleared scm.amend when Commit was pressed, before the "rewrite the last commit?" prompt. Answering Cancel returned to a panel whose amend mode had silently been dropped, so the next Commit created a brand-new commit — exactly what the user had just declined to risk. The toggle now clears where scm.committing arms, at dispatch in run_git_op, extending the rule the armed flag already followed: a cancelled confirmation leaves nothing behind. * fix(cli): answer a wait timeout in the success path's JSON shape (#589) The 124 branch returned {pane,status,timed_out} while a finished wait returns {pane,status,matched,stale,activity,message,session_id} — so the one branch a consumer writes error handling for was the one missing its fields. The timeout now carries the full shape plus timed_out, and reference.mdx documents the schema and the flag. * fix(cli): report a failed wait on stderr, even under -q (#590) wait's failures are structured exits (124, or 1 when the pane died first), so they never passed through the anyhow path whose eprintln is the only thing quiet mode cannot silence — contradicting the documented "errors still go to stderr". Both exits now print their headline to stderr, the discipline pane close already established. * docs(cli): describe owner as the workspace that may attach (#591) commands.md still claimed the CLI stamps a literal "tty7-cli" owner on the panes it spawns — the behaviour the orphan-workspaces work removed, because an owner names the workspace allowed to attach and a stranger's stamp got the panes respawned. Every spawn path now writes the workspace id, or nothing while the pane is still unfiled. Bring commands.md in line with reference.mdx, and note the absent case in both. * docs(cli): close five contract drifts between the tables and the code (#592) - The key tables listed pgup/pgdn as aliases but not pgdown, which the parser has always taken; both references name it now. - "Case-insensitive" was flat wrong for Alt: M-x keeps its case because Alt is a prefixed ESC, unlike Ctrl. Both references note the exception. - procs' ports JSON has carried addr since the field exists; both schemas show it. - TTY7_WS is tab ls's default too; both environment tables say so. - split --ratio's clamp to [0.05, 0.95] was discoverable only in code; both split sections document it. * fix(cli): doctor exits 1 when the server is unreachable (#592) doctor is the verb people run when something is not working, so an unreachable server is *the* finding — not a row to exit 0 over while `tty7 doctor || alert` never fires. The table and JSON still go out (the context rows are the other half of what doctor is for), and stderr carries the headline under -q. MockBackend grows an `unreachable` flag so the branch is testable; no Status/Routes round-trips happen once hello has failed. * fix(settings): refuse a Start-in path that names no directory (#601) The custom path was stored unchecked, and the daemon's picker then skipped it — not a directory — so every new pane silently started in the fallback directory and the typo read as a tty7 bug. Settings now marks the field red and refuses to save, the proxy row's pattern (#551), with the red line and the commit gated on one shared predicate so they can never disagree; a hand-edited config.json holding such a path gets a log::warn! naming it at the moment the fallback engages. * fix(terminal): rescan search highlights when the pane's width changes (#586) A match point is an absolute (line, column) against the width it was scanned at, so a column change reflows the text out from under every highlight. Output rescans them (Wakeup → refresh), but a quiet local pane has no output coming and the drift outlasted the resize indefinitely. set_grid_size now rescans on a column change with the output path's discipline — selection and scroll untouched — and takes the Context it needs to do so; a rows-only change reflows nothing and stays cheap. * fix(terminal): keep the grid selection when the search bar opens and closes (#584) The selection that seeds the query is the thing being searched for, yet opening the bar ran recompute_matches' unconditional clear — right for its other callers, where the user *changed* the query and the old selection names nothing — and closing cleared it again, so select → Ctrl+F → Esc lost the selection every time. The seeded selection is now restored after the opening scan, and close_search no longer clears; a query the user actually changed still retires the stale selection, the discipline refresh_matches_after_output already stated. * fix(tabs): a zoomed pane stays zoomed across a tab switch (#599) Zoom was a window-level value that activate() cleared unconditionally, so looking at another tab and coming back restored the split layout — while a zoom is a tab's temporary view state, like its focused pane. It now rides with the Tab: activate stashes the outgoing tab's zoom and brings the incoming tab's back. The clears that genuinely reshape the layout (drag, split, close) still stand, and a stashed zoom whose pane exited while the tab was away is validated away rather than restored. * fix(tabs): track an open rename box by tree id, not index (#598) The rename box held only an index, which drifts the moment any other tab closes or the strip reorders — so close_tab_inner and apply_tab_order threw the half-typed name away on any unrelated tab event, and a reorder mid-rename still left a window where the commit landed on whichever tab had taken the index over. The box now names its tab by tree id end to end (start, render match, commit): only closing the renaming tab itself ends the rename, and the name lands on the tab the box was opened on wherever it has since moved. * fix(i18n): move seven hard-coded user-facing strings into the language tables (#602) Seven spots rendered English no matter which UI language was set: the shell-integration notice that explains why a wrapper was blocked or never engaged, the titles a pane wears once its process exits or the server loses it, the loopback forward's failure line, the tray tooltip that lists running agents (whose separator also wanted a CJK enumeration comma), the cursor-shape choices in settings, the command palette's empty-result hint, and the updater's install hint. Each is a L10nKey now with en/zh/ja entries, so the parity guard keeps them translated from here on. The palette's empty state was also wrong in content, not just language: every menu suggested connecting over SSH when nothing matched, including menus that have no hosts in them. The hint now only appears in the quick-connect menu; everywhere else the palette suggests a different search instead. Verified on Linux: the title/palette/tray suites (48 tests) and the i18n parity guard all pass. * fix(terminal): show remote path completion is listing, and say when it fails (#585) Tab-completing a path on a remote workspace had two silences. The whole network round-trip painted nothing, so a slow link read as a broken Tab key; and a listing that failed was unwrapped into an empty candidate list, so "the directory is empty" and "the listing never happened" ended in the same nothing. A pill over the pane's bottom-right corner — the style the integration notice already uses, factored out — now says the listing is running from the moment it starts, and a failed listing sets a notice with its error instead of the empty vector. The failure pill stays until the next keystroke dismisses it, and the trailing notify after an empty listing closes the menu brings the "listing…" pill down with it. Verified on Linux: the new gpui test covers the idle/listing/failed states, and the neighbouring completion tests still pass. * fix(files): quote cd Here / Insert Path for the shell the pane runs (#593) Both file-tree actions wrapped a path with spaces in POSIX single quotes whatever the focused pane's shell was. In cmd.exe a single quote is an ordinary character, so `cd 'C:\Users\me\My Documents'` split at the first space and cmd complained about 'C:\Users\me\My' — while the same action was fine in PowerShell and bash, which is why only cmd users ever saw it. shell_quote_for takes the pane's shell program (the pane already knows it — the settings page lists it) and picks double quotes for cmd.exe, single quotes for everything else; an unknown shell keeps the POSIX form, and a path that needs no quoting stays bare either way. Windows paths cannot contain a double quote, so the cmd form has nothing to escape. * fix(cli): pane close fails for a pane the registry does not hold (#588) `tty7 pane close %99` printed {"closed":[99]} and exited 0 for a pane that never existed. The workspace path cannot drift this way — PaneClose answers — but an orphan has no workspace to route through, so close hangs it up directly, and that kill is fire-and-forget: the daemon never says whether it knew the pane, so Ok(()) only ever meant the bytes reached the socket. A reaper script chasing the orphans `pane ls --all` points at would read the ghost success as cleanup done. The direct path now reads the running-pane registry once per batch and refuses ids it does not hold: the miss lands in `failed` with exit 1, next to the failures kill itself can report. A pane that exits between the listing and the kill is gone either way, which is what closing it wanted, so that race still reports closed. * fix(session): a launch that leaves workspaces running says so (#597) Quitting with several windows open and starting again restored only the most recent one; every other open window was marked detached — panes alive, nothing on screen, the only trace a "left N detached" log line. The workspaces were reachable from the sidebar, but nothing said they existed, so they were easy to forget entirely. restore_one now returns how many windows it detached, and both launch paths (normal startup and the CLI-driven open) push an in-app notification into the restored window naming the count and where to reopen them. The count rides the return value rather than firing the notification inside the store, because the store has no window to notify in — and a launch that detaches nothing, like the reattach-the-last- closed case, stays silent. * fix(switcher): list the local machine's orphan panes, with a way to close them (#596) A pane whose workspace went away — an interrupted `tty7 run`, a forgotten workspace that kept its shells — was invisible everywhere in the GUI: not in the sidebar, not in the switcher, not in the tray. It kept its process and its memory, and the only way to even learn it existed was the CLI's `tty7 pane ls --all`, which a GUI-only user never runs. The switcher's local machine group now carries a "Background panes" block under its workspace rows: one line per live pane the daemon's registry holds and no workspace does — id, owner, cwd — each with a Close button. The listing is the same PaneClient::list the CLI's reaper reads, fetched off the UI thread when the panel opens; closing kills and then re-lists, so a pane that survived simply stays on the list instead of pretending to be gone. The block steps out of the way while the search field holds a query, which narrows the panel to workspaces. Local on purpose: a remote machine's orphans belong to its own daemon, and routing a listing per host is what the CLI reaper is already for. The block joins no keyboard navigation — the panes are not workspaces and the arrows have no business landing on them. * fix(updater): keep Inno's progress window on screen during the install (#600) The Windows installer ran /VERYSILENT, so from the app quitting for the update to the watcher bringing the new build up — tens of seconds, longer under an antivirus scan — the screen held nothing at all: no window, no progress, no tray note. "Clicked update, the app vanished" reads as a crash, and double-clicking the icon does nothing while the files are being replaced. The installer now runs /SILENT instead. Nothing about the flow becomes interactive — /SP-, /SUPPRESSMSGBOXES, /NORESTART and /CLOSEAPPLICATIONS are untouched — but Inno's own progress window stays on screen for the gap, which is exactly the span the user had no word about. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |