feat(chat): 支持子智能体栈和面包屑导航功能
- 引入子智能体栈结构以支持多层子智能体导航 - 替换原有单一子智能体视图为子智能体栈管理消息和状态 - 添加navigateToSubAgentLevel方法,支持通过面包屑快速跳转子智能体层级 - 调整子智能体面包屑条UI,显示完整层级的描述、类型和状态 - 优化子智能体消息流和状态更新逻辑,保持栈顶视图同步 - 更新退出子智能体视图逻辑以支持栈弹出操作 - 添加主会话入口和各层子智能体的切换按钮及状态颜色显示
This commit is contained in:
parent
175e7fc01b
commit
bf724b133c
@ -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() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Sub-agent back bar */}
|
||||
{/* Sub-agent breadcrumb bar */}
|
||||
{subAgentView && (
|
||||
<div className="shrink-0 border-b border-[var(--border-color)] bg-[var(--bg-secondary)]/80 px-4 py-2 flex items-center gap-4">
|
||||
<div className="shrink-0 border-b border-[var(--border-color)] bg-[var(--bg-secondary)]/80 px-4 py-2 flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleExitSubAgentView}
|
||||
className="flex items-center gap-1.5 text-sm text-[var(--accent-cyan)] hover:text-[var(--accent-cyan)]/80 transition-colors"
|
||||
className="flex items-center gap-1 text-sm text-[var(--accent-cyan)] hover:text-[var(--accent-cyan)]/80 transition-colors shrink-0"
|
||||
title="返回上一级"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
<span>返回主会话</span>
|
||||
</button>
|
||||
<div className="h-4 w-px bg-[var(--divider-color)]" />
|
||||
<div className="flex items-center gap-1.5 text-sm text-[var(--text-secondary)]">
|
||||
<Bot className="h-4 w-4 text-violet-400" />
|
||||
<span className="text-[var(--text-muted)]">子智能体:</span>
|
||||
<span className="text-[var(--text-primary)] font-medium">{subAgentView.description}</span>
|
||||
</div>
|
||||
<div className="h-4 w-px bg-[var(--divider-color)]" />
|
||||
<div className="flex items-center gap-1.5 text-sm">
|
||||
<span className="text-[var(--text-muted)]">类型:</span>
|
||||
<span className="text-[var(--text-secondary)]">{subAgentView.subagentType || '...'}</span>
|
||||
</div>
|
||||
<div className="h-4 w-px bg-[var(--divider-color)]" />
|
||||
<div className="flex items-center gap-1.5 text-sm">
|
||||
<span className="text-[var(--text-muted)]">状态:</span>
|
||||
<span className={`font-medium ${
|
||||
subAgentView.status === 'completed' ? 'text-emerald-400' :
|
||||
subAgentView.status === 'failed' ? 'text-red-400' :
|
||||
subAgentView.status === 'timeout' ? 'text-amber-400' :
|
||||
subAgentView.status === 'running' ? 'text-amber-400' :
|
||||
{/* Breadcrumb: 主会话 */}
|
||||
<button
|
||||
onClick={() => navigateToSubAgentLevel(-1)}
|
||||
className="flex items-center gap-1 text-sm text-[var(--text-muted)] hover:text-[var(--accent-cyan)] transition-colors shrink-0"
|
||||
title="返回主会话"
|
||||
>
|
||||
<Bot className="h-3.5 w-3.5" />
|
||||
<span>主会话</span>
|
||||
</button>
|
||||
{/* 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}
|
||||
</span>
|
||||
</div>
|
||||
return (
|
||||
<div key={level.taskId} className="flex items-center gap-2 min-w-0">
|
||||
<ChevronRight className="h-3.5 w-3.5 text-[var(--text-muted)] shrink-0" />
|
||||
{isLast ? (
|
||||
<div className="flex items-center gap-2 text-sm min-w-0">
|
||||
<span className="text-[var(--text-primary)] font-medium truncate">{level.description}</span>
|
||||
{level.subagentType && (
|
||||
<span className="text-xs text-[var(--text-muted)] bg-[var(--overlay-dim)] px-1.5 py-0.5 rounded shrink-0">{level.subagentType}</span>
|
||||
)}
|
||||
<span className={`text-xs font-medium shrink-0 ${statusColor}`}>{statusText}</span>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => navigateToSubAgentLevel(idx)}
|
||||
className="flex items-center gap-2 text-sm text-[var(--text-muted)] hover:text-[var(--accent-cyan)] transition-colors min-w-0"
|
||||
>
|
||||
<span className="truncate">{level.description}</span>
|
||||
{level.subagentType && (
|
||||
<span className="text-xs bg-[var(--overlay-dim)] px-1.5 py-0.5 rounded shrink-0">{level.subagentType}</span>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1 min-h-0">
|
||||
|
||||
@ -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<SessionSummary[]>([])
|
||||
const [selectedSessionId, setSelectedSessionId] = useState<string | null>(null)
|
||||
const [subAgentView, setSubAgentView] = useState<SubAgentView | null>(null)
|
||||
const [subAgentStack, setSubAgentStack] = useState<SubAgentView[]>([])
|
||||
const subAgentView = useMemo(() => subAgentStack.length > 0 ? subAgentStack[subAgentStack.length - 1] : null, [subAgentStack])
|
||||
const [memories, setMemories] = useState<MemorySummary[]>([])
|
||||
const [skills, setSkills] = useState<SkillSummary[]>([])
|
||||
const [todos, setTodos] = useState<TodoItemSummary[]>([])
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user