fix: say less, and say the true thing - #23
Merged
Merged
Conversation
A read of every string a user sees, on the brief that the product should read as if a person wrote it for a person. Three were wrong rather than merely long. The answers screen showed raw Markdown. Claude writes bold text, lists and tables; the screen preserved the line breaks and rendered nothing, so a reader got asterisks around every bold phrase and a table collapsed into `| | |`. This is the same defect a user caught in the release-notes dialog an hour earlier — the fix existed and had not been applied here. The renderer is now shared, and learned tables and block quotes on the way. It is called Prose rather than ReleaseNotes, since that is what it renders. A Gemini error told people to set an environment variable and restart the daemon. The key has had a field on the Cost screen since this morning and takes effect immediately. A wrong instruction costs more than a long one. And a Go package prefix reached the dialog box: `agents: "/x" is not an absolute path` is a message written for whoever was debugging it. The rest is length. Several tooltips explained the attribution algorithm where the reader had asked what a number meant — the per-directory rule was four sentences of specification on hover, and is now one. Several paragraphs defended decisions nobody had questioned: why the Telegram bot is yours, why Caprock will not type into a session it did not start, why a connection timer counts up when nothing is happening. Each of those teaches a reader to be suspicious of something they had accepted. "Everything here is measured — no invented numbers" is gone from the Cost screen. It is project rule 6 leaking onto the display: readers assume the figures are real until told otherwise, so the sentence created the doubt it was answering. Four tests broke. Three were asserting exact words rather than the claim underneath, and now assert the claim. The fourth asserted that answer text survives with its line breaks intact — the behaviour this change deliberately removes — and now asserts that the structure is rendered. Claude-Session: https://claude.ai/code/session_01DR8fggA2LRHcjNWUsqtDcF
…measured Three things a user pointed at in one sitting. Sharing was locked to all time. The picker had been removed on the reasoning that a card showing today, the week, the month and all time at once made choosing redundant — but somebody sharing a working week does not want their lifetime total to be the headline, and a card that answers four questions answers none of them loudly. The period is back and decides what the heading claims and which figure is lit; every other one stays on the card, because a week means nothing without knowing whether it was a normal week. It opens on the week. You could not see the card before sending it. The two buttons said where the picture would go and nothing said what was in it: you pressed Save, opened your downloads, and only then found out what you had chosen. The card is drawn in the sheet now, at the size it will be seen, redrawing as the period changes — which is what makes the choice worth offering at all. The Share button could not be found. It was outlined in the accent colour in a row of 11px text, beside a premium button that is a solid block of it; an outline cannot win an argument with a fill. Filled, a size up, and carrying an icon. It earns that: sharing is the only thing on this dashboard that leaves the machine. And the first screen told a lie on every load. `measured` could not tell "no answer yet" from "the answer is nothing", so for the second it took the first response to land it announced that a machine with thousands of turns had never run anything — reported as "the data took very long to load" by someone whose data was there the whole time. Measured on the 641 MB database that produced the report: nothing on that screen takes longer than 0.7s. The delay was never the problem; the confident wrong answer during it was. Claude-Session: https://claude.ai/code/session_01DR8fggA2LRHcjNWUsqtDcF
Three figures in a row with nothing over them is a puzzle. "$7,587.14 · 12.01B · 58%" reads as three unrelated numbers until you have worked out which is which, and the "by cost" sitting in the corner describes the sort order rather than the columns. Every breakdown on Now, Cost and Lifetime names them now, on the same line as the table's own title so it costs no vertical space and leaves no gap between a heading and the row it heads. The tool table has two figures rather than three, and now says so instead of leaving a hole where a third would go. It could have had tokens. It does not, and that is the interesting part: tool events carry no tokens at all — checked, and they are exactly zero across every tool. Tokens live on the assistant turn. A tool does not spend tokens; the turn that called it and read its output does. So the column could only be attributed, and every way of attributing it is wrong in a way that would not show. Give the whole turn to each call and the totals exceed what was spent. Split it evenly and a Read pulling a 200KB file scores the same as an `ls` in the same turn, when the file is the entire reason that turn was expensive. On top of that, 14% of Bash calls cannot be linked to a paying turn against 1% of Read's, so whatever split was chosen would understate Bash specifically — which is the comparison this table exists to make. A figure that looks measured and is quietly skewed is worse than a column that is not there. Separately, the all-time breakdown moved up to sit under the live pulse. It was last, below a tall activity feed, so the most legible thing on the screen was the thing you had to scroll to find. It reads as a continuation of the pulse: that shows the shape of the work, this shows what the work was. Claude-Session: https://claude.ai/code/session_01DR8fggA2LRHcjNWUsqtDcF
Five tables showed numbers with no headings and a percentage with nothing naming what it was a percentage of. Every one of them now names its columns on the same line as its title, so the row is a header rather than an extra line of chrome above one. Most-used tools gains a "returned" column, and it inverts the story the call count tells: Bash is 46,343 calls that hand back 13.4MB, Read is 11,767 calls that hand back 58.7MB. Four times fewer calls, four times more context. The loud tool is the cheap one. Bytes and not tokens on purpose. Tool results arrive in the transcript with no token attribution of their own, and the only way to show a token figure here is to invent one from a bytes-per-token ratio — which would understate Bash against Read by an order of magnitude, in a table whose whole job is comparing them. Bytes are measured; a converted token count would not be. Measured rather than computed per request: a new `tool_bytes` column, filled from the response at write time and backfilled once, plus a covering index. The ALL TIME query went 2.1s to 0.05s on a 641MB database, so the panel that took a visible beat to appear now doesn't — no cache, no extra moving part. Migration 0019 drops the index 0016 added for this same query, which 0018's index now supersedes. And the plan picker: once a plan was set the button went grey and borderless, which is exactly what an unset control looks like, so it read every session as though the plan had never been chosen. It now carries a check and keeps its border. Claude-Session: https://claude.ai/code/session_01DR8fggA2LRHcjNWUsqtDcF
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.
A read of every string a user sees, on the brief that the product should read as if a person wrote it for a person.
Three were wrong, not merely long
The answers screen showed raw Markdown. Claude writes bold text, lists and tables; the screen preserved line breaks and rendered nothing, so a reader got asterisks around every bold phrase and a table collapsed into
| | |.This is the same defect caught in the release-notes dialog an hour earlier — the fix existed and had not been applied here. The renderer is now shared (and learned tables and block quotes on the way). Renamed
ReleaseNotes→Prose, since that is what it renders.A Gemini error sent people somewhere they no longer need to go. "Set GEMINI_API_KEY in the daemon's environment and restart it" — but the key has had a field on the Cost screen since this morning and takes effect at once. A wrong instruction costs more than a long one.
A Go package prefix reached the dialog box.
agents: "/x" is not an absolute pathis a message written for whoever was debugging it.The rest is length
Several tooltips explained the attribution algorithm where the reader had asked what a number meant. The per-directory rule was four sentences of specification on hover:
Now: "Cost is charged to the folder Claude was last working in. Rows add up to the repository total."
Several paragraphs defended decisions nobody had questioned — why the Telegram bot is yours, why Caprock will not type into a session it did not start, why a connection timer counts up when idle. Each of those teaches a reader to be suspicious of something they had accepted.
"Everything here is measured — no invented numbers" is gone. That is project rule 6 leaking onto the display: readers assume figures are real until told otherwise, so the sentence created the doubt it answered.
The tests that broke are worth noting
Four failed. Three asserted exact words rather than the claim underneath —
toContain('approximately')where the text now says "roughly" — and now assert the claim.The fourth asserted that answer text survives with its line breaks intact. That is the behaviour this change deliberately removes: it passed for months while readers saw markup. It now asserts the structure is rendered.
https://claude.ai/code/session_01DR8fggA2LRHcjNWUsqtDcF
Then: say what each number is
The copy pass ended at the words. Three screens later the same brief caught the numbers.
Five tables had no column headings — a value, a bar and a percentage, with nothing naming what the percentage was of. Each now names its columns on the same line as its title, so the header row costs no extra line.
Most-used tools gained a
returnedcolumn, and it inverts the story the call count tells. Bash is 46,343 calls that hand back 13.4MB; Read is 11,767 calls that hand back 58.7MB. Four times fewer calls, four times more context. The loud tool is the cheap one — which is not visible anywhere else in the product.Bytes and not tokens, on purpose. Tool results arrive in the transcript with no token attribution of their own. The only way to print a token figure here is to invent one from a bytes-per-token ratio, which would understate Bash against Read by an order of magnitude — in the one table whose whole job is comparing them. Rule 6, in its most harmful form. Bytes are measured.
Measured at write time, not per request. A new
tool_bytescolumn, filled from the response as the event is stored and backfilled once, plus a covering index.GET /v1/history?range=allwent 2.1s → 0.05s on a 641MB database, so the panel that took a visible beat to appear no longer does. No cache, no new moving part.Migration
0019dropsidx_events_kind_ts_tool, which0018's index now supersedes — as its own migration, because0018had already run where it mattered, and an edit to an applied migration is an edit nobody receives.And the plan picker. Once a plan was set the button went grey and borderless — exactly what an unset control looks like — so every session read as though the plan had never been chosen. It now carries a check and keeps its border.