Add a wait spinner covering waits on the API #10

Merged
geusebio merged 1 commit from feature/api-wait-spinner into main 2026-08-03 17:35:56 +00:00
Owner

sc printed nothing between packing the bundle and the deployment line. On a
slow uplink with a large build context that is a long, unexplained pause, and
there was no way to tell a stalled upload from a busy platform.

What it looks like

[S⣦C] Uploading 1.4 MiB — 52%          bytes moving: an 8-step gauge
[S⠙C] Waiting for someones.computer    nothing to measure: ⠙⠸⠴⠦⠇⠋ tumbling
[S·C] Uploading 1.4 MiB — 52%          stalled 2s: resting mark, brackets breathing
[S⠿C] Uploaded 1.4 MiB in 3.2s         done: the die landed, and this line stays

A die tumbling between the brand's initials. Every glyph in the slot comes from
the braille block, whose cell is the same 2×3 grid as a die's pips, so they
all share one advance width and the line cannot jitter mid-spin — the reason the
real die faces (⚀⚁⚂⚃⚄⚅) were rejected: most monospace fonts do not cover them
and the substituted glyph arrives at different metrics.

State Slot Brackets Reached when
Uploading ⡀⣀⣄⣤⣦⣶⣷⣿ — 8-step gauge plain Progress(sent, total), 0 ≤ sent < total
Waiting ⠙⠸⠴⠦⠇⠋ — 3 of 6 pips plain nothing reported bytes, or sent ≥ total
Idle one of · breathing dim → normal → bold mid-transfer, no Progress for 2s
Done — every pip lit plain Done(summary); the line persists

Eight is the ceiling for one cell's gauge and is plenty, since the label carries
the exact percentage. is excluded from the idle pool so it means "finished"
and nothing else; the idle mark is drawn once per run so a command keeps one
character rather than flickering between them.

The seam, deliberately not wired

The gauge and the idle state need per-byte progress. UploadBundle now takes a
func(sent, total int64) and deploy passes (*ui.Spinner).Progress, so the
wiring is end to end — but the client never calls it. The body is assembled
in memory and handed to net/http in one piece, so there is no moment between
"nothing sent" and "all sent" to report.

Faking it with a (0, total) call at the start was tempting and would have been
worse: the spinner would sit in the breathing idle state for the entire upload.
So sc deploy today tumbles for the whole wait, exactly as before, and the
gauge and idle states are unreachable until the client is reworked to stream the
multipart through an io.Pipe with a counting reader — which also drops peak
memory from the whole bundle to a fixed buffer. That rework is deliberately
out of scope here
and is being done in a separate worktree.

Verification

  • gofmt, go vet, go test, and go test -race -count=3 all clean.
  • Tests cover all four states through an injected clock, the breath reaching
    both dim and bold, $NO_COLOR, the non-terminal path, idempotent finish, and
    that every resting mark comes up across 200 spinners.
  • Run against a stub API that sleeps 4s, on a real pty via script: 41 frames
    drawn, then [S⠿C] Uploaded 646 B in 4s left above the deployment line.
  • Piped to cat: no escape codes at all, just Uploading 646 B to … once
    followed by the summary. Same under NO_COLOR=1 on a pty, with the animation
    intact and the colour gone.

Terminal output is not screenshottable in the harness that produced it, so the
captures above are raw pty byte dumps rather than images.

Notes for the reviewer

  • bundle.Bundle grows a Size() helper for the label's byte count.
  • UploadBundle's signature changed; the only caller is deploy.
  • Still open, deliberately left out: the spinner covers the upload only, so
    compose parsing and context packing — often the slower half — still happen in
    silence beforehand.
`sc` printed nothing between packing the bundle and the deployment line. On a slow uplink with a large build context that is a long, unexplained pause, and there was no way to tell a stalled upload from a busy platform. ## What it looks like ``` [S⣦C] Uploading 1.4 MiB — 52% bytes moving: an 8-step gauge [S⠙C] Waiting for someones.computer nothing to measure: ⠙⠸⠴⠦⠇⠋ tumbling [S·C] Uploading 1.4 MiB — 52% stalled 2s: resting mark, brackets breathing [S⠿C] Uploaded 1.4 MiB in 3.2s done: the die landed, and this line stays ``` A die tumbling between the brand's initials. Every glyph in the slot comes from the **braille block**, whose cell is the same 2×3 grid as a die's pips, so they all share one advance width and the line cannot jitter mid-spin — the reason the real die faces (`⚀⚁⚂⚃⚄⚅`) were rejected: most monospace fonts do not cover them and the substituted glyph arrives at different metrics. | State | Slot | Brackets | Reached when | |---|---|---|---| | Uploading | `⡀⣀⣄⣤⣦⣶⣷⣿` — 8-step gauge | plain | `Progress(sent, total)`, `0 ≤ sent < total` | | Waiting | `⠙⠸⠴⠦⠇⠋` — 3 of 6 pips | plain | nothing reported bytes, or `sent ≥ total` | | Idle | one of `·` `⠒` `•` | breathing dim → normal → bold | mid-transfer, no `Progress` for 2s | | Done | `⠿` — every pip lit | plain | `Done(summary)`; the line persists | Eight is the ceiling for one cell's gauge and is plenty, since the label carries the exact percentage. `⠿` is excluded from the idle pool so it means "finished" and nothing else; the idle mark is drawn once per run so a command keeps one character rather than flickering between them. ## The seam, deliberately not wired The gauge and the idle state need per-byte progress. `UploadBundle` now takes a `func(sent, total int64)` and `deploy` passes `(*ui.Spinner).Progress`, so the wiring is end to end — **but the client never calls it.** The body is assembled in memory and handed to `net/http` in one piece, so there is no moment between "nothing sent" and "all sent" to report. Faking it with a `(0, total)` call at the start was tempting and would have been worse: the spinner would sit in the breathing idle state for the entire upload. So `sc deploy` today tumbles for the whole wait, exactly as before, and the gauge and idle states are unreachable until the client is reworked to stream the multipart through an `io.Pipe` with a counting reader — which also drops peak memory from the whole bundle to a fixed buffer. **That rework is deliberately out of scope here** and is being done in a separate worktree. ## Verification - `gofmt`, `go vet`, `go test`, and `go test -race -count=3` all clean. - Tests cover all four states through an injected clock, the breath reaching both dim and bold, `$NO_COLOR`, the non-terminal path, idempotent finish, and that every resting mark comes up across 200 spinners. - Run against a stub API that sleeps 4s, on a real pty via `script`: 41 frames drawn, then `[S⠿C] Uploaded 646 B in 4s` left above the deployment line. - Piped to `cat`: no escape codes at all, just `Uploading 646 B to …` once followed by the summary. Same under `NO_COLOR=1` on a pty, with the animation intact and the colour gone. Terminal output is not screenshottable in the harness that produced it, so the captures above are raw pty byte dumps rather than images. ## Notes for the reviewer - `bundle.Bundle` grows a `Size()` helper for the label's byte count. - `UploadBundle`'s signature changed; the only caller is `deploy`. - Still open, deliberately left out: the spinner covers the upload only, so compose parsing and context packing — often the slower half — still happen in silence beforehand.
Add a wait spinner covering waits on the API
All checks were successful
CI / build (pull_request) Successful in 19m55s
e34bd941d2
sc printed nothing at all between packing the bundle and the deployment
line, which on a slow uplink is a long unexplained pause. internal/ui
draws a die tumbling between the brand's initials — the pips are the six
dots of a braille cell, so every glyph in the slot shares one advance
width and the line cannot jitter mid-spin.

Four states:

  [S⣦C] Uploading 1.4 MiB — 52%          8-step gauge, bytes moving
  [S⠙C] Waiting for someones.computer    nothing left to measure
  [S·C] Uploading 1.4 MiB — 52%          stalled 2s, brackets breathing
  [S⠿C] Uploaded 1.4 MiB in 3.2s         done; this line persists

Colour is SGR 32/31 so it follows the user's own theme, and $NO_COLOR
drops every escape but the redraw itself. Off a terminal nothing
animates: the label prints once and only the closing summary follows, so
a CI log still records the result rather than a smear of escape codes.

The gauge and the resting state need per-byte progress. UploadBundle now
takes a func(sent, total int64) and deploy passes (*ui.Spinner).Progress,
so the wiring is end to end — but the client does not call it yet. The
body is assembled in memory and handed to net/http in one piece, so
there is no moment between "nothing sent" and "all sent" to report.
Making it granular means writing the multipart into an io.Pipe and
counting through it on the way past, which also drops peak memory from
the whole bundle to a fixed buffer. Until that lands sc deploy tumbles
for the whole wait, exactly as it does today.
geusebio changed title from WIP: Add a wait spinner covering waits on the API to Add a wait spinner covering waits on the API 2026-08-03 17:35:51 +00:00
Sign in to join this conversation.
No reviewers
No labels
in-progress
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Grey.ooo/someones.computer_agent!10
No description provided.