Repository navigation
cli: live progress and a cleaner terminal view, with a golden stdout contract - #4
Merged
Merged
Conversation
internal/ui is the shared live-terminal layer: a spinner with a label and elapsed time that appears only after 300 ms, a one-line check/cross summary, a readable error block and a small colour palette. It is off unless stderr is a terminal and nothing asked for plain output (--json, --quiet, NO_COLOR, TERM=dumb, CI), and every method is a no-op when off. output.Printer gains UI(), and Info/Warn write around a running spinner.
A spinner with a label and elapsed time appears after 300 ms on any wait:
scrape ("Scraping example.com/pricing (rendering JavaScript)"), scrape -
("Scraping 3/10 done"), batch submit and wait ("Batch job 3/10 done"), and
every other API call through Client(). A one-line check/cross summary with
status, credits and duration follows scrape, scrape - and batch wait, and API
errors keep their ERR:: code and hint in a readable block.
All of it is on stderr and only when stderr is a terminal and none of --json,
--quiet, NO_COLOR, TERM=dumb or CI is set; otherwise the output is unchanged.
Execute stops a leftover spinner so Ctrl-C always restores the cursor.
README, DESIGN and CHANGELOG say when the spinner and summary line appear, that they are stderr-only, and what turns them off.
…logs and sessions On a terminal: a spinner with a label for each wait (the skill download, the key check, the status checks, docs, log and session calls), a ✓/✗ checklist for status and auth status, marks and a count under the init/mcp/skill report, coloured status cells in the logs and sessions tables, dimmed keys in the key/value views, and a short next-steps list after login, init and install. Plain output is unchanged: styling is applied only when the stream is a terminal and --json, --quiet, NO_COLOR, TERM=dumb and CI are not in play. Hints and spinners go to stderr; stdout carries the same bytes when piped.
Lock what scripts and AI agents parse, byte for byte, before any terminal
work (spinners, colour, TUI) lands on top of it.
internal/cmd/*_golden_test.go run every command and its main modes
in-process against a fake API, as a non-interactive caller would: stdout
is not a terminal, so JSON by default. Per case the stdout bytes and the exit
code are compared with testdata/golden/<group>/<case>.{stdout,exit}. In JSON
mode the stderr problem document is locked too (.stderr), and files a command
writes with -o / --screenshot-dir (.file.*).
Every case runs three times (plain, NO_COLOR=1, CI=true); all must give the
same stdout and exit code, and stdout/stderr must contain no ANSI escape and
stderr no carriage return (an in-place spinner line).
Cases named *_human run the human (terminal) code path through the existing
humanOutput hook, since that is the only way to reach it without a pty.
Timestamps, request ids, build identity, the fake server address and temp
paths are replaced by placeholders (goldenNormalize). Regenerate with
`go test ./internal/cmd -run TestGolden -update` (or GOLDEN_UPDATE=1) and
review the diff. Big outputs (the embedded JSON Schemas) are locked by digest.
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.
What
Live progress on stderr for a person at a terminal. Until now
scrapeof a rendered page,scrape -,batch submit, pagedlogs/sessions/batch resultsand every other API call sat silent until the answer arrived (4 to 60 s for a JavaScript-rendered page), exceptbatch wait, which rewrote one counter line.Each is cleared before the result prints.
scrape,scrape -andbatch waitthen end with one summary line on stderr:API errors keep the
ERR::code and hint:Rules it follows
main(see below).--json,-q/--quiet,NO_COLOR,TERM=dumbandCI(anything but empty/0/false), and on a legacy Windows console. When off, the output is exactly what it was, includingbatch wait's old progress line and the plainerror:/hint:block.rendering JavaScriptfor--render,using a full browserfor--stealth/a pinned browser engine/--actions,extracting with AI), not which tier the API picked. The scrape call is a single blocking POST, so there is no stage to report as it climbs.✗ interruptedand exits 130 as before.Executealso stops any spinner a command left running.How
internal/ui(new): gating (ui.Live),Spinner(label, elapsed, one at a time, singleWriteper frame, label truncated to the terminal width),Done(summary line),Fail(error block), colour helpers. Every method is a no-op on a nil or off*UI, so call sites do not branch.output.Printer.UI()hands it out;Info/Warnwrite around a running spinner (retry notes land above it).api.Client.Waithook:cmd.Client()uses it so every API call gets a spinner with a plain label. A command that runs its own (scrape, batch wait) keeps its label, because only one spinner runs at a time.scrape -steps the spinner aside around each JSON line it writes to stdout, so a terminal on both streams stays readable.New dependency:
golang.org/x/termv0.6.0Used for one thing: the terminal width, so a long URL cannot wrap the spinner line (a wrapped line cannot be cleared with
\r). v0.6.0 is the release that matches thegolang.org/x/sysv0.6.0 already ingo.mod, so nothing else moves. TTY detection keeps usinggo-isatty, which was already a dependency. No spinner or TUI library: the spinner itself is about 120 lines.Evidence
Built
mainand this branch and ran both against a local fake API with delays (no live key was available), on a pseudo-TTY withscript.scrape --renderprinted nothing for 4.2 s, then the markdown. After: spinner frames0.3sto4.1s, cleared, markdown, then✓ example.com 200 · 5 credits · 4.2s.scrapemarkdown,--render --json, a blocked page,scrape -,batch submit --wait,batch wait,logs), each run with stderr piped and with stderr on a pty and stdout in a file: stdout, stderr (when piped) and exit code are byte-identical between themainand branch binaries.NO_COLOR=1,TERM=dumb,CI=true,--jsonor--quiet: 0 escape bytes, same asmain. With none of them: spinner.✗ interrupted, exit 130.Tests
internal/ui: gating table (TTY, NO_COLOR, TERM, CI, Windows), spinner lifecycle (draws after the delay, never newlines, hides then restores the cursor, idempotent Stop, one at a time), Fprintf around a spinner, off-is-inert, summary and error block, elapsed and truncation.internal/cmd/live_test.go: for scrape, scrape -, batch wait and a bare call, the same run with and without a live stderr yields identical stdout; no escape codes with--json/--quiet; a fast call draws no spinner; the error block keeps code and hint while the plain one is unchanged; no engine name reaches the live output even when the API returns one inX-Engine; Ctrl-C restores the cursor.go test -race ./...,go vet ./...and builds for linux, darwin and windows pass.Not changed:
--metastill printsengine:from the response header, and the JSON wrapper still carriesengine; both are existing, explicitly requested output.Also in this PR
init,auth,config,status,skill,mcp,docs,logs,sessions). There are spinners on their network waits, a ✓/✗ checklist forstatus, verdict lines, coloured status columns, andnexthints afterinit/auth/skill install. Styling on stdout applies only when stdout is a terminal. Piped output is unchanged, andconfig get <key>/config pathstay bare for substitution.NO_COLOR=1and withCI=true. They lock stdout, exit codes and written files byte for byte, and fail on any\x1bor\rwhen not a TTY. They pass on this branch, so the new terminal UI leaves piped output untouched. Regenerate withgo test ./internal/cmd -run TestGolden -update.go vet ./...andgo test -race ./...pass.