emit('start-edit', m)"
+ />
diff --git a/src/composables/ai/useMessageScroll.ts b/src/composables/ai/useMessageScroll.ts
new file mode 100644
index 0000000..89ed6e1
--- /dev/null
+++ b/src/composables/ai/useMessageScroll.ts
@@ -0,0 +1,89 @@
+//! 消息列表滚动控制 — 跟随/锁存/回底按钮
+//!
+//! 职责:悬浮底部按钮显隐、跟随意图锁存(isFollowingBottom)、
+//! scrollToBottom 回底操作、onContentChange 内容变化滚动策略。
+//!
+//! 不涉及工具卡片折叠逻辑(父组件注入 onNearBottom 回调解耦)。
+
+import { ref, nextTick, type Ref } from 'vue'
+
+/** 底部阈值:距底部 <80px 视为"在底部" */
+const NEAR_BOTTOM_PX = 80
+/** 内容溢出阈值:scrollHeight - clientHeight >100px 认为内容溢出容器 */
+const OVERFLOW_PX = 100
+
+export function useMessageScroll(containerRef: Ref) {
+ // 回到底部按钮:内容溢出且不在底部时显示
+ const showBackToBottom = ref(false)
+
+ // B-260618-24: 跟随意图锁存。点回到底部/滚到底/发新消息→true;用户上滑→false。
+ const isFollowingBottom = ref(true)
+ // 边沿检测:上一帧是否在底部(用于触发 onNearBottom 回调)
+ let wasNearBottom = true
+
+ /** 用户是否在底部附近(上滑查看历史时不强制拉回) */
+ function isNearBottom(): boolean {
+ const el = containerRef.value
+ if (!el) return true
+ return el.scrollHeight - el.scrollTop - el.clientHeight < NEAR_BOTTOM_PX
+ }
+
+ /** 刷新回到底部按钮状态 */
+ function refreshBackToBottom(): void {
+ const el = containerRef.value
+ if (!el) { showBackToBottom.value = false; return }
+ const overflow = el.scrollHeight - el.clientHeight > OVERFLOW_PX
+ showBackToBottom.value = overflow && !isNearBottom()
+ }
+
+ /**
+ * 滚动事件处理:刷新回底按钮 + 底部边沿检测触发回调。
+ * @param onNearBottom 滚近底部时的回调(父组件传入,如收起工具卡片)
+ */
+ function onMessagesScroll(onNearBottom?: () => void): void {
+ refreshBackToBottom()
+ const near = isNearBottom()
+ if (near && !wasNearBottom) {
+ onNearBottom?.()
+ }
+ wasNearBottom = near
+ isFollowingBottom.value = near
+ }
+
+ /** 滚动到容器底部 */
+ function scrollToBottom(smooth = false): void {
+ const el = containerRef.value
+ if (!el) return
+ isFollowingBottom.value = true // 显式回底 = 跟随意图
+ el.scrollTo({
+ top: el.scrollHeight,
+ behavior: smooth ? 'smooth' : 'instant',
+ })
+ showBackToBottom.value = false
+ }
+
+ /** 消息/流式内容变化时:跟随意图则自动滚,否则只刷新回底按钮 */
+ function onContentChange(): void {
+ if (isFollowingBottom.value) {
+ nextTick(scrollToBottom)
+ } else {
+ refreshBackToBottom()
+ }
+ }
+
+ /** 显式标记跟随意图(发新消息/切换对话时调用) */
+ function setFollowing(v: boolean): void {
+ isFollowingBottom.value = v
+ }
+
+ return {
+ showBackToBottom,
+ isFollowingBottom,
+ isNearBottom,
+ refreshBackToBottom,
+ onMessagesScroll,
+ scrollToBottom,
+ onContentChange,
+ setFollowing,
+ }
+}