Skip to content

feat: spring-driven morph animation for askbar-to-chat transition - #11

Merged
quiet-node merged 8 commits into
mainfrom
feature/animation-morph
Apr 1, 2026
Merged

feat: spring-driven morph animation for askbar-to-chat transition#11
quiet-node merged 8 commits into
mainfrom
feature/animation-morph

Conversation

@quiet-node

Copy link
Copy Markdown
Owner

Summary

  • Replaces Framer Motion's height: 'auto' snap with a spring-driven height animation (useMotionValue + useSpring) that smoothly tracks growing content during streaming
  • Adds Apple Sheet-style spring transition for the askbar→chat morph with matched cubic-bezier CSS transitions for box-shadow and border-radius
  • Rewrites auto-scroll to check scroll position fresh from the DOM on each content change, eliminating stale ref issues caused by spring-triggered layout events
  • Cleans up dead scroll-pinning ref (isUserNearBottomRef) and handleScroll callback that became unused after the auto-scroll rewrite

Test plan

  • All 137 tests pass
  • 100% code coverage maintained
  • ESLint clean (0 errors)
  • Manual QA: morph animation is smooth, auto-scroll works during streaming, scrolling up disengages auto-scroll

🤖 Generated with Claude Code

quiet-node and others added 8 commits March 31, 2026 23:11
Replace the abrupt class-swap transition with a cohesive spring-physics
morph: the container bounces open via Framer Motion layout animation,
chat content slides down from above, and the input bar glides into
position — all driven by a pronounced spring (stiffness 300, damping 20).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace Framer Motion layout transforms with explicit height animation
(0 → auto) on ConversationView. Layout transforms used CSS scale which
overflowed the native window boundary, causing bottom clipping during
the spring overshoot. Height animation grows the chat area via real CSS
height changes — no transforms, no clipping, no input bar overlap.

The isMorphing ref is set synchronously during render so the spring
transition is active on the exact frame where chat mode activates.
After 600ms the ref flips to false, switching streaming resizes to
instant (duration: 0) to prevent the input bar from lagging behind
growing content.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove morph-detection refs and transition-switching logic that caused
a visible height jump when the transition changed from spring to instant
mid-animation. The spring (stiffness 300, damping 30) naturally handles
both cases: large initial morph (0→200px) takes ~300ms for a visible
effect, while streaming increments (~10-20px) settle in <50ms.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Framer Motion's height:'auto' measures once at mount and snaps when the
spring finishes, causing a 50-100px jump when streaming tokens grow
content during the animation. Replace with useLayoutEffect that
temporarily flips to height:auto, measures natural height via
getBoundingClientRect, restores the spring value (all before paint),
and feeds the measurement to a useSpring. Capped at 600px so the flex
chain stays intact and the scroll container can scroll when full.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two fixes for auto-scroll breaking when the conversation reaches max
height:

1. Height cap: measure actual flex-available space (parent clientHeight
   minus sibling heights) instead of a hardcoded 600px cap. The spring
   now targets the exact rendered height, so the scroll container's
   internal layout matches the visible area — no more content hidden
   behind the input bar.

2. Auto-scroll: check scroll position fresh on each content change
   instead of relying on isUserNearBottomRef, which goes stale when
   spring animation triggers layout-induced scroll events. Treat
   "no overflow" as "at the bottom" so the growth-to-scroll transition
   is seamless.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The isUserNearBottomRef and handleScroll callback became dead code when
the auto-scroll effect was rewritten to check scroll position fresh from
the DOM. Remove the unused ref, callback, onScroll binding, and the test
that only exercised the dead path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Mock stubs must match framer-motion's `use`-prefixed export names;
setTargetHeight in useLayoutEffect is intentional (measure before paint).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@quiet-node
quiet-node merged commit 512b7f2 into main Apr 1, 2026
3 checks passed
@quiet-node
quiet-node deleted the feature/animation-morph branch April 1, 2026 19:19
quiet-node added a commit that referenced this pull request Apr 10, 2026
* feat: add Apple Sheet spring morph transition for askbar-to-chat mode

Replace the abrupt class-swap transition with a cohesive spring-physics
morph: the container bounces open via Framer Motion layout animation,
chat content slides down from above, and the input bar glides into
position — all driven by a pronounced spring (stiffness 300, damping 20).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: use height animation for morph to prevent clipping and overlap

Replace Framer Motion layout transforms with explicit height animation
(0 → auto) on ConversationView. Layout transforms used CSS scale which
overflowed the native window boundary, causing bottom clipping during
the spring overshoot. Height animation grows the chat area via real CSS
height changes — no transforms, no clipping, no input bar overlap.

The isMorphing ref is set synchronously during render so the spring
transition is active on the exact frame where chat mode activates.
After 600ms the ref flips to false, switching streaming resizes to
instant (duration: 0) to prevent the input bar from lagging behind
growing content.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: use always-on spring for morph height animation

Remove morph-detection refs and transition-switching logic that caused
a visible height jump when the transition changed from spring to instant
mid-animation. The spring (stiffness 300, damping 30) naturally handles
both cases: large initial morph (0→200px) takes ~300ms for a visible
effect, while streaming increments (~10-20px) settle in <50ms.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: replace height auto snap with spring-driven content tracking

Framer Motion's height:'auto' measures once at mount and snaps when the
spring finishes, causing a 50-100px jump when streaming tokens grow
content during the animation. Replace with useLayoutEffect that
temporarily flips to height:auto, measures natural height via
getBoundingClientRect, restores the spring value (all before paint),
and feeds the measurement to a useSpring. Capped at 600px so the flex
chain stays intact and the scroll container can scroll when full.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: reliable auto-scroll during spring-driven height animation

Two fixes for auto-scroll breaking when the conversation reaches max
height:

1. Height cap: measure actual flex-available space (parent clientHeight
   minus sibling heights) instead of a hardcoded 600px cap. The spring
   now targets the exact rendered height, so the scroll container's
   internal layout matches the visible area — no more content hidden
   behind the input bar.

2. Auto-scroll: check scroll position fresh on each content change
   instead of relying on isUserNearBottomRef, which goes stale when
   spring animation triggers layout-induced scroll events. Treat
   "no overflow" as "at the bottom" so the growth-to-scroll transition
   is seamless.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: remove dead scroll-pinning ref and handler

The isUserNearBottomRef and handleScroll callback became dead code when
the auto-scroll effect was rewritten to check scroll position fresh from
the DOM. Remove the unused ref, callback, onScroll binding, and the test
that only exercised the dead path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* formated code

* chore: suppress intentional eslint warnings with inline directives

Mock stubs must match framer-motion's `use`-prefixed export names;
setTargetHeight in useLayoutEffect is intentional (measure before paint).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------
quiet-node added a commit that referenced this pull request Apr 10, 2026
* feat: add Apple Sheet spring morph transition for askbar-to-chat mode

Replace the abrupt class-swap transition with a cohesive spring-physics
morph: the container bounces open via Framer Motion layout animation,
chat content slides down from above, and the input bar glides into
position — all driven by a pronounced spring (stiffness 300, damping 20).


* fix: use height animation for morph to prevent clipping and overlap

Replace Framer Motion layout transforms with explicit height animation
(0 → auto) on ConversationView. Layout transforms used CSS scale which
overflowed the native window boundary, causing bottom clipping during
the spring overshoot. Height animation grows the chat area via real CSS
height changes — no transforms, no clipping, no input bar overlap.

The isMorphing ref is set synchronously during render so the spring
transition is active on the exact frame where chat mode activates.
After 600ms the ref flips to false, switching streaming resizes to
instant (duration: 0) to prevent the input bar from lagging behind
growing content.


* fix: use always-on spring for morph height animation

Remove morph-detection refs and transition-switching logic that caused
a visible height jump when the transition changed from spring to instant
mid-animation. The spring (stiffness 300, damping 30) naturally handles
both cases: large initial morph (0→200px) takes ~300ms for a visible
effect, while streaming increments (~10-20px) settle in <50ms.


* fix: replace height auto snap with spring-driven content tracking

Framer Motion's height:'auto' measures once at mount and snaps when the
spring finishes, causing a 50-100px jump when streaming tokens grow
content during the animation. Replace with useLayoutEffect that
temporarily flips to height:auto, measures natural height via
getBoundingClientRect, restores the spring value (all before paint),
and feeds the measurement to a useSpring. Capped at 600px so the flex
chain stays intact and the scroll container can scroll when full.


* fix: reliable auto-scroll during spring-driven height animation

Two fixes for auto-scroll breaking when the conversation reaches max
height:

1. Height cap: measure actual flex-available space (parent clientHeight
   minus sibling heights) instead of a hardcoded 600px cap. The spring
   now targets the exact rendered height, so the scroll container's
   internal layout matches the visible area — no more content hidden
   behind the input bar.

2. Auto-scroll: check scroll position fresh on each content change
   instead of relying on isUserNearBottomRef, which goes stale when
   spring animation triggers layout-induced scroll events. Treat
   "no overflow" as "at the bottom" so the growth-to-scroll transition
   is seamless.


* refactor: remove dead scroll-pinning ref and handler

The isUserNearBottomRef and handleScroll callback became dead code when
the auto-scroll effect was rewritten to check scroll position fresh from
the DOM. Remove the unused ref, callback, onScroll binding, and the test
that only exercised the dead path.


* formated code

* chore: suppress intentional eslint warnings with inline directives

Mock stubs must match framer-motion's `use`-prefixed export names;
setTargetHeight in useLayoutEffect is intentional (measure before paint).


---------
quiet-node added a commit that referenced this pull request Apr 11, 2026
* feat: add Apple Sheet spring morph transition for askbar-to-chat mode

Replace the abrupt class-swap transition with a cohesive spring-physics
morph: the container bounces open via Framer Motion layout animation,
chat content slides down from above, and the input bar glides into
position — all driven by a pronounced spring (stiffness 300, damping 20).

* fix: use height animation for morph to prevent clipping and overlap

Replace Framer Motion layout transforms with explicit height animation
(0 → auto) on ConversationView. Layout transforms used CSS scale which
overflowed the native window boundary, causing bottom clipping during
the spring overshoot. Height animation grows the chat area via real CSS
height changes — no transforms, no clipping, no input bar overlap.

The isMorphing ref is set synchronously during render so the spring
transition is active on the exact frame where chat mode activates.
After 600ms the ref flips to false, switching streaming resizes to
instant (duration: 0) to prevent the input bar from lagging behind
growing content.

* fix: use always-on spring for morph height animation

Remove morph-detection refs and transition-switching logic that caused
a visible height jump when the transition changed from spring to instant
mid-animation. The spring (stiffness 300, damping 30) naturally handles
both cases: large initial morph (0→200px) takes ~300ms for a visible
effect, while streaming increments (~10-20px) settle in <50ms.

* fix: replace height auto snap with spring-driven content tracking

Framer Motion's height:'auto' measures once at mount and snaps when the
spring finishes, causing a 50-100px jump when streaming tokens grow
content during the animation. Replace with useLayoutEffect that
temporarily flips to height:auto, measures natural height via
getBoundingClientRect, restores the spring value (all before paint),
and feeds the measurement to a useSpring. Capped at 600px so the flex
chain stays intact and the scroll container can scroll when full.

* fix: reliable auto-scroll during spring-driven height animation

Two fixes for auto-scroll breaking when the conversation reaches max
height:

1. Height cap: measure actual flex-available space (parent clientHeight
   minus sibling heights) instead of a hardcoded 600px cap. The spring
   now targets the exact rendered height, so the scroll container's
   internal layout matches the visible area — no more content hidden
   behind the input bar.

2. Auto-scroll: check scroll position fresh on each content change
   instead of relying on isUserNearBottomRef, which goes stale when
   spring animation triggers layout-induced scroll events. Treat
   "no overflow" as "at the bottom" so the growth-to-scroll transition
   is seamless.

* refactor: remove dead scroll-pinning ref and handler

The isUserNearBottomRef and handleScroll callback became dead code when
the auto-scroll effect was rewritten to check scroll position fresh from
the DOM. Remove the unused ref, callback, onScroll binding, and the test
that only exercised the dead path.

* formated code

* chore: suppress intentional eslint warnings with inline directives

Mock stubs must match framer-motion's `use`-prefixed export names;
setTargetHeight in useLayoutEffect is intentional (measure before paint).

---------
nadalpiantini added a commit to Openclaw-claws/thuki that referenced this pull request Jul 27, 2026
Full audit cleanup after agency-agents tri-review (Reality Checker,
API Tester, Performance Benchmarker) flagged real issues hidden by
self-graded scoring.

Tests repaired (was 145 failing pre-existing rebrand drift):
- IntroStep.test.tsx: Spanish onboarding strings (Antes de empezar,
  Doble-tap, Empezar, etc)
- AskBarView.test.tsx: placeholder Pregúntale a Sephirot... + img
  alt Sephirot
- OnboardingView.test.tsx: Activar Accesibilidad, Abrir ajustes de
  grabación de pantalla, Cerrar y reabrir Sephirot, Activo badge
- index.test.tsx: stages text
- App.test.tsx: combined cascading replacements

Cord client improvements (agentTarsClient.ts):
- stripCodeFences() removes wrapping ```python/```json fences from
  DeepSeek responses so the bubble shows clean content (case quiet-node#6,
  quiet-node#15 from stress test were leaking raw markdown)
- Added 5 unit tests for stripCodeFences (single fence, mid-content
  preservation, no-fence passthrough)

Type pollution cleanup (useOllama.ts + App.tsx):
- New `transportError` field on Message for non-Ollama failures
  ('agent-tars-down' | 'agent-tars-timeout' | 'agent-tars-bad-response'
  | 'agent-tars-unknown') replaces the misleading errorKind: 'Other'
  reuse of OllamaErrorKind
- Error messages translated to Spanish (Alan is hispanoparlante; was
  English in earlier ship)

Integration tests (browseFlow.integration.test.tsx — NEW):
- 7 cases covering: happy path with fence stripping, proxy 504 →
  TIMEOUT, network unreachable → DOWN, stale 404 → recreate,
  UTF-8 multi-script preservation, empty content → BAD_RESPONSE,
  3-concurrent → 1 createSession (mutex)

Real-workload validation (20-case DeepSeek stress, post-patches):
- 20/20 OK, 0 errors (was 18/20 before patches)
- Latency: p50=2245ms, p95=11325ms, max=11497ms
- The 2 cases that previously timed out at 75s (quiet-node#5 rd.bank, quiet-node#11
  long.task) now complete in 8.0s and 11.5s respectively, confirming
  agent-tars internal stalls are recoverable on retry

Suite total: 30 files, 627/627 tests green.
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