Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/decisions/054-the-app-signs-its-own-updates.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,8 @@ responsibility; this is that responsibility finally built, in the only shape ad-
where nothing is wrong, which today is exactly one thing. It wears the accent rather than a colour
of its own, because there is no `--color-info` in any of the forty-three palettes and adding one
would be forty-three generated files changed to tint one strip.

> Superseded by ADR 055. The announcement moved out of the banner and into the title bar, and the
> tone had no callers left, so `info` and `InfoIcon` are gone. Everything else in this record —
> the manifest, the key, the eligibility guards, the swap script, and "never without a press" —
> stands unchanged.
100 changes: 100 additions & 0 deletions docs/decisions/055-the-update-lives-in-the-title-bar.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# 055: The update lives in the title bar

Status: Accepted

## Decision

A newer preman is announced by a chip in the title bar, not by a banner across the window.

`updateChip()` in `packages/desktop/src/renderer/model/update.ts` replaces `updateBanner()`. It
answers for three phases and `null` for the other five. `App.tsx` draws the answer as one
chrome-tier control at the trailing end of the title bar, immediately before the Settings gear:

| Phase | Chip | Press |
| ------------- | ------------------------- | ------------------ |
| `available` | `Update 0.4.2` | downloads |
| `downloading` | `Downloading 47%` | nothing to press |
| `ready` | `Restart to update 0.4.2` | quits and installs |

`idle`, `checking`, `current`, `unsupported` and `failed` draw nothing at all. The full sentence —
the one the banner used to carry, `updateHeadline()` — is the chip's tooltip, and the Settings
pane's Updates section is unchanged and still holds Skip, Check now, and every phase in words.

`Banner`'s `info` tone and `InfoIcon` are deleted. Both existed for this one caller.

## Rationale

**A bar is the shape this app uses for a problem, and this is not one.** `Banner` is
`ui/Banner.tsx`, it is a sibling of the whole resizable workspace, and `design-system.md` lists it
under "Saying something went wrong". ADR 054 knew this and answered it by adding a third tone that
means "nothing is wrong" — which is a tone arguing with its own component. The argument was visible
in the pixels: the strip was the same width as the window and the same shape as a host crash, so a
release announcement read with the urgency of an engine that had died.

**It cost a permanent row for a once-a-release fact.** The banner is `shrink-0` above the panel
group, so every pixel it takes is a pixel the editor and the console do not get, for as long as the
user does not press it — and "not now" is the honest answer to an update most of the time. A
40-pixel strip is a tax on postponing. A chip is not.

**Chrome is where a fact about the whole window already goes.** The title bar's stated job in
`App.tsx` is the workspace picker: a fact about the window rather than about the request. Which
build this is, and that there is a newer one, is the same kind of fact. The chip borrows the
picker's geometry exactly — `h-control`, `rounded-sm`, `px-1.5`, `text-xs` — so the row reads as one
strip with two things in it rather than as a strip with a notification bolted on.

**The accent stops being an exception.** ADR 054 had to argue that the banner was the one place the
accent appears without being the thing you came to press. In the title bar it _is_ the thing you
came to press, and it is the only accent in the row, so `design-system.md`'s rule holds as written
instead of carrying a carve-out.

**`downloading` becomes showable, which is the one thing the move adds.** The banner deliberately
refused that phase: the user pressed the button that started the download, and a strip across the
window reporting their own press back at them is noise. A chip has the opposite problem — it _is_
the button they pressed, and a control that disappears on the press and returns two minutes later as
a different control is a control that looks broken. So the chip stays and reports itself in place,
with the percentage where the version was. There is still no `Progress`: `design-system.md` reserves
that for a wait that is long, opaque, and has a denominator that cannot be revised, and a download
with no `content-length` fails the third. When there is no denominator the chip says the version
instead of inventing a percentage.

**The phase with nothing to press is a `<span>`, not a disabled `<button>`.** A disabled button
emits no pointer events in Chromium, so its tooltip never opens — the rule `design-system.md`
already states for the gRPC field's lock, met here for the second time. Marking it up as a status
also stops it being a tab stop that does nothing.

**It sits before the gear, not after it.** `App.tsx` opens by promising the layout never moves, and
the gear is the one control in that row people aim at without looking. This is the order in which
the chip's arrival costs nothing: the gear stays pinned against the right edge and the chip grows
into the empty run beside it. Put the chip last and the trailing group widens leftwards instead,
which moves the gear twice per release — the first draft did exactly that, and it took someone
looking at a screenshot to notice the promise had been broken by the thing quoting it.

**Its glyph is `ArrowCircleUp`, not the import tray.** `ui/icons.ts` already warns that reusing a
glyph makes two controls read as one thing, and `ImportIcon` is the sidebar's "Import from cURL",
four rows below. Up rather than down because what the chip announces is a version to move to; the
download it starts is a step on the way, and the label says so in words.

## Consequences

**`BannerTone` loses a third of itself, and that is the point.** `info` had one caller. Keeping a
tone nothing wears would leave the next reader deciding what it is for, so it is gone along with
`InfoIcon` — `ui/icons.ts` is the icon audit, and an unused export makes "how many icons does this
app use" a question with the wrong answer. `ICON_BY_TONE` collapsed too, because both remaining
tones are warnings. A tone that is not a warning has to bring its own map back.

**ADR 054's last consequence no longer holds.** "The banner grows a third tone" was true for one
release. 054 keeps its file, its number and its status; this is what replaced that paragraph.

**A user who never looks at the title bar will not learn about an update.** Accepted, and it is the
deliberate half of the trade: the previous design guaranteed they saw it by taking a row of the
window until they acted, and an update is not worth that. The automatic check still runs, the chip
is accent-tinted against otherwise neutral chrome, and `Check for updates` is in the app menu and
the Settings pane for anyone who goes looking.

**Skip is now two steps away rather than one row away.** A chip that opened a menu would be a notice
that had stopped being a single press, so Skip stays in the Settings pane where 054 put it. The
chip's tooltip names the version, which is the thing someone deciding to skip wants to read first.

**The title bar now has a control that appears and disappears.** It is the first one. It spreads
`BANNER_MOTION` rather than declaring a second easing curve, so `docs/design-system.md`'s claim that
exactly one curve is duplicated for Motion's sake stays true.
12 changes: 12 additions & 0 deletions docs/decisions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -388,4 +388,16 @@ macOS 26's app-bound data protection lets a self-updated build read its own `sta
whether TCC re-prompts — and the phase 0 change to `store.ts`, which is worth having on its own,
is what will tell us if the first one bites.

055 is a retraction inside 054 rather than a new capability. The updater shipped announcing itself
with a bar across the window, which meant `Banner` — the component `design-system.md` files under
"saying something went wrong" — grew a tone whose whole job was to say nothing was wrong. The
argument was legible in the pixels: a release announcement was the same width and the same shape as
a dead engine, and it charged a permanent row of the window for as long as the user's answer was
"not now". Moving it into the title bar costs the guarantee that the user sees it — accepted on
purpose, and the honest half of the trade — and buys back two things the bar could not have: the
accent stops needing a carve-out, because in the chrome the chip really is the thing you came to
press, and `downloading` becomes showable at all, since a control reporting its own press in place
is the opposite problem from a strip reporting it back at the presser. The third tone and its icon
are deleted rather than kept warm.

`TEMPLATE.md` is the shape of a new one.
40 changes: 27 additions & 13 deletions docs/design-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,17 +307,29 @@ Three things, and they are not interchangeable.
| the pane's content | `panes/ResponseFailure.tsx` | there is nothing to show, and why there is nothing is the information |
| one faint line | a local `Hint` | there is nothing to show and that is unremarkable: no cookies, no tests |

`Banner` is `border-{tone}/40 bg-{tone}/10`, chrome tier, `px-gutter py-1.5`, and three tones:
`danger` and `warn` wear the `WarningIcon`, and `info` wears the `InfoIcon`. `ok` and `neutral`
still do not warrant a bar. `info` is narrow on purpose — a fact the reader has to be told and can
act on, where nothing is wrong — and today it is used by exactly one thing, the updater's "a new
version is available" and "it is ready to install" (ADR 054). It is tinted with the accent rather
than a colour of its own because no palette declares a `--color-info` and adding one would be
forty-three generated files changed to tint one strip; that is the one place the accent appears
without being the thing you came to press. It takes either a
`detail` beside the message — monospace and truncated, so an id or a path — or `details` below it,
one prose line each, which is what a `PremanError` carries. It exists because four panes wanted it
and three had already written their own with three different class strings.
`Banner` is `border-{tone}/40 bg-{tone}/10`, chrome tier, `px-gutter py-1.5`, and two tones, both
wearing the `WarningIcon`. `ok` and `neutral` still do not warrant a bar, and neither does good
news: there was an `info` tone for one release, for the updater's "a new version is available", and
decision 55 moved that into the title bar and deleted the tone. A bar is the shape this app uses
for a problem, so a tone whose job was to say nothing is wrong was a tone arguing with its own
component. It takes either a `detail` beside the message — monospace and truncated, so an id or a
path — or `details` below it, one prose line each, which is what a `PremanError` carries. It exists
because four panes wanted it and three had already written their own with three different class
strings.

**A fact that is not a problem goes in the chrome, not in a bar.** The update chip in `App.tsx` is
the one case and the pattern for the next: a chrome-tier control at the trailing end of the title
bar, borrowing the workspace picker's geometry exactly — `h-control rounded-sm px-1.5 text-xs` — so
the row reads as one strip rather than as a strip with a notice bolted onto it. It is the only
accent in that row, and it is the thing you came to press, so it is not an exception to the rule
below. The phase it cannot act in is a `<span>` and not a disabled button, for the reason a field's
lead is: Chromium emits no pointer events from a disabled button, so its tooltip never opens.

Two things about it generalise. It goes **before** the row's permanent controls, never after: a
transient that arrives last widens the trailing group leftwards and shifts everything that was
already there, while one that arrives first grows into the empty run and shifts nothing. And it
carries its **own** glyph — `ui/icons.ts` is the audit precisely so that the second control to want
a download arrow has to answer why it is not the first one.

`details` is capped at eight lines and scrolls past that, with the count said beside the message
once there is more than the box shows. It is the box that is bounded and never the list: those lines
Expand Down Expand Up @@ -491,8 +503,10 @@ What actually animates:
appears at all after 150ms of waiting — a workspace of a normal size opens inside that and paints
no placeholder, which is the whole reason the delay is there. `docs/performance.md` gates both
halves of that.
- **Two surfaces with real presence**, via Motion through `ui/motion.tsx`: the banner and the
overlay-pane swap.
- **Three surfaces with real presence**, via Motion through `ui/motion.tsx`: the banner, the
overlay-pane swap, and the title bar's update chip. The chip spreads `BANNER_MOTION` rather than
restating the curve — it is the same gesture, a notice arriving — which is what keeps the
duplicated easing below a count of one.
- **The disclosure caret**, in the sidebar and the console drawer, and dnd-kit's drop animation.
- **The active tab's underline**, which travels rather than blinking. `ui/Tabs.tsx` owns the trigger
and the underline for all four tab groups — the request editor's sections, its Edit/Preview
Expand Down
Loading
Loading