Skip to content

refactor(ui): use one animation library - #59025

Merged
kitlangton merged 2 commits into
devfrom
one-motion-lib
Oct 25, 2026
Merged

kitlangton merged 2 commits into
devfrom
one-motion-lib

Conversation

@kitlangton

@kitlangton kitlangton commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

What it does. The app shipped two animation libraries: motion 12 (with framer-motion and motion-dom 12) and @kitlangton/solid-motion (motion-dom 14). This removes motion from packages/ui and packages/session-ui and runs everything on solid-motion. No animation looks or feels different. Part of the dependency trim (OCD-194).

How it works.

  1. useSpring (motion-spring.tsx, used by the strikethrough) now takes attachSpring/motionValue from solid-motion. I checked the spring solver in both versions: for visual durations .15/.25/.3/.35 and starting velocities 0/400/-900, the output is identical sample for sample (max difference 0) and settles at the same time. Retargeting mid-flight in v14 gives the same curve as a fresh v12 spring.
  2. Shell command reveal (ShellSubmessage in tool-renderer.tsx): the width spring (visualDuration .25, bounce 0) stays a spring, now on solid-motion's animate. The opacity and blur fade is now the browser's el.animate() with the same 320 ms and cubic-bezier(0.16, 1, 0.3, 1). motion 12 was already running that fade as a Web Animation, so only the library call is gone.
  3. Tool expand/collapse (basic-tool.tsx): the height spring only ran when the animated prop was set, and no caller sets it. I removed that code (the spring, contentRef, the animated branch and the prop) instead of porting it. Expanding and collapsing a tool works exactly as it does today, with no animation.
  4. Changing spring options mid-flight (useSpring): re-attaching the spring left the old animation running, because attachSpring's cleanup only unsubscribes from the source. This was also true with motion 12 on dev. It now stops the running animation and restarts toward the current target with the new options.
  5. Lint. CI lints every changed file in full, so basic-tool.tsx also needed cleanup with no behavior change: isTriggerTitle uses a real type guard instead of as JSX.Element, and GenericTool's label/args read the input with Schema.decodeUnknownOption instead of typeof checks. On sample inputs the output is the same as before.

Why solid-motion and not plain CSS. The segmented control uses motion.span and useMotionValue. All the springs need a solver that can retarget in flight and keep their velocity, and CSS linear() can't do that. The tool height spring that would have become a plain el.animate() turned out to be dead code (point 3). The fade calls animate?.() so environments without Web Animations (happy-dom) still get the final styles.

Risk. UI only. If something were wrong you'd see it in the segmented control indicator, the strikethrough, or the shell command reveal. Tool expand/collapse can't change, because its spring never ran.

Proof. I recorded before (dev) and after (this branch) in Storybook on mini, stepping the page clock at exactly 60 fps and logging the animated values on every frame.

  • Every transition settles on the same frame with 0 px overshoot: segmented control 12/13/5/12 frames, shell width 21/21, strikethrough 36/11/36.
  • The shell fade's opacity and blur are identical on every frame (0.29 → 0.51 → 0.66 …, blur 1.417 → 0.982 …).
  • A few mid-flight frames differ by up to 12 px. That's uneven frame delivery in the recording, not a different curve: the solver output above is identical.
  • Segmented control: before/after, 4× slow
  • Shell command reveal and expand/collapse: before/after, 4× slow
  • Strikethrough: before/after

Checks:

  • test-browser/motion-spring.test.ts: "springs progress to its target" (records every emitted value) and "new spring options replace the running spring" (fails without the fix): 3 pass
  • Component specs basic-tool, session-lifecycle, session-shell and tool-disclosure: 25 pass
  • segmented-control, import-boundaries and src/tools tests: pass
  • bun typecheck in ui, session-ui and app: pass
  • bun script/lint-changed.ts origin/dev: 0 problems

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

Looks sound. Nothing in packages/ still imports motion. The removed animated branch in BasicTool has no callers. The shell fade keeps the same starting style and holds its end state after el.animate(). I couldn't record animations here, so for the visual side I'm relying on your before/after videos. The new motion-spring browser test (2 pass), session-ui tests (252 pass) and tsc in session-ui and ui are all green.

Public API unchanged: BasicTool's animated prop is removed, but it's internal to session-ui and nothing passed it.

One small point on the new test (inline).

}

expect(state.progress()).toBe(1)
expect(seen.some((value) => value > 0 && value < 1)).toBe(true)

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.

This test samples the spring with real 16 ms setTimeout sleeps, and the spring settles in about 100 ms. If CI stalls for that long before the first sample, progress() is already 1 and this some(0 < v < 1) check fails at random. The settle-to-target and <= 1 checks are the useful ones. I'd drop this mid-flight assertion, or drive the frame clock directly instead of sleeping.

@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 earlier point about the flaky spring test is addressed: it now records every emitted value instead of sleeping and sampling. The useSpring options fix works. The new test fails when I put back the previous motion-spring.tsx and passes with this one. The motion-spring tests (3 pass, three runs), session-ui tests (252 pass) and tsc in ui and session-ui are green. I couldn't watch the animations themselves here, so the visual side relies on your recordings. Public API unchanged. Nothing new to raise.

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

This push is a rebase onto newer dev. The pull request's own changes match the version I last reviewed, apart from surrounding unchanged lines. The earlier point about the flaky spring test was addressed in the previous push. After the rebase, the motion-spring tests (3 pass), session-ui tests (268 pass) and tsc in ui and session-ui are green. Public API unchanged. Nothing new to raise.

@kitlangton
kitlangton merged commit a4eb2f8 into dev Oct 25, 2026
20 checks passed
@kitlangton
kitlangton deleted the one-motion-lib branch October 25, 2026 20:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant