Skip to content

fix(session-ui): slide the tool group count when it changes - #55617

Merged
Hona merged 5 commits into
anomalyco:devfrom
Hona:group-count-slide
Oct 16, 2026
Merged

Hona merged 5 commits into
anomalyco:devfrom
Hona:group-count-slide

Conversation

@Hona

@Hona Hona commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

Fixes #54653
Fixes #43643

The "Used N" tool group count jumped to its new value. It rolls again with AnimatedNumber, including when it gains or loses a digit. The number now reads correctly to screen readers, keeps numeric order in RTL, and settles on the pixel it rolled to.

Before (9 → 10 → 9)

before.mp4

After

after.mp4
AnimatedNumber Before (dev) After
Accessibility tree, 37 7, 3; while rolling also 0123456789 ×3 per digit 37, once, in every state
Text content / copy, 37 reversed digits plus strip cells 37
aria-label on a roleless span yes (prohibited ARIA) removed
RTL document, 37 shows as 73 37
9 → 10 / 10 → 9 the 1 pops in / vanishes and leaves a gap the 1 rolls in from 0 / rolls out to 0
Font swap or zoom after mount the width animates no animation
12 changes while the timeline is detached blank digit when it returns correct digit, no roll
10 → 11 while the tens digit is rolling the tens digit jumps to rest it finishes its roll
<span data-component="animated-number">
  <span class="sr-only">{label()}</span>
  <span data-slot="animated-number-value" aria-hidden="true">
    <For each={places()}>{(place) => <Digit value={Math.floor(value() / 10 ** place) % 10} … />}</For>
  </span>
</span>
  • The value is the only text: one .sr-only node in reading order. The visual layer is aria-hidden and draws every digit with content: attr(data-digit), so screen readers, copy and textContent all get the number once.

  • Digits render in reading order, keyed by place value, and the row is pinned to direction: ltr. row-reverse is gone.

  • A spare leading zero waits outside the clipped width, so a new digit rolls in from 0 as the width grows. A departing digit stays until the width transition ends, so it rolls to 0 as it is clipped away. Digits no longer shrink while the width eases.

  • The strip rolls with top on a relatively positioned strip, so the rolling and resting digit are drawn by the same text renderer. A composited transform roll ended 0.1–0.5px off the text pixel grid and snapped as it settled. Below: 5 → 4 on a 150% display, frame by frame. Mounting at a new value does not animate; reduced motion skips the roll.

  • Only a change in digit count eases the width, so a late web font swap or a zoom change never animates it.

  • A change while the number is detached settles at once. The session cache keeps up to 16 timelines alive off-page; before, each change there pushed the strip further along its 30 cells until a background tab came back with a blank digit. An unchanged digit keeps its in-flight roll instead of jumping to rest.

  • tool-group.spec.ts covers a two-digit count in LTR and RTL, one accessibility snapshot mid-roll, both digits rolling across 9 → 10 → 9, no animation on mount or font change, changes while detached, and a second change mid-roll. The last three fail on the previous commit.

  • fix(ui): expose one reading-ordered value from AnimatedNumber #54977 fixes the screen-reader half with the same sr-only / aria-hidden split; this also keeps the text content to one value and fixes RTL.

Copilot AI balanced review requested due to automatic review settings October 15, 2026 05:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The approach looks right: reusing AnimatedNumber for the group count, drawing the strip digits with CSS so they drop out of the text, and rolling with transform instead of margin-top. The roll doesn't fire on first mount, and reduced motion is still respected. I couldn't run it in a browser here because Playwright's Chromium is missing system libraries, and the change is in the web UI rather than the TUI, so I have no screenshots. The session-ui unit tests (251) pass, and the typecheck is clean in session-ui and ui.

Public API: unchanged (internal UI components and CSS only).

One concern, inline: putting the count through AnimatedNumber means counts of 10 or more are now stored in reverse order in the page, so selecting and copying the count, or a screen reader, gives 21 instead of 12. Before this PR the count was plain text and read correctly.

Related: #54653

{(count) => <span data-slot="context-tool-group-count">{count()}</span>}
{(count) => (
<span data-slot="context-tool-group-count">
<AnimatedNumber value={count()} />

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AnimatedNumber renders the digits reversed (digits() is .reverse()d) and flips them back only visually with row-reverse. Before this PR the count here was plain text "12". Now the static digits in the page read "21", so copying the header or a screen reader gives the wrong number for any group with 10 or more tools. The aria-label on the inner span doesn't help much, since it's a span with no role. This is the reading-order half of #54653. Could the digits be rendered in reading order (e.g. drop the reverse and row-reverse, keyed from the right), or the visual layer be aria-hidden with the number exposed once in order? The tool-group spec only checks a single digit ("4"), so a two-digit case would catch this.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4ee175c.

  • Digits render in reading order, keyed by place value. .reverse() and row-reverse are gone, and the row is pinned to direction: ltr, which also fixes bug(ui): AnimatedNumber renders multi-digit numbers reversed in RTL locales #43643.
  • The number is the only text: one .sr-only node. The visual layer is aria-hidden and draws every digit with content: attr(data-digit).
  • At 37 tools, the accessibility tree has a single "37" and textContent is 37. With selection forced on (the trigger is user-select: none), the header copies as Used 37 Write, Shell, Grep, Edit.
  • tool-group.spec.ts checks a two-digit count in LTR and RTL and takes one accessibility snapshot mid-roll.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed: 4ee175c fixes what I raised, and I have no remaining concerns on this thread.

  • Reading order: places() lists the digits most-significant first and keys each one by place value (animated-number.tsx). With row-reverse replaced by direction: ltr, the DOM order and the visual order are both 37, in RTL documents too. When the count goes from 9 to 10, the existing ones digit stays mounted and a tens digit is added on the left. Because the row is flex-end with overflow: hidden while the width transitions, the new digit slides in from the left.
  • One readable value: the only text node is the .sr-only label, and that class exists globally in packages/ui/src/styles/utilities.css. The static digits and the strip cells are now CSS ::before content inside an aria-hidden wrapper, so the accessibility tree and textContent each get the number once. Removing the aria-label from the roleless span is correct.
  • Other consumer: tool-count-label.tsx also uses AnimatedNumber, so it gets the same fix automatically.

I couldn't run the new tests. I ran tool-group.spec.ts against pr/head in a temporary worktree, but all 8 tests failed, including ones that existed before this PR. The cause is the environment, not the code: Chromium exits with code 127 because system libraries are missing in this sandbox. The mid-roll snapshot test reads once on purpose, so it is the one most worth watching for flakiness in CI. I removed the temporary worktree, and repo/ is unchanged.

@Hona
Hona force-pushed the group-count-slide branch from 75d4d16 to 4a39b7e Compare October 15, 2026 23:02
Hona added 4 commits October 16, 2026 09:18
Render the Used N group count with AnimatedNumber again, keep the strip digits out of the number's text content, and roll them with transform instead of margin-top.
Expose the value as one sr-only text node and draw every digit with CSS in an aria-hidden layer, render digits in reading order keyed by place value, and pin the row to ltr so RTL documents keep numeric order.

Fixes anomalyco#54653

Fixes anomalyco#43643
Keep a spare leading zero outside the clipped width so a new digit rolls in from zero, and keep a departing digit until the width has shrunk past it so it rolls to zero instead of vanishing. Digits no longer shrink while the width eases.
@Hona
Hona force-pushed the group-count-slide branch from 4a39b7e to 12a2e96 Compare October 15, 2026 23:20
Roll the digit strip with top instead of transform. A composited roll lands on whole or half pixels, so the digit shifted 0.1-0.5px when it settled at 100-200% scale; drawn by the normal text path it rests where it rolled. Ease the width only when the digit count changes, so a late font swap or zoom never animates or clips it. A change while detached (a cached session timeline) settles at once instead of leaving the strip mid-track until it runs off its 30 cells, and an unchanged digit keeps the roll it is in instead of jumping to rest.
@Hona
Hona merged commit e25b7ab into anomalyco:dev Oct 16, 2026
22 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

2 participants