From d6e38f391ac1fefac8fd11b4719be14f90461246 Mon Sep 17 00:00:00 2001 From: Rick Ye Date: Tue, 8 Sep 2026 00:57:03 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=80=9D=E8=80=83=E5=9D=97=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=E7=8A=B6=E6=80=81=E5=AF=B9=E9=BD=90=20ZCode=E2=80=94?= =?UTF-8?q?=E2=80=94=E6=B5=81=E5=BC=8F=E6=94=B6=E8=B5=B7=E3=80=81=E5=AE=8C?= =?UTF-8?q?=E6=88=90=E5=90=8E=E5=B1=95=E5=BC=80=E5=85=A8=E6=96=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../renderer/src/components/MessageItem.tsx | 6 +- .../src/renderer/src/components/Thinking.tsx | 81 ++++++++++++------- 2 files changed, 58 insertions(+), 29 deletions(-) diff --git a/apps/desktop/src/renderer/src/components/MessageItem.tsx b/apps/desktop/src/renderer/src/components/MessageItem.tsx index 2989441..abfb83c 100644 --- a/apps/desktop/src/renderer/src/components/MessageItem.tsx +++ b/apps/desktop/src/renderer/src/components/MessageItem.tsx @@ -217,11 +217,13 @@ export function AssistantTurn({ if (seg.kind === 'thinking') { const rows = thinkingRows(seg.text) if (rows.length === 0) return - // 该思考段之后已有正文段在流:思考已完成,表头从「正在思考」切回 + // 只有「最后一段且之后没有正文」才算仍在流式;否则一旦 pi 在回合内 + // 分多轮发出(静默间隙 streaming 抖动),已完成段会被误判回流式态 + const isLast = i === segments.length - 1 const followedByText = segments.some((s, k) => k > i && s.kind === 'text' && s.text.length > 0) streamNodes.push(
- +
) return diff --git a/apps/desktop/src/renderer/src/components/Thinking.tsx b/apps/desktop/src/renderer/src/components/Thinking.tsx index a5f0749..d1e8f86 100644 --- a/apps/desktop/src/renderer/src/components/Thinking.tsx +++ b/apps/desktop/src/renderer/src/components/Thinking.tsx @@ -1,10 +1,10 @@ -import { useRef, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import type { ReactNode } from 'react' /** 可展开的思考轨迹(ZCode 式实时预览)。 - * 流式收起时表头显示「正在思考 · 最新一行」持续刷新,不展示全部; - * 展开时完整展示全部思考(不截断),新内容在尾部、由会话级自动滚动跟随, - * 用户上滚即取消跟随(外层滚动已实现)。结束后收起为「思考过程」, + * 流式中默认收起:表头「正在思考 · 最新一行」持续刷新,不展示全部; + * 手动展开时限高滚动预览——新内容在尾部并自动吸底,用户上滚即取消吸底 + * (滚回底部恢复)。完成后默认展开完整展示全部思考,表头「思考 · 首行」。 * 点击表头可手动展开/收起。rows 由真实思考文本逐行喂入。 */ export function Thinking({ rows, @@ -17,16 +17,37 @@ export function Thinking({ activeLabel?: string doneLabel?: string }): ReactNode { + // 流式中默认收起,完成后默认展开;用户点击后以手动状态为准 const [manualExpanded, setManualExpanded] = useState(null) - const expanded = manualExpanded ?? streaming + const expanded = manualExpanded ?? !streaming // 流式轨迹的行必须「随到随显」:fade-up 的 both+delay 会让新行在延迟期间 // 透明但占高,流式期间底部因此持续留出一大片空白。交错入场只保留给 // 非流式一次性渲染的静态轨迹(挂载时决定,之后不变更,避免整段重播动画)。 const streamedMountRef = useRef(streaming) + // 流式展开时的滚动吸底:新内容到达且当前贴着底部时跟随;用户上滚即取消, + // 滚回底部恢复。收起时重置吸底,重新展开直接落在最新内容。完成后不限高。 + const scrollRef = useRef(null) + const pinnedRef = useRef(true) + const handleScroll = (): void => { + const el = scrollRef.current + if (!el) return + pinnedRef.current = el.scrollHeight - el.scrollTop - el.clientHeight < 24 + } + useEffect(() => { + if (!expanded) { + pinnedRef.current = true + return + } + const el = scrollRef.current + if (el && pinnedRef.current && streaming) el.scrollTop = el.scrollHeight + }, [rows, expanded, streaming]) + const latest = rows.length > 0 ? rows[rows.length - 1].primary : '' - const suffix = streaming && !expanded ? latest : '' + const firstLine = rows.length > 0 ? rows[0].primary : '' + const statusWord = streaming ? activeLabel : expanded ? '思考' : doneLabel + const suffix = streaming ? (expanded ? '' : latest) : expanded ? firstLine : '' return (
@@ -50,14 +71,14 @@ export function Thinking({ animation: 'shimmer-text 1.4s linear infinite' }} > - {activeLabel} + {statusWord} ) : ( - {doneLabel} + {statusWord} )} {suffix && ( @@ -80,7 +101,7 @@ export function Thinking({ - {/* 展开轨迹:完整展示全部思考,左侧 hairline 随内容延伸 */} + {/* 展开轨迹:流式时限高滚动预览(吸底跟随),完成后完整展示全部思考 */}
-
- -
- {rows.map((row, i) => ( -
- - {row.primary} - -
- ))} +
+
+ +
+ {rows.map((row, i) => ( +
+ + {row.primary} + +
+ ))} +