Skip to content

Console improvements - #139

Merged
graeme merged 5 commits into
mainfrom
console-improvements
Sep 2, 2026
Merged

Console improvements#139
graeme merged 5 commits into
mainfrom
console-improvements

Conversation

@graeme

@graeme graeme commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

PR: Make the console output selectable like a terminal

Summary

The console body was a List with one Text per line, so every line was its own selection scope: no drag across lines, nothing for Cmd-A to select, dead gaps between rows, and a pointer that flipped between an arrow and an I-beam. This makes it one text document, and tidies up the toolbar buttons above it.

Changes

Console output:

  • ConsoleTextView replaces the list with an NSTextView, so drag selection, Cmd-A, Cmd-C, clicking between lines and the I-beam behave the way they do in Terminal.
  • ConsoleTranscript (ViewModels) returns the minimal edit for each update rather than re-rendering the buffer, which would be quadratic over a run and would drop the user's selection every time a line arrived.
  • ANSIConsoleText renders AppKit attributed text, with colours still named on the SwiftUI token palette so the design system stays the source.
  • The view follows the tail only while the user is at the bottom, so scrolling up to read during a long install is no longer fought.
  • ConsoleBody binds to one ConsoleBodyContent from the view model instead of reaching into the selected job.

Toolbar buttons:

  • New shared BrewActionButton, used by the console toolbar and the command block header so both agree on icon-to-title spacing and highlighting. Hover fills the button; the press takes the app's selection tint.
  • Copy and Clear confirm with a tick and "Copied" / "Cleared". Save does not, since its save panel is its own confirmation.

Testing

  • scripts/test green (832 tests, plus 19 for BrewUILint). SwiftFormat, SwiftLint strict and BrewUILint clean; app and UI test targets build.
  • Driven in the real app with an injected driver: drag selects four lines, selectAll: selects the whole document, the console opens pinned to the end, and a selection survives streamed output.
  • Screenshotted the toolbar in its hover, pressed and confirming states.

PR checklist

  • Have you followed this repository's contribution and workflow guidance?
  • Have you explained what changed and why this should land now?
  • Have you run relevant local checks for the changed scope?
  • Are changes scoped and free of unrelated modifications?

  • AI was used to generate or assist with generating this PR.
  • If yes, describe exactly how AI was used and what manual verification was performed: Claude Code wrote the implementation and tests, then verified behaviour by driving and screenshotting the real app. Menu-routed Cmd-A could not be exercised that way, so it is worth one manual check.

graeme and others added 5 commits August 31, 2026 22:25
The console body renders the output buffer as a list of rows, which is why
selection there behaves nothing like a terminal's. Moving it to a single text
view makes "what changed since the last render" the view's problem: rebuilding
the whole document per line would be quadratic over a run and would drop the
user's selection every time a line arrived.

ConsoleTranscript holds the rendered lines plus their cached lengths and
returns the minimal edit to catch up — every change brew makes (append, or a
progress row redrawn in place) lands in a suffix, so an update is "replace from
the first line that differs to the end". Offsets are UTF-16 to match
NSTextStorage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018wwC5196S7pzAoAmaHev41
The console body was a List with one Text per line, so every line was its own
selection scope: a drag across lines selected nothing, ⌘A had no document to
select, the gaps between rows weren't text at all, and the pointer alternated
between an arrow and an I-beam depending on which of those it was over. An
NSTextView is a single document, so selection, ⌘A and ⌘C behave the way they do
in Terminal and the I-beam covers the whole output area.

ANSIConsoleText now renders AppKit attributed text (colours still named on the
SwiftUI token palette, bridged with NSColor, so the design system stays the
source), and new output is applied as ConsoleTranscript's minimal edit with the
selection re-applied around it — a selection made mid-install survives the lines
that arrive after it. The view follows the tail only while the user is parked at
the bottom, so scrolling up to read isn't fought; the pin also runs on layout
because SwiftUI hands over the first batch of output before the scroll view has
any size.

The body binds to one ConsoleBodyContent from the view model rather than
reaching into the selected job.

Verified against the real app with an injected driver: drag across lines selects
4 lines, selectAll: validates and selects the whole document, hit-testing the
bottom edge of the output area lands on the text view, the console opens pinned
to the end, a 200-character selection survives ~1100 characters of streamed
output, and scrolling away leaves the reader where they were. Menu-routed ⌘A
couldn't be driven — the app can't become key in a bare exec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018wwC5196S7pzAoAmaHev41
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018wwC5196S7pzAoAmaHev41
The console toolbar's Save/Copy/Clear were hand-rolled — a 3pt HStack, no
visible hover, and a borderless press you can miss — while the command block's
Copy button was a Label with its own copied-for-5-seconds state. One
BrewActionButton now serves both, so they agree on icon-to-title spacing (the
Label's, which is what the command block already used).

Pressing is unmistakable: hovering fills the button with the elevated surface
and darkens its title, and the press itself takes the app's selection tint.
Copy and Clear confirm with a tick and "Copied" / "Cleared" for five seconds —
both leave nothing on screen otherwise, so the press had no acknowledgement at
all. Save doesn't confirm: its save panel is its own.

The accessibility label stays the button's own title while the confirmation is
up, so the swap can't move an element out from under a test or a screen reader.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018wwC5196S7pzAoAmaHev41
@graeme
graeme marked this pull request as ready for review September 1, 2026 12:44
@graeme
graeme requested a review from MikeMcQuaid September 1, 2026 12:44
@graeme
graeme merged commit 902b84e into main Sep 2, 2026
10 checks passed
@graeme
graeme deleted the console-improvements branch September 2, 2026 08:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants