feat(client): a scrollable watch, and a demo with nothing to copy or repeat - #601
Open
behinddwalls wants to merge 1 commit into
Open
feat(client): a scrollable watch, and a demo with nothing to copy or repeat#601behinddwalls wants to merge 1 commit into
behinddwalls wants to merge 1 commit into
Conversation
behinddwalls
force-pushed
the
preetam/watch-tui
branch
2 times, most recently
from
August 15, 2026 00:37
e3ce570 to
e95b28a
Compare
behinddwalls
marked this pull request as ready for review
August 15, 2026 00:40
behinddwalls
force-pushed
the
preetam/watch-tui
branch
3 times, most recently
from
August 16, 2026 18:10
40195ca to
6b5fca3
Compare
…repeat ## Summary ### Why? Two things made a watch awkward to actually use. **A big table could only be trimmed.** The previous change stopped a frame taller than the window from repainting the screen, by dropping settled rows and saying how many it had dropped. That keeps the redraw honest but it is still a table you cannot read: the rows are there, and the only reason they are not on screen is that the renderer had to choose. What a reader wants is what `top` gives them — the whole table, and a way to move through it. **The gateway address had to be copied by hand, and went stale.** Compose publishes a fresh random port on every start, so a `GATEWAY_ADDR` noted from an earlier run points at a port that no longer exists, and every demo command fails with a connection refused that says nothing about why. That is not a hypothetical: it is the most common way these commands fail. ### What? **A watch of a queue is now a scrollable full-screen view.** It takes the alternate buffer while it runs, reads keys in raw mode, and gives the screen back untouched afterwards: | Key | | |---|---| | `↑` `↓` / `k` `j` | one row | | `PgUp` `PgDn` / `Space` | one screen | | `g` `G` | first row, last row | | `q` | stop watching | It follows the end of the table by default, so rows and stages appear without anyone touching it; scrolling up holds the reader's place, and scrolling back to the bottom resumes following. There is no dedicated key for that, because being at the end is what following is. The full-screen view also removes the class of bug the trimming worked around, rather than managing it: the alternate buffer never scrolls, so each frame is painted from the top and there is no previous frame to find. The trimming path remains for the case where it is still needed — a terminal on stdout but not on stdin, where there is a screen to draw on but nobody to press a key. The finished table is printed into the restored screen whole, however tall it is. Nothing is drawn over it, so a long one scrolls, which is what a reader of a completed run wants. No new dependency: `golang.org/x/term` was already in use for the window size and provides raw mode too. **Work recorded after a request settles says so.** A build for a speculation path nobody needed any more can finish after its batch has landed and be recorded against every request in it — seen in a fifty-request run, where two rows carried a `building`/`built` pair timestamped 350ms after `landed`. Rendering those like any other event read as a landed change building itself afterwards, so a terminal status marks them `[after: …]`. The orchestrator does cancel unwanted builds, but only ones still running when it next polls, and the fake runner finishes instantly — so this is mostly a demo artifact that a real runner would usually cancel instead. **`make land`, `land-status`, `land-list`, `land-watch` and `demo-requests` find the gateway themselves**, by asking Docker for the running stack's published port. `GATEWAY_ADDR` is now an override for reaching a gateway the Makefile did not start, and a stack that is not running produces a sentence saying so rather than a refused connection. The resolution is done inside each recipe rather than as a `$(shell ...)` assignment, which would shell out to Docker on every `make help`. **`demo-requests` also takes the provider from the running stack.** The two have to agree, and nothing enforced it: a stack started with `PROVIDER=git` and a `make demo-requests` that was not told so mints fake changes pointing at no repository, which the git merger rejects as commits it cannot find. Observed as fifty consecutive failures reading `not available from remote origin`, with nothing in the error to say the provider was the problem. The stack knows which one it has — it is mounted at `/etc/submitqueue` — so a run given no provider of its own asks it, and one given a provider that disagrees says so before it starts rather than after fifty rejections. Finding the port is not enough on its own, because `?=` treats a variable exported in the shell as already set — so anyone who ran the `export GATEWAY_ADDR=…` the quickstart used to recommend keeps a dead port forever and never reaches the discovery at all. That is the failure this was meant to remove, so when the address came from the environment and a local stack is running somewhere else, both are named and `unset GATEWAY_ADDR` is suggested. An address given on the command line is deliberate and passes without comment. **The quick start is three commands.** Two of its five were reading a port and exporting it, which is exactly the copying this removes — so the README and `DEVELOPMENT.md` are now start the stack, put traffic through it, stop it, and nothing in between. The README no longer sends the reader to the development setup for "full prerequisites" either, having just told them Docker is all they need. **The git sandbox no longer repacks its refs.** A fifty-request run failed one land with `git ls-remote … fatal: unterminated line in ./packed-refs`, reading a ref name cut in half. Git runs `gc --auto` after every push by default, and packing refs rewrites `packed-refs` wholesale — safe on one filesystem, where the replacement is a rename nobody can observe halfway, but this repository is written from the host and read from inside a container through a bind mount, and that boundary does not preserve the guarantee. Fifty pushes are fifty chances to fail a land that had nothing wrong with it. `gc.auto`, `receive.autogc` and `maintenance.auto` are now off on the sandbox, so there is nothing to rewrite; loose refs cost a directory that gets deleted nothing. The settings are re-applied on every start, so a sandbox made before this gains them instead of staying broken. It is the same hazard as the `loose object … is corrupt` failure fixed earlier by moving Runway's checkout to a named volume. The bare repository cannot follow it there — being readable from the host with `git log` is the point of it — so the fix is to stop the rewriting instead. ## Test Plan - ✅ drove the view through a pty with real keystrokes — `G`, two up-arrows, `PgUp`, `q` — and read the positions back out of the footer: `40-40 → 39-40 → 38-40 → 36-40 of 40`, then a clean exit - ✅ the alternate screen is entered once and left once in every run captured, so the terminal is never left on it - ✅ after `q`, all 40 rows are printed into the restored screen; after a settled 25-request run, all 25 are, with nothing hidden - ✅ `make demo-requests` and `make land-list` with no `GATEWAY_ADDR` set at all; with it set explicitly; and with no stack running, which now says `No gateway found: 'submitqueue' is not running` - ✅ reproduced the provider mismatch that prompted the detection — a `PROVIDER=git` stack with a plain `make demo-requests` — and confirmed it now creates git changes and lands them, while an explicit `PROVIDER=fake` against that stack warns before starting - ✅ reproduced the stale-export case that prompted the guard — an exported address pointing at a port from an earlier stack, with a live stack elsewhere — and confirmed it names both and suggests unsetting, while the same address given on the command line stays silent - ✅ redirected output still produces a plain log: `make demo-requests LAND=false` and piped runs take neither the screen nor the keyboard - ✅ new tests for the parts that are not a terminal: every key and escape sequence including one split across reads, and the scroll arithmetic — bounds, paging, and that scrolling up releases follow while rows arriving do not move a view that has scrolled away - ✅ the sandbox settings are applied at creation, and applied again to a sandbox that already exists — the upgrade path, since an existing one is reused rather than recreated - ✅ `make test` (105 targets), `make lint`, `make gazelle` Not verified at scale: the packed-refs failure was one land in fifty and depends on when git decides to pack against when the merger reads, so a single clean run would not prove much. What is verified is that nothing repacks any more, which is the condition that made it possible. Two behaviours worth knowing. A bare `Escape` is not acted on until another key follows, and swallows it — the alternative is misreading an arrow whose bytes arrive in separate reads, which is worse and intermittent. And with tall wrapped rows in a short window, moving up one row can land back at the bottom, because the number of rows that fit changes with their height.
behinddwalls
force-pushed
the
preetam/watch-tui
branch
from
August 16, 2026 18:17
6b5fca3 to
0a59be1
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Why?
Two things made a watch awkward to actually use.
A big table could only be trimmed. The previous change stopped a frame taller than the window from repainting the screen, by dropping settled rows and saying how many it had dropped. That keeps the redraw honest but it is still a table you cannot read: the rows are there, and the only reason they are not on screen is that the renderer had to choose. What a reader wants is what
topgives them — the whole table, and a way to move through it.The gateway address had to be copied by hand, and went stale. Compose publishes a fresh random port on every start, so a
GATEWAY_ADDRnoted from an earlier run points at a port that no longer exists, and every demo command fails with a connection refused that says nothing about why. That is not a hypothetical: it is the most common way these commands fail.What?
A watch of a queue is now a scrollable full-screen view. It takes the alternate buffer while it runs, reads keys in raw mode, and gives the screen back untouched afterwards:
↑↓/kjPgUpPgDn/SpacegGqIt follows the end of the table by default, so rows and stages appear without anyone touching it; scrolling up holds the reader's place, and scrolling back to the bottom resumes following. There is no dedicated key for that, because being at the end is what following is.
The full-screen view also removes the class of bug the trimming worked around, rather than managing it: the alternate buffer never scrolls, so each frame is painted from the top and there is no previous frame to find. The trimming path remains for the case where it is still needed — a terminal on stdout but not on stdin, where there is a screen to draw on but nobody to press a key.
The finished table is printed into the restored screen whole, however tall it is. Nothing is drawn over it, so a long one scrolls, which is what a reader of a completed run wants.
No new dependency:
golang.org/x/termwas already in use for the window size and provides raw mode too.Work recorded after a request settles says so. A build for a speculation path nobody needed any more can finish after its batch has landed and be recorded against every request in it — seen in a fifty-request run, where two rows carried a
building/builtpair timestamped 350ms afterlanded. Rendering those like any other event read as a landed change building itself afterwards, so a terminal status marks them[after: …]. The orchestrator does cancel unwanted builds, but only ones still running when it next polls, and the fake runner finishes instantly — so this is mostly a demo artifact that a real runner would usually cancel instead.make land,land-status,land-list,land-watchanddemo-requestsfind the gateway themselves, by asking Docker for the running stack's published port.GATEWAY_ADDRis now an override for reaching a gateway the Makefile did not start, and a stack that is not running produces a sentence saying so rather than a refused connection. The resolution is done inside each recipe rather than as a$(shell ...)assignment, which would shell out to Docker on everymake help.demo-requestsalso takes the provider from the running stack. The two have to agree, and nothing enforced it: a stack started withPROVIDER=gitand amake demo-requeststhat was not told so mints fake changes pointing at no repository, which the git merger rejects as commits it cannot find. Observed as fifty consecutive failures readingnot available from remote origin, with nothing in the error to say the provider was the problem. The stack knows which one it has — it is mounted at/etc/submitqueue— so a run given no provider of its own asks it, and one given a provider that disagrees says so before it starts rather than after fifty rejections.Finding the port is not enough on its own, because
?=treats a variable exported in the shell as already set — so anyone who ran theexport GATEWAY_ADDR=…the quickstart used to recommend keeps a dead port forever and never reaches the discovery at all. That is the failure this was meant to remove, so when the address came from the environment and a local stack is running somewhere else, both are named andunset GATEWAY_ADDRis suggested. An address given on the command line is deliberate and passes without comment.The quick start is three commands. Two of its five were reading a port and exporting it, which is exactly the copying this removes — so the README and
DEVELOPMENT.mdare now start the stack, put traffic through it, stop it, and nothing in between. The README no longer sends the reader to the development setup for "full prerequisites" either, having just told them Docker is all they need.The git sandbox no longer repacks its refs. A fifty-request run failed one land with
git ls-remote … fatal: unterminated line in ./packed-refs, reading a ref name cut in half. Git runsgc --autoafter every push by default, and packing refs rewritespacked-refswholesale — safe on one filesystem, where the replacement is a rename nobody can observe halfway, but this repository is written from the host and read from inside a container through a bind mount, and that boundary does not preserve the guarantee. Fifty pushes are fifty chances to fail a land that had nothing wrong with it.gc.auto,receive.autogcandmaintenance.autoare now off on the sandbox, so there is nothing to rewrite; loose refs cost a directory that gets deleted nothing. The settings are re-applied on every start, so a sandbox made before this gains them instead of staying broken.It is the same hazard as the
loose object … is corruptfailure fixed earlier by moving Runway's checkout to a named volume. The bare repository cannot follow it there — being readable from the host withgit logis the point of it — so the fix is to stop the rewriting instead.Test Plan
G, two up-arrows,PgUp,q— and read the positions back out of the footer:40-40 → 39-40 → 38-40 → 36-40 of 40, then a clean exitq, all 40 rows are printed into the restored screen; after a settled 25-request run, all 25 are, with nothing hiddenmake demo-requestsandmake land-listwith noGATEWAY_ADDRset at all; with it set explicitly; and with no stack running, which now saysNo gateway found: 'submitqueue' is not runningPROVIDER=gitstack with a plainmake demo-requests— and confirmed it now creates git changes and lands them, while an explicitPROVIDER=fakeagainst that stack warns before startingmake demo-requests LAND=falseand piped runs take neither the screen nor the keyboardmake test(105 targets),make lint,make gazelleNot verified at scale: the packed-refs failure was one land in fifty and depends on when git decides to pack against when the merger reads, so a single clean run would not prove much. What is verified is that nothing repacks any more, which is the condition that made it possible.
Two behaviours worth knowing. A bare
Escapeis not acted on until another key follows, and swallows it — the alternative is misreading an arrow whose bytes arrive in separate reads, which is worse and intermittent. And with tall wrapped rows in a short window, moving up one row can land back at the bottom, because the number of rows that fit changes with their height.Issues