mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 00:02:23 +00:00
c69a39f7a08fb448ab2a1cd260768be99b829df2
1329
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c69a39f7a0 |
docs(changelog): five more, bringing the backlog to a third
A clamped setting said nothing, so a config file asking for `font_size: 999` went on claiming it while the app ran at 256. `tty7 procs` on a pane that never existed printed "nothing running in this pane" and exited 0, which an agent cannot tell from an idle pane. A terminal title carrying a newline painted its tail over the tab strip. The refusal to write the machine file named neither the operation nor the file, on a machine where that directory is what the reader has to go and fix. And the clipboard-read policy was an upstream crate's default rather than tty7's own decision, one release away from handing any program in a pane whatever is on the clipboard. Thirty-eight of the branch's 114 user-visible fixes are now written up, which is the share 26.8.3 shipped with. The rest are smaller — wording, log lines, internal guards — and can go in as they are reviewed. |
||
|
|
ecbe43f5ab |
docs(changelog): four more, one of them the largest on the list
Every pane ran with SIGTERM blocked. The daemon blocks it on its main thread so one waiter can run the shutdown save, and that mask crossed `fork` and survived `execve` — so `kill`, `pkill`, `timeout`, a supervisor and a CI cancellation could not stop anything running in a pane, and only `kill -9` would. That is the biggest thing still missing from these notes. With it: panes the window put down while squaring its layout were never hung up, because that path dropped the daemon's reply; a killed server left its throwaway shell directories behind, 3,850 of them on this machine; and `send`, `capture` and `procs` reported a raw `os error 2` instead of saying the server was down, unlike every verb that goes to the control socket. Thirty-three written up. No code changes. |
||
|
|
58145dace1 |
docs(changelog): four more, three of them things a user would see
A newline in a filename, a remote name, or a completion description broke the row drawing it — the layout breaks on a newline whatever the row asks for, so the row grew and everything below it slid. 122 of the shipped completion specs carry one. A tab path starting at `~` was drawn with a leading ellipsis that stood for nothing, while an absolute path of the same shape kept its root. An internationalised domain came back as the bare scheme `https://`, offered as a clickable link that opens nowhere. And the PNG decode ceiling on the untrusted graphics path was the image library's 512 MiB default rather than tty7's own, in the GUI process. Twenty-nine written up. No code changes. |
||
|
|
12ab786e64 |
docs(changelog): five more, including two nobody could have noticed
Search with case sensitivity off found nothing when the query carried a capital, because the regex engine's own smart-case rule decided instead of the toggle — so the toggle looked broken exactly when someone typed `Error`. The SCM confirmations answered neither Escape nor Return outside English, and put Cancel where every other dialog puts "do it"; those are the dialogs guarding discard, reset and amend. Quick connect read a typed `-p 22` as silence and connected to whatever the alias said instead. A workspace's port forwards could stay bound for the life of the daemon with nothing logged, because an unaddressable route and an unanswered request both reported a clean teardown. And the published `tty7 wait && tty7 capture` idiom came back empty whenever the pane exited, which is one of the three states `wait` waits for by default. Twenty-five written up. No code changes. |
||
|
|
fa392a9558 |
docs(changelog): write up five more fixes, including two that lost work
Continuing the backlog. These five are the ones a reader of the release notes would most want to have been told about: macOS in-app updates failed 100% of the time, on every build and every channel, because the updater read `codesign`'s designated requirement off the wrong stream — the only way out was downloading the app by hand. Closing a tab threw away unsaved editor edits with no dialog, by the everyday route: edit a file, click back into the terminal, press ⌘W. A client opening a remote workspace could rename somebody else's — in the reported case one holding nineteen live panes. The ignore chain let a whitelist inside an excluded directory undo the exclusion, which git does not, and did not read `.git/info/exclude` at all. And `tty7 events` exited 0 when the server went away, so a reader whose lines stopped had no code to branch on. Twenty of the backlog written up so far; the list still runs longer than this. No code changes. |
||
|
|
d2a894c4a1 |
docs(changelog): write up ten fixes that shipped without an entry
This branch carries 114 user-visible fix commits and had added six changelog bullets. Recent releases run between a third and nearly all of their fixes — 26.8.3 wrote up 85 of 251, 26.8.2 wrote up 71 of 74 — so five percent is well under what this project does, and the ones missing are not small. Ten of the largest, in the order someone would care about them: a `kill -9` leaving the server unable to start again and no way to find out why; SIGTERM armed too late to save any pane's screen, so a shutdown or a logout cost every one of them up to thirty seconds; an SFTP download renaming over a file it did not pick, and a dangling symlink reading as a free name; a failed update deleting the only backup and leaving nothing at the install path; `--cwd` naming a directory that does not exist and running the command somewhere else with exit 0; a probe timeout disabling shell integration for a whole host for the life of the daemon; a signalled command reported to agents as an honest exit 1; a refused split leaving a shell nothing holds; a window reshape leaving its old shells running; and three secret-carrying types still reaching the log. No code changes. More to follow — the rest of the list is longer than one sitting. |
||
|
|
16370ed30a |
docs(cli): record what the capture settle timer actually costs
`tty7 capture` takes ~323ms against a local daemon and neither constant said why. Measured: a pane that has printed nothing costs the same as one holding 22,000 lines, `--plain` costs the same as raw, and lowering the replay grid's floor from 10,000 lines to 100 changes nothing. The grid work is ~20ms; the rest is `REPLAY_SETTLE` running out. `tty7 pane ls` over the same socket is ~5ms. It is paid because the replay has no end. The daemon sends `Size` then `Snapshot` per ring segment and stops, and the only way to know it has stopped is to wait. Live `Output` cuts the wait short, so a busy pane returns at once — but the idiom the reference recommends is `tty7 wait … && tty7 capture …`, and `wait` returns exactly when the pane has gone quiet. The recommended path always pays in full. No behaviour change; this is the note that was missing. It also records the two wrong ways out, because both look attractive from the outside. Lowering the timer trades a bounded delay for silently truncated output on a slow link or a loaded machine. An end-of-replay message is the real fix and is *not* additive — `DaemonMsg::read` rejects an unknown kind outright, so a daemon sending one to an older client breaks it. That needs a `PROTOCOL_VERSION` bump with the daemon gating on what the peer announced, which is a larger change than a comment. |
||
|
|
a77d691dad |
fix(cli): send a bad tab address to the table that has the @ numbers
Five places tell someone where tab `@` numbers come from, and they had drifted to three different answers. The three `--help` strings say `tty7 tab ls`. The two runtime errors — the ones a person actually meets, because they only appear once something has already gone wrong — said otherwise. `'@deadbeef' is not a tab address — @7 as numbered by `tty7 ls`` was simply wrong. `tty7 ls` lists workspaces: `WORKSPACE NAME TABS PANES ATTACHED`, where TABS is a count. There is no `@` anywhere in it. Someone following that instruction finds nothing and has to go looking. `no tab @N — `tty7 pane ls` shows the @ numbers` was true but sideways. `pane ls` does carry a TAB column, but the verb the person is already holding is `tab`, and `tab ls` is the table with one row per tab and the number in the first column. Both now say `tty7 tab ls`. The guard reads all three sources for anything teaching where the numbers come from and requires that answer, so a sixth place — or a sixth answer — is caught rather than assumed. It skips its own test module, which names the phrases it searches for. |
||
|
|
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. |
||
|
|
719e35ec59 |
test(machine): pin the tty7 events kinds to the CLI reference
`docs/cli/reference.mdx` says of the event stream that "the kinds and payloads are stable; the prose is not" — a promise to whoever is writing a loop against it. The stream is externally tagged, so a reader matching on known kinds meets an object it cannot place the moment a `LayoutDelta` variant is added without the page being told. All thirteen currently agree, in both directions. Nothing was holding them there. Names come from the enum rather than a list kept beside the test, so it cannot quietly agree with itself, and only the sentence that enumerates the kinds is read — a name appearing elsewhere on the page does not count. The scan asserts it found more than ten variants first, so a change in how the file is written fails loudly instead of passing for the wrong reason. Checked both ways: adding a variant, and removing a kind from the page. |
||
|
|
8390fa54e2 |
test(sftp): pin the one thing only the components walk refuses
`safe_local_name` checks a remote-supplied filename three ways: literal `.` and `..`, any `/` or `\`, and finally that `Path::components` sees exactly one `Normal`. Deleting either of the first two left every assertion passing — they cover each other on Unix, which is defence in depth working rather than a fault. The components walk is different. On Windows it is the only one that refuses a drive-relative name, and `Path::join` with one does not append, it replaces: `…\Downloads`.join(`C:evil.txt`) is `C:evil.txt`, resolved against the process's current directory on that drive. The name comes off a remote directory listing, so the server picks it, and the download would land outside the folder it was told to write into. Nothing asserted that. Now something does, under `#[cfg(windows)]`, with a `#[cfg(not(windows))]` counterpart asserting the opposite — because the answer genuinely differs: on Unix `C:evil.txt` is an ordinary filename with a colon in it and refusing it would be wrong. That difference is precisely why the decision goes through `Path::components` instead of a list of forbidden characters, and the pair says so. Stated plainly: on Unix the components walk has no observable effect that the other two checks do not already give, so no test running here can pin it. The Windows half is where it earns its place, and CI is where that half runs. |
||
|
|
3e1b8bd620 |
test(agent-hooks): pin that the hook is inert outside tty7
Removing the `TTY7` marker check from `run_agent_hook` left every test in the repo green. That check is the whole of the promise the generated hooks make: they are installed into the user's own Claude, Codex, Gemini and Copilot config, so they run on *every* invocation of those agents — including the ones in iTerm, Terminal, VS Code, or over plain ssh. What they emit is `OSC 777;notify`, urxvt's desktop-notification convention, honoured as such by more than one terminal. Without the check those terminals would get a notification, or a mouthful of escape bytes, every turn of an agent that has nothing to do with tty7. The three plugin bridges carry their own `process.env["TTY7"]` gate and are already tested for it. The nine config-driven agents have only this one. Order is pinned as well as presence: the check sits ahead of the stdin read because stdin is a pipe the caller may never write to, and a hook that blocks is a hook that hangs the agent. Checked at source rather than exercised. Exercising it means varying an environment variable, and this suite runs in one process across many threads where `set_var` is unsound; a three-line gate is not worth making the rest of this file unsafe to test. Verified against both mutations — removing the check, and moving it below the stdin read — each named precisely. |
||
|
|
a9faf7cf07 |
test(ssh): pin the two host-key decisions nothing was checking
Mutating the host-key policy table showed it is well covered: accepting a revoked key with verification off, and silently accepting an unknown or a changed key, each fail the suite. The carrying-out was not. **Saying no had nothing holding it.** Deleting the `if !accept` guard in `apply_decision` — so a person shown a changed host key declines and connects anyway — left every test in the repo green. `accepted_and_remembered` is tested on its own; nothing checked that the caller acts on its answer. Now four responses are refused: no, no-with-the-checkbox-still-ticked, cancelled, and a response of the wrong shape. It touches no filesystem, because none of them reach the recording branch. **Dropping the superseded line had nothing holding it either.** The order is the point — `check` answers `Known` on any same-algorithm match, so appending without dropping leaves the key this one replaces trusted for good, which is how a host that rotated away from a compromised key goes on accepting the old one. There was a test named after that bug, but it called the two halves itself, so removing the `forget_superseded` call from `apply_decision` changed nothing it could see. The sequence is now one function, `record_trusted`, with the order and the "if the drop fails, append nothing" rule stated where it happens. Production and that test call the same thing, so neither can drop a half alone. Verified by re-running both mutations against the new tests; each fails, naming what went wrong. |
||
|
|
69ac479c97 |
test(kitty): prove the temp-dir check is what saves a file, not the OS
`a_temp_file_transfer_outside_the_temp_dir_is_read_but_not_deleted` sent a `t=t` transfer at `/etc/hosts` and asserted it survived. It survives whether or not the check exists: the suite does not run as root, so `remove_file` fails on its own and `let _ =` swallows that. Removing the temp-dir check outright left the test green — and that check is the one standing between an escape sequence and `remove_file` on any path the user can delete, which the comment beside it names as `~/.ssh/id_ed25519`. The `/etc/hosts` half stays: it is the case that is never under a temp directory on any host we build for, and it proves reading still works. Added beside it is a victim the test user demonstrably *can* delete — created under `CARGO_MANIFEST_DIR`, with a probe file removed right next to it first, because a survivor proves nothing unless deletion was possible. If the checkout itself lives under a temp directory the second half returns early rather than asserting the opposite of what it means. Verified: dropping the temp-dir check now fails it, naming what was unlinked. |
||
|
|
bf68eb9f20 |
fix(kitty): open a file transfer non-blocking, so the fifo check can run
`read_file` refuses anything that is not a regular file, and the comment above it says why: fifos and devices, "where the open itself can block". The check sat one call after the open. `open(O_RDONLY)` on a fifo blocks until a writer arrives — for a fifo nobody intends to write to, forever — so the guard never got the chance to refuse the thing it was written to refuse. Reachable the same way everything else in this path is: the name comes out of an escape sequence, so `mkfifo` plus one line of output, or any file someone `cat`s, is enough. `resolve` runs on the daemon's pane reader thread, so the pane's output stops there and does not come back while the process lives. The open now carries `O_NONBLOCK`, which returns immediately on a fifo and lets the existing check refuse it. On a regular file the flag changes nothing — it is not consulted by `read`, and reads of regular files do not block regardless. Found by mutation: removing this check, the shm-name check, and the temp-dir unlink check each left the whole workspace suite green, so all three were being asserted only as pure functions and never where they are wired in. Testing the first one properly is what surfaced the ordering. The guard resolves a transfer naming a real fifo on a worker with a ten-second deadline, because without the fix this does not fail, it hangs — and a test that hangs says much less than one that reports what happened. Verified: with the blocking open restored it fails at ten seconds with that message. |
||
|
|
ba28a8224e |
test(kitty): hold the remote transfer gate for t=f and t=t, not only t=s
The gate that stops a remote pane resolving a file or shm transfer had one test and it sent `t=s`. Narrowing that gate to shm alone — so `t=f` and `t=t` resolved on a remote pane — left the entire workspace suite green. Checked, not assumed: 43 kitty tests and every other test in the repo passed with the guard weakened. That is the gate that matters. `query_reply` is the polite half; it tells a well-behaved sender not to bother, and its own test already probes all three media with a comment saying why. This is the half that holds when the sender ignores the refusal, and the sender on a remote pane is the far end of an ssh link naming a path on *this* machine. `read_file` will open whatever it is given — bounded and regular-files-only, but given. Now all three media are sent at a remote parser and refused, and all three at a local parser and accepted, so the guard is pinned to refusing on locality rather than on the medium. The failure names the path that would have been read. Checked against both directions: narrowing the gate to shm, and refusing regardless of locality. No behaviour change — the guard was already right. This is the test that would have noticed if it stopped being. |
||
|
|
7368872e04 |
docs(protocol): state at PROTOCOL_VERSION when it moves
`CONTROL_VERSION` carries a long comment on what it is and what happened the time it did not move. `PROTOCOL_VERSION` had none at all, and the rule for it is not the obvious one: adding a `#[serde(default)]` field deliberately does *not* bump it, because this number gates the remote handshake and bumping would turn away every older `tty7-server` over a field it could safely ignore. That reasoning existed, twice, but only as asides — on `AuthPromptKind::KeyPassphrase::rejected` four hundred lines down, and again in the test that pins both directions of it. Someone changing a message reads the constant, not the field that once raised the question. So it is said where the decision gets made: additive-with-default does not move it, removing or renaming or retyping a field does, and a new variant depends on whether the sender can know the peer is new — if it cannot, bump. No behaviour change. |
||
|
|
9ca4b27ec9 |
test(control): pin the wire shape to CONTROL_VERSION
The comment on `CONTROL_VERSION` already records what happens when the dialect and its number come apart. Between v5 and v6 the machine tree replaced four verbs, `GitStream` arrived with its chunk and end events, and two replies went away — "all of it shipped against a number that never moved, so every one of those servers still answers the hello and then drops the link on the first call." Nothing prevented that, and nothing prevented a repeat. Every message this file puts on the wire is now fingerprinted and pinned beside the version it belongs to. Change a shape and the test fails, which is the moment to decide whether a peer built against the current version can still read it — and to bump the number when it cannot, rather than finding out from a user whose server answers the hello and then goes silent. Comments and formatting are stripped so wording and rustfmt do not trip it; `#[serde(...)]` lines are kept, because renaming a field or giving it a default is exactly the kind of change the compiler cannot see and a peer cannot survive. `ControlClient` is excluded — it holds the connection, not a message. The scan asserts it found the ten types before comparing, so a change in how the file is written fails loudly instead of pinning nothing. The failure prints the value to paste back, and moving `CONTROL_VERSION` alone fails too, so the pair cannot drift in either direction. Checked against both: bumping the version without re-pinning, and adding a `#[serde(rename)]` to a field — the second being the case that compiles cleanly and would otherwise have shipped. |
||
|
|
9332f2e44b |
test(shell-integration): hold every shell to the same reported facts
Five shells ship an integration script and only PowerShell had a test that its OSC 133 marks were all there. The other four were covered piecemeal — zsh's ZDOTDIR dance, bash's rcfile chain, the WSL bootstrap — none of which would notice a mark going missing. Missing is the bad case precisely because it is silent. `133;A`/`B` bracket the prompt, `C` says a command started, `D;<code>` says it finished and how; `7;file://` is the working directory. Drop one and `tty7 wait --until free` stops waking, the tab strip stops showing exit codes, the running dot stops lighting — for that shell only, with nothing logged, because a mark that was never sent leaves no trace to find. The floor is now asserted for all five. `D` is checked with its separator, since a bare `133;D` means "finished, code unknown" — a different answer from the one all five scripts actually give. Deliberately not asserted: an OSC 0/2 title, which only bash and PowerShell send; on zsh, fish and nushell the user's own prompt owns the title and tty7 does not take it from them. Verified by making fish send a bare `133;D`, which it names. |
||
|
|
677a1dffe1 |
test(git-log): drive the lane allocator with random DAGs
`assert_lanes_line_up` already states the property the commit graph rests on — a lane carries at most one visible line at any horizontal cut, and what leaves a row's bottom is exactly what enters the next row's top — but only hand-written shapes were ever fed through it. Those cover the cases somebody thought of. Two generators now do the rest. The first lays out 4,000 topo-ordered DAGs (up to forty commits, up to four parents so octopus merges appear, roots appearing mid-page) and checks the standing invariant plus three the layout implies: one row per commit, a root sends no `Out`, and a `Pass` never crosses the row's own node lane. The second is about the thing the design is built around. `LaneAlloc` is append-only so a later page extends the graph without touching a row already on screen; 1,500 DAGs are laid out whole and again split at every position, and the two must be identical. That property is invisible to a single-page test and is exactly what a page boundary would break. Nothing was wrong — both pass as they stand. Checked against two injected regressions: not releasing a lane its commit owned fails the first, and dropping the pending map between pages fails the second, naming the seed and the cut. Together they run in half a second. |
||
|
|
e80fc24ce0 |
test(protocol): round-trip every wire message, and keep it that way
`client_roundtrip` and `daemon_roundtrip` are hand-written lists, so a variant added later goes untested unless somebody remembers it — and nothing said otherwise. Eleven had drifted out of the two lists; five of those (`Handoff`, `QueryProcs`, `Image`, `DeleteImage`, `Procs`) were not encoded or decoded by *any* test in the module. `Image` is the one carrying arbitrary binary through a length-prefixed frame, which is exactly where a framing bug would hide. All eleven are added and all of them round-trip; nothing was broken. `Image` gets 70,000 bytes cycling through every value, so it crosses the read buffer, plus an empty one; `DeleteImage` gets a payload containing the APC bytes a naive framer might resynchronise on; `Procs` gets both a populated `PaneProcs` and an empty one; `AuthResponse` and `SshStatus` get each of their own shapes. The guard reads this file's own text: the variants `pub enum ClientMsg` and `pub enum DaemonMsg` declare, against the ones the matching test names. One list instead of two, for the usual reason. It asserts it parsed more than twenty variants first, so a change in how the file is written fails loudly rather than passing for the wrong reason. Note what this does *not* need to catch: a brand-new variant does not compile until `encode` and `read` handle it, so the compiler has that half. What it catches is the half the compiler cannot — a variant that is wired up correctly and simply never tested. Verified by dropping `QueryProcs` back out of the list, which it names. |
||
|
|
f8b000ed9d |
fix(pane): clear the agent session when one agent hands over to another
`apply_agent` cleared `agent_session` only on the way to `None`, on the assumption that a pane always passes through its bare shell between agents. It does not have to. The foreground probe runs every 500ms and reads the process-group leader; `claude; codex` starts the second agent microseconds after the first exits, so two consecutive probes see `Some(Claude)` then `Some(Codex)` and the agent-less moment in between is never sampled. Everything the first agent reported then carried across. Codex arrived already `Done`, wearing Claude's closing message, holding Claude's `session_id` and the argv Claude was launched with. The tab drew a finished turn Codex had not taken, `tty7 agents` agreed, and Resume built `codex --resume <a uuid Codex has never issued>`. A change from one agent to another now clears it, alongside the change to no agent. Deliberately not on `None` -> `Some`: an agent's own hooks reach the pane before the process probe does — `apply_agent_signals` creates the session and sets `st.agent` from the event — so the probe catching up is not a change of hands, and clearing there would throw away the arriving agent's own state. The guard covers all three transitions and was checked against the old condition, which fails it with Claude's session id printed inside Codex's. |
||
|
|
5d3990dc3e |
test(agent-hooks): pin every hook event name to what the daemon reads
Twelve agents' hooks are rendered by this module and read by
`parse_agent_event` in another crate, and nothing checked that the two agree
on the names. `run_agent_hook` does not validate the event it was handed — it
puts the string straight into the OSC payload — and the far end drops an
event whose name is not an `AgentEventKind`. So a typo in a hook template, or
a rename of one variant, turns that agent's status reporting off entirely:
the dot never lights, `tty7 wait` never wakes, `tty7 agents` shows nothing,
and there is no log line anywhere because neither side thinks it failed.
The guard installs all twelve into a scratch home and reads the artifacts
back, so it follows whatever route a name actually took rather than trusting
a table the renderer might not use. Two shapes, because the agents split into
two kinds: nine write the event on a command line (`… agent-hook claude
tool-complete`), and the three plugin bridges — opencode, pi, omp — build the
command at run time and name the event in an `emit("…")` call instead. Both
are collected and both are checked. Comment lines are skipped: every bridge
spells the command out in its header, in three different comment syntaxes.
Verified against injected typos in both shapes — one in `CLAUDE_HOOK_EVENTS`,
one in a bridge's `emit` — and each is named in the failure with the agent
that would have gone quiet. As it stands all thirty-odd names parse.
|
||
|
|
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. |
||
|
|
209ebe6ca5 |
fix(gitignore): obey core.ignorecase, as git does
Differential-tested the ignore chain against `git check-ignore` on ~50 patterns — anchored, directory-only, `**` at either end and in the middle, whitelists under ordinary and excluded directories, character classes, escaped spaces, comments, CRLF, the `*` / `!*/` / `!*.c` recipe — and it agreed with git everywhere except case. `git init` probes the filesystem and sets `core.ignorecase = true` on a case-insensitive one, which is every stock macOS and Windows install, and git's ignore matching then folds case. The chain matched case-sensitively no matter what. So a `.gitignore` whose pattern differs in case from the name on disk diverged: `Build/` against a `build/`, `*.LOG` against an `a.log`. git calls those ignored. The tree drew them as tracked, and — worse than a styling difference — expanded and watched a directory git never descends. The capitalised build directory is not a corner case; the .gitignore templates and the tools that create the directory routinely disagree about it. Read from `core.ignorecase` rather than probed, because config is what git obeys and someone who set it false on a case-insensitive disk means it. Once per root, kept across `clear()`: editing a `.gitignore` cannot change the setting, and a git spawn per keystroke in the ignore file would buy nothing. The matcher cache is now keyed by the fold flag too, since a repository nested inside another can answer differently. The guard sets `core.ignorecase` explicitly in both directions rather than leaving it to the probe, so it asserts the same thing on a case-sensitive disk; its answers are `git check-ignore`'s under each setting. Checked against both injected regressions — never folding and always folding. |
||
|
|
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. |
||
|
|
21ddd01bc9 |
fix(updater): record a panic where someone can find it
`crash::install` puts a panic hook in front of `crash.log`. The GUI installs it and the server installs it. The updater did not — and of the three it is the one that needs it most. It runs *detached*, after the GUI it is replacing has exited, so its stderr is attached to nothing anybody will read. And it is doing the one job in this product that can leave an install broken. A panic mid-swap was therefore silence: the app does not come back, `tty7-updater.log` stops mid-sentence, and there is nothing anywhere that says why. All three of its `cfg`'d mains install it now, so the role travels with whichever platform failed. `crash.log` is the file the other two roles already write, in the config directory, so the three land in one place in the order they failed. The CLI stays out deliberately: it is a short-lived foreground process whose panic prints to a terminal someone is already looking at, and a second copy in `crash.log` buys nothing. The guard reads the three entry points rather than a list kept here, and was checked against the state that shipped — it names the updater. The mechanism itself was already covered: `a_panic_lands_in_the_crash_log` proves the hook writes the record. What nothing held was whether each binary calls it, which is exactly what was missing. |
||
|
|
ebfc08368b |
fix(i18n): a number in a search keyword list must survive translation
Searching Settings for `16` found ANSI colours in English and Japanese
and nothing at all in Chinese. Both other languages carry "16" in that
row's keyword list; the Chinese one did not, and neither does the Chinese
title ("ANSI 颜色"), so the row was unreachable by the number people
actually type for it.
Keywords are match data rather than prose. A digit is the part of them
that does not translate — "16" is "16" in every language — so dropping
one silently narrows what a speaker of that language can find.
The guard holds every keyword list to carrying each number English does.
A translation may add its own; it may not lose one. Only keyword lists:
digits in ordinary copy are phrasing, and the Japanese for "one per line"
carries a 1 the English has no reason to.
Found by comparing numeric literals across the three tables, which is a
check nothing else does — the existing i18n tests hold placeholders,
plural branches and vocabulary, all of which this passes.
|
||
|
|
43d59b7317 |
ci(host-boundary): .exists() asks this machine too
The guard forbids the GUI four ways of reaching the filesystem directly, because a path held by `ui::` or `terminal::` may name a file on a remote workspace's machine. `.canonicalize()` is one of them. `.exists()` was not, and it is the same question asked a shorter way: it answers about the client's disk whatever machine the path belongs to. `search::local_probe` is what the answer is supposed to look like — a local prober and a remote one behind one `Probe`, with an `Unknown` that means "nobody has asked yet". A bare `.exists()` is that split skipped. Nothing was violating it. All four call sites are local by construction — a destination the user picked in this app's own copy flow, a name it is choosing for a download, and the themes directory — so they join the allowlist with the reason, which is what the allowlist is for. `.is_file()` and `.is_dir()` are the same hazard and are deliberately not added. Between them they occur eleven more times, all on paths that are local by construction, and the script's own header makes the argument against: an allowlist that grows faster than the net it casts is one people stop reading. `.exists()` earns its four lines by being the direct analogue of a pattern already there. Checked by injecting a `.exists()` on a pane-supplied path: the guard names the file and line and exits 1. |
||
|
|
d60949437c |
docs(readme): nushell is an integrated shell, so say so
`shell_integration.rs` ships five scripts — zsh, bash, fish, nushell,
PowerShell. Both READMEs listed four of them and stopped before nushell.
The shell-integration reference page has described it all along, down to
why its wrapper works the way it does ("`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"). Only the front pages were missing it,
and the front page is where someone choosing a terminal looks — a shipped
integration nobody knows about is a feature that may as well not exist.
Both languages, because the Chinese README carries a translation of the
same table and had drifted the same way.
The guard holds every script in that file to being named in both, and
checks the scripts are non-empty first so the list cannot go stale by
quietly losing one. Checked against the page as it was: it reports
nushell missing from README.md.
|
||
|
|
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. |
||
|
|
2d5621a71c |
feat(palette): amend, refresh and the graph toggle are Git verbs too
The palette's Git group held commit, stage, unstage, discard, sync, push, pull, fetch and the branch verbs. `ScmCommitAmend`, `ScmRefresh` and `ScmToggleGraph` were bindable and unreachable from it. A group that is nine tenths complete is worse than one that is obviously partial: the three missing did not read as "not in the palette", they read as features that do not exist. The command-palette page says every action is in there "whether or not it has a keybinding", which for this group was very nearly true. All three go through the path their neighbours already use — `run_scm_action` with an `ScmIntent` that existed, and `scm_toggle_graph` for the view half — so nothing new happens, it is the same dispatch reached a second way. Amend is no more exposed than what was already there; `ScmDiscardAll` has been a palette command all along. The labels are the ones the Keybindings page already shows for these actions, so the two surfaces name them identically. The guard is scoped to Git deliberately. Plenty of actions belong nowhere near a palette — `HideApp`, `InsertNewline`, the numbered tab and workspace families — so the whole-app version of that claim is prose, not a contract. One coherent group either is complete or is not, and this holds every bindable `Scm*` action to being reachable. It matches through `key_spec` rather than by variant name, so a command spelled differently from its action — `OpenBranchPicker` for `ScmCheckoutBranch` — still counts. `the_git_group_is_its_own_section` counted ten and now counts thirteen. That number is a forcing function, not a fact worth knowing: adding a Git verb should be deliberate, and bumping it is how that is confirmed. |
||
|
|
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`.
|
||
|
|
b6b3e782b4 |
fix(doctor): name a custom shell that can never appear
A `custom_shells` entry with nothing to launch is dropped by
`append_custom`, which says so with `log::warn!` — and there is no log
unless `TTY7_LOG` is set.
The way an entry ends up empty is what makes this worth its own row.
`CustomShell` is `#[serde(default)]`, so a misspelled key *inside* an
entry — `programm` for `program` — is not a parse error but an entry with
every field defaulted. And `custom_shells` is itself a real setting, so
the unknown-key row added alongside this cannot see it: that one compares
top-level keys, and the typo is a level down.
So the file parses, `doctor` says `ok`, both key checks pass, and the
menu row simply never appears. Measured on a config holding one typo'd
entry and one good one.
custom shells 1 of them name no program, so their menu rows never
appear: entry 0 — a misspelled key inside an entry
reads as an empty one
By position, because an entry broken this way usually has no label to
name it by — that is the same misspelling, one field over.
The test pins the mechanism rather than only the outcome: it deserializes
a misspelled entry and asserts it lands as `CustomShell::default()`,
because "an unknown key is not an error here" is the whole reason the
report is needed.
|
||
|
|
1552abb010 |
fix(doctor): name the config keys tty7 does not read
Mistyping a setting name is the likeliest thing to go wrong in a
hand-edited `config.json`, and the quietest. The file still parses, so
config ok
while the setting does nothing. `note_unknown_keys` has found these all
along — it says so with `log::warn!` behind a `log_enabled!` guard, and
per `docs/reference/privacy.mdx` there is no log at all unless `TTY7_LOG`
is set. Measured on a config carrying `font_siz`, `scrollback_limitt` and
one real key: nothing anywhere named the two that did nothing.
Unlike the keybindings map, this one `doctor` can ask: `unknown_keys` is
already in the crate the CLI shares, and already guarded by a test that
no real field may ever be reported as a typo. It only needed a way in.
config keys not settings tty7 reads, so they do nothing:
font_siz, scrollback_limitt — check the spelling
against the reference page
Only when the config parsed, and only when there is something to say. A
quarantined config is running on defaults and *every* key in it is
unread; naming them all would bury the row that matters. A clean config
prints no row at all, so this reads as news.
The test asks the pure function rather than the file-reading wrapper.
`unknown_config_keys` reads `TTY7_CONFIG_DIR`, and setting that from a
test steers every other test in the process — the first draft did, and
failed three runs out of three under the parallel suite.
|
||
|
|
fa4a7e7e38 |
fix(keymap): say when a keybinding in config.json did nothing
Two places throw away an entry in `config.json`'s `keybindings` map:
`set_binding` when no action answers to the name, and `action_bindings`
when the chord will not parse. Both say so with `log::warn!`, and per
`docs/reference/privacy.mdx` there is no log at all unless `TTY7_LOG` or
`RUST_LOG` is set.
So on a default install a hand-edited typo costs the user their shortcut
in silence. The file still parses, which means `tty7 doctor` reports
config ok
while two of the three bindings in it do nothing. The only evidence left
is a key that never fires, and nothing connects that to the line that
needs fixing — measured on a config carrying one typo'd action name, one
unparseable chord, and one good binding.
The window says it now, the same way it already says a config.json did
not parse at all, and for the same reason: the symptom on its own reads
as "tty7 ignored my settings".
It is answered in the window rather than in `doctor` because `doctor`
cannot ask. The action table and the keystroke parser both live up here;
the CLI shares only the crate underneath, and moving either down to
answer one diagnostic would be the tail wagging the dog.
Two things are deliberately not faults. An empty chord is how a binding
is *unbound* — `action_bindings` skips it on purpose. And a quarantined
config is running on defaults, so its `keybindings` were never read;
complaining about them would name a map nothing consulted.
The collector is tested against all four cases, and checked against the
log the two drop-sites emit: it names exactly the pair they drop and not
the binding that works.
|
||
|
|
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. |
||
|
|
ec7a7a2fa5 |
fix(ssh): a forward with no bind address binds loopback, not the resolver's guess
Three forms build a port forward: the settings sheet, the side panel, and a `LocalForward` line read out of `~/.ssh/config`. Two of them turned a blank bind address into `127.0.0.1`. The settings sheet passed the empty string through to the bind. Where that lands is not ours to decide once it leaves: `""` is whatever getaddrinfo makes of it. On macOS and glibc today that is loopback, which is why nothing looked wrong — but it is the resolver's answer, not the app's, and under `AI_PASSIVE` semantics the same string means every interface. An SSH tunnel reachable from the network is not a state to arrive at through a default nobody chose, and the sheet's own field already shows "localhost" as its placeholder, so a user leaving it blank has been told what they are getting. The side panel's `collect` claimed, in its doc, to apply "the same conditions the settings sheet's `ForwardRuleForm::collect` applies". It did not, and that is the sort of comment that stops anyone checking. Both now call one function beside `HostPort`, along with the ssh_config parser, which had the third copy of the same literal. Tested from both ends: the rule the settings sheet builds from a blank field, and the rule the side panel builds from the same blank, are asserted equal — and against the unfixed sheet the first half fails. |
||
|
|
b23e0feb55 |
fix(editor): closing a tab no longer throws away unsaved edits
The code panel hangs off the tab, so closing the tab drops every buffer in it. `editor_close_file` asks before closing a *file*, and the file tree marks a dirty one — but nothing outside `code_editor` read `dirty` at all, and `tab_close_reason` looked at `pane.terminals()` and nothing else. Every close path that went through the tab took the edits without a word. The ordinary ⌘W is one of them. `CloseActiveTab` reaches `editor_close_active_if_focused` only while the editor has focus; with focus in the terminal — which is where it is after you edit a file and go back to the shell — it goes to `close_pane`, and a last pane takes its tab. Type into a file, click the terminal, press ⌘W, and the text is gone with no dialog. `UnsavedEdits` is checked first wherever a reason is derived. Of the three, it is the only loss that cannot be undone by doing the thing again: a killed command can be re-run and a dropped SSH link reconnected. Three paths, because the tab can end from three directions: - `tab_close_reason`, for a tab closed as a tab. - `focused_pane_close_reason`, because `close_pane_inner` carries its own `confirmed` into `close_tab_inner` so the tab does not ask twice — which means a question the pane never asked is never asked at all. - the bulk closes, which pass `confirmed` and so cannot ask. They skip instead, on the same footing as an SSH profile that asked to be warned about. Deliberately not the footing of a busy tab: busy is the state most tabs on a working window are in, so skipping it would close nothing, while unsaved edits are rare and unrecoverable. Both loss paths were run against the unfixed code and fail there. The third test — a saved buffer still closing outright — passes either way on purpose: it holds the guard to the edits rather than to the mere presence of a code panel. |
||
|
|
62d44f3d0f |
fix(doctor): say when a workspace tree was set aside
A `machine.json` that does not parse is copied to `machine.json.corrupt` and the machine comes up with no workspaces at all — every tab and every pane layout on it. `MachineStore::open` calls that "recoverable by hand", and it is, but only for a hand that knows where to look. Nothing told it. The quarantine announces itself with a `log::warn!`, and per `docs/reference/privacy.mdx` there is no log at all unless `TTY7_LOG` or `RUST_LOG` is set — so on a default install the whole thing is silent. What the user sees is `tty7 ws ls` saying "no workspaces — `tty7 new <path>` starts one", which reads as an empty machine rather than as a lost one, and gives no reason to look in the config directory. `doctor` already makes this argument for `config.json`: a file that does not parse is exactly the state someone runs `doctor` in, and none of it is visible from the rows around it. The tree is the same case with more at stake — settings are still on screen when `config.json` is quarantined; workspaces are not. The row only appears when a copy is really there. One that said "no tree was set aside" beside every intact machine would be noise on every install, and this has to read as news. Verified end to end against a running server rather than only in a test: corrupt the tree, restart, `doctor` names the copy; restore it, the workspaces come back and the row goes away. |
||
|
|
c27d88e001 |
fix(tab-strip): an ellipsis that stands for nothing dropped
`short_title` counted `~` toward a path's depth, so a home path was cut one segment earlier than an absolute one of the same shape: `/usr/local/bin` kept its root, `~/repo/025/tty7` became `…/repo/025/tty7`. The cut bought nothing. `~` and `…` are one grapheme each, so the label is the same width to the character — it just says less, and what it says is untrue: an ellipsis is a claim that something was omitted, and nothing was. The reader cannot tell that tab from one whose ancestors really are hidden. `elide_path_middle` then believes it. It reads a leading `…` as "this was elided once" and replaces the marker rather than keeping it, so narrowing such a tab gave `…/025/tty7` where a real `~` would have given `~/…/025/tty7`. The home marker was lost for good, to stand in for a segment that was always there. Counting segments only puts the home case on the footing the absolute one already had. Four segments still elides, and the marker then means what it says. The one test that failed is the one that had written the old behaviour down; it moves to a four-segment path, which is the case it was reaching for, and the three-segment boundary gets a test of its own. |
||
|
|
c6e878281d |
fix(updater): read the designated requirement off the stream it is on (#708)
`codesign -d -r-` writes the requirement to stdout and puts only the `-d`
display header (`Executable=…`) on stderr. `signing_requirement` searched
stderr, so the `designated => ` prefix could never match and every in-app
update on macOS ended at "codesign did not report a designated
requirement" — every build, every channel, with nothing a user could do
but download the app again by hand.
Verified against codesign rather than reasoned about:
$ codesign -d -r- /bin/ls
stdout: designated => identifier "com.apple.ls" and anchor apple
stderr: Executable=/bin/ls
Both streams are read now, stdout first. Which half goes where is
codesign's own business and has moved before; a requirement printed
anywhere in the output is the requirement, and the updater has no reason
to be the stricter party about where it appeared.
The parse is split out of the process call, which is the part that
matters for it staying fixed. Fused to `Command::output`, it could only
run against a real signed bundle, so nothing in a test suite ever
executed it — that is why a total failure of the macOS update path
shipped and stayed. `/bin/ls` is the bundle it was missing: Apple-signed,
on every macOS, and it answers `-d -r-` with a requirement of its own, so
the stream split is now asserted against the tool instead of against our
belief about it.
Both tests were run against the old stderr-only parse; both fail there.
|
||
|
|
3b5df0b6b5 |
fix(tree-sync): adopting a workspace is not creating one (#716)
`chosen_name` is the name a user typed for a workspace a window is about to create. It travels with the create rather than following it, because a rename sent before the workspace exists is answered `NotFound`. When the create came back without that name, `settle_chosen_name` sent it as a rename. Its own comment gave two reasons the create might not have run — the other create of this window's pair won the race, or the workspace was already there — and treated them the same. They are not the same. The first is this window finishing its own job. The second is renaming somebody else's workspace. #716 is the second one from the far end: a client opened a workspace on another machine that already held nineteen live panes, and the workspace came back named after the connecting client's local user, because that side spent a codename it had rolled for a workspace it thought it was making. A workspace this window's sibling create just made is empty, so the two cases separate on whether the workspace holds tabs — and the existing arbitration tests all pull an empty mirror, so they are exactly the case that still renames. The name is still consumed when the rename is declined. It was owed once, and adopting the workspace is how it stops being owed; parking it would only fire the rename at the next pull. This is the naming half of that report. The tab tree it also lost is not addressed here. |
||
|
|
f4df05ffca |
fix(sidebar): open the diff of the row whose counts were clicked (#706)
Every sidebar row carries its tab's `+N −M`, and the counts are their own click target. They read the git path off `self.tabs[i]` — the row — and then called `toggle_diff_overlay`, which writes to `self.active`. The two are only the same tab when the row clicked is the one already in front. Clicking another row's counts therefore opened *that* row's repository as an overlay on the tab already in front: focus never moved, the front tab showed a diff from a directory it has nothing to do with, and the overlay stayed filed under the front tab afterwards, so every later read of "the active tab's overlay" kept returning it. The row's tab now comes forward first, and the overlay lands on it. Arriving from another tab opens rather than toggles. `open_diff_overlay` is itself the toggle — the close lives there, not in its `toggle_*` wrapper — so activating the target first was not enough on its own: a target that already had that diff open and in front answered a request to show it by closing it, leaving the screen on nothing that was asked for. `may_close` is what separates the two, and only a click on the tab already in front sets it. The bounds check is the same bug by another route: `activate` no-ops on an index it does not have, which would leave the open writing to whatever tab happened to be in front, and a row can outlive its tab between render and click. All three tests were run against the unfixed code, not only a green tree; two of them fail there. |
||
|
|
9963cfb8fc |
fix(settings): let the scroll slider reach the range it documents
`mouse_scroll_multiplier` is clamped to 0.1..=10, and the reference page says so. The slider spanned 0.5..=5.0. Two things followed. Half the documented range could not be set from the window at all — 0.2 and 8 are storable, keepable values with no position on the control that owns them. And a hand-set 8x drew a thumb pinned at the slider's own maximum, three quarters of the way along a track whose end means 5; the number beside it read "8.00x", so the control and its own readout disagreed on screen. Its two sibling sliders each span exactly their clamp — window opacity 0.2..=1.0, background-image opacity 0..=1 — so this was the outlier rather than the convention. It now reads its ends from named constants beside the clamp, which is where `ui_font_size` and the panel widths already keep theirs (#550), so the two cannot part again. The step drops to a tenth: the wider span has to keep 1.00 a position you land on rather than something to be hit between two. The test holds both ends through a `sanitize`, so the ends the slider offers are ends the file keeps, and checks the reference page still names the same pair — it is the third copy of the range and the one a reader meets first. Checked against an injected wrong range, not only a green tree. |
||
|
|
b5f8d484ba |
fix(palette): stop naming a keymap action the keymap has never had
`key_spec` maps a palette command to the keymap action whose shortcut the row should show, and `effective_key` answers an action it does not know the same way it answers a deliberately unbound one: `None`. So a name that resolves to nothing does not fail — it quietly shows no shortcut, which is indistinguishable from having none. `RestartDaemon` named `"RestartDaemon"`, and the keymap has never bound it. Nothing was visibly wrong today, because the command has no shortcut either way; what was wrong is that the palette claimed a bindable action, so binding one later would still have shown nothing. It moves to the arm for commands with no action of their own. Restarting the server stays palette-only: giving it a real action means a gpui action and a handler, which is a feature rather than a fix. The guard reads the names out of `key_spec`'s own source, because the match *is* the list and a second copy here would drift the way the first one did. Both it and the settings-index guard were checked against an injected regression rather than only against a green tree — a phantom action and a removed index entry each fail them. |