fix(control): a sentence you can read, and the check that noticed it - #479
Merged
Conversation
/control's "Suggested next" line carried `truncate` — one line, ellipsis. On
prod that rendered "Suggested next (profile): Conduct a 1-month baseline m…":
594 of its 644 pixels hidden at 390px, 432 at 1440. The remainder existed
only in the `title` tooltip, and a phone has no hover.
It is the card's one actionable sentence, and clicking it loads the text into
the composer — so the sentence IS the decision, and a tenth of a sentence is
not one. OutcomeStreak.tsx, in this same directory, documents having already
fixed this exact mistake for its glyph row. /activity has always clamped the
same "what's next" content to two lines (ActivityEventRow). This makes the
three agree: line-clamp-2.
Same class in the composer's status line, worse: it shared a row with four
icons and Send, leaving it 72px — measured at 0% shown at 320px and 14% at
390px. Shorter copy cannot fix that; no sentence fits 72px. It needed the
width, so below `sm` it wraps onto a row of its own (order-last w-full) and
from `sm` up it stays exactly as it was.
THE GATE, and why it is not a lint rule.
responsive-audit already looks for clipped text, and deliberately EXEMPTS
`text-overflow: ellipsis` as "a deliberate design choice". That exemption is
right for what it was written for — a project name in a narrow rail, a
filename, an id — and 76 files under src/components truncate exactly that way.
A rule banning `truncate`, or `truncate` beside a `title`, would fire on ~42
legitimate sites and be turned off within a week.
The distinction is prose, which a static rule cannot judge but a real viewport
can measure. So the check lives where the viewports are: flag an ellipsis only
when the text READS as a sentence (>=60 chars, >=8 spaces — an id has neither)
AND under half of it is visible (a chip losing its tail is fine; a line showing
its first eight words is not).
Proven by mutation, in one environment, with only the classNames differing:
- with `truncate` -> 2 findings ("40% shown, -552px", "22% shown, -402px")
- with the fix -> silent at 320 / 390 / 768 / 1440
and it flags none of the 76 legitimate truncations.
Verified against prod before the fix (2 findings at 320 and at 390, silent at
768/1440 where the line fits) and against a local server after it.
npm run verify passes.
Separately observed and NOT addressed here: /control logs a React #418
hydration mismatch at every viewport.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UvjGNAS9CMfEGNW26tUR4P
github-actions Bot
pushed a commit
that referenced
this pull request
Sep 4, 2026
…off by 2x (#480) Three follow-ups from sweeping the earlier findings. Two of the four I had carried forward did NOT survive checking and are dropped: /api/control/dispatch is a real route, so the docs referencing it are correct; and scripts/test/push-notifications.ts exists. Verifying first is why this commit is smaller than the list that prompted it. 1. THE TEST NOBODY RAN, FOR A REASON THAT WAS NEVER TRUE scripts/test/push-notifications.ts read scripts/agent-hook-bridge.sh, which was deleted on 2026-06-11 in 956ccf6 ("delete the bash daemon and bridge files"). It threw ENOENT and the suite exited 1. Its entry in test-unit.ts SKIP said "needs push/web-push env — run manually". That was never the reason: every check in the file is a static file read and it needs no environment at all. A wrong skip reason is worse than no skip. It answers the question that would have found the bug, so the red looked accounted for and nobody looked again for three months. Retargeted rather than deleted. The behaviour moved instead of going away — a closing run reaches the operator through notify-close.ts → pushToUser → push-fanout — and scripts/test/notify-close.ts covers that module without mentioning push at all. So "an agent finished, tell the operator" was asserted by nobody. Deleting the check would have quietly ratified that hole. Removed from SKIP, so it now runs in test:unit (132/132) and therefore in CI. Mutation-proven: renaming the pushToUser call fails with "notify-close must call pushToUser" (exit 1); restored, exit 0. 2. THE BOX IS HALF THE MACHINE THE DOCS DESCRIBE Measured 2026-09-04: nproc 4, 7746 MiB, 75 G disk, 4 GB swap. That is a CX33. Three docs said CX43, 8 vCPU / 16 GB, 40 GB disk — every figure but the swap wrong, in the direction that matters: planning against 16 GB on an 8 GB box is how you meet the OOM killer. src/app/api/system/hetzner/route.ts has said "the box is a cx33" all along, so this was code and docs disagreeing with the code being right. Note the shape of the drift: CPU and RAM went DOWN while disk went UP (40→75), which is what a disk-inclusive rescale to a smaller tier looks like — plausibly to escape the disk-full problem the old caveat describes. The June runbook entry is kept as history and marked superseded rather than rewritten. The docs now point at /api/system/hetzner instead of restating numbers. A hardcoded spec is a second source of truth that rots silently, which is exactly what happened here between June and September. 3. THE COMPOSER STATUS, FINISHED #479 gave this line its own row below `sm` but kept `sm:truncate`, which left it at 57% on a 1440 viewport — "Autopilot on: queue is empty, so FleetCr…". That is the same defect as the phone's 14%, just less obvious, and there is no `title` here so the remainder is unreachable at every width. It now wraps everywhere. Costs one toolbar line when the sentence is long; truncating cost the half of the sentence that says what will happen. pnpm run verify passes (repo moved to pnpm in #478). Claude-Session: https://claude.ai/code/session_01UvjGNAS9CMfEGNW26tUR4P Co-authored-by: Mao Nakamoto <41178744+maonakamoto@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
github-actions Bot
pushed a commit
that referenced
this pull request
Sep 4, 2026
…lamp that never clamped (#482) Found by using /control in a browser as a user, on a phone-width window, start to finish: pick a project, type a prompt, send it, follow the outcome. 1. "PASTE FROM HISTORY" WAS SHOWING THE OPPOSITE OF WHAT IT PROMISES The card offered back conversational asides — "you do it. open in browser and do it" — as reusable work. Not a rendering bug: those really were dispatches. The selection was wrong. Measured on prod, 30 days: 1859 distinct custom prompts, of which 1823 (98%) were sent exactly ONCE. The query ordered by recency, so it surfaced the three most recent one-offs — by definition the prompts least likely to be wanted again — while the genuinely reused ones sat unoffered: "continue" 115×, a git-diff review prompt 63×, "go ahead" 14×, "keep going" 8×. Requiring two uses cuts the pool from 1859 to 36 and leaves exactly what the feature is named for. Nothing is lost: Activity keeps every dispatch with a re-run control. One-offs just stop competing for space on the card. 2. THE CLAMP I SHIPPED IN #479 WAS INERT, AND I COULD NOT SEE IT `block ... line-clamp-2` computes to `display: block; -webkit-line-clamp: 2`. `-webkit-line-clamp` does nothing without `display: -webkit-box`, and Tailwind's `block` utility overrode it. It LOOKED fixed because an unclamped box just wraps — which reads fine until the sentence is long. Checked live in the browser: a 460-character next_step grew that box from 48px to 96px and pushed the composer, the thing you act with, off the bottom of the viewport. So my "it renders fully now" was measuring the ABSENCE of a clamp. Dropping `block` makes it work; three lines, not two, so a typical next_step (~256 chars) still reads in full and a pathological one is bounded and reachable (clicking loads the whole text into the composer). THE GATE: responsive-audit now flags a line-clamp that is declared but not enforced. It cannot test `display === "-webkit-box"` — Chrome reported `flow-root` for a clamp working correctly — so it tests BEHAVIOUR: an element declaring N lines that renders more than N. Run against live prod it says: "Suggested next (profile)…"[ui-link-subtle-button.block.line-clamp-2] declared 2 lines, renders 4 (display:block) naming the offending class. That is this exact bug, caught in shipped code. 3. A FAILURE MESSAGE THAT NAMED THE WRONG AGENT A real dispatch returned: "launched claude (pty) + injected, but the agent isn't generating yet … Retry, or switch the project agent away from grok if this repeats." One sentence naming the agent that ran and, as advice, one that did not — a literal left over from when grok was the default. `agent` was in scope; the auth branch above it interpolates it correctly. Gated in failure-message-pairing: a failure message that NAMES an agent must interpolate ${agent}. Stated that way rather than "no agent ids", because the auth message legitimately says `~/.claude/.credentials.json` and `claude setup-token` — a real path and a real command — and already names the right agent. The check joins `+` continuations so a message split across two lines is judged whole; per-line it got that one wrong. Mutation-proven: restoring the "grok" literal → 20 passed, 1 failed, naming the line. Restored → 21/21. pnpm run verify passes. Claude-Session: https://claude.ai/code/session_01UvjGNAS9CMfEGNW26tUR4P Co-authored-by: Mao Nakamoto <41178744+maonakamoto@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.
The bug
/control's "Suggested next" line carriedtruncate— one line, ellipsis. On prod that rendered:594 of its 644 pixels hidden at 390px, 432 at 1440. The rest existed only in the
titletooltip, and a phone has no hover.It is the card's one actionable sentence, and clicking it loads the text into the composer — so the sentence is the decision, and a tenth of a sentence is not one.
OutcomeStreak.tsx, in this same directory, documents having already fixed this exact mistake for its glyph row;/activityhas always clamped the same "what's next" content to two lines (ActivityEventRow). This makes all three agree.Same class in the composer's status line, worse: it shared a row with four icons and Send, leaving it 72px — measured at 0% shown at 320px and 14% at 390px. Shorter copy cannot fix that; no sentence fits 72px. It needed the width, so below
smit wraps onto a row of its own and fromsmup it is unchanged.The gate — and why it is deliberately not a lint rule
responsive-auditalready looks for clipped text, and deliberately exemptstext-overflow: ellipsisas "a deliberate design choice". That exemption is correct for what it was written for — a project name in a narrow rail, a filename, an id — and 76 files undersrc/componentstruncate exactly that way. A rule banningtruncate, ortruncatenext to atitle, would fire on ~42 legitimate sites and be switched off within a week.The distinction is prose, which a static rule cannot judge but a real viewport can measure. So the check lives where the viewports already are, and fires only when:
Proven by mutation
One environment, one run, only the classNames differing:
truncate40% shown, -552px,22% shown, -402pxIt flags none of the 76 legitimate truncations.
Also verified as a natural before/after: against prod (old code) it reported 2 findings at 320 and at 390 and stayed silent at 768/1440 where the line genuinely fits; against a local server with the fix, silent everywhere.
Verification
npm run verify— exit 0npm run audit:responsive -- /control— clean, authenticated (the run that "passed" while landing on/sign-inwas caught by the audit's ownlanded onnote and redone)Observed, not addressed here
/controllogs a React #418 hydration mismatch at every viewport. Out of scope for this change; flagging it so it isn't lost.🤖 Generated with Claude Code
https://claude.ai/code/session_01UvjGNAS9CMfEGNW26tUR4P