Compare commits
3 Commits
2607ca4aa4
...
bf724b133c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf724b133c | ||
|
|
175e7fc01b | ||
|
|
6962ea2eb1 |
@ -144,12 +144,8 @@ fn reconstruct_task_from_db(
|
|||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
// Extract description from title: "Subagent: {description}"
|
// Extract subagent_type and description from title
|
||||||
let description = record
|
let (subagent_type, description) = parse_subagent_title(&record.title);
|
||||||
.title
|
|
||||||
.strip_prefix("Subagent: ")
|
|
||||||
.unwrap_or(&record.title)
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let now = record.updated_at;
|
let now = record.updated_at;
|
||||||
|
|
||||||
@ -161,7 +157,7 @@ fn reconstruct_task_from_db(
|
|||||||
parent_chat_id: record.chat_id.clone(),
|
parent_chat_id: record.chat_id.clone(),
|
||||||
parent_channel_name: record.channel_name.clone(),
|
parent_channel_name: record.channel_name.clone(),
|
||||||
description,
|
description,
|
||||||
subagent_type: "general".to_string(),
|
subagent_type,
|
||||||
state: TaskSessionState::Completed,
|
state: TaskSessionState::Completed,
|
||||||
created_at: record.created_at,
|
created_at: record.created_at,
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
@ -169,3 +165,18 @@ fn reconstruct_task_from_db(
|
|||||||
error: None,
|
error: None,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parse subagent title to extract type and description.
|
||||||
|
/// New format: "Subagent [type]: description"
|
||||||
|
/// Legacy format: "Subagent: description" (defaults to "general")
|
||||||
|
fn parse_subagent_title(title: &str) -> (String, String) {
|
||||||
|
if let Some(rest) = title.strip_prefix("Subagent [") {
|
||||||
|
if let Some(bracket_pos) = rest.find("]: ") {
|
||||||
|
let agent_type = rest[..bracket_pos].to_string();
|
||||||
|
let desc = rest[bracket_pos + 3..].to_string();
|
||||||
|
return (agent_type, desc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let desc = title.strip_prefix("Subagent: ").unwrap_or(title).to_string();
|
||||||
|
("general".to_string(), desc)
|
||||||
|
}
|
||||||
|
|||||||
@ -509,7 +509,7 @@ impl SubAgentRuntime for DefaultSubAgentRuntime {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 4. 在 sessions 表中创建子智能体会话(确保外键约束满足)
|
// 4. 在 sessions 表中创建子智能体会话(确保外键约束满足)
|
||||||
let session_title = format!("Subagent: {}", task.description);
|
let session_title = format!("Subagent [{}]: {}", session.subagent_type, task.description);
|
||||||
if let Err(e) = self.conversation_repository.ensure_session(
|
if let Err(e) = self.conversation_repository.ensure_session(
|
||||||
&session.session_id,
|
&session.session_id,
|
||||||
&session.parent_channel_name,
|
&session.parent_channel_name,
|
||||||
@ -638,7 +638,7 @@ impl SubAgentRuntime for DefaultSubAgentRuntime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. 确保 sessions 表中存在子智能体会话记录
|
// 3. 确保 sessions 表中存在子智能体会话记录
|
||||||
let session_title = format!("Subagent: {}", session.description);
|
let session_title = format!("Subagent [{}]: {}", session.subagent_type, session.description);
|
||||||
if let Err(e) = self.conversation_repository.ensure_session(
|
if let Err(e) = self.conversation_repository.ensure_session(
|
||||||
&session.session_id,
|
&session.session_id,
|
||||||
&session.parent_channel_name,
|
&session.parent_channel_name,
|
||||||
|
|||||||
101
web/src/App.tsx
101
web/src/App.tsx
@ -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>
|
||||||
|
)}
|
||||||
|
<span className={`text-xs font-medium shrink-0 ${statusColor}`}>{statusText}</span>
|
||||||
</div>
|
</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">
|
||||||
@ -663,6 +686,13 @@ function App() {
|
|||||||
onNavigateToSubAgent={handleNavigateToSubAgent}
|
onNavigateToSubAgent={handleNavigateToSubAgent}
|
||||||
onStop={handleStopExecution}
|
onStop={handleStopExecution}
|
||||||
showThinking={showThinking}
|
showThinking={showThinking}
|
||||||
|
todoPanel={
|
||||||
|
<TodoPanel
|
||||||
|
todos={todos}
|
||||||
|
requestTodoList={refreshTodoList}
|
||||||
|
sendCommand={sendMemoryCommand}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -731,13 +761,6 @@ function App() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 悬浮 Todo 面板 */}
|
|
||||||
<TodoPanel
|
|
||||||
todos={todos}
|
|
||||||
requestTodoList={refreshTodoList}
|
|
||||||
sendCommand={sendMemoryCommand}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 系统配置页面 */}
|
{/* 系统配置页面 */}
|
||||||
{configPageOpen && (
|
{configPageOpen && (
|
||||||
<ConfigPage onClose={() => setConfigPageOpen(false)} onSaveConnection={handleSaveConnection} />
|
<ConfigPage onClose={() => setConfigPageOpen(false)} onSaveConnection={handleSaveConnection} />
|
||||||
|
|||||||
@ -11,6 +11,8 @@ interface ChatContainerProps {
|
|||||||
onNavigateToSubAgent?: (taskId: string, description: string) => void
|
onNavigateToSubAgent?: (taskId: string, description: string) => void
|
||||||
onStop?: () => void
|
onStop?: () => void
|
||||||
showThinking?: boolean
|
showThinking?: boolean
|
||||||
|
/** 浮动待办面板,绝对定位在消息区域上方 */
|
||||||
|
todoPanel?: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChatContainer({
|
export function ChatContainer({
|
||||||
@ -22,11 +24,13 @@ export function ChatContainer({
|
|||||||
onNavigateToSubAgent,
|
onNavigateToSubAgent,
|
||||||
onStop,
|
onStop,
|
||||||
showThinking = true,
|
showThinking = true,
|
||||||
|
todoPanel,
|
||||||
}: ChatContainerProps) {
|
}: ChatContainerProps) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col">
|
<div className="flex h-full flex-col relative">
|
||||||
<div className="flex-1 overflow-hidden">
|
<div className="flex-1 overflow-hidden relative">
|
||||||
<MessageList messages={messages} onNavigateToSubAgent={onNavigateToSubAgent} showThinking={showThinking} />
|
<MessageList messages={messages} onNavigateToSubAgent={onNavigateToSubAgent} showThinking={showThinking} />
|
||||||
|
{todoPanel}
|
||||||
</div>
|
</div>
|
||||||
<MessageInput
|
<MessageInput
|
||||||
onSend={onSendMessage}
|
onSend={onSendMessage}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useState, useCallback, useEffect, useRef } from 'react'
|
import { useState, useCallback, useEffect, useRef } from 'react'
|
||||||
import { ClipboardList, ChevronUp, ChevronDown, Circle } from 'lucide-react'
|
import { ClipboardList, ChevronDown, RefreshCw } from 'lucide-react'
|
||||||
import type { TodoItemSummary, Command } from '../../types/protocol'
|
import type { TodoItemSummary, Command } from '../../types/protocol'
|
||||||
|
|
||||||
interface TodoPanelProps {
|
interface TodoPanelProps {
|
||||||
@ -8,33 +8,22 @@ interface TodoPanelProps {
|
|||||||
sendCommand: (cmd: Command) => void
|
sendCommand: (cmd: Command) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── status helpers ───────────────────────────────────── */
|
/* ── status config ────────────────────────────────────── */
|
||||||
|
|
||||||
interface StatusStyle { label: string; border: string; dot: string; text: string; icon: string }
|
interface StatusCfg { label: string; color: string; dot: string }
|
||||||
|
|
||||||
const STATUS: Record<string, StatusStyle> = {
|
const STATUS: Record<string, StatusCfg> = {
|
||||||
in_progress: { label: '进行中', border: 'border-amber-400/60', dot: 'bg-amber-400 shadow-[0_0_6px_#fbbf24]', text: 'text-amber-300', icon: '●' },
|
in_progress: { label: '进行中', color: 'text-amber-400', dot: 'bg-amber-400' },
|
||||||
pending: { label: '待处理', border: 'border-slate-500/50', dot: 'bg-slate-500', text: 'text-slate-400', icon: '○' },
|
pending: { label: '待处理', color: 'text-slate-400', dot: 'bg-slate-500' },
|
||||||
completed: { label: '已完成', border: 'border-emerald-400/40', dot: 'bg-emerald-400', text: 'text-emerald-400', icon: '✓' },
|
completed: { label: '已完成', color: 'text-emerald-400', dot: 'bg-emerald-400' },
|
||||||
cancelled: { label: '已取消', border: 'border-red-400/40', dot: 'bg-red-400', text: 'text-red-400', icon: '✕' },
|
cancelled: { label: '已取消', color: 'text-slate-500', dot: 'bg-slate-600' },
|
||||||
}
|
}
|
||||||
|
|
||||||
function statusStyle(s: string): StatusStyle {
|
function statusCfg(s: string): StatusCfg {
|
||||||
return STATUS[s] ?? { label: s, border: 'border-[var(--border-color)]', dot: 'bg-[var(--text-muted)]', text: 'text-[var(--text-muted)]', icon: '?' }
|
return STATUS[s] ?? { label: s, color: 'text-slate-400', dot: 'bg-slate-500' }
|
||||||
}
|
}
|
||||||
|
|
||||||
const PRIORITY: Record<string, string> = {
|
|
||||||
high: 'text-rose-400',
|
|
||||||
medium: 'text-amber-400',
|
|
||||||
low: 'text-slate-400',
|
|
||||||
}
|
|
||||||
|
|
||||||
function priorityDot(p: string) { return PRIORITY[p] ?? 'text-slate-400' }
|
|
||||||
|
|
||||||
/* ── group helpers ────────────────────────────────────── */
|
|
||||||
|
|
||||||
const GROUP_ORDER = ['in_progress', 'pending', 'completed', 'cancelled']
|
const GROUP_ORDER = ['in_progress', 'pending', 'completed', 'cancelled']
|
||||||
const COLLAPSED_DEFAULT = new Set(['completed', 'cancelled'])
|
|
||||||
|
|
||||||
function groupTodos(todos: TodoItemSummary[]): Map<string, TodoItemSummary[]> {
|
function groupTodos(todos: TodoItemSummary[]): Map<string, TodoItemSummary[]> {
|
||||||
const map = new Map<string, TodoItemSummary[]>()
|
const map = new Map<string, TodoItemSummary[]>()
|
||||||
@ -50,34 +39,49 @@ function groupTodos(todos: TodoItemSummary[]): Map<string, TodoItemSummary[]> {
|
|||||||
|
|
||||||
function PulseDot() {
|
function PulseDot() {
|
||||||
return (
|
return (
|
||||||
<span className="relative flex h-2 w-2">
|
<span className="relative flex h-1.5 w-1.5 shrink-0">
|
||||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-amber-400 opacity-75" />
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-amber-400 opacity-75" />
|
||||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-amber-400 shadow-[0_0_6px_#fbbf24]" />
|
<span className="relative inline-flex rounded-full h-1.5 w-1.5 bg-amber-400" />
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── position persistence ─────────────────────────────── */
|
||||||
|
|
||||||
|
const POS_KEY = 'picobot-todo-pos'
|
||||||
|
|
||||||
|
function loadPos(): { x: number; y: number } {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(POS_KEY)
|
||||||
|
if (raw) return JSON.parse(raw)
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
return { x: 0, y: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
function savePos(pos: { x: number; y: number }) {
|
||||||
|
try { localStorage.setItem(POS_KEY, JSON.stringify(pos)) } catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
/* ── TodoPanel ────────────────────────────────────────── */
|
/* ── TodoPanel ────────────────────────────────────────── */
|
||||||
|
|
||||||
export function TodoPanel({ todos, requestTodoList, sendCommand }: TodoPanelProps) {
|
export function TodoPanel({ todos, requestTodoList, sendCommand }: TodoPanelProps) {
|
||||||
const [expanded, setExpanded] = useState(() => {
|
const [expanded, setExpanded] = useState(() => {
|
||||||
try { return localStorage.getItem('picobot-todo-panel-open') === 'true' } catch { return false }
|
try { return localStorage.getItem('picobot-todo-expanded') === 'true' } catch { return false }
|
||||||
})
|
})
|
||||||
const [collapsedGroups, setCollapsedGroups] = useState<Set<string>>(() => new Set(COLLAPSED_DEFAULT))
|
const [collapsedGroups, setCollapsedGroups] = useState<Set<string>>(() => new Set(['completed', 'cancelled']))
|
||||||
|
const [pos, setPos] = useState(loadPos)
|
||||||
const prevTodoIdsRef = useRef<Set<string>>(new Set())
|
const prevTodoIdsRef = useRef<Set<string>>(new Set())
|
||||||
|
const dragRef = useRef<{ startX: number; startY: number; startPos: { x: number; y: number }; moved: boolean } | null>(null)
|
||||||
|
|
||||||
// 持久化展开状态
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem('picobot-todo-panel-open', String(expanded))
|
localStorage.setItem('picobot-todo-expanded', String(expanded))
|
||||||
}, [expanded])
|
}, [expanded])
|
||||||
|
|
||||||
// 当有新 todo 出现时自动展开
|
// auto-expand on new items
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const newIds = new Set(todos.map(t => t.id))
|
const newIds = new Set(todos.map(t => t.id))
|
||||||
const hasNewItems = todos.some(t => !prevTodoIdsRef.current.has(t.id))
|
const hasNewItems = todos.some(t => !prevTodoIdsRef.current.has(t.id))
|
||||||
if (hasNewItems && todos.length > 0) {
|
if (hasNewItems && todos.length > 0) setExpanded(true)
|
||||||
setExpanded(true)
|
|
||||||
}
|
|
||||||
prevTodoIdsRef.current = newIds
|
prevTodoIdsRef.current = newIds
|
||||||
}, [todos])
|
}, [todos])
|
||||||
|
|
||||||
@ -93,105 +97,149 @@ export function TodoPanel({ todos, requestTodoList, sendCommand }: TodoPanelProp
|
|||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// ── 空状态 ──
|
const handleRefresh = useCallback(() => sendCommand(requestTodoList()), [sendCommand, requestTodoList])
|
||||||
if (totalCount === 0) {
|
|
||||||
return (
|
/* ── drag handling ──────────────────────────────────── */
|
||||||
<div className="fixed bottom-4 right-4 z-50">
|
|
||||||
<button
|
const handleDragStart = useCallback((e: React.MouseEvent) => {
|
||||||
onClick={() => sendCommand(requestTodoList())}
|
if ((e.target as HTMLElement).closest('button')) return
|
||||||
className="flex items-center gap-2 px-3 py-2 rounded-xl bg-[var(--bg-tertiary)]/70 backdrop-blur-sm border border-[var(--border-color)] text-[var(--text-muted)] hover:text-[var(--text-secondary)] hover:border-[var(--border-accent)] transition-all duration-300 shadow-lg text-xs"
|
e.preventDefault()
|
||||||
title="刷新待办"
|
|
||||||
>
|
const startX = e.clientX
|
||||||
<ClipboardList className="h-3.5 w-3.5" />
|
const startY = e.clientY
|
||||||
<span>待办</span>
|
const startPos = { ...pos }
|
||||||
</button>
|
dragRef.current = { startX, startY, startPos, moved: false }
|
||||||
</div>
|
|
||||||
)
|
const handleMove = (ev: MouseEvent) => {
|
||||||
|
if (!dragRef.current) return
|
||||||
|
const dx = ev.clientX - dragRef.current.startX
|
||||||
|
const dy = ev.clientY - dragRef.current.startY
|
||||||
|
if (Math.abs(dx) > 3 || Math.abs(dy) > 3) dragRef.current.moved = true
|
||||||
|
setPos({
|
||||||
|
x: Math.max(0, dragRef.current.startPos.x - dx),
|
||||||
|
y: Math.max(0, dragRef.current.startPos.y + dy),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 折叠态 ──
|
const handleUp = () => {
|
||||||
|
document.removeEventListener('mousemove', handleMove)
|
||||||
|
document.removeEventListener('mouseup', handleUp)
|
||||||
|
document.body.style.userSelect = ''
|
||||||
|
document.body.style.cursor = ''
|
||||||
|
if (dragRef.current) {
|
||||||
|
setPos(prev => { savePos(prev); return prev })
|
||||||
|
dragRef.current = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', handleMove)
|
||||||
|
document.addEventListener('mouseup', handleUp)
|
||||||
|
document.body.style.userSelect = 'none'
|
||||||
|
document.body.style.cursor = 'grabbing'
|
||||||
|
}, [pos])
|
||||||
|
|
||||||
|
/* ── minimized: circle button ───────────────────────── */
|
||||||
|
|
||||||
if (!expanded) {
|
if (!expanded) {
|
||||||
return (
|
return (
|
||||||
<div className="fixed bottom-4 right-4 z-50">
|
<div
|
||||||
<button
|
className="absolute z-30"
|
||||||
onClick={() => setExpanded(true)}
|
style={{ top: `${16 + pos.y}px`, right: `${16 + pos.x}px` }}
|
||||||
className="flex items-center gap-2 px-3 py-2 rounded-xl bg-[var(--bg-tertiary)]/80 backdrop-blur-sm border border-[var(--border-color)] hover:border-[var(--accent-cyan)]/30 transition-all duration-300 shadow-lg group"
|
|
||||||
>
|
>
|
||||||
{inProgressCount > 0 ? <PulseDot /> : <ClipboardList className="h-3.5 w-3.5 text-[var(--text-muted)]" />}
|
<div
|
||||||
<span className="text-sm font-medium text-[var(--text-secondary)] group-hover:text-[var(--accent-cyan)] transition-colors">
|
className="relative cursor-grab active:cursor-grabbing select-none"
|
||||||
待办 ({totalCount})
|
onMouseDown={handleDragStart}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={() => { if (totalCount > 0) setExpanded(true); else handleRefresh() }}
|
||||||
|
className="relative w-12 h-12 rounded-full bg-[var(--bg-tertiary)]/90 backdrop-blur-md border border-[var(--border-color)] hover:border-[var(--accent-cyan)]/40 shadow-lg hover:shadow-[0_0_20px_var(--shadow-glow-sm)] transition-all duration-300 group"
|
||||||
|
title="待办"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col items-center justify-center">
|
||||||
|
<ClipboardList className="h-3.5 w-3.5 text-[var(--text-muted)] group-hover:text-[var(--accent-cyan)] transition-colors" />
|
||||||
|
<span className="text-[8px] font-bold text-[var(--text-muted)] group-hover:text-[var(--accent-cyan)] transition-colors mt-0.5">
|
||||||
|
ToDo
|
||||||
</span>
|
</span>
|
||||||
<ChevronUp className="h-3.5 w-3.5 text-[var(--text-muted)]" />
|
</div>
|
||||||
|
{totalCount > 0 && (
|
||||||
|
<span className={`absolute -top-1 -right-1 min-w-[18px] h-[18px] rounded-full flex items-center justify-center text-[10px] font-bold shadow-md ${
|
||||||
|
inProgressCount > 0
|
||||||
|
? 'bg-amber-400 text-black'
|
||||||
|
: 'bg-[var(--bg-secondary)] text-[var(--text-secondary)] border border-[var(--border-color)]'
|
||||||
|
}`}>
|
||||||
|
{totalCount}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{inProgressCount > 0 && (
|
||||||
|
<span className="absolute inset-0 rounded-full border-2 border-amber-400/30 animate-ping" />
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 展开态 ──
|
/* ── expanded: full card ────────────────────────────── */
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed bottom-4 right-4 z-50 w-72 max-h-[60vh] flex flex-col rounded-xl border border-[var(--border-color)] bg-[var(--bg-tertiary)]/95 backdrop-blur-md shadow-2xl overflow-hidden transition-all duration-300">
|
<div
|
||||||
{/* 标题栏 */}
|
className="absolute z-30"
|
||||||
<div className="shrink-0 flex items-center justify-between px-3 py-2.5 border-b border-[var(--border-color)]">
|
style={{ top: `${16 + pos.y}px`, right: `${16 + pos.x}px` }}
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<ClipboardList className="h-4 w-4 text-[var(--accent-cyan)]" />
|
|
||||||
<span className="text-sm font-semibold text-[var(--text-primary)]">待办事项</span>
|
|
||||||
<span className="text-[11px] text-[var(--text-muted)] tabular-nums">共 {totalCount} 项</span>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => setExpanded(false)}
|
|
||||||
className="p-1 rounded hover:bg-[var(--overlay-subtle)] text-[var(--text-muted)] hover:text-[var(--text-secondary)] transition-colors"
|
|
||||||
>
|
>
|
||||||
<ChevronDown className="h-4 w-4" />
|
<div className="w-72 max-h-[50vh] flex flex-col rounded-xl bg-[var(--bg-tertiary)]/95 backdrop-blur-md border border-[var(--border-color)] shadow-2xl overflow-hidden">
|
||||||
|
{/* title bar (drag handle) */}
|
||||||
|
<div
|
||||||
|
className="shrink-0 flex items-center gap-2 px-3 py-2 border-b border-[var(--border-color)]/50 cursor-grab active:cursor-grabbing select-none"
|
||||||
|
onMouseDown={handleDragStart}
|
||||||
|
>
|
||||||
|
<span className="text-[var(--text-muted)]/30 text-[10px] leading-none select-none">⠿</span>
|
||||||
|
<ClipboardList className="h-3 w-3 text-[var(--accent-cyan)]" />
|
||||||
|
<span className="text-[11px] font-semibold text-[var(--text-secondary)]">待办</span>
|
||||||
|
<span className="text-[10px] text-[var(--text-muted)]/70 tabular-nums">{totalCount}</span>
|
||||||
|
{inProgressCount > 0 && <PulseDot />}
|
||||||
|
<div className="ml-auto flex items-center gap-0.5">
|
||||||
|
<button onClick={handleRefresh} className="p-1 rounded text-[var(--text-muted)]/50 hover:text-[var(--accent-cyan)] transition-colors" title="刷新">
|
||||||
|
<RefreshCw className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
<button onClick={() => setExpanded(false)} className="p-1 rounded text-[var(--text-muted)]/50 hover:text-[var(--text-secondary)] transition-colors" title="缩小">
|
||||||
|
<ChevronDown className="h-3 w-3" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 列表区 */}
|
{/* list */}
|
||||||
<div className="flex-1 overflow-y-auto scrollbar-hide px-2 py-2 space-y-2">
|
<div className="flex-1 overflow-y-auto scrollbar-hide px-3 py-2">
|
||||||
{GROUP_ORDER.map(status => {
|
{GROUP_ORDER.map(status => {
|
||||||
const items = grouped.get(status)
|
const items = grouped.get(status)
|
||||||
if (!items || items.length === 0) return null
|
if (!items || items.length === 0) return null
|
||||||
|
|
||||||
const style = statusStyle(status)
|
const cfg = statusCfg(status)
|
||||||
const isCollapsed = collapsedGroups.has(status)
|
const isCollapsed = collapsedGroups.has(status)
|
||||||
const isTerminal = status === 'completed' || status === 'cancelled'
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={status}>
|
<div key={status} className="mt-1 first:mt-0">
|
||||||
{/* 分组标题 */}
|
|
||||||
<button
|
<button
|
||||||
onClick={() => toggleGroup(status)}
|
onClick={() => toggleGroup(status)}
|
||||||
className="flex items-center gap-1.5 w-full px-1 py-1 text-xs text-[var(--text-muted)] hover:text-[var(--text-secondary)] transition-colors"
|
className="flex items-center gap-1.5 w-full py-0.5 group"
|
||||||
>
|
>
|
||||||
{isCollapsed ? (
|
<span className={`h-1.5 w-1.5 rounded-full ${cfg.dot} shrink-0`} />
|
||||||
<ChevronUp className="h-3 w-3" />
|
<span className={`text-[10px] font-medium ${cfg.color}`}>{cfg.label}</span>
|
||||||
) : (
|
<span className={`text-[10px] ${cfg.color} opacity-60 tabular-nums`}>{items.length}</span>
|
||||||
<ChevronDown className="h-3 w-3" />
|
<span className="ml-auto text-[var(--text-muted)]/40">
|
||||||
)}
|
{isCollapsed
|
||||||
<span className={style.text}>{style.icon}</span>
|
? <ChevronDown className="h-2.5 w-2.5 rotate-[-90deg]" />
|
||||||
<span>{style.label}</span>
|
: <ChevronDown className="h-2.5 w-2.5" />
|
||||||
<span className="tabular-nums">({items.length})</span>
|
}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* 卡片列表 */}
|
|
||||||
{!isCollapsed && (
|
{!isCollapsed && (
|
||||||
<div className="space-y-1">
|
<div className="ml-3 border-l border-[var(--border-color)]/30 pl-2 mt-0.5 space-y-px">
|
||||||
{items.map(item => (
|
{items.map(item => (
|
||||||
<div
|
<div key={item.id} className="py-[3px]">
|
||||||
key={item.id}
|
<span className="text-[12px] leading-snug text-[var(--text-primary)]/90 break-words">
|
||||||
className={`rounded-lg border-l-2 ${style.border} border border-[var(--border-color)] bg-[var(--overlay-hover)]/50 px-2.5 py-1.5 transition-colors hover:border-[var(--border-accent)]`}
|
|
||||||
>
|
|
||||||
<div className="flex items-start gap-2">
|
|
||||||
<span className={`mt-0.5 shrink-0 ${priorityDot(item.priority)}`}>
|
|
||||||
<Circle className="h-2 w-2 fill-current" />
|
|
||||||
</span>
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<p className={`text-[13px] leading-snug text-[var(--text-primary)] break-words ${
|
|
||||||
isTerminal ? 'line-through opacity-50' : ''
|
|
||||||
}`}>
|
|
||||||
{item.content}
|
{item.content}
|
||||||
</p>
|
</span>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -200,15 +248,6 @@ export function TodoPanel({ todos, requestTodoList, sendCommand }: TodoPanelProp
|
|||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 底部刷新 */}
|
|
||||||
<div className="shrink-0 border-t border-[var(--border-color)] px-3 py-1.5">
|
|
||||||
<button
|
|
||||||
onClick={() => sendCommand(requestTodoList())}
|
|
||||||
className="text-[10px] text-[var(--text-muted)] hover:text-[var(--accent-cyan)] transition-colors"
|
|
||||||
>
|
|
||||||
刷新
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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]
|
||||||
|
newStack[newStack.length - 1] = {
|
||||||
|
...top,
|
||||||
subagentType: msg.subagent_type,
|
subagentType: msg.subagent_type,
|
||||||
status: msg.status,
|
status: msg.status,
|
||||||
summary: msg.summary,
|
summary: msg.summary,
|
||||||
}
|
}
|
||||||
: prev
|
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: [],
|
||||||
}
|
}
|
||||||
|
setSubAgentStack((prev) => {
|
||||||
|
const newStack = [...prev, newView]
|
||||||
// Sync ref immediately so WebSocket response routing works correctly
|
// Sync ref immediately so WebSocket response routing works correctly
|
||||||
subAgentViewRef.current = newView
|
subAgentViewRef.current = newView
|
||||||
setSubAgentView(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(() => {
|
||||||
|
setSubAgentStack((prev) => {
|
||||||
|
if (prev.length <= 1) {
|
||||||
subAgentViewRef.current = null
|
subAgentViewRef.current = null
|
||||||
setSubAgentView(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