skip to content
Jobs

Drag to Select and Copy in the Monitor

Grab text out of any pane or error popup with the mouse, and copy it on release

Gantry milestones
Text selection and clipboard copy in the terminal UI.

Gantry's build monitor is a terminal UI: a feed of activity cards, a sprint sidebar, a chat transcript, and popups for errors, prompts, sessions, and stats. To read closely you often want to lift text out — the message from an error popup, a line from the transcript — but the terminal's own click-and-drag selection had been turned off. To keep the draggable column divider working, the monitor asked the terminal for mouse-motion events, and that capture also swallowed hover, which lit up menu items.

This job restored hover and gave the monitor its own text selection instead of the terminal's. Every pane and popup now records the exact text it drew each frame, a mouse drag selects inside whatever region it started in, and letting go copies that text to the system clipboard and the terminal's primary selection. The selected cells are tinted so you can see what you grabbed. The terminal's native selection stays available under Shift-drag for anyone who wants the raw pixels instead.

Build

Selection is an interactive feature, but the gate cannot click a mouse — so the work was cut into pieces each provable without one: record-what-you-drew, route-the-gesture, tint-the-result.

Text selection is driven by mouse motion, and no automated test can move a mouse. That is the constraint the whole decomposition is shaped around: the feature had to be split into pieces that a test could exercise without any pointer at all, and the boundaries are drawn exactly where that becomes possible.

The first split separates recording from routing. Two sprints do nothing but register, each frame, the text every pane and popup actually drew — a stable id, the rectangle it occupied this frame, and the exact rendered lines — with structural chrome deliberately left unregistered so it can never be selected. The briefs are explicit that this is kept apart from any mouse logic precisely so the registered geometry can be asserted on its own, against a deterministically drawn frame, before a gesture exists to consume it. Only once blocks exist does a later sprint refactor the event loop's inline mouse handling into a function a test can call with synthetic press, drag, and release events, and assert the extracted string. Registration proves what is selectable; routing proves what a gesture does with it; neither test needs a real pointer.

Two other pieces were isolated for their own reasons. The mouse-mode change — the one standalone, user-visible fix, since it restores menu hover on its own — was landed first and alone, so its trade (giving up native selection in favour of app-level selection) could be reasoned about before anything depended on it. The highlight tint was deferred to last as a strictly additive post-pass, so it could be built and tested against selections the routing sprint already produced without touching how any pane renders.

The run stayed green across every sprint, with no re-plan and nothing sent back. For an interactive feature that the gate structurally cannot drive, that is the decomposition doing its job: the difficulty of proving mouse behaviour without a mouse was dissolved into three separately testable claims in advance, rather than fought at any one boundary.

Feature

The monitor gained its own text selection: every pane and popup records its drawn text, a drag copies on release to clipboard and primary, and the tint marks what was grabbed.

Before this job, the monitor's text was on screen but out of reach. The terminal's own selection was suppressed to keep the column divider draggable, and that same suppression had quietly cost menu hover. There was no way inside the tool to lift an error message or a transcript line into the clipboard.

The design that answers this puts a selection engine into the app's per-frame state alongside the existing redrawn-every-frame layout facts. Each draw clears a block registry and refills it: the feed cards keyed on card identity so a selection survives scrolling, the sidebar, the chat, and — while a popup is open — only that popup's text, so a drag stays inside the modal and the panes beneath go un-hit-testable. A press anchors a selection in the block under the cursor unless it lands on the divider gutter, which keeps its resize meaning; a drag extends within the block; a release copies the extracted text and tints the covered cells. Clipboard writes go through the selection crate alone, and the tint is a post-pass so no pane changed how it renders.

At today's `HEAD` the design still stands, and it has grown rather than eroded. The state module still carries the shared block-id scheme, and it now covers more popups than the original plan named, including a recent-runs modal and a settings modal — the convention was built to extend, and it was extended. The selection tint is still a distinct theme style applied as the frame's last paint step. The most visible drift is the dependency: the selection crate that began as a sibling path checkout is now vendored under `vendor/`, so a plain checkout builds with no external path dependency — the same move already made for the menu and clipboard crates. Most of the lines the job introduced are still present across all of its files, none of which have been removed.

unavailable sessions
unavailable context
80,696 in / 215,222 out tokens
$30.67 cost
0 x 5 milestones x sprints
unavailable edits
unavailable commands
1h 16m duration
unavailable roles
0 x 0 fixes x replans
- harness