Compare commits

..

No commits in common. "c4d10c6413195c20622c526828ec16e4bc980d23" and "f808bd09ea1dc12c39ef919a679da4f6412c7f60" have entirely different histories.

26 changed files with 15 additions and 2063 deletions

View File

@ -1,74 +0,0 @@
use crate::command::context::CommandContext;
use crate::command::handler::{CommandHandler, CommandMetadata};
use crate::command::response::{CommandError, CommandResponse};
use crate::command::Command;
use crate::protocol::TodoItemSummary;
use crate::storage::SessionStore;
use async_trait::async_trait;
use std::sync::Arc;
pub struct ListTodosCommandHandler {
store: Arc<SessionStore>,
}
impl ListTodosCommandHandler {
pub fn new(store: Arc<SessionStore>) -> Self {
Self { store }
}
}
#[async_trait]
impl CommandHandler for ListTodosCommandHandler {
fn can_handle(&self, cmd: &Command) -> bool {
matches!(cmd, Command::ListTodos)
}
fn metadata(&self) -> Option<CommandMetadata> {
Some(CommandMetadata {
name: "list_todos",
description: "列出当前 Todo 列表",
usage: "/list_todos",
})
}
async fn handle(
&self,
_cmd: Command,
ctx: CommandContext,
) -> Result<CommandResponse, CommandError> {
// scope_key = topic_id.unwrap_or(session_id)
let scope_key = ctx
.topic_id
.as_deref()
.filter(|t| !t.is_empty())
.or(ctx.session_id.as_deref())
.ok_or_else(|| {
CommandError::new(
"MISSING_CONTEXT",
"Cannot list todos: no session_id or topic_id in command context",
)
})?;
let records = self
.store
.list_todos(scope_key)
.map_err(|e| CommandError::new("LIST_TODOS_ERROR", e.to_string()))?;
let summaries: Vec<TodoItemSummary> = records
.into_iter()
.map(|r| TodoItemSummary {
id: r.id,
content: r.content,
status: r.status,
priority: r.priority,
created_at: r.created_at,
updated_at: r.updated_at,
})
.collect();
let todos_json = serde_json::to_string(&summaries)
.map_err(|e| CommandError::new("SERIALIZE_ERROR", e.to_string()))?;
Ok(CommandResponse::success(ctx.request_id).with_metadata("todos", &todos_json))
}
}

View File

@ -5,7 +5,6 @@ pub mod list_channels;
pub mod list_memories; pub mod list_memories;
pub mod list_scheduler_jobs; pub mod list_scheduler_jobs;
pub mod list_skills; pub mod list_skills;
pub mod list_todos;
pub mod memory_crud; pub mod memory_crud;
pub mod list_sessions; pub mod list_sessions;
pub mod list_sessions_by_channel; pub mod list_sessions_by_channel;

View File

@ -73,8 +73,6 @@ pub enum Command {
DeleteMemory { id: String }, DeleteMemory { id: String },
/// 列出所有技能 /// 列出所有技能
ListSkills, ListSkills,
/// 列出当前 Todo 列表
ListTodos,
} }
impl Command { impl Command {
@ -102,7 +100,6 @@ impl Command {
Command::UpdateMemory { .. } => "update_memory", Command::UpdateMemory { .. } => "update_memory",
Command::DeleteMemory { .. } => "delete_memory", Command::DeleteMemory { .. } => "delete_memory",
Command::ListSkills => "list_skills", Command::ListSkills => "list_skills",
Command::ListTodos => "list_todos",
} }
} }
} }

View File

@ -3,7 +3,6 @@ use std::sync::Arc;
use crate::agent::{AgentError, AgentLoop, CompositeSystemPromptProvider}; use crate::agent::{AgentError, AgentLoop, CompositeSystemPromptProvider};
use crate::config::LLMProviderConfig; use crate::config::LLMProviderConfig;
use crate::gateway::agent_prompt_provider::AgentPromptProvider; use crate::gateway::agent_prompt_provider::AgentPromptProvider;
use crate::gateway::todo_prompt_provider::TodoPromptProvider;
use crate::skills::{SkillPromptProvider, SkillRuntime}; use crate::skills::{SkillPromptProvider, SkillRuntime};
use crate::storage::persistent_session_id; use crate::storage::persistent_session_id;
use crate::storage::PromptInjectionRepository; use crate::storage::PromptInjectionRepository;
@ -54,7 +53,6 @@ impl AgentFactory {
self.prompt_repository.clone(), self.prompt_repository.clone(),
)), )),
Box::new(SkillPromptProvider::new(self.skills.clone())), Box::new(SkillPromptProvider::new(self.skills.clone())),
Box::new(TodoPromptProvider::new()),
])); ]));
AgentLoop::with_tools_and_system_prompt_provider( AgentLoop::with_tools_and_system_prompt_provider(

View File

@ -105,14 +105,9 @@
- 调用工具的时候必须同时用简短的话告诉用户你调用工具是做什么 - 调用工具的时候必须同时用简短的话告诉用户你调用工具是做什么
- 无需担心创建子智能体过多的问题请按用户或者skill的要求创建对应数量的子智能体这样可以隔离上下文更好完成工作 - 无需担心创建子智能体过多的问题请按用户或者skill的要求创建对应数量的子智能体这样可以隔离上下文更好完成工作
- 思考的时候建议用中文思考 - 思考的时候建议用中文思考
- 涉及到时间的都用get_time工具获取避免时间不准确
## 定时任务 ## 定时任务
- 默认创建静默任务silent_agent_task在独立后台会话中执行不干扰主对话 - 默认创建静默任务silent_agent_task在独立后台会话中执行不干扰主对话
- 静默模式下如需发送消息给用户prompt中需显式使用 send_session_message 工具 - 静默模式下如需发送消息给用户prompt中需显式使用 send_session_message 工具
## todo工具使用规范
- 严格按照既定的未完成的todo工作项执行任务如果工作项不在适用就更新不得随意遗漏工作项
- 禁止将未完成的工作项标记为已完成

View File

@ -190,14 +190,6 @@ impl AgentExecutionService {
// 只有当是最新回合时才触发历史压缩 // 只有当是最新回合时才触发历史压缩
let should_schedule_compaction = is_current_turn; let should_schedule_compaction = is_current_turn;
// 拦截 todo_write 结果:持久化 + 前端推送
if is_current_turn {
session.intercept_todo_write_results(
&request.result.emitted_messages,
request.chat_id,
);
}
Ok(FinalizedAgentResult { Ok(FinalizedAgentResult {
outbound_messages, outbound_messages,
should_schedule_compaction, should_schedule_compaction,

View File

@ -25,7 +25,6 @@ pub mod session_message_service;
pub mod session_pool; pub mod session_pool;
pub mod static_files; pub mod static_files;
pub mod tool_registry_factory; pub mod tool_registry_factory;
pub mod todo_prompt_provider;
pub mod ws; pub mod ws;
use axum::{Router, routing}; use axum::{Router, routing};

View File

@ -3,8 +3,6 @@
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::RwLock;
use crate::agent::AgentError; use crate::agent::AgentError;
use crate::bus::MessageBus; use crate::bus::MessageBus;
use crate::config::{LLMProviderConfig, MemoryMaintenanceConfig, SubagentsConfig, TaskConfig}; use crate::config::{LLMProviderConfig, MemoryMaintenanceConfig, SubagentsConfig, TaskConfig};
@ -20,7 +18,6 @@ use crate::tools::{
SessionMessageSender, SubAgentRuntimeConfig, SubagentCatalog, ToolRegistry, SessionMessageSender, SubAgentRuntimeConfig, SubagentCatalog, ToolRegistry,
}; };
use crate::tools::task::repository::TaskRepository; use crate::tools::task::repository::TaskRepository;
use crate::tools::todo_write::TodoItem;
use super::agent_factory::AgentFactory; use super::agent_factory::AgentFactory;
use super::cli_session::CliSessionService; use super::cli_session::CliSessionService;
@ -120,11 +117,6 @@ pub(crate) fn build_session_manager_with_sender(
task_config.clone(), task_config.clone(),
); );
// Create shared todo state for TodoWriteTool
let todo_state: Arc<RwLock<HashMap<String, Vec<TodoItem>>>> =
Arc::new(RwLock::new(HashMap::new()));
let factory = factory.with_todo_state(todo_state);
// Create MCP Initializer (async, non-blocking) // Create MCP Initializer (async, non-blocking)
// MCP servers connect in background task // MCP servers connect in background task
let mut mcp_initializer = McpInitializer::with_config(mcp_config); let mut mcp_initializer = McpInitializer::with_config(mcp_config);

View File

@ -385,84 +385,6 @@ impl Session {
let _ = self.user_tx.send(msg).await; let _ = self.user_tx.send(msg).await;
} }
/// 扫描 agent 结果中的 todo_write 工具消息,
/// 提取 todos 并做持久化 + 前端推送(同步版本)。
pub(crate) fn intercept_todo_write_results(
&self,
emitted_messages: &[ChatMessage],
chat_id: &str,
) {
for msg in emitted_messages {
if msg.role != "tool" {
continue;
}
if msg.tool_name.as_deref() != Some("todo_write") {
continue;
}
// 解析工具返回的 JSON
let parsed: serde_json::Value = match serde_json::from_str(&msg.content) {
Ok(v) => v,
Err(_) => continue,
};
let Some(todos_array) = parsed
.get("current_todos")
.and_then(|v| v.as_array())
else {
continue;
};
// 计算持久化所需的 key
let session_id = crate::storage::persistent_session_id(&self.channel_name, chat_id);
let topic_id = self.current_topic(chat_id);
let scope_key = topic_id.map(|t| t.to_string()).unwrap_or_else(|| session_id.clone());
// 转换为 TodoRecord 并持久化
let records: Vec<crate::storage::TodoRecord> = todos_array
.iter()
.filter_map(|item| {
Some(crate::storage::TodoRecord {
id: item.get("id")?.as_str()?.to_string(),
scope_key: scope_key.clone(),
session_id: session_id.clone(),
topic_id: topic_id.map(|t| t.to_string()),
content: item.get("content")?.as_str()?.to_string(),
status: item.get("status")?.as_str()?.to_string(),
priority: item.get("priority")?.as_str()?.to_string(),
created_at: item.get("created_at")?.as_i64()?,
updated_at: item.get("updated_at")?.as_i64()?,
})
})
.collect();
// 持久化到 SQLite
if let Err(e) = self.store.replace_todos(&scope_key, &records) {
tracing::warn!(error = %e, scope_key = %scope_key, "Failed to persist todo list");
}
// 推送到前端(使用 try_send 避免异步)
let summaries: Vec<crate::protocol::TodoItemSummary> = records
.iter()
.map(|r| crate::protocol::TodoItemSummary {
id: r.id.clone(),
content: r.content.clone(),
status: r.status.clone(),
priority: r.priority.clone(),
created_at: r.created_at,
updated_at: r.updated_at,
})
.collect();
let _ = self.user_tx.try_send(crate::protocol::WsOutbound::TodoList {
todos: summaries,
scope_key: scope_key.clone(),
});
break; // 只处理第一个成功的 todo_write
}
}
/// 获取 provider_config 引用 /// 获取 provider_config 引用
pub fn provider_config(&self) -> &LLMProviderConfig { pub fn provider_config(&self) -> &LLMProviderConfig {
&self.provider_config &self.provider_config

View File

@ -1,71 +0,0 @@
use crate::agent::{SystemPrompt, SystemPromptContext, SystemPromptProvider};
pub struct TodoPromptProvider;
impl TodoPromptProvider {
pub fn new() -> Self {
Self
}
}
impl SystemPromptProvider for TodoPromptProvider {
fn build(&self, _context: &SystemPromptContext) -> Option<SystemPrompt> {
Some(SystemPrompt {
content: TODO_WRITE_INSTRUCTIONS.to_string(),
context: Some("todo_write".to_string()),
})
}
}
const TODO_WRITE_INSTRUCTIONS: &str = r#"
## TodoWrite
使 `todo_write`
### 使
- 3 使 todo_write
- todo
### merge
- `merge: false` todo
- `merge: true` **使 merge=true id**
###
- `pending`
- `in_progress`
- `completed`
- `cancelled`
###
1. `in_progress`
2. `in_progress`
3. `completed` `cancelled`
4. completed
5. `content`
6. ** `id`** id idid todo_write `current_todos`
### 使
pending in_progress id
```json
{"merge": true, "todos": [{"content": "修复登录 bug", "status": "in_progress"}]}
```
```json
{"merge": true, "todos": [{"content": "补充测试", "status": "pending"}]}
```
** id** current_todos
```json
{"merge": true, "todos": [{"id": "abc-123", "content": "修复登录 bug", "status": "completed"}]}
```
```json
{"merge": true, "todos": [
{"id": "abc-123", "content": "修复登录 bug", "status": "completed"},
{"content": "代码审查", "status": "in_progress"}
]}
```
"#;

View File

@ -1,19 +1,16 @@
use std::collections::{HashMap, HashSet}; use std::collections::HashSet;
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::RwLock;
use crate::config::TaskConfig; use crate::config::TaskConfig;
use crate::mcp::McpClientManager; use crate::mcp::McpClientManager;
use crate::skills::SkillRuntime; use crate::skills::SkillRuntime;
use crate::storage::{MemoryRepository, SchedulerJobRepository, SkillEventRepository}; use crate::storage::{MemoryRepository, SchedulerJobRepository, SkillEventRepository};
use crate::tools::todo_write::TodoItem;
use crate::tools::{ use crate::tools::{
BashTool, CalculatorTool, FileEditTool, FileReadTool, FileWriteTool, BashTool, CalculatorTool, FileEditTool, FileReadTool, FileWriteTool,
HttpRequestTool, MemoryManageTool, MemorySearchTool, HttpRequestTool, MemoryManageTool, MemorySearchTool,
SchedulerManageTool, SessionMessageSender, SessionSendTool, SkillActivateTool, SchedulerManageTool, SessionMessageSender, SessionSendTool, SkillActivateTool,
SkillManageTool, SubAgentRuntime, TaskTool, TimeTool, SkillManageTool, SubAgentRuntime, TaskTool, TimeTool,
TodoWriteTool, ToolRegistry, WebFetchTool, ToolRegistry, WebFetchTool,
}; };
pub(crate) struct ToolRegistryFactory { pub(crate) struct ToolRegistryFactory {
@ -28,7 +25,6 @@ pub(crate) struct ToolRegistryFactory {
task_config: TaskConfig, task_config: TaskConfig,
subagent_runtime: Option<Arc<dyn SubAgentRuntime>>, subagent_runtime: Option<Arc<dyn SubAgentRuntime>>,
mcp_manager: Option<Arc<McpClientManager>>, mcp_manager: Option<Arc<McpClientManager>>,
todo_state: Option<Arc<RwLock<HashMap<String, Vec<TodoItem>>>>>,
} }
impl ToolRegistryFactory { impl ToolRegistryFactory {
@ -55,18 +51,9 @@ impl ToolRegistryFactory {
task_config, task_config,
subagent_runtime: None, subagent_runtime: None,
mcp_manager: None, mcp_manager: None,
todo_state: None,
} }
} }
pub(crate) fn with_todo_state(
mut self,
state: Arc<RwLock<HashMap<String, Vec<TodoItem>>>>,
) -> Self {
self.todo_state = Some(state);
self
}
pub(crate) fn with_subagent_runtime( pub(crate) fn with_subagent_runtime(
mut self, mut self,
runtime: Arc<dyn SubAgentRuntime>, runtime: Arc<dyn SubAgentRuntime>,
@ -111,11 +98,6 @@ impl ToolRegistryFactory {
if self.is_enabled("memory_manage") { if self.is_enabled("memory_manage") {
registry.register(MemoryManageTool::new(self.memories.clone())); registry.register(MemoryManageTool::new(self.memories.clone()));
} }
if self.is_enabled("todo_write") {
if let Some(ref state) = self.todo_state {
registry.register(TodoWriteTool::new(state.clone()));
}
}
if self.is_enabled("session_send") { if self.is_enabled("session_send") {
registry.register(SessionSendTool::new(self.session_message_sender.clone())); registry.register(SessionSendTool::new(self.session_message_sender.clone()));
} }
@ -216,13 +198,6 @@ impl ToolRegistryFactory {
registry.register(SessionSendTool::new(self.session_message_sender.clone())); registry.register(SessionSendTool::new(self.session_message_sender.clone()));
} }
// Todo 追踪工具
if self.is_enabled("todo_write") {
if let Some(ref state) = self.todo_state {
registry.register(TodoWriteTool::new(state.clone()));
}
}
// 注册 MCP 工具(如果提供) // 注册 MCP 工具(如果提供)
if let Some(mcp_tools) = mcp_tools { if let Some(mcp_tools) = mcp_tools {
for tool in mcp_tools { for tool in mcp_tools {

View File

@ -12,7 +12,6 @@ use crate::command::handlers::list_channels::ListChannelsCommandHandler;
use crate::command::handlers::list_memories::ListMemoriesCommandHandler; use crate::command::handlers::list_memories::ListMemoriesCommandHandler;
use crate::command::handlers::list_skills::ListSkillsCommandHandler; use crate::command::handlers::list_skills::ListSkillsCommandHandler;
use crate::command::handlers::list_scheduler_jobs::ListSchedulerJobsCommandHandler; use crate::command::handlers::list_scheduler_jobs::ListSchedulerJobsCommandHandler;
use crate::command::handlers::list_todos::ListTodosCommandHandler;
use crate::command::handlers::memory_crud::MemoryCrudCommandHandler; use crate::command::handlers::memory_crud::MemoryCrudCommandHandler;
use crate::command::handlers::list_sessions::ListSessionsCommandHandler; use crate::command::handlers::list_sessions::ListSessionsCommandHandler;
use crate::command::handlers::list_sessions_by_channel::ListSessionsByChannelCommandHandler; use crate::command::handlers::list_sessions_by_channel::ListSessionsByChannelCommandHandler;
@ -423,8 +422,6 @@ async fn handle_inbound(
router.register(Box::new(ListMemoriesCommandHandler::new(store.clone()))); router.register(Box::new(ListMemoriesCommandHandler::new(store.clone())));
// 注册 list_skills 处理器 // 注册 list_skills 处理器
router.register(Box::new(ListSkillsCommandHandler::new(skills_for_handler))); router.register(Box::new(ListSkillsCommandHandler::new(skills_for_handler)));
// 注册 list_todos 处理器
router.register(Box::new(ListTodosCommandHandler::new(store.clone())));
// 注册 memory_crud 处理器 // 注册 memory_crud 处理器
router.register(Box::new(MemoryCrudCommandHandler::new(store.clone()))); router.register(Box::new(MemoryCrudCommandHandler::new(store.clone())));
// 注册 load_chat_messages 处理器 // 注册 load_chat_messages 处理器
@ -522,13 +519,6 @@ async fn handle_inbound(
} }
} }
// 处理 Todo 列表
if let Some(todos_json) = response.metadata.get("todos") {
if let Ok(todos) = serde_json::from_str::<Vec<crate::protocol::TodoItemSummary>>(todos_json) {
let _ = sender.send(WsOutbound::TodoList { todos, scope_key: String::new() }).await;
}
}
// 处理记忆列表 // 处理记忆列表
if let Some(memories_json) = response.metadata.get("memories") { if let Some(memories_json) = response.metadata.get("memories") {
if let Ok(memories) = serde_json::from_str::<Vec<crate::protocol::MemorySummary>>(memories_json) { if let Ok(memories) = serde_json::from_str::<Vec<crate::protocol::MemorySummary>>(memories_json) {
@ -777,7 +767,7 @@ fn chat_message_to_ws_outbound(msg: &crate::bus::ChatMessage) -> Vec<WsOutbound>
let tc_reasoning = if has_content_or_reasoning { None } else { msg.reasoning_content.clone() }; let tc_reasoning = if has_content_or_reasoning { None } else { msg.reasoning_content.clone() };
for tool_call in tool_calls { for tool_call in tool_calls {
outbound.push(WsOutbound::ToolCall { outbound.push(WsOutbound::ToolCall {
id: tool_call.id.clone(), id: msg.id.clone(),
tool_call_id: tool_call.id.clone(), tool_call_id: tool_call.id.clone(),
tool_name: tool_call.name.clone(), tool_name: tool_call.name.clone(),
arguments: tool_call.arguments.clone(), arguments: tool_call.arguments.clone(),
@ -808,7 +798,7 @@ fn chat_message_to_ws_outbound(msg: &crate::bus::ChatMessage) -> Vec<WsOutbound>
let tool_state = msg.tool_state.as_ref().unwrap_or(&ToolMessageState::Completed); let tool_state = msg.tool_state.as_ref().unwrap_or(&ToolMessageState::Completed);
match tool_state { match tool_state {
ToolMessageState::Completed => vec![WsOutbound::ToolResult { ToolMessageState::Completed => vec![WsOutbound::ToolResult {
id: msg.tool_call_id.clone().unwrap_or_else(|| uuid::Uuid::new_v4().to_string()), id: msg.id.clone(),
tool_call_id: msg.tool_call_id.clone().unwrap_or_default(), tool_call_id: msg.tool_call_id.clone().unwrap_or_default(),
tool_name: msg.tool_name.clone().unwrap_or_default(), tool_name: msg.tool_name.clone().unwrap_or_default(),
content: msg.content.clone(), content: msg.content.clone(),
@ -819,7 +809,7 @@ fn chat_message_to_ws_outbound(msg: &crate::bus::ChatMessage) -> Vec<WsOutbound>
timestamp: Some(msg.timestamp / 1000), timestamp: Some(msg.timestamp / 1000),
}], }],
ToolMessageState::PendingUserAction => vec![WsOutbound::ToolPending { ToolMessageState::PendingUserAction => vec![WsOutbound::ToolPending {
id: msg.tool_call_id.clone().unwrap_or_else(|| uuid::Uuid::new_v4().to_string()), id: msg.id.clone(),
tool_call_id: msg.tool_call_id.clone().unwrap_or_default(), tool_call_id: msg.tool_call_id.clone().unwrap_or_default(),
tool_name: msg.tool_name.clone().unwrap_or_default(), tool_name: msg.tool_name.clone().unwrap_or_default(),
content: msg.content.clone(), content: msg.content.clone(),

View File

@ -82,17 +82,6 @@ pub struct SkillSummary {
pub source: String, pub source: String,
} }
/// Todo item 摘要(发送给前端)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TodoItemSummary {
pub id: String,
pub content: String,
pub status: String,
pub priority: String,
pub created_at: i64,
pub updated_at: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SchedulerJobSummary { pub struct SchedulerJobSummary {
pub id: String, pub id: String,
@ -213,8 +202,6 @@ pub enum WsOutbound {
task_id: String, task_id: String,
description: String, description: String,
subagent_type: String, subagent_type: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
topic_id: Option<String>,
}, },
#[serde(rename = "session_established")] #[serde(rename = "session_established")]
SessionEstablished { session_id: String }, SessionEstablished { session_id: String },
@ -268,11 +255,6 @@ pub enum WsOutbound {
}, },
#[serde(rename = "execution_cancelled")] #[serde(rename = "execution_cancelled")]
ExecutionCancelled { message: String }, ExecutionCancelled { message: String },
#[serde(rename = "todo_list")]
TodoList {
todos: Vec<TodoItemSummary>,
scope_key: String,
},
#[serde(rename = "pong")] #[serde(rename = "pong")]
Pong, Pong,
} }

View File

@ -32,7 +32,7 @@ pub(crate) fn ws_outbound_from_chat_message(message: &ChatMessage) -> Vec<WsOutb
// AssistantResponse 已携带 reasoning 时ToolCall 不再重复 // AssistantResponse 已携带 reasoning 时ToolCall 不再重复
let tc_reasoning = if has_content_or_reasoning { None } else { message.reasoning_content.clone() }; let tc_reasoning = if has_content_or_reasoning { None } else { message.reasoning_content.clone() };
outbound.extend(tool_calls.iter().map(|tool_call| WsOutbound::ToolCall { outbound.extend(tool_calls.iter().map(|tool_call| WsOutbound::ToolCall {
id: tool_call.id.clone(), id: message.id.clone(),
tool_call_id: tool_call.id.clone(), tool_call_id: tool_call.id.clone(),
tool_name: tool_call.name.clone(), tool_name: tool_call.name.clone(),
arguments: tool_call.arguments.clone(), arguments: tool_call.arguments.clone(),
@ -63,7 +63,7 @@ pub(crate) fn ws_outbound_from_chat_message(message: &ChatMessage) -> Vec<WsOutb
.unwrap_or(&ToolMessageState::Completed) .unwrap_or(&ToolMessageState::Completed)
{ {
ToolMessageState::Completed => vec![WsOutbound::ToolResult { ToolMessageState::Completed => vec![WsOutbound::ToolResult {
id: message.tool_call_id.clone().unwrap_or_else(|| uuid::Uuid::new_v4().to_string()), id: message.id.clone(),
tool_call_id: message.tool_call_id.clone().unwrap_or_default(), tool_call_id: message.tool_call_id.clone().unwrap_or_default(),
tool_name: message.tool_name.clone().unwrap_or_default(), tool_name: message.tool_name.clone().unwrap_or_default(),
content: message.content.clone(), content: message.content.clone(),
@ -74,7 +74,7 @@ pub(crate) fn ws_outbound_from_chat_message(message: &ChatMessage) -> Vec<WsOutb
timestamp: None, timestamp: None,
}], }],
ToolMessageState::PendingUserAction => vec![WsOutbound::ToolPending { ToolMessageState::PendingUserAction => vec![WsOutbound::ToolPending {
id: message.tool_call_id.clone().unwrap_or_else(|| uuid::Uuid::new_v4().to_string()), id: message.id.clone(),
tool_call_id: message.tool_call_id.clone().unwrap_or_default(), tool_call_id: message.tool_call_id.clone().unwrap_or_default(),
tool_name: message.tool_name.clone().unwrap_or_default(), tool_name: message.tool_name.clone().unwrap_or_default(),
content: message.content.clone(), content: message.content.clone(),
@ -172,7 +172,6 @@ pub(crate) fn ws_outbound_from_outbound_message(message: &OutboundMessage) -> Ve
task_id: message.metadata.get("task_id").cloned().unwrap_or_default(), task_id: message.metadata.get("task_id").cloned().unwrap_or_default(),
description: message.metadata.get("task_description").cloned().unwrap_or_default(), description: message.metadata.get("task_description").cloned().unwrap_or_default(),
subagent_type: message.metadata.get("task_subagent_type").cloned().unwrap_or_default(), subagent_type: message.metadata.get("task_subagent_type").cloned().unwrap_or_default(),
topic_id: message.metadata.get("topic_id").cloned(),
}], }],
} }
} }

View File

@ -14,13 +14,13 @@ pub mod records;
pub use error::StorageError; pub use error::StorageError;
pub use ports::{ pub use ports::{
ConversationRepository, MemoryRepository, PromptInjectionRepository, SchedulerJobRepository, ConversationRepository, MemoryRepository, PromptInjectionRepository, SchedulerJobRepository,
SkillEventRepository, TodoRepository, SkillEventRepository,
}; };
pub use records::{ pub use records::{
allowed_namespace_names, get_namespace_description, is_valid_namespace, allowed_namespace_names, get_namespace_description, is_valid_namespace,
ALLOWED_MEMORY_NAMESPACES, GLOBAL_SCOPE_KEY, MemoryRecord, MemoryUpsert, SchedulerJobRecord, ALLOWED_MEMORY_NAMESPACES, GLOBAL_SCOPE_KEY, MemoryRecord, MemoryUpsert, SchedulerJobRecord,
SchedulerJobState, SchedulerJobStatus, SchedulerJobUpsert, SessionRecord, SkillEventRecord, SchedulerJobState, SchedulerJobStatus, SchedulerJobUpsert, SessionRecord, SkillEventRecord,
TodoRecord, TopicRecord, TopicRecord,
}; };
#[derive(Clone)] #[derive(Clone)]
@ -217,7 +217,6 @@ impl SessionStore {
ensure_messages_schema(&conn)?; ensure_messages_schema(&conn)?;
ensure_scheduler_schema(&conn)?; ensure_scheduler_schema(&conn)?;
ensure_memory_scope_key_migration(&conn)?; ensure_memory_scope_key_migration(&conn)?;
ensure_todos_schema(&conn)?;
drop(conn); drop(conn);
@ -1492,74 +1491,6 @@ impl SessionStore {
) )
.map_err(StorageError::from) .map_err(StorageError::from)
} }
pub fn replace_todos(
&self,
scope_key: &str,
items: &[TodoRecord],
) -> Result<Vec<TodoRecord>, StorageError> {
let conn = self.pool.get()?;
let now = current_timestamp();
// Delete existing todos for this scope_key
conn.execute(
"DELETE FROM todos WHERE scope_key = ?1",
params![scope_key],
)?;
// Insert new todos
for item in items {
conn.execute(
"INSERT INTO todos (id, scope_key, session_id, topic_id, content, status, priority, created_at, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
params![
item.id,
scope_key,
item.session_id,
item.topic_id,
item.content,
item.status,
item.priority,
item.created_at,
now,
],
)?;
}
drop(conn);
self.list_todos(scope_key)
}
pub fn list_todos(&self, scope_key: &str) -> Result<Vec<TodoRecord>, StorageError> {
let conn = self.pool.get()?;
let mut stmt = conn.prepare(
"SELECT id, scope_key, session_id, topic_id, content, status, priority, created_at, updated_at
FROM todos
WHERE scope_key = ?1
ORDER BY created_at ASC",
)?;
let rows = stmt.query_map(params![scope_key], |row| {
Ok(TodoRecord {
id: row.get(0)?,
scope_key: row.get(1)?,
session_id: row.get(2)?,
topic_id: row.get(3)?,
content: row.get(4)?,
status: row.get(5)?,
priority: row.get(6)?,
created_at: row.get(7)?,
updated_at: row.get(8)?,
})
})?;
let mut todos = Vec::new();
for row in rows {
todos.push(row?);
}
Ok(todos)
}
} }
pub fn persistent_session_id(channel_name: &str, chat_id: &str) -> String { pub fn persistent_session_id(channel_name: &str, chat_id: &str) -> String {
@ -1869,42 +1800,6 @@ fn ensure_memory_scope_key_migration(conn: &Connection) -> Result<(), StorageErr
Ok(()) Ok(())
} }
fn ensure_todos_schema(conn: &Connection) -> Result<(), StorageError> {
let table_exists: bool = conn
.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='todos'",
[],
|row| row.get::<_, i64>(0),
)
.map(|count| count > 0)?;
if !table_exists {
conn.execute_batch(
"
CREATE TABLE IF NOT EXISTS todos (
id TEXT PRIMARY KEY,
scope_key TEXT NOT NULL,
session_id TEXT NOT NULL,
topic_id TEXT,
content TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
priority TEXT NOT NULL DEFAULT 'medium',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_todos_scope
ON todos(scope_key, created_at ASC);
CREATE INDEX IF NOT EXISTS idx_todos_session
ON todos(session_id);
",
)?;
}
Ok(())
}
fn has_column( fn has_column(
conn: &Connection, conn: &Connection,
table_name: &str, table_name: &str,
@ -2114,7 +2009,7 @@ fn load_messages_after(
messages.push(row?); messages.push(row?);
} }
Ok(messages) Ok(messages)
} }
fn current_timestamp() -> i64 { fn current_timestamp() -> i64 {
std::time::SystemTime::now() std::time::SystemTime::now()

View File

@ -1,6 +1,6 @@
use super::{ use super::{
MemoryRecord, MemoryUpsert, SchedulerJobRecord, SchedulerJobState, SchedulerJobStatus, MemoryRecord, MemoryUpsert, SchedulerJobRecord, SchedulerJobState, SchedulerJobStatus,
SchedulerJobUpsert, SessionRecord, SkillEventRecord, StorageError, TodoRecord, SchedulerJobUpsert, SessionRecord, SkillEventRecord, StorageError,
}; };
use crate::bus::ChatMessage; use crate::bus::ChatMessage;
@ -145,18 +145,6 @@ pub trait SkillEventRepository: Send + Sync + 'static {
) -> Result<Vec<SkillEventRecord>, StorageError>; ) -> Result<Vec<SkillEventRecord>, StorageError>;
} }
pub trait TodoRepository: Send + Sync + 'static {
/// Replace all todos for a scope (full replacement pattern).
fn replace_todos(
&self,
scope_key: &str,
todo_records: &[TodoRecord],
) -> Result<Vec<TodoRecord>, StorageError>;
/// Load all todos for a scope, ordered by created_at.
fn list_todos(&self, scope_key: &str) -> Result<Vec<TodoRecord>, StorageError>;
}
impl ConversationRepository for super::SessionStore { impl ConversationRepository for super::SessionStore {
fn ensure_channel_session( fn ensure_channel_session(
&self, &self,
@ -368,17 +356,3 @@ impl SkillEventRepository for super::SessionStore {
super::SessionStore::list_skill_events(self, session_id) super::SessionStore::list_skill_events(self, session_id)
} }
} }
impl TodoRepository for super::SessionStore {
fn replace_todos(
&self,
scope_key: &str,
todo_records: &[TodoRecord],
) -> Result<Vec<TodoRecord>, StorageError> {
super::SessionStore::replace_todos(self, scope_key, todo_records)
}
fn list_todos(&self, scope_key: &str) -> Result<Vec<TodoRecord>, StorageError> {
super::SessionStore::list_todos(self, scope_key)
}
}

View File

@ -35,19 +35,6 @@ pub fn allowed_namespace_names() -> Vec<&'static str> {
ALLOWED_MEMORY_NAMESPACES.iter().map(|(name, _)| *name).collect() ALLOWED_MEMORY_NAMESPACES.iter().map(|(name, _)| *name).collect()
} }
#[derive(Debug, Clone)]
pub struct TodoRecord {
pub id: String,
pub scope_key: String,
pub session_id: String,
pub topic_id: Option<String>,
pub content: String,
pub status: String,
pub priority: String,
pub created_at: i64,
pub updated_at: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SkillEventRecord { pub struct SkillEventRecord {
pub id: String, pub id: String,

View File

@ -14,7 +14,6 @@ pub mod skill_activate;
pub mod skill_manage; pub mod skill_manage;
pub mod task; pub mod task;
pub mod time; pub mod time;
pub mod todo_write;
pub mod traits; pub mod traits;
pub mod web_fetch; pub mod web_fetch;
@ -40,7 +39,6 @@ pub use task::{
SubagentCatalog, TaskError, TaskRepository, TaskTool, SubagentCatalog, TaskError, TaskRepository, TaskTool,
}; };
pub use time::TimeTool; pub use time::TimeTool;
pub use todo_write::TodoWriteTool;
pub use traits::{Tool, ToolContext, ToolResult}; pub use traits::{Tool, ToolContext, ToolResult};
pub use web_fetch::WebFetchTool; pub use web_fetch::WebFetchTool;

View File

@ -52,8 +52,6 @@ impl SubagentPromptBuilder {
2. 使\n\ 2. 使\n\
3. \n\ 3. \n\
4. \n\n\ 4. \n\n\
:\n\
使 `todo_write` in_progress3使\n\n\
: 访" : 访"
} else { } else {
&def.prompt_template &def.prompt_template

View File

@ -250,7 +250,6 @@ impl DefaultSubAgentRuntime {
let mut metadata = HashMap::new(); let mut metadata = HashMap::new();
metadata.insert("subagent_task_id".to_string(), session.id.clone()); metadata.insert("subagent_task_id".to_string(), session.id.clone());
metadata.insert("is_subagent_event".to_string(), "true".to_string()); metadata.insert("is_subagent_event".to_string(), "true".to_string());
metadata.insert("topic_id".to_string(), session.parent_topic_id.clone().unwrap_or_default());
let emitter = Arc::new(PersistingEmittedMessageHandler::new( let emitter = Arc::new(PersistingEmittedMessageHandler::new(
SubAgentEmitter { SubAgentEmitter {
@ -425,7 +424,6 @@ impl SubAgentRuntime for DefaultSubAgentRuntime {
metadata.insert("task_id".to_string(), session.id.clone()); metadata.insert("task_id".to_string(), session.id.clone());
metadata.insert("task_description".to_string(), session.description.clone()); metadata.insert("task_description".to_string(), session.description.clone());
metadata.insert("task_subagent_type".to_string(), session.subagent_type.clone()); metadata.insert("task_subagent_type".to_string(), session.subagent_type.clone());
metadata.insert("topic_id".to_string(), session.parent_topic_id.clone().unwrap_or_default());
let event = OutboundMessage { let event = OutboundMessage {
channel: session.parent_channel_name.clone(), channel: session.parent_channel_name.clone(),

View File

@ -61,7 +61,7 @@ impl SubagentDef {
Self { Self {
name: "general".to_string(), name: "general".to_string(),
description: "通用型子代理 - 处理复杂多步骤任务".to_string(), description: "通用型子代理 - 处理复杂多步骤任务".to_string(),
prompt_template: "你是一个专注的子代理,正在执行一个独立任务。\n\n任务描述: {{description}}\n\n你应该:\n1. 专注于完成任务,不要偏离目标\n2. 使用可用的工具进行必要操作\n3. 完成后给出简洁的总结\n4. 不要尝试创建新的子代理任务\n\n任务追踪:\n你可以使用 `todo_write` 工具追踪子任务进度。规则:同一时间只有一个 in_progress完成后再标记下一个3步以上才使用。\n\n注意: 你没有访问主对话历史的权限,这是一个独立的执行上下文。".to_string(), prompt_template: "你是一个专注的子代理,正在执行一个独立任务。\n\n任务描述: {{description}}\n\n你应该:\n1. 专注于完成任务,不要偏离目标\n2. 使用可用的工具进行必要操作\n3. 完成后给出简洁的总结\n4. 不要尝试创建新的子代理任务\n\n注意: 你没有访问主对话历史的权限,这是一个独立的执行上下文。".to_string(),
body: None, body: None,
allowed_tools: None, allowed_tools: None,
max_execution_secs: None, max_execution_secs: None,

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,6 @@ import { TopicList } from './components/Sidebar/TopicList'
import { SchedulerJobList } from './components/Sidebar/SchedulerJobList' import { SchedulerJobList } from './components/Sidebar/SchedulerJobList'
import { MemoryPanel } from './components/Panel/MemoryPanel' import { MemoryPanel } from './components/Panel/MemoryPanel'
import { SkillList } from './components/Panel/SkillList' import { SkillList } from './components/Panel/SkillList'
import { TodoPanel } from './components/Panel/TodoPanel'
import { ConnectionStatus } from './components/ConnectionStatus' import { ConnectionStatus } from './components/ConnectionStatus'
import { ChannelSelector } from './components/Header/ChannelSelector' import { ChannelSelector } from './components/Header/ChannelSelector'
import { SessionSelector } from './components/Header/SessionSelector' import { SessionSelector } from './components/Header/SessionSelector'
@ -43,8 +42,6 @@ function App() {
// 技能 // 技能
skills, skills,
requestSkillList, requestSkillList,
todos,
requestTodoList,
// 定时任务 // 定时任务
schedulerJobs, schedulerJobs,
sidebarTab, sidebarTab,
@ -323,7 +320,7 @@ function App() {
} }
}, [sidebarTab, status, handleCommand, sendMessage, requestSchedulerJobList]) }, [sidebarTab, status, handleCommand, sendMessage, requestSchedulerJobList])
// 连接就绪时自动拉取记忆、技能和待办列表 // 连接就绪时自动拉取记忆和技能列表
useEffect(() => { useEffect(() => {
if (status === 'connected') { if (status === 'connected') {
const memCmd = requestMemoryList() const memCmd = requestMemoryList()
@ -332,11 +329,8 @@ function App() {
const skillCmd = requestSkillList() const skillCmd = requestSkillList()
handleCommand(skillCmd) handleCommand(skillCmd)
sendMessage({ type: 'command', payload: JSON.stringify(skillCmd) }) sendMessage({ type: 'command', payload: JSON.stringify(skillCmd) })
const todoCmd = requestTodoList()
handleCommand(todoCmd)
sendMessage({ type: 'command', payload: JSON.stringify(todoCmd) })
} }
}, [status, handleCommand, sendMessage, requestMemoryList, requestSkillList, requestTodoList]) }, [status, handleCommand, sendMessage, requestMemoryList, requestSkillList])
const handleRefreshMemories = useCallback(() => { const handleRefreshMemories = useCallback(() => {
const cmd = requestMemoryList() const cmd = requestMemoryList()
@ -683,13 +677,6 @@ function App() {
</div> </div>
)} )}
</div> </div>
{/* 悬浮 Todo 面板 */}
<TodoPanel
todos={todos}
requestTodoList={requestTodoList}
sendCommand={sendMemoryCommand}
/>
</div> </div>
) )
} }

View File

@ -1,223 +0,0 @@
import { useState, useCallback, useEffect, useRef } from 'react'
import { ClipboardList, ChevronUp, ChevronDown, Circle } from 'lucide-react'
import type { TodoItemSummary, Command } from '../../types/protocol'
interface TodoPanelProps {
todos: TodoItemSummary[]
requestTodoList: () => Command
sendCommand: (cmd: Command) => void
}
/* ── status helpers ───────────────────────────────────── */
interface StatusStyle { label: string; border: string; dot: string; text: string; icon: string }
const STATUS: Record<string, StatusStyle> = {
in_progress: { label: '进行中', border: 'border-amber-400/60', dot: 'bg-amber-400 shadow-[0_0_6px_#fbbf24]', text: 'text-amber-300', icon: '●' },
pending: { label: '待处理', border: 'border-slate-500/50', dot: 'bg-slate-500', text: 'text-slate-400', icon: '○' },
completed: { label: '已完成', border: 'border-emerald-400/40', dot: 'bg-emerald-400', text: 'text-emerald-400', icon: '✓' },
cancelled: { label: '已取消', border: 'border-red-400/40', dot: 'bg-red-400', text: 'text-red-400', icon: '✕' },
}
function statusStyle(s: string): StatusStyle {
return STATUS[s] ?? { label: s, border: 'border-[var(--border-color)]', dot: 'bg-[var(--text-muted)]', text: 'text-[var(--text-muted)]', icon: '?' }
}
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 COLLAPSED_DEFAULT = new Set(['completed', 'cancelled'])
function groupTodos(todos: TodoItemSummary[]): Map<string, TodoItemSummary[]> {
const map = new Map<string, TodoItemSummary[]>()
for (const t of todos) {
const list = map.get(t.status) ?? []
list.push(t)
map.set(t.status, list)
}
return map
}
/* ── pulse dot ────────────────────────────────────────── */
function PulseDot() {
return (
<span className="relative flex h-2 w-2">
<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>
)
}
/* ── TodoPanel ────────────────────────────────────────── */
export function TodoPanel({ todos, requestTodoList, sendCommand }: TodoPanelProps) {
const [expanded, setExpanded] = useState(() => {
try { return localStorage.getItem('picobot-todo-panel-open') === 'true' } catch { return false }
})
const [collapsedGroups, setCollapsedGroups] = useState<Set<string>>(() => new Set(COLLAPSED_DEFAULT))
const prevTodoIdsRef = useRef<Set<string>>(new Set())
// 持久化展开状态
useEffect(() => {
localStorage.setItem('picobot-todo-panel-open', String(expanded))
}, [expanded])
// 当有新 todo 出现时自动展开
useEffect(() => {
const newIds = new Set(todos.map(t => t.id))
const hasNewItems = todos.some(t => !prevTodoIdsRef.current.has(t.id))
if (hasNewItems && todos.length > 0) {
setExpanded(true)
}
prevTodoIdsRef.current = newIds
}, [todos])
const grouped = groupTodos(todos)
const inProgressCount = grouped.get('in_progress')?.length ?? 0
const totalCount = todos.length
const toggleGroup = useCallback((status: string) => {
setCollapsedGroups(prev => {
const next = new Set(prev)
if (next.has(status)) next.delete(status); else next.add(status)
return next
})
}, [])
// ── 空状态 ──
if (totalCount === 0) {
return (
<div className="fixed bottom-4 right-4 z-50">
<button
onClick={() => sendCommand(requestTodoList())}
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"
title="刷新待办"
>
<ClipboardList className="h-3.5 w-3.5" />
<span></span>
</button>
</div>
)
}
// ── 折叠态 ──
if (!expanded) {
return (
<div className="fixed bottom-4 right-4 z-50">
<button
onClick={() => setExpanded(true)}
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)]" />}
<span className="text-sm font-medium text-[var(--text-secondary)] group-hover:text-[var(--accent-cyan)] transition-colors">
({totalCount})
</span>
<ChevronUp className="h-3.5 w-3.5 text-[var(--text-muted)]" />
</button>
</div>
)
}
// ── 展开态 ──
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="shrink-0 flex items-center justify-between px-3 py-2.5 border-b border-[var(--border-color)]">
<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" />
</button>
</div>
{/* 列表区 */}
<div className="flex-1 overflow-y-auto scrollbar-hide px-2 py-2 space-y-2">
{GROUP_ORDER.map(status => {
const items = grouped.get(status)
if (!items || items.length === 0) return null
const style = statusStyle(status)
const isCollapsed = collapsedGroups.has(status)
const isTerminal = status === 'completed' || status === 'cancelled'
return (
<div key={status}>
{/* 分组标题 */}
<button
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"
>
{isCollapsed ? (
<ChevronUp className="h-3 w-3" />
) : (
<ChevronDown className="h-3 w-3" />
)}
<span className={style.text}>{style.icon}</span>
<span>{style.label}</span>
<span className="tabular-nums">({items.length})</span>
</button>
{/* 卡片列表 */}
{!isCollapsed && (
<div className="space-y-1">
{items.map(item => (
<div
key={item.id}
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}
</p>
<div className="flex items-center gap-2 mt-0.5">
<span className={`text-[10px] ${priorityDot(item.priority)}`}>
{item.priority}
</span>
<span className={`text-[10px] ${style.text}`}>
{style.icon} {style.label}
</span>
</div>
</div>
</div>
</div>
))}
</div>
)}
</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>
)
}

View File

@ -20,8 +20,6 @@ import type {
MemoryList, MemoryList,
SkillSummary, SkillSummary,
SkillList, SkillList,
TodoItemSummary,
TodoList,
SchedulerJobList, SchedulerJobList,
SchedulerJobSummary, SchedulerJobSummary,
SchedulerJobSessionLookup, SchedulerJobSessionLookup,
@ -94,10 +92,6 @@ interface UseChatReturn {
skills: SkillSummary[] skills: SkillSummary[]
requestSkillList: () => Command requestSkillList: () => Command
// Todo 状态
todos: TodoItemSummary[]
requestTodoList: () => Command
// 定时任务状态 // 定时任务状态
schedulerJobs: SchedulerJobSummary[] schedulerJobs: SchedulerJobSummary[]
sidebarTab: 'topics' | 'scheduler' sidebarTab: 'topics' | 'scheduler'
@ -146,7 +140,6 @@ export function useChat(): UseChatReturn {
const [subAgentView, setSubAgentView] = useState<SubAgentView | null>(null) const [subAgentView, setSubAgentView] = useState<SubAgentView | null>(null)
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 [schedulerJobs, setSchedulerJobs] = useState<SchedulerJobSummary[]>([]) const [schedulerJobs, setSchedulerJobs] = useState<SchedulerJobSummary[]>([])
const [sidebarTab, setSidebarTab] = useState<'topics' | 'scheduler'>('topics') const [sidebarTab, setSidebarTab] = useState<'topics' | 'scheduler'>('topics')
const [schedulerView, setSchedulerView] = useState<SchedulerJobView | null>(null) const [schedulerView, setSchedulerView] = useState<SchedulerJobView | null>(null)
@ -188,15 +181,6 @@ export function useChat(): UseChatReturn {
return undefined 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) // Convert a server message to ChatMessage (extracted from handleServerMessage logic)
const serverMessageToChatMessage = (message: WsOutbound): ChatMessage | null => { const serverMessageToChatMessage = (message: WsOutbound): ChatMessage | null => {
switch (message.type) { switch (message.type) {
@ -327,20 +311,12 @@ export function useChat(): UseChatReturn {
appendToSubAgentViewMessage(message) appendToSubAgentViewMessage(message)
return return
} }
// 丢弃其他子智能体的消息,避免 fall through 到主消息处理
if (msgSubagentTaskId) {
return
}
} }
// In main view, skip sub-agent messages (they belong to sub-agent view). // 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. // But use the task_id to associate with the running task tool card.
const msgSubagentTaskId = getSubagentTaskId(message) const msgSubagentTaskId = getSubagentTaskId(message)
if (msgSubagentTaskId) { if (msgSubagentTaskId) {
// 只 backfill 当前话题的 task tool_call避免跨话题串扰
const msgTopicId = getTopicId(message)
if (msgTopicId && msgTopicId !== selectedTopicRef.current) return
setMessages((prev) => { setMessages((prev) => {
for (let i = prev.length - 1; i >= 0; i--) { for (let i = prev.length - 1; i >= 0; i--) {
if (prev[i].type === 'tool_call' && prev[i].toolName === 'task' && !prev[i].subagentTaskId) { if (prev[i].type === 'tool_call' && prev[i].toolName === 'task' && !prev[i].subagentTaskId) {
@ -364,9 +340,6 @@ export function useChat(): UseChatReturn {
case 'task_started': { case 'task_started': {
const msg = message as TaskStarted const msg = message as TaskStarted
// 只 backfill 当前话题的 task tool_call避免跨话题串扰
if (msg.topic_id && msg.topic_id !== selectedTopicRef.current) break
// 立即更新对应的 task tool_call让用户可以点击查看实时进度 // 立即更新对应的 task tool_call让用户可以点击查看实时进度
setMessages((prev) => { setMessages((prev) => {
for (let i = prev.length - 1; i >= 0; i--) { for (let i = prev.length - 1; i >= 0; i--) {
@ -579,11 +552,6 @@ export function useChat(): UseChatReturn {
setSkills(msg.skills) setSkills(msg.skills)
break break
} }
case 'todo_list': {
const msg = message as TodoList
setTodos(msg.todos)
break
}
case 'channel_list': { case 'channel_list': {
const msg = message as ChannelList const msg = message as ChannelList
@ -634,7 +602,6 @@ export function useChat(): UseChatReturn {
const selectTopic = useCallback((topicId: string) => { const selectTopic = useCallback((topicId: string) => {
setSelectedTopic(topicId) setSelectedTopic(topicId)
setMessages([]) setMessages([])
setSubAgentView(null)
}, []) }, [])
const createTopic = useCallback((title?: string): Command => { const createTopic = useCallback((title?: string): Command => {
@ -680,7 +647,6 @@ export function useChat(): UseChatReturn {
setTopics([]) setTopics([])
setSelectedTopic(null) setSelectedTopic(null)
setMessages([]) setMessages([])
setSubAgentView(null)
setIsLoading(true) setIsLoading(true)
}, [selectedChannel]) }, [selectedChannel])
@ -690,7 +656,6 @@ export function useChat(): UseChatReturn {
setTopics([]) setTopics([])
setSelectedTopic(null) setSelectedTopic(null)
setMessages([]) setMessages([])
setSubAgentView(null)
setIsLoading(true) setIsLoading(true)
}, [selectedSessionId]) }, [selectedSessionId])
@ -762,10 +727,6 @@ export function useChat(): UseChatReturn {
return { type: 'list_skills' } return { type: 'list_skills' }
}, []) }, [])
const requestTodoList = useCallback((): Command => {
return { type: 'list_todos' }
}, [])
// 定时任务方法 // 定时任务方法
const requestSchedulerJobList = useCallback((): Command => { const requestSchedulerJobList = useCallback((): Command => {
return { type: 'list_scheduler_jobs' } return { type: 'list_scheduler_jobs' }
@ -854,8 +815,6 @@ export function useChat(): UseChatReturn {
deleteMemory, deleteMemory,
skills, skills,
requestSkillList, requestSkillList,
todos,
requestTodoList,
schedulerJobs, schedulerJobs,
sidebarTab, sidebarTab,
setSidebarTab, setSidebarTab,

View File

@ -100,7 +100,6 @@ export interface TaskStarted {
task_id: string task_id: string
description: string description: string
subagent_type: string subagent_type: string
topic_id?: string
} }
export interface SessionEstablished { export interface SessionEstablished {
@ -201,21 +200,6 @@ export interface SkillList {
skills: SkillSummary[] skills: SkillSummary[]
} }
export interface TodoItemSummary {
id: string
content: string
status: string
priority: string
created_at: number
updated_at: number
}
export interface TodoList {
type: 'todo_list'
todos: TodoItemSummary[]
scope_key: string
}
export interface SchedulerJobSessionLookup { export interface SchedulerJobSessionLookup {
channel: string channel: string
chat_id: string chat_id: string
@ -274,7 +258,6 @@ export type WsOutbound =
| SchedulerJobList | SchedulerJobList
| MemoryList | MemoryList
| SkillList | SkillList
| TodoList
| ExecutionCancelled | ExecutionCancelled
| Pong | Pong
@ -388,10 +371,6 @@ export interface ListSkillsCommand {
type: 'list_skills' type: 'list_skills'
} }
export interface ListTodosCommand {
type: 'list_todos'
}
export type Command = export type Command =
| CreateSessionCommand | CreateSessionCommand
| ListSessionsCommand | ListSessionsCommand
@ -414,7 +393,6 @@ export type Command =
| UpdateMemoryCommand | UpdateMemoryCommand
| DeleteMemoryCommand | DeleteMemoryCommand
| ListSkillsCommand | ListSkillsCommand
| ListTodosCommand
// ============================================================================ // ============================================================================
// UI Types // UI Types