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,