diff --git a/frontend-web/src/components/workspace/agents/agent-chat-sidebar.tsx b/frontend-web/src/components/workspace/agents/agent-chat-sidebar.tsx index 45f39694..4ac99677 100644 --- a/frontend-web/src/components/workspace/agents/agent-chat-sidebar.tsx +++ b/frontend-web/src/components/workspace/agents/agent-chat-sidebar.tsx @@ -64,7 +64,7 @@ import { import { useDeleteThread, useRenameThread, - useThreads, + useThreadsPage, useToggleThreadFavorite, useToggleThreadPin, } from "@/core/threads/hooks"; @@ -351,8 +351,8 @@ export function AgentChatSidebar({ activeAgentId }: { activeAgentId?: string }) } /** - * 「最近的对话」面板(参考首页 GeneralQaSidebar 的「最近的对话」区块)。 - * 跨智能体的近期会话,点击打开;支持重命名/置顶/收藏/分享/导出/删除。 + * 当前智能体的「最近对话」面板(参考首页 GeneralQaSidebar 的「最近的对话」区块)。 + * 按 agent_id 在服务端过滤;支持重命名/置顶/收藏/分享/导出/删除。 */ function RecentChatsPane({ activeAgentId }: { activeAgentId?: string }) { const { t } = useI18n(); @@ -361,7 +361,12 @@ function RecentChatsPane({ activeAgentId }: { activeAgentId?: string }) { const pathOf = usePathOfThread(); const { thread_id: threadIdFromPath } = useParams<{ thread_id: string }>(); - const { data: threads = [] } = useThreads(); + const { data: threadPage } = useThreadsPage({ + page: 1, + pageSize: 50, + metadata: { agent_id: activeAgentId ?? "" }, + }); + const threads = threadPage?.threads ?? []; const { mutate: deleteThread } = useDeleteThread(); const { mutate: renameThread } = useRenameThread(); const { toggle: togglePin } = useToggleThreadPin(); @@ -373,8 +378,8 @@ function RecentChatsPane({ activeAgentId }: { activeAgentId?: string }) { const [renameValue, setRenameValue] = useState(""); const [shareThreadId, setShareThreadId] = useState(null); - // 排除调度/会商/报告结构&写作配置向导等系统会话,与首页一致; - // 智能体创建(bootstrap)会话保留展示(带机器人图标、点击回创建页);置顶优先。 + // 服务端先按当前智能体过滤;客户端再排除调度/会商/报告结构&写作配置向导等 + // 系统会话。智能体创建(bootstrap)会话保留展示(带机器人图标、点击回创建页);置顶优先。 const visibleThreads = useMemo( () => sortThreadsPinnedFirst(threads.filter((thread) => !isHiddenFromRecentChats(thread))), [threads], @@ -479,7 +484,11 @@ function RecentChatsPane({ activeAgentId }: { activeAgentId?: string }) { ) : ( filteredThreads.map((thread) => { const isBoot = isBootstrapThread(thread); - const href = isBoot ? bootstrapResumePath(thread.thread_id) : pathOf(thread); + // 正常记录依据自身保存的 agent_id 跳转;早期记录可能尚未写入 + // agent_id,此时保留当前智能体上下文,避免从智能体页误落到通用问答。 + const href = isBoot + ? bootstrapResumePath(thread.thread_id) + : pathOf(thread, { agent_id: activeAgentId }); const isActive = href === pathname; const pinned = isThreadPinned(thread); const favorited = isThreadFavorite(thread); diff --git a/frontend-web/src/components/workspace/input-box.tsx b/frontend-web/src/components/workspace/input-box.tsx index 4d0a0be8..3deab8dc 100644 --- a/frontend-web/src/components/workspace/input-box.tsx +++ b/frontend-web/src/components/workspace/input-box.tsx @@ -758,7 +758,7 @@ export function InputBox({ )} /> )} - {!frontendRagActive && !hideContextTools && !hideMemoryTool && 文件 } - {!frontendRagActive && !hideContextTools && | null; +}): string | undefined { + return pickAgentRef(thread.context, thread.metadata); +} + export function pathOfThread( thread: ThreadRouteTarget, context?: ThreadAgentRef | null, @@ -36,7 +48,7 @@ export function pathOfThread( const agentRef = typeof thread === "string" ? pickAgentRef(context, null) - : pickAgentRef(thread.context, thread.metadata); + : agentRefOfThread(thread) ?? pickAgentRef(context, null); return agentRef ? `${base}/agents/${encodeURIComponent(agentRef)}/chats/${threadId}` diff --git a/frontend-web/src/pages/AgentChatPage.tsx b/frontend-web/src/pages/AgentChatPage.tsx index 54f67752..57e0d7cb 100644 --- a/frontend-web/src/pages/AgentChatPage.tsx +++ b/frontend-web/src/pages/AgentChatPage.tsx @@ -3,6 +3,7 @@ import { BotIcon, Compass, + PanelLeftOpenIcon, PanelRightOpenIcon, PlusSquare, } from "lucide-react"; @@ -13,6 +14,7 @@ import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } fro import type { PromptInputMessage } from "@/components/ai-elements/prompt-input"; import { Button } from "@/components/ui/button"; +import { useSidebarSafe } from "@/components/ui/sidebar"; import { AgentChatSidebar } from "@/components/workspace/agents/agent-chat-sidebar"; import { AgentDetailPanel } from "@/components/workspace/agents/agent-detail-panel"; import { AgentQuestionChips } from "@/components/workspace/agents/agent-question-chips"; @@ -225,6 +227,7 @@ export default function AgentChatPage({ const { t } = useI18n(); const [showFollowups, setShowFollowups] = useState(false); const router = useRouter(); + const workspaceSidebar = useSidebarSafe(); const { agent_id: agentIdFromPath } = useParams<{ agent_id: string; @@ -806,6 +809,23 @@ export default function AgentChatPage({
{/* Header in normal flow, fixed height. */}
+ {/* 工作区左栏收起后,在智能体聊天区左上角提供展开入口。 */} + {showAgentList && + !isEmbedded && + workspaceSidebar && + !workspaceSidebar.open && ( + + )}
)} - - - {/* 右侧智能体详情面板收起时的「展开」入口:仅收起态显示 (展开态的「收起」按钮在面板右上角)。桌面端专用(lg+), 窄屏面板走抽屉交互;嵌入态面板固定渲染,不显示该开关。 */} @@ -919,6 +922,23 @@ export default function AgentChatPage({ enabled={tokenUsageEnabled} messages={displayThread.messages} /> + + +
@@ -1101,7 +1121,6 @@ export default function AgentChatPage({ onAskQuestion={(text) => handleSubmit({ text, files: [] })} collapsed={detailCollapsed} onCollapsedChange={setDetailCollapsed} - showRecentChats={!taskCtxOn} compactAppearanceActions={compactAppearanceActions} /> diff --git a/frontend-web/src/pages/WorkspaceRoutes.tsx b/frontend-web/src/pages/WorkspaceRoutes.tsx index d6edf46a..cf1cb3dc 100644 --- a/frontend-web/src/pages/WorkspaceRoutes.tsx +++ b/frontend-web/src/pages/WorkspaceRoutes.tsx @@ -1,7 +1,8 @@ -import type { CSSProperties, ReactNode } from 'react'; +import { useEffect, useState, type CSSProperties, type ReactNode } from 'react'; import { Navigate, Route, Routes, useLocation, useParams } from 'react-router-dom'; import { getStoredAuth } from '@/core/auth'; +import { getAPIClient } from '@/core/api'; import { isSentimentAgentVisible } from '@/core/sentiment-agent/config'; import { resolveChatsIframeEntryRedirect, @@ -25,6 +26,7 @@ import { ArtifactsProvider } from '@/components/workspace/artifacts'; import { DocumentRewritePresentationProvider } from '@/core/document-rewrite/context'; import { RouteBaseProvider } from '@/core/route-base'; +import { agentRefOfThread } from '@/core/threads/utils'; import ChatsPage from './ChatsPage'; import ChatPage from './ChatPage'; @@ -341,12 +343,80 @@ function ChatThreadEntryRoute({ return ( - + ); } +/** + * Older bookmarks and a few historic list entries use the generic + * ``/chats/:thread_id`` path even for an agent-owned conversation. Recover the + * owning agent from the persisted thread record before rendering so those + * links keep the agent workspace (agent list and detail panel) instead of + * falling back to the general Q&A page. Task workspace threads intentionally + * remain on their explicit task routes because their surrounding layout is + * part of the task context. + */ +function AgentAwareChatThreadContent() { + const { thread_id: threadId } = useParams<{ thread_id: string }>(); + const location = useLocation(); + const [agentId, setAgentId] = useState(null); + const [resolved, setResolved] = useState(false); + + useEffect(() => { + let cancelled = false; + setAgentId(null); + setResolved(false); + + if (!threadId) { + setResolved(true); + return () => { + cancelled = true; + }; + } + + void getAPIClient() + .threads.get(threadId) + .then((thread) => { + if (cancelled) return; + const record = thread as { + context?: { agent_id?: string; agent_name?: string } | null; + metadata?: Record | null; + }; + // Task-bound threads need their task host to preserve the purpose gate, + // action partitioning, and other task-specific controls. + const isTaskThread = typeof record.metadata?.taskId === 'string'; + setAgentId(isTaskThread ? null : agentRefOfThread(record) ?? null); + }) + .catch(() => { + if (!cancelled) setAgentId(null); + }) + .finally(() => { + if (!cancelled) setResolved(true); + }); + + return () => { + cancelled = true; + }; + }, [threadId]); + + if (!resolved) { + return
; + } + + if (threadId && agentId) { + return ( + + ); + } + + return ; +} + /** * `chats_iframe` is reserved for the Brainy postMessage protocol. `from=iframe` * and `chatMode=iframe` opt into this page; ordinary iframe/Wujie embedding