From 46813c23cd9829d69a8cec91399bc0a1dc0accc5 Mon Sep 17 00:00:00 2001 From: Rick Ye Date: Thu, 10 Sep 2026 02:16:26 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=9B=9E=E5=90=88=E6=B4=BB=E8=B7=83?= =?UTF-8?q?=E5=88=A4=E5=AE=9A=E6=94=BE=E5=AE=BD=E2=80=94=E2=80=94=E5=8E=8B?= =?UTF-8?q?=E7=BC=A9/=E9=80=9A=E7=9F=A5=E8=A1=8C=E5=B0=BE=E9=9A=8F?= =?UTF-8?q?=E4=B8=8D=E5=86=8D=E8=AF=AF=E6=94=B6=E8=B5=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/desktop/scripts/smoke-turn-events.mjs | 79 +++++++++++++++++++ .../src/renderer/src/components/ChatView.tsx | 16 +++- 2 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 apps/desktop/scripts/smoke-turn-events.mjs diff --git a/apps/desktop/scripts/smoke-turn-events.mjs b/apps/desktop/scripts/smoke-turn-events.mjs new file mode 100644 index 0000000..8933f20 --- /dev/null +++ b/apps/desktop/scripts/smoke-turn-events.mjs @@ -0,0 +1,79 @@ +/** + * 回合事件时序冒烟:验证 agent_settled 是否会在多回合工作(含 >600ms 慢工具)中途发出。 + * 背景:渲染层回合折叠绑定 agent_settled(54b99a1),若 settled 在回合间被发出, + * 折叠会随 live 抖动反复展开/收起。本脚本在隔离的临时 cwd 下跑一轮真实多回合工作。 + * 运行:node scripts/smoke-turn-events.mjs + */ +import { mkdtempSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { + createAgentSessionServices, + createAgentSessionFromServices, + defineTool, + getAgentDir, + SessionManager +} from '@earendil-works/pi-coding-agent' +import { Type } from '@earendil-works/pi-ai' + +const cwd = mkdtempSync(join(tmpdir(), 'aso-smoke-')) +const t0 = Date.now() +const ts = () => `${((Date.now() - t0) / 1000).toFixed(2).padStart(7)}s` + +const slowTool = defineTool({ + name: 'slow_probe', + label: '慢探针', + description: '等待 1500ms 后返回 done(模拟浏览器工具耗时,测试回合折叠时序)', + promptSnippet: 'slow_probe: 测试用慢工具', + parameters: Type.Object({}), + async execute() { + await new Promise((r) => setTimeout(r, 1500)) + return { content: [{ type: 'text', text: 'done' }] } + } +}) + +const loggingExtension = { + name: 'smoke-log', + factory: (pi) => { + for (const type of ['session_start', 'agent_start', 'agent_end', 'agent_settled', 'message_start', 'message_end', 'tool_call']) { + pi.on(type, (event) => console.log(ts(), `[ext] ${type}${event?.toolName ? ` (${event.toolName})` : ''}`)) + } + } +} + +const toolExtension = { + name: 'smoke-tools', + factory: (pi) => { + pi.registerTool(slowTool) + } +} + +const services = await createAgentSessionServices({ + cwd, + agentDir: getAgentDir(), + resourceLoaderOptions: { extensionFactories: [loggingExtension, toolExtension] } +}) + +const sessionManager = SessionManager.continueRecent(cwd) +const { session } = await createAgentSessionFromServices({ services, sessionManager }) + +const unsubscribe = session.subscribe((event) => { + const message = event.message ?? {} + const detail = + event.type === 'tool_execution_start' || event.type === 'tool_execution_end' + ? ` (${event.toolName})` + : event.type === 'message_start' || event.type === 'message_end' + ? ` (role=${message.role ?? '?'})` + : '' + console.log(ts(), `[session] ${event.type}${detail}`) +}) + +console.log(ts(), '--- prompt 开始:两次串行 slow_probe + 总结 ---') +try { + await session.prompt('调用 slow_probe 工具两次(串行调用,不要并行),然后只回复"完成"两个字。') + console.log(ts(), '--- prompt 返回(整个工作结束) ---') +} finally { + unsubscribe() + await session.abort?.().catch(() => undefined) + process.exit(0) +} diff --git a/apps/desktop/src/renderer/src/components/ChatView.tsx b/apps/desktop/src/renderer/src/components/ChatView.tsx index 31fddbd..6ffac1c 100644 --- a/apps/desktop/src/renderer/src/components/ChatView.tsx +++ b/apps/desktop/src/renderer/src/components/ChatView.tsx @@ -108,7 +108,14 @@ function renderTurns( // turnActive/streaming 是会话级信号:只有延伸到消息列表末尾的最后一个 // assistant 组是「活跃回合」。透传给历史回合会让它们误显示 // 「已工作/处理中」并保持过程区展开。 - const isLive = j === messages.length + // 放宽:会话级 turnActive 期间,组后仅尾随非真实用户消息(自动压缩/分支 + // 摘要通知行)时不算离开末尾——否则压缩间隙 live 跌落,600ms 自动收起后 + // 续跑又展开,形成展开/收起反复横跳(慢工具/长会话反复压缩尤甚)。 + let liveEnd = j + while (liveEnd < messages.length && !(messages[liveEnd].role === 'user' && !messages[liveEnd].local)) { + liveEnd += 1 + } + const isLive = j === messages.length || (turnActive && liveEnd === messages.length) // 本回合之后第一条真实用户消息(跳过压缩/摘要行):分叉锚点,'before' 含本回合答案为止 let nextUser = j while (nextUser < messages.length && !(messages[nextUser].role === 'user' && !messages[nextUser].local)) { @@ -183,7 +190,8 @@ export function ChatView({ onFork, onForkAt, onPickSuggestion, - onQuoteSelection + onQuoteSelection, + visible = true }: { messages: UiMessage[] tools: Record @@ -212,6 +220,8 @@ export function ChatView({ onForkAt: (entryId: string) => void onPickSuggestion: (text: string) => void onQuoteSelection: (selection: string) => void + /** 聊天列是否可见(切到插件视图时 display:none 保活;隐藏期滚动测量为 0,切回重新吸底) */ + visible?: boolean }): ReactNode { const scrollRef = useRef(null) const pinnedRef = useRef(true) @@ -280,7 +290,7 @@ export function ChatView({ el.scrollTop = el.scrollHeight }) return () => cancelAnimationFrame(raf) - }, [messages, tools, streaming]) + }, [messages, tools, streaming, visible]) // 压缩结果通知 8 秒后自动消退 useEffect(() => {