fix(agent): improve chat navigation

This commit is contained in:
yu
2026-07-20 10:25:13 +08:00
parent d91e67a5ad
commit b7d0fe10bc
3 changed files with 58 additions and 17 deletions
+1
View File
@@ -7,6 +7,7 @@
+ [修复] 多标签页共享 Codex 会话改由 Agent 广播同步,按线程过滤消息和状态,并禁止运行中切换会话。
+ [修复] 本地 Agent 统一广播 Codex 运行状态,新连接标签页会同步禁用发送,并明确区分工具完成与本轮完成。
+ [修复] 本地 Agent 上传的图片附件可创建为画布图片节点并连接生成流程,不再出现参考图空节点。
+ [修复] Agent 对话切换时自动定位到最新消息,并在向上浏览记录时提供快速回到底部按钮。
+ [新增] 左侧画布面板新增「提示词库」Tab,按来源折叠分组。
+ [修复] 处于「提示词库/资产」Tab 时拖动画布元素卡顿,面板内容不再随节点更新重渲染。
+ [新增] 提示词来源增加自定义脚本抓取功能、支持新增来源。
@@ -10,3 +10,4 @@ description: 当前版本已实现但仍需人工验证的变更项
- 本地 Agent 多标签页会话同步:所有标签页共享同一个站点级 Codex 活跃线程;任一页面发送消息、新建、恢复或删除会话后,其他页面应同步活跃线程和聊天记录;Agent 输出仅显示在事件所属线程,运行中不能新建、恢复、删除或再次发送任务。
- 本地 Agent 运行状态同步:在一个标签页运行较长 Codex 任务,等待某张工具卡显示「工具完成」后再打开或刷新第二个标签页;第二个标签页应立即显示 Codex 正在运行并禁用发送,整轮结束后两个标签页同时恢复;工具卡只显示「工具完成」,整轮结束由「本轮完成」表示。
- 本地 Agent 图片附件落画布:在右侧 Agent 上传参考图并要求基于商品信息创建生图流程,附件应创建为保持原比例的真实图片节点,分析提示词应创建为文本节点,二者都应连接到生成配置节点;刷新页面后参考图仍可显示并参与生成。任务中途切换到其他标签页时,附件只能写入发起任务的标签页;若发起标签页关闭,附件读取应失败且不能落入其他画布。
- Agent 对话滚动:从历史或日志切回对话、恢复其他会话时应自动定位到最新消息;手动向上浏览后应显示向下箭头且新消息不强制打断阅读,点击箭头后平滑回到底部并继续跟随新消息。
@@ -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<HTMLDivElement>(null);
const followMessagesRef = useRef(true);
const [showScrollToBottom, setShowScrollToBottom] = useState(false);
const canvasContextRef = useRef<AgentCanvasContext | null>(useAgentStore.getState().canvasContext);
const confirmToolsRef = useRef(confirmTools);
const pendingToolRef = useRef<AgentPendingToolCall | null>(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: <PlugZap className="size-3.5" /> },
{ value: "chat", label: "对话" },
{ value: "chat", label: "对话", icon: <MessageSquare className="size-3.5" /> },
{ value: "history", label: "历史", icon: <History className="size-3.5" />, count: threads.length },
{ value: "log", label: "日志", icon: <Terminal className="size-3.5" />, count: eventLogs.length },
]}
@@ -658,20 +682,35 @@ export function CanvasLocalAgentPanel({ embedded, headless, autoConnect }: { emb
/>
) : (
<>
<div ref={listRef} className="thin-scrollbar min-h-0 flex-1 space-y-4 overflow-y-auto p-4">
{messages.map((item) => (
<AgentChatMessage key={item.id} item={agentMessageToChatMessage(item)} theme={theme} user={user} />
))}
{pendingTool ? (
<AgentPendingToolCard
summary={summarizeCanvasAgentOps(pendingTool.input?.ops || []) || toolName(pendingTool.name)}
detail={{ requestId: pendingTool.requestId, name: pendingTool.name, input: pendingTool.input }}
theme={theme}
onReject={rejectPendingTool}
onApprove={approvePendingTool}
/>
<div className="relative min-h-0 flex-1">
<div ref={listRef} className="thin-scrollbar h-full space-y-4 overflow-y-auto px-4 pb-12 pt-4" onScroll={updateScrollState}>
{messages.map((item) => (
<AgentChatMessage key={item.id} item={agentMessageToChatMessage(item)} theme={theme} user={user} />
))}
{pendingTool ? (
<AgentPendingToolCard
summary={summarizeCanvasAgentOps(pendingTool.input?.ops || []) || toolName(pendingTool.name)}
detail={{ requestId: pendingTool.requestId, name: pendingTool.name, input: pendingTool.input }}
theme={theme}
onReject={rejectPendingTool}
onApprove={approvePendingTool}
/>
) : null}
{waiting && !pendingTool ? <AgentWorkingMessage theme={theme} /> : null}
</div>
{showScrollToBottom ? (
<Tooltip title="滚动到底部" placement="left">
<Button
type="text"
shape="circle"
aria-label="滚动到底部"
className="!absolute bottom-3 left-1/2 z-10 !h-8 !w-8 !min-w-8 -translate-x-1/2 backdrop-blur transition hover:-translate-y-0.5"
style={{ background: theme.toolbar.panel, border: `1px solid ${theme.node.stroke}`, color: theme.node.text }}
icon={<ChevronDown className="size-4" />}
onClick={() => scrollToBottom()}
/>
</Tooltip>
) : null}
{waiting && !pendingTool ? <AgentWorkingMessage theme={theme} /> : null}
</div>
<AgentChatComposer
prompt={prompt}