Skip to content

fix(ui): draw the reasoning chevron from the Astryx icon registry - #2228

Merged
Astro-Han merged 3 commits into
mainfrom
fix/ui-reasoning-chevron-stroke
Aug 5, 2026
Merged

fix(ui): draw the reasoning chevron from the Astryx icon registry#2228
Astro-Han merged 3 commits into
mainfrom
fix/ui-reasoning-chevron-stroke

Conversation

@Astro-Han

@Astro-Han Astro-Han commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Summary

The disclosure chevron on the Thinking row rendered with a visibly heavier stroke than the identical chevron on tool rows.

Root cause: two different chevron sources. Tool rows use Astryx <Icon icon="chevronDown" size="xsm"> — a 24-viewBox glyph carrying the theme's stroke-width: 1.75. ChatReasoning, ejected from the Astryx lab package, hand-wrote its own chevron at 12 viewBox / strokeWidth 1.5. Both are then forced to 10x10 by chat-message.css, so the effective stroke was 1.25px vs 0.73px — the same-looking glyph, ~70% heavier on one row. Path geometry was otherwise identical.

The reasoning chevron now goes through the registry (<Icon icon="chevronDown" size="xsm" color="inherit" />), so the app has one chevron authority and one place to retune it. The registry's string mode wraps its svg in an astryx-icon span, so the 10x10 rule now covers the wrapper (> *) on both rows, not just on tool rows — the two selectors collapse into one :is() pair.

The file header already documents the product-dialect deviations layered on the ejected lab DOM (cursor, hover wash, chevron size); this deviation is recorded there too.

Verification

Measured in Storybook (Product/Shell Official AppShell → NativeConversation) against the live DOM, not eyeballed — getComputedStyle(path).strokeWidth × path.getScreenCTM().a for the trailing chevron of each disclosure:

viewBox stroke (user units) CTM scale effective stroke
before — reasoning 0 0 12 12 1.5 0.8333 1.25px
before — tool calls 0 0 24 24 1.75 0.4167 0.7292px
after — reasoning 0 0 24 24 1.75 0.4167 0.7292px
after — tool calls 0 0 24 24 1.75 0.4167 0.7292px

Layout and motion are unchanged: both chevron wrappers still measure 14x14 with a 10x10 svg, margin-inline-start: auto intact, and expanding still rotates the chevron 180°.

(One measuring trap worth recording: read the expanded chevron's transform in a throttled background tab and you get an identity matrix, because the CSSTransition sits at currentTime: 0 forever. Force a paint first, or getAnimations().forEach(a => a.finish()).)

Also run:

  • npm run format / npm run format:check / npm run lint — clean
  • npm run typecheck (all workspaces) — clean
  • node --test "packages/ui/dist/**/*.test.js" — 342 pass, 0 fail, including the new chevron contract
  • apps/desktop main tests — the 12 failures in project-root-controller / project-management reproduce on a clean main checkout (tmpdir symlink resolution) and are untouched by this change
  • Not run: Playwright E2E — no user journey changes.

Two contracts guard the invariant, one per half, each verified to fail on the state it claims to catch:

  • processing-block.test.tsx slices the turn at the two row roots and compares each row's own chevron markup. Restoring the pre-fix component fails on "the reasoning row must draw exactly one registry icon"; swapping the row to chevronRight fails on "both rows must render the same chevron markup".
  • chat-disclosure-chevron-contract.test.ts (new, at the existing css-test-helpers seam) asserts each row declares both its chevron svg and its wrapper 10x10. Dropping the reasoning arm from the :is() fails; dropping the wrapper arm fails. It pins the outcome, not the wording — resplitting or reordering the selector list stays green.

The ejected lab ChatReasoning hand-wrote its own chevron (12 viewBox,
strokeWidth 1.5) while tool rows use Astryx `Icon icon="chevronDown"`
(24 viewBox, theme stroke 1.75). chat-message.css forces both to 10x10,
so the same-looking glyph rendered with 1.25px of stroke on the Thinking
row against 0.73px on tool rows.

Route the reasoning chevron through the registry so the app has one
chevron authority, extend the 10x10 rule to the registry icon's wrapper
span on both rows, and pin the shared source with a markup contract.
The markup contract compared one filtered pool of registry icons across the
whole turn, so the reasoning row — whose pre-fix chevron was not a registry
icon at all — fell out of the pool and left the tool rows agreeing with
themselves. Only the negative viewBox assertion caught the regression the
test was named for. Slice the turn at the two row roots and compare each
row's own chevron instead: the pre-fix component and a swap to a different
registry icon now both fail on the assertion that states the invariant.

Add the CSS half at the css-test-helpers seam. The markup test cannot see
chat-message.css, so nothing stopped a later edit from dropping a row (or the
wrapper arm) out of the 10x10 rule and desyncing the two chevrons again.
@Astro-Han
Astro-Han marked this pull request as ready for review August 5, 2026 11:16
@Astro-Han
Astro-Han merged commit 5038401 into main Aug 5, 2026
12 checks passed
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