simulate: print the job view into the terminal instead of windowing it in the alt screen - #928
Merged
Merged
Conversation
… the alt screen Opening a job used to window renderDetail inside the alt screen: a job's instructions, transcript, and logs were paged through a ~12-row-shorter viewport with "N more lines" markers, and the terminal's own scrollback, selection, and search were unavailable for the one view that is mostly text worth reading. The job view now leaves the alt screen and is printed with tea.Println, so the terminal owns it. flushDetail runs after every message and emits only what the view gained since the last call, so growth while a job is still running (a log line, the transcript arriving with the summary) appends instead of redrawing. A render that rewrites what came before is reprinted whole, since scrollback cannot be edited in place; a resize rebaselines without reprinting, because printed text cannot re-wrap. The list view is untouched: it keeps the alt screen, its page windowing, and its cursor. Going back re-enters the alt screen and leaves the job in the scrollback. While a job is open the live region is just its status line and the hint bar, and up/down/pgup/pgdown are swallowed so they cannot move the list cursor hidden underneath. Removes detailScrollOff, scrolledDetail, and scrollActive's detail case.
lipgloss's Style.Render pads a Width(n) block: after wrapping it runs alignTextHorizontal, which pads every row out to the widest line or to n, whichever is larger. That is invisible in the alt screen but is real whitespace once a row is printed to the terminal — a job view arrived with a ragged column of trailing spaces beside the instructions, expectations, result, transcript, and logs, and the padding came along when the text was selected and copied. wrapLines now calls ansi.Wrap, which is the wrap lipgloss itself runs (via x/cellbuf) before the align pass, so the padding is never added rather than added and trimmed back off. The detail and transcript renderers go through wrapLines instead of each holding a fixed-width style of their own. Boxes and modals keep Style.Render — their padding is structural. x/ansi was already in the module graph as a lipgloss dependency; this only promotes it to a direct require.
Instructions, expectations, the result, the transcript, and the summary all wrapped to the terminal width less their indent. On a 200-column terminal that sets a paragraph to 194 columns, which is hard to read back across and leaves the last few words orphaned on a line of their own (193 + 7 for a typical result sentence). proseWidth caps the measure at 100 columns and keeps the existing floor, so wide terminals get a readable column instead of a full-width one; narrow terminals are unchanged.
Visiting several jobs left every one of them stacked in the terminal, so scrolling up from the current job ran into the previous one. Opening a job now erases the screen and the scrollback ahead of the print, and the terminal holds one job at a time. Erasing only the previous job is not possible: once its rows scroll off, ESC[3J is the only sequence that reaches them, and it takes the whole scrollback with it — including whatever preceded the run. The sequences ride along with the print rather than being written to stdout from inside a Cmd. A Cmd's side effects are not ordered against the event loop (Program.Send only queues the message), so a direct write could land while the alt screen was still active and clear that buffer instead; going through tea.Println puts the clear in the same message as the text, after the alt screen is left, written by the renderer that owns stdout.
j and l sit either side of k on the home row, so they stand in for the left and right arrows: l opens the job under the cursor, j backs out of a job or an expanded description. The hint bar keeps naming the arrows, which are what a first-time reader will reach for.
The cap was justified partly as fixing an orphaned last line, which it does not: the sentence that prompted it still ends in a 7-column "failed." at 100 columns, the same tail it had at 194. Greedy first-fit puts whatever is left on the final line at any measure; avoiding that needs optimal-fit line breaking with a short-last-line penalty (Knuth-Plass), which nothing in the Go terminal stack offers. What the cap does buy is the line length itself, so the comment now says that and records where the number comes from.
theomonnom
approved these changes
Jul 31, 2026
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.
Opening a job in
lk agent simulatewindowedrenderDetailinside the alt screen: instructions, transcript, and logs were paged through a viewport ~12 rows shorter than the terminal, with↑ N more linesmarkers, and the terminal's own scrolling, selection, and search were unavailable for the one view that is mostly text worth reading.The job view now leaves the alt screen and is printed with
tea.Println, so the terminal owns it. The list view is untouched — it keeps the alt screen, its page windowing, and its cursor. Going back re-enters the alt screen.How it behaves
flushDetailruns after every message and emits only what the view gained since the last call, so growth while a job is still running (a log line, the transcript arriving with the summary) appends instead of redrawing. A render that rewrites what came before is reprinted whole, since scrollback cannot be edited in place; a resize rebaselines without reprinting, because printed text cannot re-wrap.ESC[3Jis the only sequence that reaches them, and it takes the whole scrollback with it — including whatever preceded the run. That trade-off is deliberate.up/down/pgup/pgdownare swallowed so they cannot move the list cursor hidden underneath.jandlsit either side ofkon the home row and now stand in for the left/right arrows:lopens,jbacks out.Removes
detailScrollOff,scrolledDetail, andscrollActive's detail case.Two wrapping bugs the change exposed
Both were invisible in the alt screen and became real once rows were printed to the terminal:
Style.Renderpads aWidth(n)block — after wrapping it runsalignTextHorizontal, which pads every row out to the widest line or ton. Printed rows kept that padding, and it came along when the text was selected and copied.wrapLinesnow callsansi.Wrap, which is the wrap lipgloss itself runs (viax/cellbuf) before the align pass, so the padding is never added rather than added and trimmed back off. Measured on a representative result sentence at width 40: lipgloss padded 5/5 rows;ansi.Wrap,ansi.Wordwrapandcellbuf.Wrappadded 0/5 with identical line breaks. Boxes and modals keepStyle.Render— their padding is structural.proseWidthcaps the measure at 100 columns and keeps the existing floor; narrow terminals are unchanged.x/ansiwas already in the module graph as a lipgloss dependency;go.modonly promotes it to a direct require.Verification
go build ./...,go vet ./cmd/lk,go test ./cmd/lkpass.tea.Printlnis silently dropped while the alt screen is active, and a raw write inside aCmdis not ordered against the event loop (Program.Sendonly queues the message), so the sequences ride along inside the print. Verified by driving a bubbletea program of the same shape through a pty and ordering the captured bytes:enterAlt @18 → exitAlt @68 → ESC[3J @89 → job text @93, and again on the second open/close cycle.detailTail), the wrap measure arithmetic (proseWidth), thatwrapLinesrows carry no right padding, and the key handling (asserting on model fields).