From b7d0fe10bc13021b887ba29c4e7a7dd9a8363962 Mon Sep 17 00:00:00 2001 From: yu <2799341137@qq.com> Date: Mon, 20 Jul 2026 10:25:13 +0800 Subject: [PATCH] fix(agent): improve chat navigation --- CHANGELOG.md | 1 + docs/content/docs/progress/pending-test.mdx | 1 + .../canvas/canvas-local-agent-panel.tsx | 73 ++++++++++++++----- 3 files changed, 58 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cca1ee9..24c15b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ + [修复] 多标签页共享 Codex 会话改由 Agent 广播同步,按线程过滤消息和状态,并禁止运行中切换会话。 + [修复] 本地 Agent 统一广播 Codex 运行状态,新连接标签页会同步禁用发送,并明确区分工具完成与本轮完成。 + [修复] 本地 Agent 上传的图片附件可创建为画布图片节点并连接生成流程,不再出现参考图空节点。 ++ [修复] Agent 对话切换时自动定位到最新消息,并在向上浏览记录时提供快速回到底部按钮。 + [新增] 左侧画布面板新增「提示词库」Tab,按来源折叠分组。 + [修复] 处于「提示词库/资产」Tab 时拖动画布元素卡顿,面板内容不再随节点更新重渲染。 + [新增] 提示词来源增加自定义脚本抓取功能、支持新增来源。 diff --git a/docs/content/docs/progress/pending-test.mdx b/docs/content/docs/progress/pending-test.mdx index 9e6abe8..52b4212 100644 --- a/docs/content/docs/progress/pending-test.mdx +++ b/docs/content/docs/progress/pending-test.mdx @@ -10,3 +10,4 @@ description: 当前版本已实现但仍需人工验证的变更项 - 本地 Agent 多标签页会话同步:所有标签页共享同一个站点级 Codex 活跃线程;任一页面发送消息、新建、恢复或删除会话后,其他页面应同步活跃线程和聊天记录;Agent 输出仅显示在事件所属线程,运行中不能新建、恢复、删除或再次发送任务。 - 本地 Agent 运行状态同步:在一个标签页运行较长 Codex 任务,等待某张工具卡显示「工具完成」后再打开或刷新第二个标签页;第二个标签页应立即显示 Codex 正在运行并禁用发送,整轮结束后两个标签页同时恢复;工具卡只显示「工具完成」,整轮结束由「本轮完成」表示。 - 本地 Agent 图片附件落画布:在右侧 Agent 上传参考图并要求基于商品信息创建生图流程,附件应创建为保持原比例的真实图片节点,分析提示词应创建为文本节点,二者都应连接到生成配置节点;刷新页面后参考图仍可显示并参与生成。任务中途切换到其他标签页时,附件只能写入发起任务的标签页;若发起标签页关闭,附件读取应失败且不能落入其他画布。 +- Agent 对话滚动:从历史或日志切回对话、恢复其他会话时应自动定位到最新消息;手动向上浏览后应显示向下箭头且新消息不强制打断阅读,点击箭头后平滑回到底部并继续跟随新消息。 diff --git a/web/src/components/canvas/canvas-local-agent-panel.tsx b/web/src/components/canvas/canvas-local-agent-panel.tsx index 4476b96..a8828a3 100644 --- a/web/src/components/canvas/canvas-local-agent-panel.tsx +++ b/web/src/components/canvas/canvas-local-agent-panel.tsx @@ -2,7 +2,7 @@ import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "rea import { useNavigate, useSearchParams } from "react-router-dom"; import { App, Button, Input, Segmented, Tooltip } from "antd"; import copyToClipboard from "copy-to-clipboard"; -import { Copy, FolderOpen, History, KeyRound, Link2, LoaderCircle, PlugZap, Plus, RefreshCw, Square, Terminal, Trash2 } from "lucide-react"; +import { ChevronDown, Copy, FolderOpen, History, KeyRound, Link2, LoaderCircle, MessageSquare, PlugZap, Plus, RefreshCw, Square, Terminal, Trash2 } from "lucide-react"; import { canvasThemes } from "@/lib/canvas-theme"; import { imageMetadata } from "@/lib/canvas/canvas-node-factory"; @@ -20,6 +20,7 @@ import { AgentChatComposer, AgentChatMessage, AgentPanelTabs, AgentPendingToolCa const MAX_ATTACHMENTS = 6; const MAX_ATTACHMENT_PAYLOAD_BYTES = 28 * 1024 * 1024; +const SCROLL_BOTTOM_THRESHOLD = 48; const DEFAULT_AGENT_URL = "http://127.0.0.1:17371"; const AGENT_CONNECT_STEPS = [ { title: "方式一:在 Codex 中使用插件", text: "在 Codex app 安装 Infinite Canvas 插件后,通过插件启动画布,插件会自动启动本地 Agent 并带上连接信息。" }, @@ -90,6 +91,8 @@ export function CanvasLocalAgentPanel({ embedded, headless, autoConnect }: { emb const pushEventLog = useAgentStore((state) => state.addEventLog); const clearEventLogs = useAgentStore((state) => state.clearEventLogs); const listRef = useRef(null); + const followMessagesRef = useRef(true); + const [showScrollToBottom, setShowScrollToBottom] = useState(false); const canvasContextRef = useRef(useAgentStore.getState().canvasContext); const confirmToolsRef = useRef(confirmTools); const pendingToolRef = useRef(null); @@ -143,9 +146,30 @@ export function CanvasLocalAgentPanel({ embedded, headless, autoConnect }: { emb useEffect(() => { pendingToolRef.current = pendingTool; }, [pendingTool]); + const updateScrollState = useCallback(() => { + const list = listRef.current; + if (!list) return; + const atBottom = list.scrollHeight - list.scrollTop - list.clientHeight <= SCROLL_BOTTOM_THRESHOLD; + followMessagesRef.current = atBottom; + setShowScrollToBottom(!atBottom); + }, []); + const scrollToBottom = useCallback((behavior: ScrollBehavior = "smooth") => { + const list = listRef.current; + if (!list) return; + followMessagesRef.current = true; + list.scrollTo({ top: list.scrollHeight, behavior }); + setShowScrollToBottom(false); + }, []); useEffect(() => { - listRef.current?.scrollTo({ top: listRef.current.scrollHeight }); - }, [messages, pendingTool, waiting]); + if (activeTab !== "chat") return; + const frame = requestAnimationFrame(() => scrollToBottom("auto")); + return () => cancelAnimationFrame(frame); + }, [activeTab, activeThreadId, scrollToBottom]); + useEffect(() => { + if (activeTab !== "chat") return; + const frame = requestAnimationFrame(() => (followMessagesRef.current ? scrollToBottom("auto") : updateScrollState())); + return () => cancelAnimationFrame(frame); + }, [activeTab, messages, pendingTool, scrollToBottom, updateScrollState, waiting]); useEffect(() => () => attachmentUrlsRef.current.forEach((url) => URL.revokeObjectURL(url)), []); useEffect(() => { @@ -603,7 +627,7 @@ export function CanvasLocalAgentPanel({ embedded, headless, autoConnect }: { emb theme={theme} items={[ { value: "setup", label: "连接", icon: }, - { value: "chat", label: "对话" }, + { value: "chat", label: "对话", icon: }, { value: "history", label: "历史", icon: , count: threads.length }, { value: "log", label: "日志", icon: , count: eventLogs.length }, ]} @@ -658,20 +682,35 @@ export function CanvasLocalAgentPanel({ embedded, headless, autoConnect }: { emb /> ) : ( <> -
- {messages.map((item) => ( - - ))} - {pendingTool ? ( - +
+
+ {messages.map((item) => ( + + ))} + {pendingTool ? ( + + ) : null} + {waiting && !pendingTool ? : null} +
+ {showScrollToBottom ? ( + +