From bf724b133cf4f9002d72e0b8e3f89c7fef63edec Mon Sep 17 00:00:00 2001 From: oudecheng <13802883547@139.com> Date: Thu, 18 Jun 2026 14:53:05 +0800 Subject: [PATCH] =?UTF-8?q?feat(chat):=20=E6=94=AF=E6=8C=81=E5=AD=90?= =?UTF-8?q?=E6=99=BA=E8=83=BD=E4=BD=93=E6=A0=88=E5=92=8C=E9=9D=A2=E5=8C=85?= =?UTF-8?q?=E5=B1=91=E5=AF=BC=E8=88=AA=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 引入子智能体栈结构以支持多层子智能体导航 - 替换原有单一子智能体视图为子智能体栈管理消息和状态 - 添加navigateToSubAgentLevel方法,支持通过面包屑快速跳转子智能体层级 - 调整子智能体面包屑条UI,显示完整层级的描述、类型和状态 - 优化子智能体消息流和状态更新逻辑,保持栈顶视图同步 - 更新退出子智能体视图逻辑以支持栈弹出操作 - 添加主会话入口和各层子智能体的切换按钮及状态颜色显示 --- web/src/App.tsx | 89 ++++++++++++++++++++------------ web/src/hooks/useChat.ts | 106 +++++++++++++++++++++++++++------------ 2 files changed, 130 insertions(+), 65 deletions(-) diff --git a/web/src/App.tsx b/web/src/App.tsx index a9dfb9e..67a7e73 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' -import { Zap, ArrowLeft, Bot, Clock, Sun, Moon, PanelRightOpen, X, Brain, Settings as SettingsIcon } from 'lucide-react' +import { Zap, ArrowLeft, Bot, Clock, Sun, Moon, PanelRightOpen, X, Brain, Settings as SettingsIcon, ChevronRight } from 'lucide-react' import { ChatContainer } from './components/Chat/ChatContainer' import { TopicList } from './components/Sidebar/TopicList' import { SchedulerJobList } from './components/Sidebar/SchedulerJobList' @@ -40,6 +40,7 @@ function App() { isReadOnly, // 子智能体视图 subAgentView, + subAgentStack, // 记忆 memories, requestMemoryList, @@ -85,6 +86,7 @@ function App() { topicRefreshTrigger, enterSubAgentView, exitSubAgentView, + navigateToSubAgentLevel, handleStop, } = useChat() @@ -607,45 +609,66 @@ function App() { )} - {/* Sub-agent back bar */} + {/* Sub-agent breadcrumb bar */} {subAgentView && ( -
+
-
-
- - 子智能体: - {subAgentView.description} -
-
-
- 类型: - {subAgentView.subagentType || '...'} -
-
-
- 状态: - navigateToSubAgentLevel(-1)} + className="flex items-center gap-1 text-sm text-[var(--text-muted)] hover:text-[var(--accent-cyan)] transition-colors shrink-0" + title="返回主会话" + > + + 主会话 + + {/* Breadcrumb: each sub-agent level */} + {subAgentStack.map((level, idx) => { + const isLast = idx === subAgentStack.length - 1 + const statusText = + level.status === 'completed' ? '已完成' : + level.status === 'failed' ? '失败' : + level.status === 'timeout' ? '超时' : + level.status === 'running' ? '执行中' : + level.status === 'loading' ? '加载中...' : + level.status + const statusColor = + level.status === 'completed' ? 'text-emerald-400' : + level.status === 'failed' ? 'text-red-400' : + level.status === 'timeout' ? 'text-amber-400' : + level.status === 'running' ? 'text-amber-400' : 'text-[var(--text-secondary)]' - }`}> - {subAgentView.status === 'completed' ? '已完成' : - subAgentView.status === 'failed' ? '失败' : - subAgentView.status === 'timeout' ? '超时' : - subAgentView.status === 'running' ? '执行中' : - subAgentView.status === 'loading' ? '加载中...' : - subAgentView.status} - -
+ return ( +
+ + {isLast ? ( +
+ {level.description} + {level.subagentType && ( + {level.subagentType} + )} + {statusText} +
+ ) : ( + + )} +
+ ) + })}
)}
diff --git a/web/src/hooks/useChat.ts b/web/src/hooks/useChat.ts index 92d5075..ccf9d3e 100644 --- a/web/src/hooks/useChat.ts +++ b/web/src/hooks/useChat.ts @@ -59,8 +59,9 @@ interface UseChatReturn { // 是否只读 isReadOnly: boolean - // 子智能体视图 + // 子智能体视图(栈结构,支持面包屑导航) subAgentView: SubAgentView | null + subAgentStack: SubAgentView[] // 方法 handleMessage: (content: string, attachments?: Attachment[]) => void @@ -86,6 +87,7 @@ interface UseChatReturn { // 子智能体导航方法 enterSubAgentView: (taskId: string, description: string) => Command exitSubAgentView: () => void + navigateToSubAgentLevel: (index: number) => void // 记忆状态 memories: MemorySummary[] @@ -149,7 +151,8 @@ export function useChat(): UseChatReturn { const [topicRefreshTrigger, setTopicRefreshTrigger] = useState(0) const [sessions, setSessions] = useState([]) const [selectedSessionId, setSelectedSessionId] = useState(null) - const [subAgentView, setSubAgentView] = useState(null) + const [subAgentStack, setSubAgentStack] = useState([]) + const subAgentView = useMemo(() => subAgentStack.length > 0 ? subAgentStack[subAgentStack.length - 1] : null, [subAgentStack]) const [memories, setMemories] = useState([]) const [skills, setSkills] = useState([]) const [todos, setTodos] = useState([]) @@ -302,11 +305,12 @@ export function useChat(): UseChatReturn { // stream_delta: accumulate into existing message by ID, or create new if (message.type === 'stream_delta') { const msg = message as StreamDelta - setSubAgentView((prev) => { - if (!prev) return prev - const existingIdx = prev.messages.findIndex(m => m.id === msg.id && m.type === 'message') + setSubAgentStack((prev) => { + if (prev.length === 0) return prev + const top = prev[prev.length - 1] + const existingIdx = top.messages.findIndex(m => m.id === msg.id && m.type === 'message') if (existingIdx >= 0) { - const updated = [...prev.messages] + const updated = [...top.messages] const existing = updated[existingIdx] updated[existingIdx] = { ...existing, @@ -315,10 +319,15 @@ export function useChat(): UseChatReturn { ? (existing.reasoningContent || '') + msg.reasoning_delta : existing.reasoningContent, } - return { ...prev, messages: updated } + const newStack = [...prev] + newStack[newStack.length - 1] = { ...top, messages: updated } + return newStack } const chatMsg = serverMessageToChatMessage(message) - return chatMsg ? { ...prev, messages: [...prev.messages, chatMsg] } : prev + if (!chatMsg) return prev + const newStack = [...prev] + newStack[newStack.length - 1] = { ...top, messages: [...top.messages, chatMsg] } + return newStack }) return } @@ -327,17 +336,22 @@ export function useChat(): UseChatReturn { // Other messages: assistant_response replaces streamed message by ID const chatMsg = serverMessageToChatMessage(message) if (chatMsg) { - setSubAgentView((prev) => { - if (!prev) return prev + setSubAgentStack((prev) => { + if (prev.length === 0) return prev + const top = prev[prev.length - 1] if (message.type === 'assistant_response') { - const existingIdx = prev.messages.findIndex(m => m.id === chatMsg.id && m.type === 'message') + const existingIdx = top.messages.findIndex(m => m.id === chatMsg.id && m.type === 'message') if (existingIdx >= 0) { - const updated = [...prev.messages] + const updated = [...top.messages] updated[existingIdx] = chatMsg - return { ...prev, messages: updated } + const newStack = [...prev] + newStack[newStack.length - 1] = { ...top, messages: updated } + return newStack } } - return { ...prev, messages: [...prev.messages, chatMsg] } + const newStack = [...prev] + newStack[newStack.length - 1] = { ...top, messages: [...top.messages, chatMsg] } + return newStack }) } } @@ -366,16 +380,18 @@ export function useChat(): UseChatReturn { if (currentSubAgentView) { if (message.type === 'task_messages_loaded') { const msg = message as TaskMessagesLoaded - setSubAgentView((prev) => - prev - ? { - ...prev, - subagentType: msg.subagent_type, - status: msg.status, - summary: msg.summary, - } - : prev - ) + setSubAgentStack((prev) => { + if (prev.length === 0) return prev + const top = prev[prev.length - 1] + const newStack = [...prev] + newStack[newStack.length - 1] = { + ...top, + subagentType: msg.subagent_type, + status: msg.status, + summary: msg.summary, + } + return newStack + }) return } @@ -756,7 +772,7 @@ export function useChat(): UseChatReturn { const selectTopic = useCallback((topicId: string) => { setSelectedTopic(topicId) setMessages([]) - setSubAgentView(null) + setSubAgentStack([]) }, []) const createTopic = useCallback((title?: string): Command => { @@ -802,7 +818,7 @@ export function useChat(): UseChatReturn { setTopics([]) setSelectedTopic(null) setMessages([]) - setSubAgentView(null) + setSubAgentStack([]) setIsLoading(true) }, [selectedChannel]) @@ -812,7 +828,7 @@ export function useChat(): UseChatReturn { setTopics([]) setSelectedTopic(null) setMessages([]) - setSubAgentView(null) + setSubAgentStack([]) setIsLoading(true) }, [selectedSessionId]) @@ -849,9 +865,12 @@ export function useChat(): UseChatReturn { status: 'loading', messages: [], } - // Sync ref immediately so WebSocket response routing works correctly - subAgentViewRef.current = newView - setSubAgentView(newView) + setSubAgentStack((prev) => { + const newStack = [...prev, newView] + // Sync ref immediately so WebSocket response routing works correctly + subAgentViewRef.current = newView + return newStack + }) return { type: 'load_task_messages', task_id: taskId, @@ -859,8 +878,29 @@ export function useChat(): UseChatReturn { }, []) const exitSubAgentView = useCallback(() => { - subAgentViewRef.current = null - setSubAgentView(null) + setSubAgentStack((prev) => { + if (prev.length <= 1) { + subAgentViewRef.current = null + return [] + } + const newStack = prev.slice(0, -1) + subAgentViewRef.current = newStack[newStack.length - 1] + return newStack + }) + }, []) + + const navigateToSubAgentLevel = useCallback((index: number) => { + setSubAgentStack((prev) => { + if (index < 0) { + // -1 means go back to main session (clear all) + subAgentViewRef.current = null + return [] + } + if (index >= prev.length) return prev + const newStack = prev.slice(0, index + 1) + subAgentViewRef.current = newStack.length > 0 ? newStack[newStack.length - 1] : null + return newStack + }) }, []) // 记忆方法 @@ -957,6 +997,7 @@ export function useChat(): UseChatReturn { channels, selectedChannel, subAgentView, + subAgentStack, handleMessage, handleCommand, clearMessages, @@ -974,6 +1015,7 @@ export function useChat(): UseChatReturn { selectSession, enterSubAgentView, exitSubAgentView, + navigateToSubAgentLevel, memories, requestMemoryList, createMemory,