Skip to content

Manager: Improve UI stability on onboarding checklist - #36341

Merged
Sidnioulz merged 3 commits into
nextfrom
fix/checklist-replay-on-state-churn
Oct 20, 2026
Merged

Sidnioulz merged 3 commits into
nextfrom
fix/checklist-replay-on-state-churn

Conversation

@Sidnioulz

@Sidnioulz Sidnioulz commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

What I did

When working on tree 2.0, my agent stumbled upon an instability in the checklist widget DOM. We are apparently refiring state syncs with mutable data even when the store state has not effectively changed.

This PR adds an equality check to state changes within the underlying store to keep the DOM more stable by not replacing DOM rows with themselves (which triggers Emotion changes).

The PR also fixes a CLS bug where an item that is checked off the list keeps taking 32px of height whilst being animated out, resulting in a temporary state where the checklist shows 4 rows and grows in height.

Checklist for Contributors

Testing

The changes in this PR are covered in the following automated tests:

  • stories
  • unit tests
  • integration tests
  • end-to-end tests

Manual testing

  1. Run the internal UI: cd code && yarn storybook:ui
  2. Look at the beautiful onboarding checklist; isn't it a cutie?
  3. Open your DevTools and inspect the checklist rows
  4. Do whatever task necessary to complete one of the 3 visible steps in your checklist
  5. Look out in the DevTools for DOM mutations, they should be minimal
  6. Look out in the UI for CLS, you should see none; the completed row should be replaced cleanly in-place

Documentation

  • Add or update documentation reflecting your changes
  • If you are deprecating/removing a feature, make sure to update
    MIGRATION.MD

Checklist for Maintainers

  • When this PR is ready for testing, make sure to add ci:normal, ci:merged or ci:daily GH label to it to run a specific set of sandboxes. The particular set of sandboxes can be found in code/lib/cli-storybook/src/sandbox-templates.ts

  • Declare whether manual QA will be needed for this PR during the next release, through qa:needed or qa:skip

  • Make sure this PR contains one of the labels below:

    Available labels
    • bug: Internal changes that fixes incorrect behavior.
    • maintenance: User-facing maintenance tasks.
    • dependencies: Upgrading (sometimes downgrading) dependencies.
    • build: Internal-facing build tooling & test updates. Will not show up in release changelog.
    • cleanup: Minor cleanup style change. Will not show up in release changelog.
    • documentation: Documentation only changes. Will not show up in release changelog.
    • feature request: Introducing a new feature.
    • BREAKING CHANGE: Changes that break compatibility in some way with current major version.
    • other: Changes that don't fit in the above categories.

🤖 Generated with Claude Code

https://claude.ai/code/session_014kbAkZeM9HcHMvLjPzK6oG

Two guards, one per layer.

useUniversalStore (manager and preview variants) notified React on
every setState in the no-selector path, so a state replaced with a
deeply equal copy re-rendered every consumer; the manager variant also
overwrote its snapshot ref before notifying, defeating the
referential-equality guard in getSnapshot. Both hooks now apply the
same deep-equality comparison the selector path always had. Their API
is unchanged.

The checklist widget registered its transition keys with setItem on
every items-array identity change, and setItem resets the transition
state of a key it already tracks. Any store update or story index
refresh therefore collapsed and faded back in every visible row. Keys
are now registered once. The StableAcrossStateSyncs story pins the
symptom: it replays a content-identical store sync and asserts that no
row changes its class or loses its DOM node.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014gogULoeyEc5aKMdtgS8Mf
@Sidnioulz Sidnioulz added bug ci:normal Run our default set of CI jobs (choose this for most PRs). qa:skip Pull Requests that do not need any QA. (e.g. documentation) labels Sep 16, 2026 — with Claude
@Sidnioulz Sidnioulz self-assigned this Sep 16, 2026
Sidnioulz pushed a commit that referenced this pull request Sep 16, 2026
The universal store dedupe and the checklist transition guard move to
#36341 so this diff carries only the sidebar rebuild.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014gogULoeyEc5aKMdtgS8Mf
@Sidnioulz Sidnioulz added qa:needed Pull Requests that will need manual QA prior to release. and removed qa:skip Pull Requests that do not need any QA. (e.g. documentation) labels Sep 16, 2026

@ghengeveld ghengeveld left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Code wise I'm fine with this, the approach seems sound.

However, looking at this story (wait for it to animate), it seems the transition is broken. It looks like it's already broken like that on next though, so it's not caused by this PR. Nevertheless we should address it. I verified that the issue exists not just in the story but in the actual sidebar as well.

I think it would help to have a story with some dynamic controls so we can play around with the checklist state easily.

When a task completes, its row exits while the next task's row enters.
The exiting row never actually collapsed: ActionList items carry a
32px min-height that overrides block-size: 0, so for the length of
the exit transition the list held four full-height rows and the
widget grew by one row before snapping back.

Collapsed items now drop their min-height as well, and min-height is
part of the transitioned properties so both rows resize over the same
duration. The entering row starts collapsed through @starting-style
instead of react-transition-state's preEnter step, which lands the
enter and the exit in the same commit and keeps the two heights
complementary from the first frame. The StableHeightOnTaskCompletion
story records the widget height on every DOM mutation and asserts it
never grows across a completion.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014kbAkZeM9HcHMvLjPzK6oG
@Sidnioulz Sidnioulz changed the title Manager: Stop checklist animation replays on state churn Manager: Stop checklist row replays and keep widget height stable on task completion Oct 14, 2026
@Sidnioulz Sidnioulz changed the title Manager: Stop checklist row replays and keep widget height stable on task completion Manager: Improve UI stability on onboarding checklist Oct 14, 2026
@Sidnioulz Sidnioulz added qa:skip Pull Requests that do not need any QA. (e.g. documentation) and removed qa:needed Pull Requests that will need manual QA prior to release. labels Oct 14, 2026
@Sidnioulz
Sidnioulz marked this pull request as ready for review October 14, 2026 15:28
@coderabbitai

coderabbitai Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Walkthrough

Store hooks now skip updates when compared states are deeply equal. Checklist transitions register each key once, and ActionList uses shared collapsed and expanded styles. New stories check row stability during state syncs and card height during task completion.

Changes

Checklist state and transitions

Layer / File(s) Summary
Suppress updates for equal store states
code/core/src/shared/universal-store/use-universal-store-manager.ts, code/core/src/shared/universal-store/use-universal-store-preview.ts, code/core/src/shared/universal-store/use-universal-store-manager.test.ts
The manager and preview hooks compare selected values or full states before updating consumers. A test checks that replacing the store state with an equal-content object does not re-render the consumer and preserves the original state object.
Register checklist transitions and define animation styles
code/core/src/manager/components/sidebar/ChecklistWidget.tsx, code/core/src/components/components/ActionList/ActionList.tsx, code/core/src/manager/components/sidebar/ChecklistWidget.stories.tsx
The checklist widget registers each transition key once. ActionList uses shared collapsed and expanded styles, with min-height included in its transitions. New stories check row stability across state syncs and card height during task completion.

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to e45eb

No confirmed issue currently prevents merging after normal checks.

✨ Finishing Touches: 💡 1
🛠️ Fix failing CI checks: 💡
  • Commit to this branch
  • Create a new PR
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

🔇 Additional comments: 7
code/core/src/shared/universal-store/use-universal-store-manager.ts: 1

11-11: LGTM!

Also applies to: 48-49

code/core/src/shared/universal-store/use-universal-store-preview.ts: 1

10-11: LGTM!

Also applies to: 46-47

code/core/src/shared/universal-store/use-universal-store-manager.test.ts: 1

96-118: LGTM!

code/core/src/components/components/ActionList/ActionList.tsx: 1

19-22: LGTM!

Also applies to: 80-80, 92-96

code/core/src/manager/components/sidebar/ChecklistWidget.tsx: 1

64-76: 🎯 Functional Correctness

Stale keys do not accumulate for returning tasks in this implementation.

The allItems passed to useTransitionArray contains all checklist items derived from the static checklistData.sections. Items never leave this array—they remain present with different computed state flags (completion status, availability, etc.). The renderItems subset changes which items are displayed, but item IDs always remain in allItems.

The transition state keys are registered when items enter allItems, which occurs only once per application session for each checklist ID. Since items do not leave and re-enter allItems, the scenario of a previously-removed key returning is not reachable.

code/core/src/manager/components/sidebar/ChecklistWidget.stories.tsx: 2

174-174: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

⚠️ Unverified finding
Verification ran but could not confirm this finding. It is shown for review, not as a verified issue.

Module-level cardHeights is shared state across story renders.

The array is cleared only inside play. The decorator observer pushes heights during mount, so entries recorded before play runs are cleared at Line 196. This is correct for the current single-story use. Reuse of the decorator, or two renders in one page such as docs mode, could mix measurements. Keeping the array in a useRef or in the story context would remove the risk.

Also applies to: 196-196


144-144: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

⚠️ Unverified finding
Verification ran but could not confirm this finding. It is shown for review, not as a verified issue.

Fixed wait() delays make these stories slow and timing-dependent.

The stories wait 3000 ms twice and 600 ms once. A slow CI runner can fail them or give false passes. The 600 ms wait in StableAcrossStateSyncs is the weakest. It only proves that no class change happens in that window. Prefer waitFor conditions where possible. Otherwise, keep the delays tied to --transition-duration.

Also applies to: 165-165, 194-194, 211-211


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 639e6830-096e-480e-a278-637162c07f0d
📥 Commits

Reviewing files that changed from the base of the PR and between 37aadd1 and e45eb6f.

📒 Files selected for processing: 6
  • code/core/src/components/components/ActionList/ActionList.tsx
  • code/core/src/manager/components/sidebar/ChecklistWidget.stories.tsx
  • code/core/src/manager/components/sidebar/ChecklistWidget.tsx
  • code/core/src/shared/universal-store/use-universal-store-manager.test.ts
  • code/core/src/shared/universal-store/use-universal-store-manager.ts
  • code/core/src/shared/universal-store/use-universal-store-preview.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.

@Sidnioulz Sidnioulz added the sev:S4 Low priority. Wait for community PRs. label Oct 14, 2026
@ghengeveld

Copy link
Copy Markdown
Member

The new equality check can leave the UI out of sync because previousState comes from the sender, rather than the receiving hook’s current snapshot.

I reproduced this with two stores starting at 0: the manager changes locally to 1, then receives a 0 → 0 update from the other runtime before that runtime has received the manager’s change. The manager’s store becomes 0, but the hook still returns 1 because this comparison suppresses notification.

The same test passes against the base branch and fails against this PR. Could we compare the incoming value with the last snapshot exposed by the hook instead? The preview hook appears to need the same adjustment.

@Sidnioulz

Copy link
Copy Markdown
Contributor Author

Discussed with the team: we know that concurrent events can be lossy, but we cannot know if the manager or preview's event should be the one that's honoured without building a shared clock system. Considering the lifespan of UniversalStore, we've decided to not pay the price of building such a system to handle this edge case.

@Sidnioulz
Sidnioulz merged commit dbea635 into next Oct 20, 2026
169 of 171 checks passed
@Sidnioulz
Sidnioulz deleted the fix/checklist-replay-on-state-churn branch October 20, 2026 11:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug ci:normal Run our default set of CI jobs (choose this for most PRs). qa:skip Pull Requests that do not need any QA. (e.g. documentation) sev:S4 Low priority. Wait for community PRs.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants