Every number below is transcribed from ReAnalyseConfirmSheet.swift and the strings
from bristlenose/locales/en/desktop.json. This is a render of what the code
produces, not a proposal for what it should be.
The intent drawing lives in analysis-lifecycle-states.html §“The confirmation — measured, not vague”. It was drawn in the web design system; the sheet is native. Where the two differ, this page shows the native side.
22 Aug 2026 — the buttons changed. Rendering the sheet is what surfaced it: Cancel held the default and the confirm button carried the destructive style, which the HIG contradicts on three counts. The artefacts below are the corrected sheet; the section directly after this one holds the before-and-after.
A project with fourteen sessions and all five kinds of curation present.
Re-analyse “Onboarding diaries”?
Bristlenose will transcribe and analyse all 14 interviews again from scratch.
Re-analyse “Onboarding diaries”?
Bristlenose will transcribe and analyse all 14 interviews again from scratch.
Same geometry, same strings. Only which button is the default, and whether the other one is styled destructive.
The sheet put .keyboardShortcut(.defaultAction) on Cancel, and on macOS the
default button carries the accent fill — so the prominent blue control in a destructive
confirmation was the way out, sitting to the left of a red-labelled
Re-analyse. Apple’s alert guidance contradicts that three times over: don’t make Cancel
the default; always place the default on the trailing side; and reserve the destructive style
for actions people did not deliberately choose. All three follow from the first.
Empty Trash is Apple’s own example of this exact case. Because the person chose it, the alert does not apply the destructive style, and the convenience of Return-to-confirm outweighs restating that the action destroys. Re-analyse… is chosen from a menu the same way. The sheet’s old comment argued the opposite — Return must not finish the sentence for them — which is a real concern, but the HIG’s answer to it is give no button the default, never put the default on Cancel.
Two other confirmations ship in this app and both conform. The Locate-error alert puts the
default on its action; the Disconnect-account alert deliberately omits the destructive role,
with a comment citing Empty Trash by name and noting that dropping it “also restores
Return-to-confirm”. Both are native .alerts, so AppKit places their buttons. This
sheet hand-rolls its row, which is how it drifted out of a convention it never opted into.
The intent drawing gave the trailing button the visual weight — it was only the wrong colour, red where the platform draws blue. The emphasis was never the disagreement; the hue was.
Native text styles: .headline is 13pt semibold and .callout is 12pt.
The whole sheet therefore runs a little smaller and tighter than the drawing, which used
--bn-text-heading (18px) for the title and the mono face for the counted lines.
The counted lines here are the system face at 12pt, secondary, indented 12pt — a list, not a
code block.
| Element | Swift | Rendered |
|---|---|---|
| Sheet | .frame(width: 420) · .padding(22) | 420 × 314.5pt (SwiftUI), 376pt content |
| Stack | VStack(alignment:.leading, spacing:14) | 14pt between every block |
| Icon | arrow.clockwise, .system(size:26), secondary | 26pt, ~50% label |
| Title | .font(.headline) | 13pt semibold |
| Rebuild line | .callout, secondary, .fixedSize(vertical:) | 12pt, wraps to 2 lines |
| Loss header | .callout.weight(.medium) | 12pt medium, full label |
| Loss lines | .callout, secondary, .padding(.leading,12) | 12pt, 3pt apart, 12pt indent |
| Buttons | Cancel .cancelAction · Re-analyse .defaultAction | 22pt tall; default takes the accent fill, trailing |
Measured by SwiftUI: the canonical sheet is 420 × 314.5pt, and the long-name sheet 330.5pt — the title takes a second line and the sheet grows 16pt. The counted block is the only other part that varies; in the CSS renders here, four omitted lines take 72pt with them. Treat the CSS heights on this page as ~7.5pt light.
The glyph is a stroked stand-in — the real one is the filled SF Symbol arrow.clockwise,
which is slightly heavier and sits a touch lower on its baseline. And the gap between the two
buttons is SwiftUI’s platform default rather than a number in the source; 10pt is drawn here.
Neither changes a decision, but neither is measured.
Zero-count kinds are omitted, so the sheet’s height is a function of how much the researcher has done. Three real shapes.
Re-analyse “Pilot”?
Bristlenose will transcribe and analyse 1 interview again from scratch.
Re-analyse “Ward rounds”?
Bristlenose will transcribe and analyse all 9 interviews again from scratch.
With a single loss line the counted block is a header and one indented item, and the 14pt gaps above and below leave it floating in the middle of a sheet that is otherwise mostly air. It still reads, but it is the shape where “measured, not vague” buys least — six starred quotes is a smaller thing to weigh than the sheet’s own ceremony.
askToReAnalyse reads the counts first and starts the run without asking when they
are all zero. So the sheet has a floor of one line and no empty state to draw.
Everything else here is CSS standing in for macOS. This one is the layout chain
itself — same VStack, same modifiers — put through ImageRenderer at 2×.
It had been an open question: the title carries no .fixedSize while the line
beneath it does, and that asymmetry usually means something truncated once. It doesn’t. The
long name takes a second line in full, no ellipsis, and the sheet grows 16pt to hold it.
The proof is that adding .fixedSize(horizontal: false, vertical: true) to the
title changes nothing. Rendered both ways, the two PNGs are byte-identical — so the
modifier would be a no-op here, and its absence on the title is not a latent bug. The one on
the line below is doing no work either, on this layout.
The CSS renders above are ~7.5pt short of the real thing: 314.5pt for the canonical sheet, not 307, and 330.5pt for this one, not 323. The gap is the SF Symbol’s line box and the real push-button metrics, both of which the stand-ins under-measure. Widths are exact — 420pt is set, not derived. The figures quoted elsewhere on this page have been corrected to the SwiftUI measurements.
One the gate prevents, one it doesn’t. Drawn as the code would render them.
Re-analyse “Onboarding diaries”?
Bristlenose will transcribe and analyse all 0 interviews again from scratch.
?? 0 would render — the gate prevents itRe-analyse “Q3 Renewals — Enterprise Admin Diary Study (Wave 2)”?
Bristlenose will transcribe and analyse all 22 interviews again from scratch.
The sheet takes its count from
projectIndex.unanalysed[project.id]?.sessionCount ?? 0, and that snapshot is nil
until the folder watcher’s async scan lands. The curation counts come from a direct database
read and do not wait for the watcher, so on the face of it the sheet could show five
accurate loss lines above a sentence saying nothing will be rebuilt.
It can’t, because reAnalyseIsOffered ends with
(data?.sessionCount ?? 0) > 0 — the command is unavailable in exactly the window
where the sheet would say zero, and all three routes to askToReAnalyse pass that
gate. So the drawing above is what the fallback would render, kept for the record.
What is worth naming is the coupling: the sheet’s second line is only ever true because a predicate in a different file happens to test the same value. Nothing in the sheet says so, and nothing would fail if the gate loosened.
The menu-command case in ContentView.swift is commented “Ask, always … skipping it
on a project that happens to carry no curation would make the ellipsis a lie”. The function it
calls does exactly that skip, four lines into its own body, with the opposite rationale. Both
landed in the same commit on 19 Aug, so this is a comment left behind by a decision rather than
a live disagreement — but it argues a real point (a re-analysis re-spends on the provider even
with nothing curated) against behaviour that has already shipped.
Drawn wrapping here, and that is what the layout does; see the ImageRenderer section above. A researcher with two similarly-named waves keeps the word that tells them which study they are about to destroy, which matters because the title is the only place the project is named.
No folder path, no --clean, no output directory, no “don’t ask again”. The cost is
real every time and the count changes every time. The pixels keep that promise.