PicoBot/web/src/hooks/useChat.ts
ooodc fc7df67474 feat(streaming): 支持流式文本增量与结束信号功能
- 新增 StreamDelta 和 StreamEnd 类型,支持流式数据增量传输
- 扩展 LLMProvider trait,添加带回调的 chat_with_streaming 接口
- 修改 OpenAI Provider 实现,支持流式聊天回调传输增量数据
- Agent 流处理改为异步消费增量消息并传递给前端
- 保证流式增量和最终消息使用相同消息 ID 以便前端替换
- 修改消息总线和协议层,支持携带和识别流式消息的消息 ID
- 客户端 CLI 通过增量输出实现交互式流式响应显示
- Web 前端接收流式增量消息,追加到对应消息,实现实时显示
- 各通道(飞书、微信)支持转发流式增量和结束消息
- 任务工具运行时添加消息 ID 支持,保持消息一致性
- 统一消息构造函数新增流式增量和结束信号的构建方法
2026-06-14 10:24:52 +08:00

923 lines
27 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useCallback, useEffect, useRef, useMemo } from 'react'
import type {
Command,
ChatMessage,
Topic,
WsOutbound,
AssistantResponse,
ToolCall,
ToolResult,
ToolPending,
SessionEstablished,
SessionList,
SessionSummary,
TopicList,
TopicSummary,
TaskMessagesLoaded,
TaskStarted,
Attachment,
MemorySummary,
MemoryList,
SkillSummary,
SkillList,
TodoItemSummary,
TodoList,
SchedulerJobList,
SchedulerJobSummary,
SchedulerJobSessionLookup,
Channel,
ChannelList,
StreamDelta,
} from '../types/protocol'
// 简化后的层级状态
interface UseChatReturn {
// 连接状态
connectionId: string | null
isConnected: boolean
// 简化的层级状态
sessions: SessionSummary[]
selectedSessionId: string | null
session: SessionSummary | null
sessionId: string | null
chatId: string
topics: Topic[]
selectedTopic: string | null
// 消息
messages: ChatMessage[]
isLoading: boolean
// 通道状态
channels: Channel[]
selectedChannel: string
isWritable: boolean
// 是否只读
isReadOnly: boolean
// 子智能体视图
subAgentView: SubAgentView | null
// 方法
handleMessage: (content: string, attachments?: Attachment[]) => void
handleCommand: (command: Command) => void
clearMessages: () => void
handleServerMessage: (message: WsOutbound) => void
// Topic 方法
selectTopic: (topicId: string) => void
createTopic: (title?: string) => Command
switchTopic: (topicId: string) => Command
deleteTopic: (topicId: string) => Command
// 初始化方法
requestSessionList: () => Command
requestTopicList: () => Command | null
topicRefreshTrigger: number
requestChannelList: () => Command
selectChannel: (channelId: string) => void
selectSession: (sessionId: string) => void
// 子智能体导航方法
enterSubAgentView: (taskId: string, description: string) => Command
exitSubAgentView: () => void
// 记忆状态
memories: MemorySummary[]
requestMemoryList: () => Command
createMemory: (namespace: string, key: string, content: string) => Command
updateMemory: (id: string, content: string) => Command
deleteMemory: (id: string) => Command
// 技能状态
skills: SkillSummary[]
requestSkillList: () => Command
// Todo 状态
todos: TodoItemSummary[]
requestTodoList: () => Command
requestSubAgentTodoList: (subTaskId: string) => Command
// 定时任务状态
schedulerJobs: SchedulerJobSummary[]
sidebarTab: 'topics' | 'scheduler'
setSidebarTab: (tab: 'topics' | 'scheduler') => void
requestSchedulerJobList: () => Command
// 定时任务执行对话查看
schedulerView: SchedulerJobView | null
enterSchedulerJobView: (lookup: SchedulerJobSessionLookup, jobId: string, description: string) => Command
exitSchedulerJobView: () => void
// 停止当前 Agent 执行
handleStop: () => Command
}
interface SubAgentView {
taskId: string
description: string
subagentType: string
status: string
summary?: string
messages: ChatMessage[]
}
interface SchedulerJobView {
jobId: string
description: string
channel: string
chatId: string
messages: ChatMessage[]
}
const DEFAULT_CHAT_ID = 'default'
export function useChat(): UseChatReturn {
const [messages, setMessages] = useState<ChatMessage[]>([])
const [isLoading, setIsLoading] = useState(false)
// 简化的状态管理
const [connectionId, setConnectionId] = useState<string | null>(null)
const [topics, setTopics] = useState<Topic[]>([])
const [selectedTopic, setSelectedTopic] = useState<string | null>(null)
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 [memories, setMemories] = useState<MemorySummary[]>([])
const [skills, setSkills] = useState<SkillSummary[]>([])
const [todos, setTodos] = useState<TodoItemSummary[]>([])
const [schedulerJobs, setSchedulerJobs] = useState<SchedulerJobSummary[]>([])
const [sidebarTab, setSidebarTab] = useState<'topics' | 'scheduler'>('topics')
const [schedulerView, setSchedulerView] = useState<SchedulerJobView | null>(null)
const [channels, setChannels] = useState<Channel[]>([])
const [selectedChannel, setSelectedChannel] = useState<string>('websocket')
// Message ID generator
const messageIdCounter = useRef(0)
const generateMessageId = () => {
messageIdCounter.current += 1
return `msg_${Date.now()}_${messageIdCounter.current}`
}
// Ref to track subAgentView and schedulerView for use in callbacks
const subAgentViewRef = useRef<SubAgentView | null>(null)
const schedulerViewRef = useRef<SchedulerJobView | null>(null)
const topicsRef = useRef<Topic[]>([])
const selectedTopicRef = useRef<string | null>(null)
const pendingNewTopicRef = useRef(false)
const isConnected = useMemo(() => connectionId !== null, [connectionId])
const selectedSession = useMemo(
() => sessions.find(s => s.session_id === selectedSessionId) ?? null,
[sessions, selectedSessionId]
)
const sessionId = useMemo(() => selectedSession?.session_id ?? null, [selectedSession])
const chatId = useMemo(() => sessionId ?? DEFAULT_CHAT_ID, [sessionId])
const isWritable = useMemo(
() => channels.find(c => c.id === selectedChannel)?.isWritable ?? false,
[channels, selectedChannel]
)
// Extract subagent_task_id from a message if present
const getSubagentTaskId = (message: WsOutbound): string | undefined => {
if (message.type === 'tool_call' || message.type === 'tool_result'
|| message.type === 'tool_pending' || message.type === 'assistant_response') {
return (message as ToolCall | ToolResult | ToolPending | AssistantResponse).subagent_task_id
}
return undefined
}
// Extract topic_id from a message if present
const getTopicId = (message: WsOutbound): string | undefined => {
if (message.type === 'tool_call' || message.type === 'tool_result'
|| message.type === 'tool_pending' || message.type === 'assistant_response') {
return (message as ToolCall | ToolResult | ToolPending | AssistantResponse).topic_id
}
return undefined
}
// Convert a server message to ChatMessage (extracted from handleServerMessage logic)
const serverMessageToChatMessage = (message: WsOutbound): ChatMessage | null => {
switch (message.type) {
case 'assistant_response': {
const msg = message as AssistantResponse
const role = msg.role === 'user' || msg.role === 'tool' ? msg.role : 'assistant'
return {
id: msg.id,
role: role as ChatMessage['role'],
content: msg.content,
timestamp: (message as any).timestamp ?? Math.floor(Date.now() / 1000),
type: 'message',
attachments: msg.attachments,
subagentTaskId: msg.subagent_task_id,
reasoningContent: msg.reasoning_content,
}
}
case 'tool_call': {
const msg = message as ToolCall
return {
id: msg.id,
role: 'tool',
content: msg.content,
timestamp: (message as any).timestamp ?? Math.floor(Date.now() / 1000),
type: 'tool_call',
toolName: msg.tool_name,
toolCallId: msg.tool_call_id,
arguments: msg.arguments,
subagentTaskId: msg.subagent_task_id,
reasoningContent: msg.reasoning_content,
}
}
case 'tool_result': {
const msg = message as ToolResult
return {
id: msg.id,
role: 'tool',
content: msg.content,
timestamp: (message as any).timestamp ?? Math.floor(Date.now() / 1000),
type: 'tool_result',
toolName: msg.tool_name,
toolCallId: msg.tool_call_id,
subagentTaskId: msg.subagent_task_id,
durationMs: msg.duration_ms,
}
}
case 'tool_pending': {
const msg = message as ToolPending
return {
id: msg.id,
role: 'tool',
content: `${msg.content}\n\n${msg.resume_hint}`,
timestamp: (message as any).timestamp ?? Math.floor(Date.now() / 1000),
type: 'tool_pending',
toolName: msg.tool_name,
toolCallId: msg.tool_call_id,
subagentTaskId: msg.subagent_task_id,
}
}
case 'error': {
return {
id: generateMessageId(),
role: 'assistant',
content: `Error: ${message.message}`,
timestamp: (message as any).timestamp ?? Math.floor(Date.now() / 1000),
type: 'message',
}
}
default:
return null
}
}
// Append a server message to the sub-agent view
const appendToSubAgentViewMessage = (message: WsOutbound) => {
const chatMsg = serverMessageToChatMessage(message)
if (chatMsg) {
setSubAgentView((prev) =>
prev
? { ...prev, messages: [...prev.messages, chatMsg] }
: prev
)
}
}
const handleServerMessage = useCallback((message: WsOutbound) => {
console.log('Received message:', message)
// Route to scheduler job view if active
const currentSchedulerView = schedulerViewRef.current
if (currentSchedulerView) {
// Route chat messages to the scheduler view
const chatMsg = serverMessageToChatMessage(message)
if (chatMsg) {
setSchedulerView((prev) =>
prev
? { ...prev, messages: [...prev.messages, chatMsg] }
: prev
)
return
}
// Non-chat messages (session_list, topic_list, etc.) fall through to main handler
}
// Route to sub-agent view if active
const currentSubAgentView = subAgentViewRef.current
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
)
return
}
// Only accept messages explicitly tagged with matching subagent_task_id.
// History messages are now tagged by the backend (send_task_messages),
// and live sub-agent messages are tagged by SubAgentEmitter.
const msgSubagentTaskId = getSubagentTaskId(message)
if (msgSubagentTaskId && msgSubagentTaskId === currentSubAgentView.taskId) {
appendToSubAgentViewMessage(message)
return
}
// 丢弃其他子智能体的消息,避免 fall through 到主消息处理
if (msgSubagentTaskId) {
return
}
}
// In main view, skip sub-agent messages (they belong to sub-agent view).
// But use the task_id to associate with the running task tool card.
const msgSubagentTaskId = getSubagentTaskId(message)
if (msgSubagentTaskId) {
// 只 backfill 当前话题的 task tool_call避免跨话题串扰
const msgTopicId = getTopicId(message)
if (msgTopicId && msgTopicId !== selectedTopicRef.current) return
setMessages((prev) => {
for (let i = prev.length - 1; i >= 0; i--) {
if (prev[i].type === 'tool_call' && prev[i].toolName === 'task' && !prev[i].subagentTaskId) {
const updated = [...prev]
updated[i] = { ...updated[i], subagentTaskId: msgSubagentTaskId }
return updated
}
}
return prev
})
return
}
switch (message.type) {
case 'session_established': {
const msg = message as SessionEstablished
setConnectionId(msg.session_id)
console.log('Connection established:', msg.session_id)
break
}
case 'task_started': {
const msg = message as TaskStarted
// 只 backfill 当前话题的 task tool_call避免跨话题串扰
if (msg.topic_id && msg.topic_id !== selectedTopicRef.current) break
// 立即更新对应的 task tool_call让用户可以点击查看实时进度
setMessages((prev) => {
for (let i = prev.length - 1; i >= 0; i--) {
if (prev[i].type === 'tool_call' && prev[i].toolName === 'task' && !prev[i].subagentTaskId) {
const updated = [...prev]
updated[i] = { ...updated[i], subagentTaskId: msg.task_id }
return updated
}
}
return prev
})
break
}
case 'session_list': {
const msg = message as SessionList
console.log('Session list received:', msg)
// 清空旧数据(切换通道时避免数据污染)
setTopics([])
setSelectedTopic(null)
setMessages([])
// 存储全部 session
setSessions(msg.sessions)
// 自动选中:优先保持当前选中,否则选第一个
setSelectedSessionId(prev => {
if (prev && msg.sessions.some(s => s.session_id === prev)) {
return prev
}
return msg.sessions.length > 0 ? msg.sessions[0].session_id : null
})
setIsLoading(false)
break
}
case 'session_created': {
// 创建新 Topic 后更新列表
setIsLoading(false)
break
}
case 'session_loaded': {
setIsLoading(false)
break
}
case 'topic_list': {
const msg = message as TopicList
console.log('Topic list received:', msg)
// 转换 topics 格式
const newTopics: Topic[] = msg.topics.map((t: TopicSummary) => ({
id: t.topic_id,
session_id: t.session_id,
title: t.title,
description: t.description || undefined,
message_count: Number(t.message_count),
created_at: t.created_at,
updated_at: t.last_active_at,
}))
setTopics(newTopics)
// 新建话题后自动聚焦到新话题(列表按 last_active_at DESC 排序,第一个即最新)
if (pendingNewTopicRef.current) {
pendingNewTopicRef.current = false
if (newTopics.length > 0) {
setSelectedTopic(newTopics[0].id)
setMessages([])
}
}
setIsLoading(false)
break
}
case 'stream_delta': {
const msg = message as StreamDelta
if (msg.topic_id && msg.topic_id !== selectedTopicRef.current) return
setMessages((prev) => {
const existingIdx = prev.findIndex(m => m.id === msg.id && m.type === 'message')
if (existingIdx >= 0) {
// 追加到已有消息
const updated = [...prev]
const existing = updated[existingIdx]
updated[existingIdx] = {
...existing,
content: existing.content + msg.delta,
reasoningContent: msg.reasoning_delta
? (existing.reasoningContent || '') + msg.reasoning_delta
: existing.reasoningContent,
}
return updated
}
// 创建新消息
return [
...prev,
{
id: msg.id,
role: 'assistant' as const,
content: msg.delta,
timestamp: Math.floor(Date.now() / 1000),
type: 'message' as const,
reasoningContent: msg.reasoning_delta,
},
]
})
setIsLoading(false)
break
}
case 'stream_end': {
// 流式结束,无需额外操作,后续 assistant_response 会替换完整内容
break
}
case 'assistant_response': {
const msg = message as AssistantResponse
// 按 topic_id 隔离:如果消息属于其他话题则丢弃
if (msg.topic_id && msg.topic_id !== selectedTopicRef.current) return
const role = msg.role === 'user' || msg.role === 'tool' ? msg.role : 'assistant'
setMessages((prev) => {
// 如果流式消息已存在(相同 id替换它
const existingIdx = prev.findIndex(m => m.id === msg.id && m.type === 'message')
const newMsg: ChatMessage = {
id: msg.id,
role,
content: msg.content,
timestamp: (message as any).timestamp ?? Math.floor(Date.now() / 1000),
type: 'message',
attachments: msg.attachments,
reasoningContent: msg.reasoning_content,
}
if (existingIdx >= 0) {
const updated = [...prev]
updated[existingIdx] = newMsg
return updated
}
return [...prev, newMsg]
})
setIsLoading(false)
// 当前话题无描述时,可能刚触发了异步生成,标记需要刷新
const currentTopic = topicsRef.current.find(t => t.id === selectedTopicRef.current)
if (currentTopic && !currentTopic.description) {
setTopicRefreshTrigger(n => n + 1)
}
break
}
case 'tool_call': {
const msg = message as ToolCall
// 按 topic_id 隔离:如果消息属于其他话题则丢弃
if (msg.topic_id && msg.topic_id !== selectedTopicRef.current) return
setMessages((prev) => [
...prev,
{
id: msg.id,
role: 'tool',
content: msg.content,
timestamp: (message as any).timestamp ?? Math.floor(Date.now() / 1000),
type: 'tool_call',
toolName: msg.tool_name,
toolCallId: msg.tool_call_id,
arguments: msg.arguments,
subagentTaskId: msg.subagent_task_id,
reasoningContent: msg.reasoning_content,
},
])
break
}
case 'tool_result': {
const msg = message as ToolResult
// 按 topic_id 隔离:如果消息属于其他话题则丢弃
if (msg.topic_id && msg.topic_id !== selectedTopicRef.current) return
setMessages((prev) => [
...prev,
{
id: msg.id,
role: 'tool',
content: msg.content,
timestamp: (message as any).timestamp ?? Math.floor(Date.now() / 1000),
type: 'tool_result',
toolName: msg.tool_name,
toolCallId: msg.tool_call_id,
subagentTaskId: msg.subagent_task_id,
durationMs: msg.duration_ms,
},
])
break
}
case 'tool_pending': {
const msg = message as ToolPending
// 按 topic_id 隔离:如果消息属于其他话题则丢弃
if (msg.topic_id && msg.topic_id !== selectedTopicRef.current) return
setMessages((prev) => [
...prev,
{
id: msg.id,
role: 'tool',
content: `${msg.content}\n\n${msg.resume_hint}`,
timestamp: (message as any).timestamp ?? Math.floor(Date.now() / 1000),
type: 'tool_pending',
toolName: msg.tool_name,
toolCallId: msg.tool_call_id,
},
])
break
}
case 'execution_cancelled': {
setMessages((prev) => [
...prev,
{
id: generateMessageId(),
role: 'assistant',
content: (message as { type: 'execution_cancelled'; message: string }).message,
timestamp: (message as any).timestamp ?? Math.floor(Date.now() / 1000),
type: 'message',
},
])
setIsLoading(false)
break
}
case 'error': {
setMessages((prev) => [
...prev,
{
id: generateMessageId(),
role: 'assistant',
content: `Error: ${message.message}`,
timestamp: (message as any).timestamp ?? Math.floor(Date.now() / 1000),
type: 'message',
},
])
setIsLoading(false)
break
}
case 'scheduler_job_list': {
const msg = message as SchedulerJobList
setSchedulerJobs(msg.jobs)
break
}
case 'memory_list': {
const msg = message as MemoryList
setMemories(msg.memories)
break
}
case 'skill_list': {
const msg = message as SkillList
setSkills(msg.skills)
break
}
case 'todo_list': {
const msg = message as TodoList
setTodos(msg.todos)
break
}
case 'channel_list': {
const msg = message as ChannelList
console.log('Channel list received:', msg)
setChannels(msg.channels)
break
}
case 'pong':
// 忽略这些消息
break
}
}, [])
const handleMessage = useCallback((content: string, attachments?: Attachment[]) => {
setMessages((prev) => [
...prev,
{
id: generateMessageId(),
role: 'user',
content,
timestamp: Math.floor(Date.now() / 1000),
type: 'message',
attachments: attachments || [],
},
])
setIsLoading(true)
}, [])
const handleCommand = useCallback((command: Command) => {
switch (command.type) {
case 'create_session':
case 'switch_topic':
case 'load_topic':
case 'list_sessions':
case 'list_sessions_by_channel':
case 'delete_topic':
case 'list_topics':
setIsLoading(true)
break
}
}, [])
const clearMessages = useCallback(() => {
setMessages([])
}, [])
// Topic 操作方法
const selectTopic = useCallback((topicId: string) => {
setSelectedTopic(topicId)
setMessages([])
setSubAgentView(null)
}, [])
const createTopic = useCallback((title?: string): Command => {
pendingNewTopicRef.current = true
return {
type: 'create_session',
title: title || `话题 ${new Date().toLocaleString('zh-CN', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}`,
}
}, [])
const switchTopic = useCallback((topicId: string): Command => {
return {
type: 'switch_topic',
topic_id: topicId,
}
}, [])
const deleteTopic = useCallback((topicId: string): Command => {
return {
type: 'delete_topic',
topic_id: topicId,
}
}, [])
// 初始化方法
const requestSessionList = useCallback((): Command => {
return {
type: 'list_sessions_by_channel',
channel_name: selectedChannel,
include_archived: false,
}
}, [selectedChannel])
const requestChannelList = useCallback((): Command => {
return { type: 'list_channels' }
}, [])
const selectChannel = useCallback((channelId: string) => {
if (channelId === selectedChannel) return
setSelectedChannel(channelId)
setSessions([])
setSelectedSessionId(null)
setTopics([])
setSelectedTopic(null)
setMessages([])
setSubAgentView(null)
setIsLoading(true)
}, [selectedChannel])
const selectSession = useCallback((sessionId: string) => {
if (sessionId === selectedSessionId) return
setSelectedSessionId(sessionId)
setTopics([])
setSelectedTopic(null)
setMessages([])
setSubAgentView(null)
setIsLoading(true)
}, [selectedSessionId])
const requestTopicList = useCallback((): Command | null => {
if (!sessionId) return null
return {
type: 'list_topics',
session_id: sessionId,
}
}, [sessionId])
// Keep refs in sync with state
useEffect(() => {
subAgentViewRef.current = subAgentView
}, [subAgentView])
useEffect(() => {
schedulerViewRef.current = schedulerView
}, [schedulerView])
useEffect(() => {
topicsRef.current = topics
}, [topics])
useEffect(() => {
selectedTopicRef.current = selectedTopic
}, [selectedTopic])
const enterSubAgentView = useCallback((taskId: string, description: string): Command => {
const newView: SubAgentView = {
taskId,
description,
subagentType: '',
status: 'loading',
messages: [],
}
// Sync ref immediately so WebSocket response routing works correctly
subAgentViewRef.current = newView
setSubAgentView(newView)
return {
type: 'load_task_messages',
task_id: taskId,
}
}, [])
const exitSubAgentView = useCallback(() => {
subAgentViewRef.current = null
setSubAgentView(null)
}, [])
// 记忆方法
const requestMemoryList = useCallback((): Command => {
return { type: 'list_memories' }
}, [])
const createMemory = useCallback((namespace: string, key: string, content: string): Command => {
return { type: 'create_memory', namespace, key, content }
}, [])
const updateMemory = useCallback((id: string, content: string): Command => {
return { type: 'update_memory', id, content }
}, [])
const deleteMemory = useCallback((id: string): Command => {
return { type: 'delete_memory', id }
}, [])
const requestSkillList = useCallback((): Command => {
return { type: 'list_skills' }
}, [])
const requestTodoList = useCallback((): Command => {
return { type: 'list_todos' }
}, [])
const requestSubAgentTodoList = useCallback((subTaskId: string): Command => {
return { type: 'list_todos', task_id: subTaskId }
}, [])
// 定时任务方法
const requestSchedulerJobList = useCallback((): Command => {
return { type: 'list_scheduler_jobs' }
}, [])
const enterSchedulerJobView = useCallback(
(lookup: SchedulerJobSessionLookup, jobId: string, description: string): Command => {
const newView: SchedulerJobView = {
jobId,
description,
channel: lookup.channel,
chatId: lookup.chat_id,
messages: [],
}
schedulerViewRef.current = newView
setSchedulerView(newView)
return {
type: 'load_chat_messages',
channel: lookup.channel,
chat_id: lookup.chat_id,
}
},
[]
)
const exitSchedulerJobView = useCallback(() => {
schedulerViewRef.current = null
setSchedulerView(null)
}, [])
const handleStop = useCallback((): Command => {
return { type: 'stop_execution' }
}, [])
// Memoize messages: sub-agent view > scheduler view > main
const resolvedMessages = useMemo(() => {
if (subAgentView) {
return subAgentView.messages
}
if (schedulerView) {
return schedulerView.messages
}
return messages
}, [subAgentView, schedulerView, messages])
// 只读状态由当前通道决定
const isReadOnly = !isWritable
return {
connectionId,
isConnected,
sessions,
selectedSessionId,
session: selectedSession,
sessionId,
chatId,
topics,
selectedTopic,
messages: resolvedMessages,
isLoading,
isReadOnly,
isWritable,
channels,
selectedChannel,
subAgentView,
handleMessage,
handleCommand,
clearMessages,
handleServerMessage,
selectTopic,
createTopic,
switchTopic,
deleteTopic,
requestSessionList,
requestTopicList,
topicRefreshTrigger,
requestChannelList,
selectChannel,
selectSession,
enterSubAgentView,
exitSubAgentView,
memories,
requestMemoryList,
createMemory,
updateMemory,
deleteMemory,
skills,
requestSkillList,
todos,
requestTodoList,
requestSubAgentTodoList,
schedulerJobs,
sidebarTab,
setSidebarTab,
requestSchedulerJobList,
schedulerView,
enterSchedulerJobView,
exitSchedulerJobView,
handleStop,
}
}