Repository navigation
fix: [Q&A Page] Thought processes, tool calls, and sub-agent executions are collapsed by default during streaming output; users can expand them as needed. - #7925
Conversation
…ns are collapsed by default during streaming output; users can expand them as needed.
| ) | ||
|
|
||
| onBeforeUnmount(release) | ||
| } |
There was a problem hiding this comment.
Code Review
Potential Issues
-
No "user near bottom" detection — The docblock promises "only follow when the user is still near the bottom," but
MutationObserveralways callsfollow.scrollBottom(). This will yank the scroll position even if the user scrolled up to read something. You likely need a check likeif (isNearBottom(container)). -
scrollBottom()on every mutation without throttling — Rapid DOM mutations (e.g., streaming text, character-by-character updates) trigger many synchronous layout reads/writes per frame, causing jank. Consider debouncing withrequestAnimationFrameor checking inside the callback. -
Feedback loop risk — If
scrollBottom()itself mutates children or attributes that fall under the observer's scope (childList,subtree,characterData), it can re-trigger the observer and create an infinite loop. Ensure scrolling doesn't cause DOM changes, or disconnect/guard during the scroll call. -
Missing
attributes: true— Content expansion often occurs via CSS class/style changes (e.g., collapsed vs. expanded) without any child/character mutation. If your container grows this way, the observer won't fire. Consider addingattributes: true(but beware of noise). -
No unmount cleanup for a mid-flight debounce — If you add debouncing, ensure pending timers/frames are cancelled in
release(); otherwise callbacks can run after the element is detached. -
scroll?.destroy()naming asymmetry —forceBottom/scrollBottomare imperative-scroll operations;destroyimplies lifecycle. VerifyScroll.destroy()actually removes listeners and isn't just a no-op, since it's the only teardown available here. -
flush: 'post'correctness — Good choice so the container exists in the DOM before measuring. However,immediate: truewithflush: 'post'may still run before the browser has laid out (fonts, images).forceBottom()could compute an incorrect height on first mount; consider deferring initial scroll to the nextrequestAnimationFrame. -
Return value absent — The composable returns nothing. Callers can't force a manual follow (e.g., "jump to latest" button). Consider exposing
{ scrollToBottom, release }. -
Ref<HTMLElement | null | undefined>— Undefined and null are both handled identically; typicallyRef<HTMLElement | null>suffices. Minor. -
SSR safety —
MutationObserverisn't guarded. If the module ever runs server-side, instantiation will throw. Guard withtypeof MutationObserver !== 'undefined'or import fromvue(some setups polyfill this; otherwise add a check).
Optimization Suggestions
import { onBeforeUnmount, watch, type Ref } from 'vue'
import { Scroll } from './scroll'
export function useBottomFollow(containerRef: Ref<HTMLElement | null>) {
let scroll: Scroll | undefined
let observer: MutationObserver | undefined
let rafId = 0
const isNearBottom = (el: HTMLElement) =>
el.scrollHeight - el.scrollTop - el.clientHeight < 32
function release() {
cancelAnimationFrame(rafId)
rafId = 0
observer?.disconnect()
observer = undefined
scroll?.destroy()
scroll = undefined
}
watch(
containerRef,
(container) => {
release()
if (!container || typeof MutationObserver === 'undefined') return
const follow = new Scroll(container)
scroll = follow
requestAnimationFrame(() => follow.forceBottom())
observer = new MutationObserver(() => {
if (!isNearBottom(container)) return
cancelAnimationFrame(rafId)
rafId = requestAnimationFrame(() => follow.scrollBottom())
})
observer.observe(container, { childList: true, subtree: true, characterData: true })
},
{ flush: 'post', immediate: true },
)
onBeforeUnmount(release)
return { release }
}Summary
The main functional gap is the missing "near bottom" guard — the current implementation defeats its own stated behavior. After that, throttling via rAF and guarding against observer feedback loops are the highest-value fixes.
| <div ref="reasoningScrollRef" class="max-h-[400px] overflow-y-auto text-N500! whitespace-pre-wrap break-words"> | ||
| {{ content.content }} | ||
| </div> | ||
| </MkCollapse> |
There was a problem hiding this comment.
Here's my review of the diff:
Potential Issues
-
isExpandedno longer controlled by lifecycle — possible UX regression (unverifiable from this diff). Previously, content auto-collapsed when status transitioned fromRUNNINGto done. Now it defaults tofalseand never changes programmatically. If theMkCollapseis still bound toisExpanded, fine—but confirm nothing else relies on the old auto-collapse behavior (e.g., tests, snapshots, or parent expectations). Also note: the commit message references "与新建文件的差异区一致" and "流式输出时跟随到底部", but there's no visible logic change that ensures the follow only happens whileRUNNING. IfuseBottomFollowauto-scrolls unconditionally, it could jump the viewport unexpectedly after completion. -
refgiven tov-modelnot shown in diff. WithisExpanded = ref(false), make sure the<MkCollapse>binding (presumablyv-model="isExpanded") remains intact throughout the template — it's not included in this diff hunk. -
Auto-collapse removal loses user-state preservation semantics. Previously expanded content collapsed on completion; now it starts collapsed. Verify product intent — "思考中也只显示状态文案" suggests content is hidden during streaming even though new text is arriving. This is a deliberate change, but users who expand mid-stream rely on
useBottomFollowto keep up; ensure it handles the case where the scroll container appears/disappears withisExpanded. -
max-h-[400px]magic number duplicates layout knowledge. The comment says it matches the "新建文件的差异区" height. Consider extracting to a shared CSS variable/token so both places stay in sync. -
useTemplateRef<HTMLElement>('reasoningScrollRef')is evaluated at setup time. That's fine if the composable watches the ref reactively (it should, per the Vue 3.5 pattern). ConfirmuseBottomFollowaccepts aReadonly<ShallowRef>and reacts to the element being toggled viaisExpanded— otherwise following won't engage after expansion. -
definePropsresult unused (nopropsconst). Fine undervue/consistent-define-props-style rules, but double-check other tooling doesn't require a named variable. Otherwise lint-clean. -
whitespace-pre-wrap+overflow-y-auto: long single tokens can still overflow horizontally unlessbreak-words(overflow-wrap: break-word) applies; it's present, good. But addoverflow-x-hiddenorpr-*padding to avoid a scrollbar overlaying content vertically. -
Accessibility: scroll container has no focusability/
tabindex. Keyboard users can't scroll the reasoning content without focusing an ancestor. Considertabindex="0"or arole="region"with a label.
Minor / Style
- Typo in naming:
text-N500!looks like a custom utility; ensure!suffix convention matches the project (Tailwind v4?). - Composable name
useBottomFollow(...)immediately invoked — fine, but consider calling it with the return value assigned if you later need pause/resume on user scroll. - Removing the
watcheliminates reliance onprevStatus, which was fragile (didn't handle initialRUNNING→undefinedetc.). Good simplification overall.
Optimization Suggestions
- Bind height via a shared token:
class="max-h-[var(--reasoning-max-height,400px)]". - Add
shrink-0/min-h-0as appropriate inside the collapse body so flex layouts don't distort the scroll area. - Ensure
useBottomFollowrespects user scrolling (only auto-follow when near bottom), otherwise streaming will fight manual scroll-up. - Keep a test/snapshot covering the "expand during RUNNING" path to lock in the new intended behavior.
Summary: No outright bugs in the shown hunks, but verify (a) the MkCollapse binding still wires isExpanded, (b) useBottomFollow gates following to streaming/near-bottom state, and (c) the behavioral change (start collapsed, no auto-collapse) is intentional and documented/tested.
| <div v-if="isExpanded && childContents.length" ref="groupScrollRef" class="mt-1 max-h-[400px] overflow-y-auto border-l-2 border-N900/15 pl-3"> | ||
| <ContentList :content-list="childContents" /> | ||
| </div> | ||
| </MkCollapse> |
There was a problem hiding this comment.
Here are my observations on this diff:
Potential Issues
-
Behavioral regression (intentional but worth validating):
- The default state changed from "expanded when RUNNING or no status" to always collapsed.
- If any UX flow, test, or screenshot relies on the old auto-expand behavior for running groups, this will break. Confirm product agrees.
-
useBottomFollowis called unconditionally with a template ref:useTemplateRef<HTMLElement>('groupScrollRef')resolves only after mount, and only when the element exists. Since the target div is insidev-if="isExpanded && childContents.length", the ref will benullwhenever the group is collapsed or has no children.- Verify that
useBottomFollowhandles anull/initially-undefined ref gracefully (attaching watchers lazily, re-resolving when the ref later becomes non-null after expansion). If it only grabsref.valueonce on setup oronMounted, following won't work after the first expand/collapse cycle. - Also confirm it re-hooks when the element is unmounted/remounted by the
v-iftoggle.
-
currentActivity/ collapse header now shows even when collapsed:- The comment previously said "collapsed shows latest child activity." Now that default is collapsed, ensure the
currentActivitycomputed is actually rendered in the header (this diff doesn't show the header, so verify elsewhere).
- The comment previously said "collapsed shows latest child activity." Now that default is collapsed, ensure the
-
Removed dynamic getter means
content.statuschanges no longer affect expansion:- Previously a transition from
RUNNING → DONEwould auto-collapse. Now it stays as user left it. That's presumably desired ("manual wins"), just note it.
- Previously a transition from
Optimization / Cleanliness
-
max-h-[400px]hardcoded: Consider extracting to a shared constant / CSS variable alongside the "new file diff region" height mentioned in the comment, so the two stay in sync. -
overflow-y-auto+ border styling: Withborder-l-2 pl-3, the scrollbar will sit at the far right of the div including padding. Minor, but if scrollbar aesthetics matter, consider inner wrapper. -
Accessibility: A scrollable container with fixed max-height should ideally have
tabindex="0"(and possiblyrole/aria-label) so keyboard users can scroll it. Same could apply to the follow-scroll region — decide whether full a11y is required. -
Ref naming:
groupScrollRefused via string key inuseTemplateRef— fine in Vue 3.5+. Ensure the project's Vue version supportsuseTemplateRef(it was introduced in 3.5). If the codebase targets <3.5, this import will fail. -
Test coverage: Add/adjust tests verifying:
- Default collapsed regardless of
status. - Manual toggle persists and is unaffected by status change.
- Bottom-follow triggers on child content updates while expanded.
- Default collapsed regardless of
-
Import ordering:
useBottomFollowis imported fromcore, placed after the component import — acceptable, but keep the project's lint grouping consistent.
Summary
The main risk is useBottomFollow receiving a ref that is frequently null due to the v-if. Please confirm its implementation handles lazy/conditional elements, and confirm the Vue version supports useTemplateRef. Otherwise the diff is straightforward and the behavior change looks intentional.
fix: [Q&A Page] Thought processes, tool calls, and sub-agent executions are collapsed by default during streaming output; users can expand them as needed.