Skip to content

refactor(desktop): GSAP-driven animation — scroll, collapse, entrance, approval#4225

Merged
esengine merged 12 commits into
esengine:main-v2from
CVEngineer66:refactor/gsap-animation-system
Jun 13, 2026
Merged

refactor(desktop): GSAP-driven animation — scroll, collapse, entrance, approval#4225
esengine merged 12 commits into
esengine:main-v2from
CVEngineer66:refactor/gsap-animation-system

Conversation

@CVEngineer66

@CVEngineer66 CVEngineer66 commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

概述 / Summary

将桌面端聊天面板的动画系统全面迁移到 GSAP 驱动:用精确的高度 tween 替代 CSS max-height hack,消除流式滚动的抖动,为审批卡片添加平滑的进场/退场动画。同时清理了废弃的 minimal 展示模式和 expandThinking 设置。

Migrates the desktop chat transcript's animation to GSAP: replaces CSS max-height hacks with precise height tweens, eliminates streaming scroll jitter, adds smooth enter/exit transitions for approval cards, and removes the obsolete minimal display mode and expandThinking setting.


改动内容 / What changed

1. GSAP 折叠系统 / GSAP collapse system (useGSAPCollapse)

用统一的 useGSAPCollapse hook 替换所有 max-height: 0 → 5000px 的 CSS 过渡。通过 GSAP 精确测量 scrollHeight 并 tween height 属性。使用 useLayoutEffect 在浏览器 paint 之前设置初始折叠状态,消除"闪开"帧。WarmTurnCard 关闭时通过 prevHeight 选项捕获展开高度后再收缩。

Replaces all CSS max-height: 0 → 5000px transitions on collapsible sections (reasoning, tool cards, read-only batches, turn collapse) with a single hook that measures scrollHeight precisely and tweens height via GSAP. Uses useLayoutEffect to set the initial collapsed state before browser paint. WarmTurnCard captures expanded height before DOM swap via prevHeight option.

2. 流式滚动:即时到位 / Streaming scroll: instant, not GSAP tween

旧代码每个 token 都运行 gsap.to(el, { scrollTo, duration: 0.12 }),造成永无止境的 kill/restart 循环。现在流式期间改用 el.scrollTop = el.scrollHeight 直接赋值。onScroll 能区分内容增长(工具调用 output 到达使 scrollHeight 被动变大)和用户主动上滚,避免工具调用展开时误杀自动滚动。

Replaced gsap.to(scrollTo) during streaming with direct scrollTop assignment — the old 120ms tween was perpetually killed/restarted by new tokens. onScroll distinguishes content growth (tool output arriving pushes scrollHeight up) from user-initiated scroll-up by tracking scrollHeight deltas.

3. 入口动画批处理 / Entrance animation batching (useEntranceAnimation)

  • 首帧预充值 seen ID 集合,历史条目不执行入口动画
  • 新增 resetKey 参数:切换会话时重置 seen + firstRun,避免新会话的 500 条历史全部逐条 fadeIn+slideUp(≈20 秒动画)
  • 合并为单个 useEffect(原来是两个,各跑一次 querySelectorAll 扫描整个 DOM 子树)

Pre-seeds seen ID set on first mount so history items never animate. resetKey parameter clears seen + firstRun on session switch, preventing ~20 seconds of cascading entrance animations on 500+ items. Merged into a single useEffect — removed the duplicate querySelectorAll scan.

4. 审批卡片退场动画 / Approval card exit animation

模型同时创建多个文件等待审批时,卡片之前是生硬消失。现在 ApprovalModal 使用 gsap.to(el, { opacity: 0, y: 8, duration: 0.12 }) 退场动画后再调用回答回调。添加 key={approval.id} 修复 React 实例复用 bug——第二张卡继承了第一张卡的 closingRef 导致后续点击被拦截。

Smooth exit animation (opacity + translateY, 120ms) replaces jarring instant removal when model creates multiple files requiring approval. key={approval.id} fixes React instance reuse where the second card inherited a stale closingRef.

5. Markdown 光标优化 / Markdown cursor optimization

injectStreamingCursordeepestLastInlineElement (O(depth)) 替代 TreeWalker (O(nodes)),光标已在正确位置时跳过整个 DOM 写入。

Replaced TreeWalker (O(nodes)) with deepestLastInlineElement (O(depth)). Skips DOM mutation entirely when cursor is already correctly positioned.

6. 删除 minimal 展示模式 / Removed minimal display mode

minimal 模式在代码中与 compact 完全等价。从类型系统、SettingsPanel UI、bridge、types 和三种语言文件中移除。默认值改为 compact

The minimal mode was identical to compact in code. Removed from type system, SettingsPanel UI, bridge bindings, types, and all 3 locale files. Default switched to compact.

7. 删除 expandThinking 设置 / Removed expandThinking setting

思考过程现在硬编码为"运行中展开、完成折叠"(AssistantMessage 内部逻辑不变)。用户手动展开不会被强制关闭。从 App.tsx、Transcript、SettingsPanel、bridge、types 和语言文件中移除。

Hardcoded: always auto-open while streaming, auto-close on completion. Manual toggle is preserved and never force-closed. Removed from 9 files.

8. JumpBar 性能优化 / JumpBar performance

  • markerCache 缓存元素位置,仅在 questions 变化或 .jump-scroll 滚动时重建
  • mousemove 不再每次调用 getBoundingClientRect(避免 layout thrashing)
  • setHovered 仅在目标问题变化时触发 React 重渲染
  • onClick 恢复单击跳转
  • barTop 每次调用实时读取 DOM(nav 是 sticky,位置随 transcript 滚动变化)

Position cache eliminates getBoundingClientRect on every mousemove. setHovered only triggers React re-render when the target question changes. onClick restored for single-click jump. Fresh barTop each call for sticky nav.

9. 移除 useScrollManager / Removed useScrollManager

滚动逻辑从独立模块内联回 Transcript。复杂度从 130 行的独立 hook 降为直接 scrollTop = scrollHeight + useLayoutEffect

Scrolling logic inlined from a separate 130-line module into Transcript. Simpler, fewer cross-module deps.


已知遗留 / Known Caveats

  • Go 后端仍序列化 ExpandThinkingsettings_app.go:158),前端忽略,无功能影响
  • TurnCollapse/ToolCard/ReadOnlyBatch 改为始终渲染 children(GSAP 测量需要),VDOM 树略大,实测 500 条会话无性能影响

wufengfan added 11 commits June 13, 2026 02:18
- Install gsap + @gsap/react with Flip and ScrollToPlugin
- Create useGSAPCollapse hook (dynamic height tween, replaces max-height hacks)
- Create useScrollManager (GSAP ScrollToPlugin, replaces rAF scrollTop thrash)
- Create useEntranceAnimation (fadeIn+slideUp stagger for new items)
- Unify tool__body / reasoning__body / readonly-batch__body / turn-collapse__body
  collapse: all use GSAP height animation instead of max-height:5000px
- Fix AssistantMessage reasoning auto-close race condition (userOverridden ref)
- WarmTurnCard: GSAP collapse animation for expand/collapse
- Markdown: O(depth) deepestLastInlineElement replaces O(n) TreeWalker,
  skip cursor inject when already positioned correctly
- CSS: remove all max-height transition blocks, add overflow:hidden where needed
- All animations respect prefers-reduced-motion
…ed fold borders

- useEntranceAnimation: pre-seed seen set on first mount so history items
  never animate; add deps parameter so querySelectorAll only runs when
  items.length changes (not on every streaming token)
- useGSAPCollapse: skip gsap.set on initial mount for elements that default
  to collapsed (already handled — no change needed)
- Unify border-left + padding across reasoning__body, tool__body,
  readonly-batch__body, turn-collapse__body for consistent visual hierarchy
- tool--open class drives expanded padding for tool__body matching the
  readonly-batch / turn-collapse pattern
Before: hot zone (30 turns) + warm zone cards ALL render synchronously
in one frame.  For sessions with 200+ turns this blocks the UI for
hundreds of milliseconds.

Now:
  Phase 0 (frame 1): only latest 5 turns — instant paint
  Phase 1 (rAF):     full hot zone (up to 30 turns)
  Phase 2 (+80ms):   warm zone collapsed cards

- effectiveHotStart replaces hotStartIdx during phase 0
- showWarmZone gates WarmZone rendering until phase 2
- useEntranceAnimation: pre-seed seen set on mount, add deps parameter
  so querySelectorAll only runs when items.length changes
When intermediate steps (tools, phases) complete in under 500 ms,
rendering them as a collapsible 'processed' card wastes space and
adds unnecessary UI complexity.  Now they render inline as individual
ToolCard / PhaseCard elements instead.
…-pass entrance scan

GSAP Performance skill analysis found two bottlenecks in the initial render
path for long sessions (500+ items):

1. useGSAPCollapse called gsap.set(el, { height: 0/auto }) on mount for
   every collapsed element (~400 calls).  While each is sub-ms, the
   cumulative GSAP property-resolution and style-invalidation cost delays
   first paint.  Replaced with el.style.height = '0px' — the element's
   initial collapsed state is already guaranteed by overflow:hidden in CSS.

2. useEntranceAnimation used two separate useEffect calls (mount-only
   pre-seed + deps-driven scan), each running querySelectorAll over the
   entire DOM subtree.  Merged into a single effect with a firstRun flag:
   first call pre-seeds the seen set (no animation), subsequent calls only
   scan for genuinely new elements.
…content

useEffect fires AFTER paint, so every collapsible card rendered its full
content for one frame before height=0 took effect — visible as a 'flash
open then collapse' on initial load and session switch.

useLayoutEffect fires synchronously after React commit, BEFORE the
browser paints.  The initial style.height = '0px' is applied before the
first frame, so content starts collapsed — no flash.

For toggle animations, useLayoutEffect also means the first frame of the
GSAP fromTo tween is painted immediately (no 1-frame delay), making the
animation feel more responsive.
- Replace gsap.to(scrollTo) during streaming with direct scrollTop=scrollHeight,
  eliminating 120ms tween kill/restart jitter on every token
- useEntranceAnimation: add resetKey param to clear seen set + firstRun on
  session switch, preventing 500-history-item entrance animations
- Transcript: pass sessionKey to useEntranceAnimation; remove unused
  scrollToBottom from useScrollManager destructuring
- ApprovalModal: key={approval.id} fixes React instance reuse bug that
  blocked subsequent approval cards; GSAP exit animation (opacity+y, 120ms)
  replaces jarring instant-removal pop
- displayMode: removed 'minimal' option (was identical to compact), now
  only standard | compact. Default changed from minimal to compact.
- expandThinking: removed the setting entirely. Thinking process now
  always auto-opens while streaming and auto-closes on completion.
  Manual toggle is preserved — user expansion is never force-closed.
- Cleaned up associated UI, types, bridge bindings, and locale strings.
When WarmTurnCard closes, React swaps expanded children → preview
BEFORE useGSAPCollapse measures scrollHeight, so the collapse tween
started from ~40px (preview) instead of the full expanded height.

Fix: render both preview and body always (display:none toggle), capture
scrollHeight before the swap in the click handler, pass it to
useGSAPCollapse via a new prevHeight option so the close tween starts
from the correct (expanded) height.
@github-actions github-actions Bot added desktop Wails desktop app (desktop/**) v2 Go rewrite (1.x) — main-v2 branch, active development labels Jun 12, 2026
@esengine esengine merged commit 679f340 into esengine:main-v2 Jun 13, 2026
14 checks passed
esengine added a commit that referenced this pull request Jun 13, 2026
…l, dead code (#4238)

* fix(desktop): map legacy "minimal" display mode to compact

#4225 removed the minimal mode (it was identical to compact) but left
existing users stranded: DesktopDisplayMode() still returned "minimal"
for a persisted toml, which the frontend now rejects, so those users
fell back to standard (verbose) instead of the equivalent compact.
Normalize the legacy value to compact on both the Go read path and the
localStorage hydrate path.

* fix(desktop): drop scroll-behavior:smooth fighting instant streaming scroll

The transcript pins to the bottom during streaming via a direct
scrollTop = scrollHeight write, which #4225 documents as instant to
avoid the perpetual tween-chase jitter. A global scroll-behavior:smooth
on .transcript turns that write into an animated scroll in Chromium,
reintroducing the lag it set out to remove. The two call sites that want
smooth (jump-bar, warm-turn expand) already pass behavior explicitly.

* refactor(desktop): drop dead code left by the GSAP animation merge

- remove the no-op footer-height effect (empty if body, unused ref)
- drop the constant `false` from the hot-zone useMemo deps
- delete unused DUR_SLOWER/EASE_IN_OUT/EASE_SCROLL exports
- correct stale "minimal mode" comments and the entrance-hook usage doc
CVEngineer66 pushed a commit to CVEngineer66/DeepSeek-Reasonix that referenced this pull request Jun 13, 2026
…ng scroll, dead code (esengine#4238)

* fix(desktop): map legacy "minimal" display mode to compact

esengine#4225 removed the minimal mode (it was identical to compact) but left
existing users stranded: DesktopDisplayMode() still returned "minimal"
for a persisted toml, which the frontend now rejects, so those users
fell back to standard (verbose) instead of the equivalent compact.
Normalize the legacy value to compact on both the Go read path and the
localStorage hydrate path.

* fix(desktop): drop scroll-behavior:smooth fighting instant streaming scroll

The transcript pins to the bottom during streaming via a direct
scrollTop = scrollHeight write, which esengine#4225 documents as instant to
avoid the perpetual tween-chase jitter. A global scroll-behavior:smooth
on .transcript turns that write into an animated scroll in Chromium,
reintroducing the lag it set out to remove. The two call sites that want
smooth (jump-bar, warm-turn expand) already pass behavior explicitly.

* refactor(desktop): drop dead code left by the GSAP animation merge

- remove the no-op footer-height effect (empty if body, unused ref)
- drop the constant `false` from the hot-zone useMemo deps
- delete unused DUR_SLOWER/EASE_IN_OUT/EASE_SCROLL exports
- correct stale "minimal mode" comments and the entrance-hook usage doc
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

desktop Wails desktop app (desktop/**) v2 Go rewrite (1.x) — main-v2 branch, active development

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants