Skip to content

1.0.0 overlay: expanded issue card (step 13) - #25

Merged
Harrys76 merged 16 commits into
mainfrom
wp30/overlay-expanded
Oct 26, 2026
Merged

Harrys76 merged 16 commits into
mainfrom
wp30/overlay-expanded

Conversation

@Harrys76

@Harrys76 Harrys76 commented Oct 20, 2026 •

Copy link
Copy Markdown
Owner

Step 13 of the 1.0.0 overlay plan (doc/plan_overlay_1_0_0.md): the expanded issue card. Stacked on #24.

What the expanded card shows

Below a 1 px cardDivider, at the full card width from the severity icon's edge:

  • Measured. For an issue with a measurement: the value and a phrase for its family ("10.0 ms in one build on the UI thread", "88% of the widget tree sits under DemoScaffold"), and a gauge (SleuthMeasurementGauge, one CustomPaint) with ticks at the warning and critical lines. A line explains a held severity ("Held at critical: …") or a debug rate judged on its latest second ("Not critical: …"). The semantics label reads the value and both lines. The issue's detail follows it.
  • Where, Route, When, Evidence, Check in. Where has the widget and, when the run tracks widget creation, its file:line with Copy. When reads "Last seen 2 s ago · in 5 of the last 8 scans" from the controller's recurrence data and refreshes on the scan pulse. Evidence is the detector's reason and the confidence; Check in names the DevTools view. The label column is sized to the widest label and stacks above the values once it passes a third of the row.
  • Why it rebuilds (debug runs, rebuild_debug_* and setstate_scope): "0 started by the widget itself · 5 forced by its parent X", from the current page's latest DebugSnapshot. Capped counters make the row say the counts can be low. Without debug callbacks it says how to turn them on; an empty window reads "No rebuilds in the last N s".
  • Caused by and Effects. A listed card expands, scrolls into view and takes focus. While a reveal is pending the list builds every card for one frame, so an off-screen card above or below can reveal itself. A hidden or filtered card shows a toast with Show; Show turns the severity on without clearing the expansions. A card folded under another opens inline there. A related issue no longer reported stays as plain text.
  • Links to Rebuild stats (on cards from the current screen, once it has counted rebuilds) and Startup metrics.
  • Fix with its effort ("Fix · medium effort") and code in a monospace block with Copy code.
  • Buttons: Show on screen, Learn more, Ask AI, Copy details, Hide, Not useful. Each is a 48 px hit box around a pill at least 32 px tall. Not useful marks the issue in the controller's store (exports and MCP) with a toast; a second tap removes the mark and takes the toast back.

A tap on the details no longer collapses the card. Hide moves focus to the card that takes its place, and offers no Undo for a card whose issue is gone.

Decisions and deviations

  • Fix split rule. The plan's rule ("lines containing ( ) { } ; => or starting with flutter /dart ") broke real hints: animation: _controller, ended a call early, bullets like • Call cancel() on … became code, and sentences after a code line were swallowed. splitFixHint also continues a run on indented lines and closing brackets, treats // lines as code, and keeps bullets and lines ending in . as prose. test/utils/_fixtures/fix_hint_split.txt is the reviewed split of all 53 FixHintBuilder methods.
  • Detail paragraph. The mock drops it; the card keeps it on every issue, since it can hold what the number does not (an http_error_spike's failing URLs and status codes).
  • Dropped lines. Ancestors, Widget, During (now in When), Source, the debug fidelity tags, "About this detection", the effort badges and the Ask AI shimmer. ShaderMask and AnimatedBuilder leave the framework denylist (49 framework entries).
  • Gauge colour. The fill is the severity graphic colour where it keeps 3:1 on the Measured box, else the severity text colour (light warning). A 2 px gap in the box colour separates fill and track, so the high-contrast presets' mid-grey track does not need 3:1 against the fill.
  • Gauge scale. The gauge ends a quarter past the higher line, or at 100 for a percent, so the ticks stay put and the fill follows the value. A value past the end fills the track with two slanted cuts near its end, as on a chart's broken bar, and the text above gives the number. The first version scaled to the value: on an iPhone a sustained jank count of 46 to 80 frames against a line of 3 kept the fill at 80 % while the critical tick moved on every scan.
  • Fingerprint. It now includes the Measured row's text: a change the row rounds away does not rebuild the list.
  • Check in names views DevTools has (Frame Analysis, Raster Stats, Timeline Events, Memory, Network).

Review

The UI and UX design reviews were applied before opening:

  • outlined pills and code box use cardDivider (border equals cardDefault in dark);
  • an unreported related row is plain text, not a row that looks like a link;
  • Copy hit boxes no longer set the height of the line they sit on;
  • link rows draw their fill on a Material, so the press highlight shows;
  • icons grow with text up to the chrome clamp, stacked rows get spacingMd;
  • the gap between gauge fill and track;
  • the filtered-out Show path, the folded-target path, the off-screen reveal, Hide focus, the stats link gating, the gone related rows, the Not useful toast and the stale Hide.

Each behaviour fix has a test that fails when the fix is reverted.

Codex's first round found six more, fixed in d2a22fb with a regression test each:

  • the fingerprint now includes the gauge's line labels, so a detector that moves only a threshold refreshes the card;
  • value phrases name the measurement's own window instead of a fixed "10 s", "5 s" or "1 s", and none when it has none (the 240-frame buffer spans about 4 s at 60 Hz; raster_dominance's VM leg has no window);
  • a related issue no longer reported stays as a plain row even after the causal graph drops it from the card's ids;
  • Show on a target both filtered out and hidden turns both off;
  • Why it rebuilds separates the total forced by several parents from the busiest parent's share, and names that parent only when its own share passes the widget's setState count;
  • hiding the last card clears the cached keys, so focus falls back to the header.

The second round found three in Why it rebuilds, fixed in the next commit: the cached snapshot is dropped at evidence boundaries (page, tab, hot reload, aborted transition scan); the count is described as rebuilds the widget started itself, since it takes dependencies and listenables too; capped counters qualify the counts and drop the advice.

The third round found two cases of a kept card reading another card's data, fixed in the next commit: last-seen times are per card occurrence, and the cached debug snapshot records its screen so a card from another screen does not show that screen's counts.

The fourth round found three more, fixed in the next commit: the detail shows under the Measured row too; last-seen times expire after as many unseen cycles as a trend; Hide's focus hand-off builds and scrolls the next card into view, and the host keeps the list built until the scroll ends, so neither a revealed nor a focused card unmounts mid-scroll.

The fifth round found two screen-identity gaps, fixed in the next commit: the debug snapshot records the same screen identity issues are stamped with (currentScreen: route name plus the active session's scaffold hash, null on an ignored route), and the Rebuild stats link shows only on cards from the current screen.

The sixth round found three more, fixed in the next commit: the fingerprint includes the scaffold hash (two first-visit tabs on one route reporting the same issue); Why it rebuilds checks that Sleuth actually holds the rebuild callback and says when rebuilds are not counted; the Rebuild stats link reads its count on every scan.

The seventh round found two live-update gaps, fixed in the next commit: the Rebuild stats link checks the card's screen on every scan and tap, and the labelled rows rebuild on the scan pulse so When appears once its data exists.

The eighth round found that Why it rebuilds floored its window to whole seconds; the next commit carries the snapshot's Duration and names it as the value lines do.

The ninth round found nothing.

Verification

  • fvm flutter analyze: 0 issues. Format check clean. fvm dart doc --dry-run: 0 warnings.
  • fvm flutter test --exclude-tags benchmark: 4,979 passed.
  • Benchmarks (--tags benchmark --concurrency=1): 44 passed.
  • Floor lane on Flutter 3.32.8: analyze clean, 4,979 passed. The list's cacheExtent keeps an ignore: deprecated_member_use: its replacement is not in 3.32.
  • Example app tests: 348 passed. Sidecar: 653 passed.

Not in this PR

Fixes from the iPhone walk

  • Measured value in Copy details and Ask AI. Both now carry the Measured row as sentences ("Measured: 244 ms in one build on the UI thread. Warning at 8.0 ms. Critical at 16.0 ms."), through measuredSentence and copyDetailsText.
  • Evidence wording. The row read "..., so possible: no runtime signal confirmed it." after a reason ending in advice. The confidence is now its own sentence: "Sleuth rates it possible, since no runtime signal confirmed it."

Hidden cards across the upgrade (d8de8e2)

The "is hidden" and "is filtered out" toasts show a card again by removing every key that hides it (hideKeysOf). A key carried over from a store written before 1.0 matches any widget of the stable id (#24), so hideKeysOf lists that form too and Show clears it. A test fails without it.

Fixes from the final review

  • Escalation is not a hold. A debug rate turns critical on its latest window while the value averages two, so 15/s then 35/s gave a critical card at 25/s with "Held at critical". HeldRate now keeps the latest window's rate, the rebuild and repaint debug cards carry it (extraTraceArgs.latestRatePerSec), and the note reads "Critical: the latest second went over the line".
  • Names stay as written. The Measured line resolves the window part and the value's place before putting names in, so a tracked resource named "sock[1]" or "x{v}" keeps its text.
  • Denylist pin. The audit lists AnimatedBuilder, Checkbox, ShaderMask and TweenAnimationBuilder as app-owned and caps the framework section at 49, so they cannot return to the denylist.

🤖 Generated with Claude Code

@Harrys76
Harrys76 force-pushed the wp29/overlay-collapsed-card branch from 023a479 to 234de72 Compare October 23, 2026 18:11
@Harrys76
Harrys76 force-pushed the wp30/overlay-expanded branch from a4c97a4 to 18e1573 Compare October 23, 2026 18:11
@Harrys76
Harrys76 force-pushed the wp29/overlay-collapsed-card branch from 234de72 to eee3b63 Compare October 23, 2026 19:22
@Harrys76
Harrys76 force-pushed the wp30/overlay-expanded branch 3 times, most recently from d8de8e2 to 3d57320 Compare October 26, 2026 16:44
@Harrys76
Harrys76 force-pushed the wp29/overlay-collapsed-card branch from 8a43ecb to 10c07b5 Compare October 26, 2026 16:45
Harrys76 and others added 16 commits October 27, 2026 00:41
measurement_text.dart formats a measurement by unit and pairs it with a
phrase for its family, so the card can read "10.0 ms in one build on the
UI thread". The table covers the 43 families whose detectors stamp a
measurement. A line explains a severity the value does not reach (held)
or one judged on another reading (a debug rate's latest second).

splitFixHint separates prose from code in a fix hint. Code starts after a
line ending in a colon when the next line has brackets, `;` or `=>`, or is
a command or comment; indented lines and closing brackets continue it.
Bullets and lines ending in a full stop stay prose, which the plan's
first rule got wrong for several hints. A reviewed table pins the split of
all 53 FixHintBuilder methods.

IssueMetadataBuilder now returns the Evidence line (the detector's reason
and the confidence) and the Check in line (the DevTools view). The
profile line that said timing "matches production" is gone.

The controller keeps when each issue was last in a scan's results and
the latest debug snapshot, for the When and Why it rebuilds rows. The
issues fingerprint includes the Measured row's text, so the notifier
fires when the row changes and not for a change it rounds away.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An expanded card shows its details below a divider at the full card
width, as the decided design draws them:

- Measured: the value and its phrase, a gauge with the warning and
  critical lines, and a line when the severity and value disagree. A
  2 px gap in the box colour separates the fill from the track, and the
  fill falls back to the severity text colour where its shade misses
  3:1 on the box. Issues without a measurement show their detail.
- Where (widget and file:line with Copy), Route, When, Evidence and Check
  in, with labels that stack above their values at large text.
- Why it rebuilds on rebuild and setState cards in debug runs.
- Caused by and Effects rows that open the related card: a listed card
  expands, scrolls into view (the list builds every card for that frame)
  and takes focus; a hidden or filtered one offers Show, which keeps the
  open cards expanded; a folded one opens inline. A related issue no
  longer reported stays as plain text.
- Links to Rebuild stats (once the screen has counted rebuilds) and to
  Startup metrics.
- The fix with its effort label and code blocks with Copy code.
- Labelled 48 px buttons: Show on screen, Learn more, Ask AI, Copy
  details, Hide and Not useful. Not useful marks the issue for exports
  and MCP output; a second tap removes the mark and its toast.

A tap on the details no longer collapses the card. Hide moves focus to
the card that takes its place and offers no Undo for a card whose issue
is gone. "About this detection", the effort badges, the debug fidelity
tags and the Ask AI shimmer are gone, so ShaderMask and AnimatedBuilder
leave the framework denylist. The guide describes the new card.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The issues fingerprint includes the gauge's line labels, so a detector
  that moves only its warning or critical line refreshes the card.
- Value phrases name the measurement's own window ("in the last 4.2 s")
  instead of a fixed 10 s, 5 s or 1 s, and name none when it has none,
  as raster_dominance's VM leg does.
- A related issue no longer reported stays as a plain row even when the
  causal graph drops it from the card's ids: the card remembers the ids
  each section showed.
- Show on a target both filtered out and hidden turns both off, so the
  target opens.
- Why it rebuilds gives the total forced by several parents and the
  busiest parent's own share, and names that parent as the one to fix
  only when its share passes the widget's own setState count.
- Hiding the last card clears the list's cached keys, so focus falls back
  to the header instead of the removed card.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nows

- The controller drops its cached debug snapshot when the evidence key
  changes (a new page or tab, a hot reload) and on an aborted transition
  scan, so a card no longer shows the previous page's counts.
- The row says "started by the widget itself" instead of "from its own
  setState": the count also takes changed dependencies and listenables,
  and a StatelessWidget has no setState. The advice reads "Most of these
  rebuilds come from X, so start there".
- When the debug counters capped their widget types, the row says the
  counts can be low, gives no parent to start with, and does not claim
  there were no rebuilds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The controller keeps last-seen times per card, by list key and
  occurrence id, so a card whose occurrence went no longer reads "Last
  seen just now" from a twin under the same stable id.
- The cached debug snapshot records its screen (route and scaffold
  hash). A card whose issue was reported on another screen does not read
  that screen's counts as its own; its row asks to open its screen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The detail shows under the Measured row too. For http_error_spike and
  others it holds what the number does not, such as the failing URLs and
  status codes; an inline related issue shows its value line and detail.
- Last-seen times expire after as many unseen scan cycles as a trend, so
  structural findings on ever-new elements no longer grow the map for
  the whole session.
- Hide hands focus to the next card through the same path a related row
  uses: the list builds every card, the card scrolls into view and takes
  focus. ensureVisibleWithMotion returns the scroll's future, and the
  host keeps the list built until it completes, so a card scrolled in
  from outside the cache is not unmounted mid-scroll and loses no focus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ped with

- The controller's currentScreen is the route name and the active route
  session's scaffold hash, the pair `_aggregateIssues` stamps on issues.
  The debug snapshot records it, so on a route the config ignores (no
  session, so issues carry no hash) a card still reads its screen's
  counts.
- The Rebuild stats link shows only on a card from the current screen:
  the link opens the active session's counts, which a card kept from
  another screen must not claim.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The issues fingerprint includes the scaffold hash. Two first visits of
  tabs on one route that report the same issue differ only there, so the
  card now receives the second tab's issue instead of keeping the first
  and calling the current screen another one.
- Why it rebuilds reads counts only while Sleuth holds the framework's
  rebuild callback. With debug callbacks on but
  `advanced.rebuildAttribution` off, or DevTools holding the callback,
  the row says rebuilds are not counted instead of "No rebuilds".
- The Rebuild stats link reads the screen's count on every scan, so an
  expanded card shows the current total and gains the link once counts
  start.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The Rebuild stats link's count and tap check that the card is on the
  current screen each time. A card kept after its issue went, followed
  by a screen with no issues, leaves the issue list unchanged, and the
  link read the new screen's counts as its own.
- The labelled rows rebuild on the scan pulse, so a card expanded before
  its last-seen or recurrence data existed gains the When row when the
  data arrives.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The row floored the snapshot's elapsed time to whole seconds, at least
one, so 20 rebuilds over a 500 ms scan read as 20 in the last second.
RebuildCause carries the Duration and the row formats it as the value
lines do ("last 500 ms", "last 2.5 s").

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The gauge ended a quarter past the larger of the value and the lines.
A value past the lines, such as a sustained jank count of 77 against
a line of 3, then filled 80 % of the track on every scan, and the
line's tick moved instead of the fill.

The scale now ends a quarter past the higher line, or at 100 for a
percent whose lines are at most 100, so a value at or under the line
draws as before. A value past the end fills the track with two
slanted cuts near its end, as on a chart's broken bar; the text above
the gauge gives the number.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copy details and the Ask AI prompt left out the measured value and its
lines, the number the expanded card leads with. Both now add the
Measured row as sentences ("Measured: 244 ms in one build on the UI
thread. Warning at 8.0 ms. Critical at 16.0 ms."): Copy details after
its header lines, the prompt after the confidence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Evidence row joined the detector's reason and the confidence with
", so", which read badly after a reason that ends in advice: "Structural
scan only. Enable debug callbacks for paint evidence, so possible: no
runtime signal confirmed it." The confidence is now a sentence of its
own: "... paint evidence. Sleuth rates it possible, since no runtime
signal confirmed it."

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RateHysteresis turns a held warning critical on the latest window's
rate, while the measured value averages two windows. Windows of 15 then
35 rebuilds per second gave a critical card at 25/s, under the 30/s
line, with the note "Held at critical", a hold that never happened.
HeldRate now keeps the latest window's rate, the rebuild and repaint
debug cards carry it in extraTraceArgs (latestRatePerSec), and the
note reads "Critical: the latest second went over the line" when that
rate is over the line.

The Measured line also placed the window and the value from the
template before putting names in, so a tracked resource named
"sock[1]" or "x{v}" lost its brackets or split the line at the name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The overlay no longer uses AnimatedBuilder, Checkbox, ShaderMask or
TweenAnimationBuilder, so they left the framework denylist and an
app's own uses count again. The audit lists them with the other
app-owned widgets, so they cannot return to the denylist or to the
overlay, and the framework section is capped at its new size of 49.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The "is hidden" and "is filtered out" toasts show a card again by
removing every key that hides it (hideKeysOf). A key carried over from
a store written before 1.0 matches any widget of the stable id, so
hideKeysOf now lists that form too, and Show clears it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Harrys76
Harrys76 force-pushed the wp30/overlay-expanded branch from 3d57320 to 60056ac Compare October 26, 2026 17:59
@Harrys76
Harrys76 force-pushed the wp29/overlay-collapsed-card branch from 10c07b5 to 22f281a Compare October 26, 2026 17:59
Base automatically changed from wp29/overlay-collapsed-card to main October 26, 2026 19:02
@Harrys76
Harrys76 merged commit c066a73 into main Oct 26, 2026
5 of 10 checks passed
@Harrys76
Harrys76 deleted the wp30/overlay-expanded branch October 26, 2026 19:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant