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 { 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 { ChatContainer } from './components/Chat/ChatContainer'
|
||||||
import { TopicList } from './components/Sidebar/TopicList'
|
import { TopicList } from './components/Sidebar/TopicList'
|
||||||
import { SchedulerJobList } from './components/Sidebar/SchedulerJobList'
|
import { SchedulerJobList } from './components/Sidebar/SchedulerJobList'
|
||||||
@ -40,6 +40,7 @@ function App() {
|
|||||||
isReadOnly,
|
isReadOnly,
|
||||||
// 子智能体视图
|
// 子智能体视图
|
||||||
subAgentView,
|
subAgentView,
|
||||||
|
subAgentStack,
|
||||||
// 记忆
|
// 记忆
|
||||||
memories,
|
memories,
|
||||||
requestMemoryList,
|
requestMemoryList,
|
||||||
@ -85,6 +86,7 @@ function App() {
|
|||||||
topicRefreshTrigger,
|
topicRefreshTrigger,
|
||||||
enterSubAgentView,
|
enterSubAgentView,
|
||||||
exitSubAgentView,
|
exitSubAgentView,
|
||||||
|
navigateToSubAgentLevel,
|
||||||
handleStop,
|
handleStop,
|
||||||
} = useChat()
|
} = useChat()
|
||||||
|
|
||||||
@ -607,45 +609,66 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* Sub-agent back bar */}
|
{/* Sub-agent breadcrumb bar */}
|
||||||
{subAgentView && (
|
{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
|
<button
|
||||||
onClick={handleExitSubAgentView}
|
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" />
|
<ArrowLeft className="h-4 w-4" />
|
||||||
<span>返回主会话</span>
|
|
||||||
</button>
|
</button>
|
||||||
<div className="h-4 w-px bg-[var(--divider-color)]" />
|
{/* Breadcrumb: 主会话 */}
|
||||||
<div className="flex items-center gap-1.5 text-sm text-[var(--text-secondary)]">
|
<button
|
||||||
<Bot className="h-4 w-4 text-violet-400" />
|
onClick={() => navigateToSubAgentLevel(-1)}
|
||||||
<span className="text-[var(--text-muted)]">子智能体:</span>
|
className="flex items-center gap-1 text-sm text-[var(--text-muted)] hover:text-[var(--accent-cyan)] transition-colors shrink-0"
|
||||||
<span className="text-[var(--text-primary)] font-medium">{subAgentView.description}</span>
|
title="返回主会话"
|
||||||
</div>
|
>
|
||||||
<div className="h-4 w-px bg-[var(--divider-color)]" />
|
<Bot className="h-3.5 w-3.5" />
|
||||||
<div className="flex items-center gap-1.5 text-sm">
|
<span>主会话</span>
|
||||||
<span className="text-[var(--text-muted)]">类型:</span>
|
</button>
|
||||||
<span className="text-[var(--text-secondary)]">{subAgentView.subagentType || '...'}</span>
|
{/* Breadcrumb: each sub-agent level */}
|
||||||
</div>
|
{subAgentStack.map((level, idx) => {
|
||||||
<div className="h-4 w-px bg-[var(--divider-color)]" />
|
const isLast = idx === subAgentStack.length - 1
|
||||||
<div className="flex items-center gap-1.5 text-sm">
|
const statusText =
|
||||||
<span className="text-[var(--text-muted)]">状态:</span>
|
level.status === 'completed' ? '已完成' :
|
||||||
<span className={`font-medium ${
|
level.status === 'failed' ? '失败' :
|
||||||
subAgentView.status === 'completed' ? 'text-emerald-400' :
|
level.status === 'timeout' ? '超时' :
|
||||||
subAgentView.status === 'failed' ? 'text-red-400' :
|
level.status === 'running' ? '执行中' :
|
||||||
subAgentView.status === 'timeout' ? 'text-amber-400' :
|
level.status === 'loading' ? '加载中...' :
|
||||||
subAgentView.status === 'running' ? 'text-amber-400' :
|
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)]'
|
'text-[var(--text-secondary)]'
|
||||||
}`}>
|
return (
|
||||||
{subAgentView.status === 'completed' ? '已完成' :
|
<div key={level.taskId} className="flex items-center gap-2 min-w-0">
|
||||||
subAgentView.status === 'failed' ? '失败' :
|
<ChevronRight className="h-3.5 w-3.5 text-[var(--text-muted)] shrink-0" />
|
||||||
subAgentView.status === 'timeout' ? '超时' :
|
{isLast ? (
|
||||||
subAgentView.status === 'running' ? '执行中' :
|
<div className="flex items-center gap-2 text-sm min-w-0">
|
||||||
subAgentView.status === 'loading' ? '加载中...' :
|
<span className="text-[var(--text-primary)] font-medium truncate">{level.description}</span>
|
||||||
subAgentView.status}
|
{level.subagentType && (
|
||||||
</span>
|
<span className="text-xs text-[var(--text-muted)] bg-[var(--overlay-dim)] px-1.5 py-0.5 rounded shrink-0">{level.subagentType}</span>
|
||||||
</div>
|
)}
|
||||||
|
<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>
|
||||||
)}
|
)}
|
||||||
<div className="flex-1 min-h-0">
|
<div className="flex-1 min-h-0">
|
||||||
|
|||||||
@ -59,8 +59,9 @@ interface UseChatReturn {
|
|||||||
// 是否只读
|
// 是否只读
|
||||||
isReadOnly: boolean
|
isReadOnly: boolean
|
||||||
|
|
||||||
// 子智能体视图
|
// 子智能体视图(栈结构,支持面包屑导航)
|
||||||
subAgentView: SubAgentView | null
|
subAgentView: SubAgentView | null
|
||||||
|
subAgentStack: SubAgentView[]
|
||||||
|
|
||||||
// 方法
|
// 方法
|
||||||
handleMessage: (content: string, attachments?: Attachment[]) => void
|
handleMessage: (content: string, attachments?: Attachment[]) => void
|
||||||
@ -86,6 +87,7 @@ interface UseChatReturn {
|
|||||||
// 子智能体导航方法
|
// 子智能体导航方法
|
||||||
enterSubAgentView: (taskId: string, description: string) => Command
|
enterSubAgentView: (taskId: string, description: string) => Command
|
||||||
exitSubAgentView: () => void
|
exitSubAgentView: () => void
|
||||||
|
navigateToSubAgentLevel: (index: number) => void
|
||||||
|
|
||||||
// 记忆状态
|
// 记忆状态
|
||||||
memories: MemorySummary[]
|
memories: MemorySummary[]
|
||||||
@ -149,7 +151,8 @@ export function useChat(): UseChatReturn {
|
|||||||
const [topicRefreshTrigger, setTopicRefreshTrigger] = useState(0)
|
const [topicRefreshTrigger, setTopicRefreshTrigger] = useState(0)
|
||||||
const [sessions, setSessions] = useState<SessionSummary[]>([])
|
const [sessions, setSessions] = useState<SessionSummary[]>([])
|
||||||
const [selectedSessionId, setSelectedSessionId] = useState<string | null>(null)
|
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 [memories, setMemories] = useState<MemorySummary[]>([])
|
||||||
const [skills, setSkills] = useState<SkillSummary[]>([])
|
const [skills, setSkills] = useState<SkillSummary[]>([])
|
||||||
const [todos, setTodos] = useState<TodoItemSummary[]>([])
|
const [todos, setTodos] = useState<TodoItemSummary[]>([])
|
||||||
@ -302,11 +305,12 @@ export function useChat(): UseChatReturn {
|
|||||||
// stream_delta: accumulate into existing message by ID, or create new
|
// stream_delta: accumulate into existing message by ID, or create new
|
||||||
if (message.type === 'stream_delta') {
|
if (message.type === 'stream_delta') {
|
||||||
const msg = message as StreamDelta
|
const msg = message as StreamDelta
|
||||||
setSubAgentView((prev) => {
|
setSubAgentStack((prev) => {
|
||||||
if (!prev) return prev
|
if (prev.length === 0) return prev
|
||||||
const existingIdx = prev.messages.findIndex(m => m.id === msg.id && m.type === 'message')
|
const top = prev[prev.length - 1]
|
||||||
|
const existingIdx = top.messages.findIndex(m => m.id === msg.id && m.type === 'message')
|
||||||
if (existingIdx >= 0) {
|
if (existingIdx >= 0) {
|
||||||
const updated = [...prev.messages]
|
const updated = [...top.messages]
|
||||||
const existing = updated[existingIdx]
|
const existing = updated[existingIdx]
|
||||||
updated[existingIdx] = {
|
updated[existingIdx] = {
|
||||||
...existing,
|
...existing,
|
||||||
@ -315,10 +319,15 @@ export function useChat(): UseChatReturn {
|
|||||||
? (existing.reasoningContent || '') + msg.reasoning_delta
|
? (existing.reasoningContent || '') + msg.reasoning_delta
|
||||||
: existing.reasoningContent,
|
: existing.reasoningContent,
|
||||||
}
|
}
|
||||||
return { ...prev, messages: updated }
|
const newStack = [...prev]
|
||||||
|
newStack[newStack.length - 1] = { ...top, messages: updated }
|
||||||
|
return newStack
|
||||||
}
|
}
|
||||||
const chatMsg = serverMessageToChatMessage(message)
|
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
|
return
|
||||||
}
|
}
|
||||||
@ -327,17 +336,22 @@ export function useChat(): UseChatReturn {
|
|||||||
// Other messages: assistant_response replaces streamed message by ID
|
// Other messages: assistant_response replaces streamed message by ID
|
||||||
const chatMsg = serverMessageToChatMessage(message)
|
const chatMsg = serverMessageToChatMessage(message)
|
||||||
if (chatMsg) {
|
if (chatMsg) {
|
||||||
setSubAgentView((prev) => {
|
setSubAgentStack((prev) => {
|
||||||
if (!prev) return prev
|
if (prev.length === 0) return prev
|
||||||
|
const top = prev[prev.length - 1]
|
||||||
if (message.type === 'assistant_response') {
|
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) {
|
if (existingIdx >= 0) {
|
||||||
const updated = [...prev.messages]
|
const updated = [...top.messages]
|
||||||
updated[existingIdx] = chatMsg
|
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 (currentSubAgentView) {
|
||||||
if (message.type === 'task_messages_loaded') {
|
if (message.type === 'task_messages_loaded') {
|
||||||
const msg = message as TaskMessagesLoaded
|
const msg = message as TaskMessagesLoaded
|
||||||
setSubAgentView((prev) =>
|
setSubAgentStack((prev) => {
|
||||||
prev
|
if (prev.length === 0) return prev
|
||||||
? {
|
const top = prev[prev.length - 1]
|
||||||
...prev,
|
const newStack = [...prev]
|
||||||
subagentType: msg.subagent_type,
|
newStack[newStack.length - 1] = {
|
||||||
status: msg.status,
|
...top,
|
||||||
summary: msg.summary,
|
subagentType: msg.subagent_type,
|
||||||
}
|
status: msg.status,
|
||||||
: prev
|
summary: msg.summary,
|
||||||
)
|
}
|
||||||
|
return newStack
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -756,7 +772,7 @@ export function useChat(): UseChatReturn {
|
|||||||
const selectTopic = useCallback((topicId: string) => {
|
const selectTopic = useCallback((topicId: string) => {
|
||||||
setSelectedTopic(topicId)
|
setSelectedTopic(topicId)
|
||||||
setMessages([])
|
setMessages([])
|
||||||
setSubAgentView(null)
|
setSubAgentStack([])
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const createTopic = useCallback((title?: string): Command => {
|
const createTopic = useCallback((title?: string): Command => {
|
||||||
@ -802,7 +818,7 @@ export function useChat(): UseChatReturn {
|
|||||||
setTopics([])
|
setTopics([])
|
||||||
setSelectedTopic(null)
|
setSelectedTopic(null)
|
||||||
setMessages([])
|
setMessages([])
|
||||||
setSubAgentView(null)
|
setSubAgentStack([])
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
}, [selectedChannel])
|
}, [selectedChannel])
|
||||||
|
|
||||||
@ -812,7 +828,7 @@ export function useChat(): UseChatReturn {
|
|||||||
setTopics([])
|
setTopics([])
|
||||||
setSelectedTopic(null)
|
setSelectedTopic(null)
|
||||||
setMessages([])
|
setMessages([])
|
||||||
setSubAgentView(null)
|
setSubAgentStack([])
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
}, [selectedSessionId])
|
}, [selectedSessionId])
|
||||||
|
|
||||||
@ -849,9 +865,12 @@ export function useChat(): UseChatReturn {
|
|||||||
status: 'loading',
|
status: 'loading',
|
||||||
messages: [],
|
messages: [],
|
||||||
}
|
}
|
||||||
// Sync ref immediately so WebSocket response routing works correctly
|
setSubAgentStack((prev) => {
|
||||||
subAgentViewRef.current = newView
|
const newStack = [...prev, newView]
|
||||||
setSubAgentView(newView)
|
// Sync ref immediately so WebSocket response routing works correctly
|
||||||
|
subAgentViewRef.current = newView
|
||||||
|
return newStack
|
||||||
|
})
|
||||||
return {
|
return {
|
||||||
type: 'load_task_messages',
|
type: 'load_task_messages',
|
||||||
task_id: taskId,
|
task_id: taskId,
|
||||||
@ -859,8 +878,29 @@ export function useChat(): UseChatReturn {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const exitSubAgentView = useCallback(() => {
|
const exitSubAgentView = useCallback(() => {
|
||||||
subAgentViewRef.current = null
|
setSubAgentStack((prev) => {
|
||||||
setSubAgentView(null)
|
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,
|
channels,
|
||||||
selectedChannel,
|
selectedChannel,
|
||||||
subAgentView,
|
subAgentView,
|
||||||
|
subAgentStack,
|
||||||
handleMessage,
|
handleMessage,
|
||||||
handleCommand,
|
handleCommand,
|
||||||
clearMessages,
|
clearMessages,
|
||||||
@ -974,6 +1015,7 @@ export function useChat(): UseChatReturn {
|
|||||||
selectSession,
|
selectSession,
|
||||||
enterSubAgentView,
|
enterSubAgentView,
|
||||||
exitSubAgentView,
|
exitSubAgentView,
|
||||||
|
navigateToSubAgentLevel,
|
||||||
memories,
|
memories,
|
||||||
requestMemoryList,
|
requestMemoryList,
|
||||||
createMemory,
|
createMemory,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user