From 58611a6bc194ecab82bcc02bb818cd4ec5b00077 Mon Sep 17 00:00:00 2001 From: xintaofei Date: Wed, 1 Apr 2026 10:08:20 +0800 Subject: [PATCH 1/8] =?UTF-8?q?=E6=B6=88=E6=81=AF=E6=B8=A0=E9=81=93?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E4=BC=9A=E8=AF=9D=E7=9B=B8=E5=85=B3=E6=8C=87?= =?UTF-8?q?=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/bin/codeg_server.rs | 7 +- .../src/chat_channel/backends/telegram.rs | 14 +- .../src/chat_channel/command_dispatcher.rs | 99 +- src-tauri/src/chat_channel/i18n.rs | 30 +- src-tauri/src/chat_channel/manager.rs | 27 +- src-tauri/src/chat_channel/mod.rs | 3 + src-tauri/src/chat_channel/session_bridge.rs | 75 ++ .../src/chat_channel/session_commands.rs | 848 ++++++++++++++++++ .../chat_channel/session_event_subscriber.rs | 458 ++++++++++ src-tauri/src/chat_channel/types.rs | 9 + .../entities/chat_channel_sender_context.rs | 35 + src-tauri/src/db/entities/mod.rs | 1 + src-tauri/src/db/entities/prelude.rs | 1 + ...0401_000001_chat_channel_sender_context.rs | 124 +++ src-tauri/src/db/migration/mod.rs | 2 + src-tauri/src/db/service/mod.rs | 1 + .../src/db/service/sender_context_service.rs | 101 +++ src-tauri/src/lib.rs | 4 +- .../settings/channel-commands-tab.tsx | 8 + src/i18n/messages/ar.json | 8 + src/i18n/messages/de.json | 10 +- src/i18n/messages/en.json | 8 + src/i18n/messages/es.json | 8 + src/i18n/messages/fr.json | 8 + src/i18n/messages/ja.json | 8 + src/i18n/messages/ko.json | 8 + src/i18n/messages/pt.json | 8 + src/i18n/messages/zh-CN.json | 8 + src/i18n/messages/zh-TW.json | 8 + 29 files changed, 1915 insertions(+), 14 deletions(-) create mode 100644 src-tauri/src/chat_channel/session_bridge.rs create mode 100644 src-tauri/src/chat_channel/session_commands.rs create mode 100644 src-tauri/src/chat_channel/session_event_subscriber.rs create mode 100644 src-tauri/src/db/entities/chat_channel_sender_context.rs create mode 100644 src-tauri/src/db/migration/m20260401_000001_chat_channel_sender_context.rs create mode 100644 src-tauri/src/db/service/sender_context_service.rs diff --git a/src-tauri/src/bin/codeg_server.rs b/src-tauri/src/bin/codeg_server.rs index 9ed8e5a..b013876 100644 --- a/src-tauri/src/bin/codeg_server.rs +++ b/src-tauri/src/bin/codeg_server.rs @@ -53,7 +53,12 @@ async fn main() { // Start chat channel background tasks (event subscriber, command dispatcher, scheduler, auto-connect) state .chat_channel_manager - .start_background(state.event_broadcaster.clone(), state.db.conn.clone()) + .start_background( + state.event_broadcaster.clone(), + state.db.conn.clone(), + state.connection_manager.clone_ref(), + state.emitter.clone(), + ) .await; // Build router diff --git a/src-tauri/src/chat_channel/backends/telegram.rs b/src-tauri/src/chat_channel/backends/telegram.rs index 8362e1f..59ce5f0 100644 --- a/src-tauri/src/chat_channel/backends/telegram.rs +++ b/src-tauri/src/chat_channel/backends/telegram.rs @@ -162,6 +162,9 @@ impl ChatChannelBackend for TelegramBackend { if let Ok(body) = resp.json::().await { if let Some(updates) = body.get("result").and_then(|r| r.as_array()) { + if !updates.is_empty() { + eprintln!("[Telegram] got {} update(s)", updates.len()); + } for update in updates { if let Some(uid) = update.get("update_id").and_then(|u| u.as_i64()) @@ -184,6 +187,7 @@ impl ChatChannelBackend for TelegramBackend { let at_bot = format!("@{}", bot_username); if !text.to_lowercase().contains(&at_bot) { + eprintln!("[Telegram] skipped group msg without @bot: {text}"); continue; } } @@ -196,7 +200,8 @@ impl ChatChannelBackend for TelegramBackend { .and_then(|i| i.as_i64()) .map(|i| i.to_string()) .unwrap_or_default(); - let _ = command_tx + eprintln!("[Telegram] dispatching: {clean_text}"); + let send_result = command_tx .send(IncomingCommand { channel_id, sender_id, @@ -204,9 +209,16 @@ impl ChatChannelBackend for TelegramBackend { metadata: update.clone(), }) .await; + if let Err(e) = send_result { + eprintln!("[Telegram] command_tx.send failed: {e}"); + } + } else { + eprintln!("[Telegram] update without /message/text"); } } } + } else { + eprintln!("[Telegram] failed to parse response body"); } } Err(e) => { diff --git a/src-tauri/src/chat_channel/command_dispatcher.rs b/src-tauri/src/chat_channel/command_dispatcher.rs index e7b3559..dd1f841 100644 --- a/src-tauri/src/chat_channel/command_dispatcher.rs +++ b/src-tauri/src/chat_channel/command_dispatcher.rs @@ -1,14 +1,19 @@ +use std::sync::Arc; use std::time::{Duration, Instant}; use sea_orm::DatabaseConnection; -use tokio::sync::mpsc; +use tokio::sync::{mpsc, Mutex}; use tokio::task::JoinHandle; use super::command_handlers; use super::i18n::{self, Lang}; use super::manager::ChatChannelManager; +use super::session_bridge::SessionBridge; +use super::session_commands; use super::types::IncomingCommand; +use crate::acp::manager::ConnectionManager; use crate::db::service::{app_metadata_service, chat_channel_message_log_service}; +use crate::web::event_bridge::EventEmitter; const COMMAND_PREFIX_KEY: &str = "chat_command_prefix"; const DEFAULT_COMMAND_PREFIX: &str = "/"; @@ -52,12 +57,19 @@ pub fn spawn_command_dispatcher( mut command_rx: mpsc::Receiver, manager: ChatChannelManager, db_conn: DatabaseConnection, + conn_mgr: ConnectionManager, + emitter: EventEmitter, + bridge: Arc>, ) -> JoinHandle<()> { tokio::spawn(async move { let mut config = CommandConfigCache::new(); while let Some(cmd) = command_rx.recv().await { let text = cmd.command_text.trim(); + eprintln!( + "[ChatChannel] received command from channel={} sender={}: {:?}", + cmd.channel_id, cmd.sender_id, text + ); // Log inbound command let _ = chat_channel_message_log_service::create_log( @@ -73,7 +85,25 @@ pub fn spawn_command_dispatcher( config.refresh_if_needed(&db_conn).await; - let response = dispatch_command(text, &config.prefix, &db_conn, &manager, config.lang).await; + let response = dispatch_command( + text, + &config.prefix, + &db_conn, + &manager, + &conn_mgr, + &emitter, + &bridge, + cmd.channel_id, + &cmd.sender_id, + config.lang, + ) + .await; + + eprintln!( + "[ChatChannel] dispatch result: title={:?}, body_len={}", + response.title, + response.body.len() + ); // Send response back via the same channel let send_result = manager.send_to_channel(cmd.channel_id, &response).await; @@ -102,17 +132,36 @@ pub fn spawn_command_dispatcher( }) } +#[allow(clippy::too_many_arguments)] async fn dispatch_command( text: &str, prefix: &str, db: &DatabaseConnection, manager: &ChatChannelManager, + conn_mgr: &ConnectionManager, + emitter: &EventEmitter, + bridge: &Arc>, + channel_id: i32, + sender_id: &str, lang: Lang, ) -> super::types::RichMessage { - // Strip prefix; if text doesn't start with it, show help + // Strip prefix; if text doesn't start with it, try as follow-up let without_prefix = match text.strip_prefix(prefix) { Some(rest) => rest, - None => return command_handlers::handle_help(prefix, lang), + None => { + // Check if sender has an active session for follow-up + let has_session = { + let guard = bridge.lock().await; + guard.find_by_sender(channel_id, sender_id).is_some() + }; + if has_session { + return session_commands::handle_followup( + db, text, channel_id, sender_id, conn_mgr, bridge, lang, + ) + .await; + } + return command_handlers::handle_help(prefix, lang); + } }; let parts: Vec<&str> = without_prefix.splitn(2, ' ').collect(); @@ -120,6 +169,7 @@ async fn dispatch_command( let args = parts.get(1).map(|s| s.trim()).unwrap_or(""); match command.as_str() { + // Existing commands "recent" => command_handlers::handle_recent(db, lang).await, "search" => { if args.is_empty() { @@ -140,6 +190,47 @@ async fn dispatch_command( "today" => command_handlers::handle_today(db, lang).await, "status" => command_handlers::handle_status(manager, lang).await, "help" | "start" => command_handlers::handle_help(prefix, lang), + + // Session commands + "folder" => { + session_commands::handle_folder(db, args, channel_id, sender_id, lang).await + } + "agent" => { + session_commands::handle_agent(db, args, channel_id, sender_id, lang).await + } + "task" | "do" => { + session_commands::handle_task( + db, args, channel_id, sender_id, conn_mgr, emitter, bridge, lang, + ) + .await + } + "sessions" => { + session_commands::handle_sessions(db, channel_id, sender_id, lang).await + } + "resume" => { + session_commands::handle_resume( + db, args, channel_id, sender_id, conn_mgr, emitter, bridge, lang, + ) + .await + } + "cancel" => { + session_commands::handle_cancel(db, channel_id, sender_id, conn_mgr, bridge, lang) + .await + } + "approve" => { + let always = args.eq_ignore_ascii_case("always"); + session_commands::handle_permission_response( + true, always, db, channel_id, sender_id, conn_mgr, bridge, lang, + ) + .await + } + "deny" => { + session_commands::handle_permission_response( + false, false, db, channel_id, sender_id, conn_mgr, bridge, lang, + ) + .await + } + _ => super::types::RichMessage::info(i18n::unknown_command(lang, prefix, &command)) .with_title(i18n::unknown_command_title(lang)), } diff --git a/src-tauri/src/chat_channel/i18n.rs b/src-tauri/src/chat_channel/i18n.rs index b87b806..f944e20 100644 --- a/src-tauri/src/chat_channel/i18n.rs +++ b/src-tauri/src/chat_channel/i18n.rs @@ -610,12 +610,23 @@ pub fn help_title(lang: Lang) -> &'static str { pub fn help_body(lang: Lang, prefix: &str) -> String { match lang { Lang::ZhCn => format!( - "{prefix}recent - 最近 5 条会话\n\ + "📂 {prefix}folder - 选择工作目录\n\ + 🤖 {prefix}agent - 选择 Agent\n\ + 🚀 {prefix}task <描述> - 创建会话并执行任务\n\ + 📋 {prefix}sessions - 当前目录的活跃会话\n\ + ▶️ {prefix}resume - 恢复已有会话\n\ + ⏹️ {prefix}cancel - 取消当前任务\n\ + ✅ {prefix}approve [always] - 批准权限请求\n\ + ❌ {prefix}deny - 拒绝权限请求\n\ + \n\ + {prefix}recent - 最近 5 条会话\n\ {prefix}search <关键词> - 搜索会话\n\ {prefix}detail - 会话详情\n\ {prefix}today - 今日活动汇总\n\ {prefix}status - 渠道连接状态\n\ - {prefix}help - 显示帮助" + {prefix}help - 显示帮助\n\ + \n\ + 💡 有活跃会话时,直接发文本即可继续对话" ), Lang::ZhTw => format!( "{prefix}recent - 最近 5 條對話\n\ @@ -682,12 +693,23 @@ pub fn help_body(lang: Lang, prefix: &str) -> String { {prefix}help - عرض المساعدة" ), Lang::En => format!( - "{prefix}recent - 5 most recent conversations\n\ + "📂 {prefix}folder - Select working folder\n\ + 🤖 {prefix}agent - Select agent\n\ + 🚀 {prefix}task - Create session & run task\n\ + 📋 {prefix}sessions - Active sessions in folder\n\ + ▶️ {prefix}resume - Resume a session\n\ + ⏹️ {prefix}cancel - Cancel current task\n\ + ✅ {prefix}approve [always] - Approve permission\n\ + ❌ {prefix}deny - Deny permission\n\ + \n\ + {prefix}recent - 5 most recent conversations\n\ {prefix}search - Search conversations\n\ {prefix}detail - Conversation details\n\ {prefix}today - Today's activity summary\n\ {prefix}status - Channel connection status\n\ - {prefix}help - Show help" + {prefix}help - Show help\n\ + \n\ + 💡 When a session is active, just type text to continue the conversation" ), } } diff --git a/src-tauri/src/chat_channel/manager.rs b/src-tauri/src/chat_channel/manager.rs index 974f836..0f1fd2e 100644 --- a/src-tauri/src/chat_channel/manager.rs +++ b/src-tauri/src/chat_channel/manager.rs @@ -5,9 +5,11 @@ use sea_orm::DatabaseConnection; use tokio::sync::{mpsc, Mutex}; use super::error::ChatChannelError; +use super::session_bridge::SessionBridge; use super::traits::ChatChannelBackend; use super::types::*; -use crate::web::event_bridge::WebEventBroadcaster; +use crate::acp::manager::ConnectionManager; +use crate::web::event_bridge::{EventEmitter, WebEventBroadcaster}; struct ActiveChannel { id: i32, @@ -212,28 +214,49 @@ impl ChatChannelManager { &self, broadcaster: Arc, db_conn: DatabaseConnection, + conn_mgr: ConnectionManager, + emitter: EventEmitter, ) { // Store broadcaster for status event emission *self.inner.broadcaster.lock().await = Some(broadcaster.clone()); let db_conn2 = db_conn.clone(); + // Create shared session bridge + let bridge = Arc::new(Mutex::new(SessionBridge::new())); + // Spawn event subscriber let manager_for_events = self.clone_ref(); super::event_subscriber::spawn_event_subscriber( - broadcaster, + broadcaster.clone(), manager_for_events, db_conn.clone(), ); + // Spawn session event subscriber (ACP event routing to channels) + let manager_for_session_events = self.clone_ref(); + super::session_event_subscriber::spawn_session_event_subscriber( + broadcaster, + bridge.clone(), + manager_for_session_events, + conn_mgr.clone_ref(), + db_conn.clone(), + ); + // Spawn command dispatcher if let Some(command_rx) = self.take_command_receiver().await { + eprintln!("[ChatChannel] command dispatcher started"); let manager_for_cmds = self.clone_ref(); super::command_dispatcher::spawn_command_dispatcher( command_rx, manager_for_cmds, db_conn.clone(), + conn_mgr, + emitter, + bridge, ); + } else { + eprintln!("[ChatChannel] WARNING: command_rx already taken, dispatcher NOT started"); } // Spawn daily report scheduler diff --git a/src-tauri/src/chat_channel/mod.rs b/src-tauri/src/chat_channel/mod.rs index a52edf5..87aa7d4 100644 --- a/src-tauri/src/chat_channel/mod.rs +++ b/src-tauri/src/chat_channel/mod.rs @@ -7,5 +7,8 @@ pub mod i18n; pub mod manager; pub mod message_formatter; pub mod scheduler; +pub mod session_bridge; +pub mod session_commands; +pub mod session_event_subscriber; pub mod traits; pub mod types; diff --git a/src-tauri/src/chat_channel/session_bridge.rs b/src-tauri/src/chat_channel/session_bridge.rs new file mode 100644 index 0000000..d0edaf3 --- /dev/null +++ b/src-tauri/src/chat_channel/session_bridge.rs @@ -0,0 +1,75 @@ +use std::collections::HashMap; +use std::time::Instant; + +use crate::acp::types::PermissionOptionInfo; +use crate::chat_channel::types::SentMessageId; + +pub struct PendingPermission { + pub request_id: String, + pub tool_description: String, + pub options: Vec, + pub sent_message_id: Option, +} + +pub struct ActiveSession { + pub channel_id: i32, + pub sender_id: String, + pub conversation_id: i32, + pub connection_id: String, + pub content_buffer: String, + pub tool_calls: Vec, + pub last_flushed: Instant, + pub pending_prompt: Option, + pub permission_pending: Option, +} + +#[derive(Default)] +pub struct SessionBridge { + sessions: HashMap, +} + +impl SessionBridge { + pub fn new() -> Self { + Self::default() + } + + pub fn register(&mut self, connection_id: String, session: ActiveSession) { + self.sessions.insert(connection_id, session); + } + + pub fn remove(&mut self, connection_id: &str) -> Option { + self.sessions.remove(connection_id) + } + + pub fn get(&self, connection_id: &str) -> Option<&ActiveSession> { + self.sessions.get(connection_id) + } + + pub fn get_mut(&mut self, connection_id: &str) -> Option<&mut ActiveSession> { + self.sessions.get_mut(connection_id) + } + + pub fn find_by_sender(&self, channel_id: i32, sender_id: &str) -> Option<&ActiveSession> { + self.sessions.values().find(|s| { + s.channel_id == channel_id && s.sender_id == sender_id + }) + } + + pub fn find_by_sender_mut( + &mut self, + channel_id: i32, + sender_id: &str, + ) -> Option<&mut ActiveSession> { + self.sessions.values_mut().find(|s| { + s.channel_id == channel_id && s.sender_id == sender_id + }) + } + + pub fn all_sessions(&self) -> impl Iterator { + self.sessions.values() + } + + pub fn all_sessions_mut(&mut self) -> impl Iterator { + self.sessions.values_mut() + } +} diff --git a/src-tauri/src/chat_channel/session_commands.rs b/src-tauri/src/chat_channel/session_commands.rs new file mode 100644 index 0000000..b73ba02 --- /dev/null +++ b/src-tauri/src/chat_channel/session_commands.rs @@ -0,0 +1,848 @@ +use std::collections::BTreeMap; +use std::sync::Arc; +use std::time::Instant; + +use sea_orm::DatabaseConnection; +use tokio::sync::Mutex; + +use super::i18n::Lang; +use super::session_bridge::{ActiveSession, SessionBridge}; +use super::types::RichMessage; +use crate::acp::manager::ConnectionManager; +use crate::acp::registry::all_acp_agents; +use crate::acp::types::PromptInputBlock; +use crate::db::entities::conversation; +use crate::db::service::{conversation_service, folder_service, sender_context_service}; +use crate::models::agent::AgentType; +use crate::web::event_bridge::EventEmitter; + +// ── /folder ── + +pub async fn handle_folder( + db: &DatabaseConnection, + args: &str, + channel_id: i32, + sender_id: &str, + lang: Lang, +) -> RichMessage { + if args.is_empty() { + return list_folders(db, channel_id, sender_id, lang).await; + } + + // Try parse as index (1-based) + if let Ok(idx) = args.parse::() { + return select_folder_by_index(db, idx, channel_id, sender_id, lang).await; + } + + // Treat as path + select_folder_by_path(db, args, channel_id, sender_id, lang).await +} + +async fn list_folders( + db: &DatabaseConnection, + channel_id: i32, + sender_id: &str, + lang: Lang, +) -> RichMessage { + let folders = match folder_service::list_folders(db).await { + Ok(f) => f, + Err(e) => return RichMessage::error(format!("Failed to list folders: {e}")), + }; + + if folders.is_empty() { + return RichMessage::info(t(lang, "No folders found.", "没有找到项目目录。")) + .with_title(t(lang, "Working Folder", "工作目录")); + } + + let ctx = sender_context_service::get_or_create(db, channel_id, sender_id) + .await + .ok(); + + let mut body = String::new(); + for (i, f) in folders.iter().take(10).enumerate() { + let current = ctx + .as_ref() + .and_then(|c| c.current_folder_id) + .map(|id| id == f.id) + .unwrap_or(false); + let marker = if current { " [*]" } else { "" }; + body.push_str(&format!( + "{}. {}{} ({})\n", + i + 1, + f.name, + marker, + f.path + )); + } + + body.push_str(&format!( + "\n{}", + t( + lang, + "Reply /folder to select.", + "回复 /folder <数字> 选择目录。" + ) + )); + + RichMessage::info(body.trim_end()) + .with_title(t(lang, "Working Folder", "工作目录")) +} + +async fn select_folder_by_index( + db: &DatabaseConnection, + idx: usize, + channel_id: i32, + sender_id: &str, + lang: Lang, +) -> RichMessage { + if idx == 0 { + return RichMessage::info(t(lang, "Index starts from 1.", "序号从 1 开始。")); + } + + let folders = match folder_service::list_folders(db).await { + Ok(f) => f, + Err(e) => return RichMessage::error(format!("Failed to list folders: {e}")), + }; + + let Some(folder) = folders.get(idx - 1) else { + return RichMessage::info(t( + lang, + "Index out of range. Use /folder to list.", + "序号超出范围,请使用 /folder 查看列表。", + )); + }; + + let _ = sender_context_service::update_folder(db, channel_id, sender_id, Some(folder.id)) + .await; + + RichMessage::info(format!("{} ({})", folder.name, folder.path)) + .with_title(t(lang, "Folder Selected", "已选择目录")) +} + +async fn select_folder_by_path( + db: &DatabaseConnection, + path: &str, + channel_id: i32, + sender_id: &str, + lang: Lang, +) -> RichMessage { + let entry = match folder_service::add_folder(db, path).await { + Ok(e) => e, + Err(e) => return RichMessage::error(format!("Failed to add folder: {e}")), + }; + + let _ = + sender_context_service::update_folder(db, channel_id, sender_id, Some(entry.id)).await; + + RichMessage::info(format!("{} ({})", entry.name, entry.path)) + .with_title(t(lang, "Folder Selected", "已选择目录")) +} + +// ── /agent ── + +pub async fn handle_agent( + db: &DatabaseConnection, + args: &str, + channel_id: i32, + sender_id: &str, + lang: Lang, +) -> RichMessage { + if args.is_empty() { + return list_agents(db, channel_id, sender_id, lang).await; + } + + // Try parse as index + if let Ok(idx) = args.parse::() { + return select_agent_by_index(db, idx, channel_id, sender_id, lang).await; + } + + // Try parse as agent type name + select_agent_by_name(db, args, channel_id, sender_id, lang).await +} + +async fn list_agents( + db: &DatabaseConnection, + channel_id: i32, + sender_id: &str, + lang: Lang, +) -> RichMessage { + let agents = all_acp_agents(); + let ctx = sender_context_service::get_or_create(db, channel_id, sender_id) + .await + .ok(); + + let mut body = String::new(); + for (i, at) in agents.iter().enumerate() { + let at_str = agent_type_to_string(*at); + let current = ctx + .as_ref() + .and_then(|c| c.current_agent_type.as_deref()) + .map(|s| s == at_str) + .unwrap_or(false); + let marker = if current { " [*]" } else { "" }; + body.push_str(&format!("{}. {}{}\n", i + 1, at, marker)); + } + + body.push_str(&format!( + "\n{}", + t( + lang, + "Reply /agent or /agent to select.", + "回复 /agent <数字> 或 /agent <名称> 选择。" + ) + )); + + RichMessage::info(body.trim_end()) + .with_title(t(lang, "Agent Selection", "选择 Agent")) +} + +async fn select_agent_by_index( + db: &DatabaseConnection, + idx: usize, + channel_id: i32, + sender_id: &str, + lang: Lang, +) -> RichMessage { + let agents = all_acp_agents(); + if idx == 0 || idx > agents.len() { + return RichMessage::info(t( + lang, + "Index out of range. Use /agent to list.", + "序号超出范围,请使用 /agent 查看列表。", + )); + } + + let at = agents[idx - 1]; + let at_str = agent_type_to_string(at); + let _ = sender_context_service::update_agent(db, channel_id, sender_id, Some(at_str)).await; + + RichMessage::info(at.to_string()) + .with_title(t(lang, "Agent Selected", "已选择 Agent")) +} + +async fn select_agent_by_name( + db: &DatabaseConnection, + name: &str, + channel_id: i32, + sender_id: &str, + lang: Lang, +) -> RichMessage { + let at = match parse_agent_type(name) { + Some(a) => a, + None => { + return RichMessage::info(format!( + "{}{}", + t(lang, "Unknown agent: ", "未知 Agent: "), + name + )); + } + }; + + let at_str = agent_type_to_string(at); + let _ = sender_context_service::update_agent(db, channel_id, sender_id, Some(at_str)).await; + + RichMessage::info(at.to_string()) + .with_title(t(lang, "Agent Selected", "已选择 Agent")) +} + +// ── /task ── + +#[allow(clippy::too_many_arguments)] +pub async fn handle_task( + db: &DatabaseConnection, + task_description: &str, + channel_id: i32, + sender_id: &str, + conn_mgr: &ConnectionManager, + emitter: &EventEmitter, + bridge: &Arc>, + lang: Lang, +) -> RichMessage { + if task_description.is_empty() { + return RichMessage::info(t( + lang, + "Usage: /task ", + "用法: /task <任务描述>", + )); + } + + // 1. Load sender context + let ctx = match sender_context_service::get_or_create(db, channel_id, sender_id).await { + Ok(c) => c, + Err(e) => return RichMessage::error(format!("Failed to load context: {e}")), + }; + + let folder_id = match ctx.current_folder_id { + Some(id) => id, + None => { + return RichMessage::info(t( + lang, + "No folder selected. Use /folder first.", + "未选择工作目录,请先使用 /folder 选择。", + )); + } + }; + + // 2. Get folder info + let folder = match folder_service::get_folder_by_id(db, folder_id).await { + Ok(Some(f)) => f, + _ => { + return RichMessage::info(t( + lang, + "Folder not found. Use /folder to select.", + "目录不存在,请使用 /folder 重新选择。", + )); + } + }; + + // 3. Resolve agent type + let agent_type = resolve_agent_type(&ctx.current_agent_type, &folder.default_agent_type); + + // 4. Create conversation record + let conv = match conversation_service::create( + db, + folder_id, + agent_type, + Some(truncate_title(task_description)), + folder.git_branch.clone(), + ) + .await + { + Ok(c) => c, + Err(e) => return RichMessage::error(format!("Failed to create conversation: {e}")), + }; + + // 5. Spawn ACP agent + let owner_label = format!("chat_channel:{}:{}", channel_id, sender_id); + let connection_id = match conn_mgr + .spawn_agent( + agent_type, + Some(folder.path.clone()), + None, + BTreeMap::new(), + owner_label, + emitter.clone(), + ) + .await + { + Ok(id) => id, + Err(e) => { + // Clean up the conversation record + let _ = conversation_service::update_status( + db, + conv.id, + conversation::ConversationStatus::Cancelled, + ) + .await; + return RichMessage::error(format!( + "{}{e}", + t(lang, "Failed to start agent: ", "启动 Agent 失败: ") + )); + } + }; + + // 6. Register in bridge (prompt will be sent after SessionStarted event) + { + let session = ActiveSession { + channel_id, + sender_id: sender_id.to_string(), + conversation_id: conv.id, + connection_id: connection_id.clone(), + content_buffer: String::new(), + tool_calls: Vec::new(), + last_flushed: Instant::now(), + pending_prompt: Some(task_description.to_string()), + permission_pending: None, + }; + bridge.lock().await.register(connection_id.clone(), session); + } + + // 7. Update sender context + let _ = sender_context_service::update_session( + db, + channel_id, + sender_id, + Some(conv.id), + Some(connection_id), + ) + .await; + + RichMessage::info(format!( + "[{}] #{} @ {}", + agent_type, conv.id, folder.name, + )) + .with_title(t(lang, "Task Started", "任务已启动")) +} + +// ── /sessions ── + +pub async fn handle_sessions( + db: &DatabaseConnection, + channel_id: i32, + sender_id: &str, + lang: Lang, +) -> RichMessage { + let ctx = match sender_context_service::get_or_create(db, channel_id, sender_id).await { + Ok(c) => c, + Err(e) => return RichMessage::error(format!("Failed to load context: {e}")), + }; + + let folder_id = match ctx.current_folder_id { + Some(id) => id, + None => { + return RichMessage::info(t( + lang, + "No folder selected. Use /folder first.", + "未选择工作目录,请先使用 /folder 选择。", + )); + } + }; + + let folder = match folder_service::get_folder_by_id(db, folder_id).await { + Ok(Some(f)) => f, + _ => { + return RichMessage::info(t( + lang, + "Folder not found.", + "目录不存在。", + )); + } + }; + + let convs = match conversation_service::list_by_folder( + db, + folder_id, + None, + None, + None, + Some("in_progress".to_string()), + ) + .await + { + Ok(c) => c, + Err(e) => return RichMessage::error(format!("Failed to list sessions: {e}")), + }; + + if convs.is_empty() { + return RichMessage::info(t( + lang, + "No active sessions in this folder.", + "当前目录没有进行中的会话。", + )) + .with_title(format!( + "{} - {}", + t(lang, "Sessions", "会话列表"), + folder.name + )); + } + + let mut body = String::new(); + for (i, c) in convs.iter().take(10).enumerate() { + let title = c.title.as_deref().unwrap_or("(untitled)"); + let current = ctx + .current_conversation_id + .map(|id| id == c.id) + .unwrap_or(false); + let marker = if current { " [*]" } else { "" }; + body.push_str(&format!( + "{}. [{}] {} (#{}){} \n", + i + 1, + c.agent_type, + title, + c.id, + marker, + )); + } + + body.push_str(&format!( + "\n{}", + t( + lang, + "Reply /resume to continue.", + "回复 /resume 继续会话。" + ) + )); + + RichMessage::info(body.trim_end()).with_title(format!( + "{} - {}", + t(lang, "Sessions", "会话列表"), + folder.name + )) +} + +// ── /resume ── + +#[allow(clippy::too_many_arguments)] +pub async fn handle_resume( + db: &DatabaseConnection, + args: &str, + channel_id: i32, + sender_id: &str, + conn_mgr: &ConnectionManager, + emitter: &EventEmitter, + bridge: &Arc>, + lang: Lang, +) -> RichMessage { + let conversation_id: i32 = match args.parse() { + Ok(id) => id, + Err(_) => { + return RichMessage::info(t( + lang, + "Usage: /resume ", + "用法: /resume <会话ID>", + )); + } + }; + + let conv = match conversation_service::get_by_id(db, conversation_id).await { + Ok(c) => c, + Err(_) => { + return RichMessage::info(t( + lang, + "Conversation not found.", + "会话不存在。", + )); + } + }; + + let folder = match folder_service::get_folder_by_id(db, conv.folder_id).await { + Ok(Some(f)) => f, + _ => { + return RichMessage::info(t(lang, "Folder not found.", "目录不存在。")); + } + }; + + // Spawn agent with session_id for resume + let owner_label = format!("chat_channel:{}:{}", channel_id, sender_id); + let connection_id = match conn_mgr + .spawn_agent( + conv.agent_type, + Some(folder.path.clone()), + conv.external_id.clone(), + BTreeMap::new(), + owner_label, + emitter.clone(), + ) + .await + { + Ok(id) => id, + Err(e) => { + return RichMessage::error(format!( + "{}{e}", + t(lang, "Failed to start agent: ", "启动 Agent 失败: ") + )); + } + }; + + // Register in bridge (no pending prompt for resume) + { + let session = ActiveSession { + channel_id, + sender_id: sender_id.to_string(), + conversation_id: conv.id, + connection_id: connection_id.clone(), + content_buffer: String::new(), + tool_calls: Vec::new(), + last_flushed: Instant::now(), + pending_prompt: None, + permission_pending: None, + }; + bridge.lock().await.register(connection_id.clone(), session); + } + + // Update sender context + let _ = sender_context_service::update_session( + db, + channel_id, + sender_id, + Some(conv.id), + Some(connection_id), + ) + .await; + let _ = sender_context_service::update_folder(db, channel_id, sender_id, Some(conv.folder_id)) + .await; + + let title = conv.title.as_deref().unwrap_or("(untitled)"); + RichMessage::info(format!( + "[{}] #{} {} @ {}", + conv.agent_type, conv.id, title, folder.name, + )) + .with_title(t(lang, "Session Resumed", "会话已恢复")) +} + +// ── /cancel ── + +pub async fn handle_cancel( + db: &DatabaseConnection, + channel_id: i32, + sender_id: &str, + conn_mgr: &ConnectionManager, + bridge: &Arc>, + lang: Lang, +) -> RichMessage { + let ctx = match sender_context_service::get_or_create(db, channel_id, sender_id).await { + Ok(c) => c, + Err(e) => return RichMessage::error(format!("Failed to load context: {e}")), + }; + + let connection_id = match &ctx.current_connection_id { + Some(id) => id.clone(), + None => { + return RichMessage::info(t( + lang, + "No active session to cancel.", + "没有进行中的任务可取消。", + )); + } + }; + + // Cancel the ACP connection + let _ = conn_mgr.cancel(&connection_id).await; + + // Remove from bridge + bridge.lock().await.remove(&connection_id); + + // Update conversation status + if let Some(conv_id) = ctx.current_conversation_id { + let _ = conversation_service::update_status( + db, + conv_id, + conversation::ConversationStatus::Cancelled, + ) + .await; + } + + // Clear session from context + let _ = sender_context_service::clear_session(db, channel_id, sender_id).await; + + RichMessage::info(t( + lang, + "Current task has been cancelled.", + "当前任务已取消。", + )) + .with_title(t(lang, "Task Cancelled", "任务已取消")) +} + +// ── /approve, /deny ── + +#[allow(clippy::too_many_arguments)] +pub async fn handle_permission_response( + approve: bool, + always: bool, + db: &DatabaseConnection, + channel_id: i32, + sender_id: &str, + conn_mgr: &ConnectionManager, + bridge: &Arc>, + lang: Lang, +) -> RichMessage { + let ctx = match sender_context_service::get_or_create(db, channel_id, sender_id).await { + Ok(c) => c, + Err(e) => return RichMessage::error(format!("Failed to load context: {e}")), + }; + + let connection_id = match &ctx.current_connection_id { + Some(id) => id.clone(), + None => { + return RichMessage::info(t( + lang, + "No active session.", + "没有活跃的会话。", + )); + } + }; + + let pending = { + let mut bridge_guard = bridge.lock().await; + let session = match bridge_guard.get_mut(&connection_id) { + Some(s) => s, + None => { + return RichMessage::info(t( + lang, + "No active session found.", + "未找到活跃的会话。", + )); + } + }; + session.permission_pending.take() + }; + + let pending = match pending { + Some(p) => p, + None => { + return RichMessage::info(t( + lang, + "No pending permission request.", + "没有待处理的权限请求。", + )); + } + }; + + // Find the appropriate option_id + let option_id = if approve { + pending + .options + .iter() + .find(|o| o.kind == "allow" || o.kind == "allowForSession") + .or_else(|| pending.options.first()) + .map(|o| o.option_id.clone()) + } else { + pending + .options + .iter() + .find(|o| o.kind == "deny") + .or_else(|| pending.options.last()) + .map(|o| o.option_id.clone()) + }; + + let Some(option_id) = option_id else { + return RichMessage::info(t( + lang, + "No valid permission option found.", + "未找到有效的权限选项。", + )); + }; + + if let Err(e) = conn_mgr + .respond_permission(&connection_id, &pending.request_id, &option_id) + .await + { + return RichMessage::error(format!( + "{}{e}", + t( + lang, + "Failed to respond to permission: ", + "权限响应失败: " + ) + )); + } + + // Update auto_approve if requested + if always && approve { + let _ = + sender_context_service::update_auto_approve(db, channel_id, sender_id, true).await; + } + + let action = if approve { + t(lang, "Approved", "已批准") + } else { + t(lang, "Denied", "已拒绝") + }; + + let mut msg = RichMessage::info(format!("{}: {}", action, pending.tool_description)); + if always && approve { + msg = msg.with_field( + "", + t( + lang, + "Auto-approve enabled for this session.", + "已启用自动批准。", + ), + ); + } + msg.with_title(t(lang, "Permission Response", "权限响应")) +} + +// ── follow-up (non-command text) ── + +pub async fn handle_followup( + db: &DatabaseConnection, + text: &str, + channel_id: i32, + sender_id: &str, + conn_mgr: &ConnectionManager, + bridge: &Arc>, + lang: Lang, +) -> RichMessage { + let ctx = match sender_context_service::get_or_create(db, channel_id, sender_id).await { + Ok(c) => c, + Err(e) => return RichMessage::error(format!("Failed to load context: {e}")), + }; + + let connection_id = match &ctx.current_connection_id { + Some(id) => id.clone(), + None => { + return RichMessage::info(t( + lang, + "No active session. Use /task to start one.", + "没有活跃的会话,请使用 /task 开始新任务。", + )); + } + }; + + // Check connection exists in bridge + { + let bridge_guard = bridge.lock().await; + if bridge_guard.get(&connection_id).is_none() { + // Connection lost, clear context + drop(bridge_guard); + let _ = sender_context_service::clear_session(db, channel_id, sender_id).await; + return RichMessage::info(t( + lang, + "Session connection lost. Use /task to start a new one.", + "会话连接已断开,请使用 /task 开始新任务。", + )); + } + } + + // Send prompt to agent + let blocks = vec![PromptInputBlock::Text { + text: text.to_string(), + }]; + + if let Err(e) = conn_mgr.send_prompt(&connection_id, blocks).await { + // Connection may have died + bridge.lock().await.remove(&connection_id); + let _ = sender_context_service::clear_session(db, channel_id, sender_id).await; + return RichMessage::error(format!( + "{}{e}", + t(lang, "Failed to send message: ", "发送消息失败: ") + )); + } + + RichMessage::info(t(lang, "Message sent.", "消息已发送。")) +} + +// ── Helpers ── + +fn t(lang: Lang, en: &str, zh: &str) -> String { + match lang { + Lang::ZhCn | Lang::ZhTw => zh.to_string(), + _ => en.to_string(), + } +} + +fn agent_type_to_string(at: AgentType) -> String { + serde_json::to_value(at) + .ok() + .and_then(|v| v.as_str().map(String::from)) + .unwrap_or_default() +} + +fn parse_agent_type(name: &str) -> Option { + let normalized = name.to_lowercase().replace([' ', '-'], "_"); + serde_json::from_value(serde_json::Value::String(normalized)).ok() +} + +fn resolve_agent_type( + sender_agent: &Option, + folder_default: &Option, +) -> AgentType { + if let Some(ref at_str) = sender_agent { + if let Some(at) = parse_agent_type(at_str) { + return at; + } + } + if let Some(at) = folder_default { + return *at; + } + AgentType::ClaudeCode +} + +fn truncate_title(s: &str) -> String { + if s.len() <= 80 { + s.to_string() + } else { + format!("{}...", &s[..77]) + } +} diff --git a/src-tauri/src/chat_channel/session_event_subscriber.rs b/src-tauri/src/chat_channel/session_event_subscriber.rs new file mode 100644 index 0000000..3343629 --- /dev/null +++ b/src-tauri/src/chat_channel/session_event_subscriber.rs @@ -0,0 +1,458 @@ +use std::sync::Arc; +use std::time::{Duration, Instant}; + +use sea_orm::DatabaseConnection; +use tokio::sync::Mutex; +use tokio::task::JoinHandle; + +use super::i18n::Lang; +use super::session_bridge::{PendingPermission, SessionBridge}; +use super::types::{MessageLevel, RichMessage}; +use crate::acp::manager::ConnectionManager; +use crate::acp::types::PromptInputBlock; +use crate::db::service::{ + app_metadata_service, conversation_service, sender_context_service, +}; +use crate::web::event_bridge::WebEventBroadcaster; + +use super::manager::ChatChannelManager; + +const FLUSH_INTERVAL_SECS: u64 = 5; +const BUFFER_FLUSH_THRESHOLD: usize = 500; +const MAX_MESSAGE_LEN: usize = 2000; +const MESSAGE_LANGUAGE_KEY: &str = "chat_message_language"; + +pub fn spawn_session_event_subscriber( + broadcaster: Arc, + bridge: Arc>, + manager: ChatChannelManager, + conn_mgr: ConnectionManager, + db_conn: DatabaseConnection, +) -> JoinHandle<()> { + let mut rx = broadcaster.subscribe(); + + tokio::spawn(async move { + let mut last_heartbeat = Instant::now(); + + loop { + tokio::select! { + result = rx.recv() => { + let event = match result { + Ok(e) => e, + Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => { + eprintln!("[SessionEventSub] lagged {n} events"); + continue; + } + Err(_) => break, + }; + + if event.channel == "acp://event" { + handle_acp_event_payload( + &event.payload, + &bridge, + &manager, + &conn_mgr, + &db_conn, + ) + .await; + } + } + _ = tokio::time::sleep(Duration::from_secs(FLUSH_INTERVAL_SECS)) => { + if last_heartbeat.elapsed() >= Duration::from_secs(FLUSH_INTERVAL_SECS) { + flush_progress(&bridge, &manager).await; + last_heartbeat = Instant::now(); + } + } + } + } + }) +} + +async fn get_lang(db: &DatabaseConnection) -> Lang { + app_metadata_service::get_value(db, MESSAGE_LANGUAGE_KEY) + .await + .ok() + .flatten() + .map(|v| Lang::from_str_lossy(&v)) + .unwrap_or_default() +} + +async fn handle_acp_event_payload( + payload: &serde_json::Value, + bridge: &Arc>, + manager: &ChatChannelManager, + conn_mgr: &ConnectionManager, + db: &DatabaseConnection, +) { + let event_type = match payload.get("type").and_then(|v| v.as_str()) { + Some(t) => t, + None => return, + }; + let connection_id = match payload.get("connection_id").and_then(|v| v.as_str()) { + Some(id) => id, + None => return, + }; + + match event_type { + "session_started" => { + let session_id = payload + .get("session_id") + .and_then(|v| v.as_str()) + .unwrap_or(""); + + let mut guard = bridge.lock().await; + if let Some(session) = guard.get_mut(connection_id) { + let _ = conversation_service::update_external_id( + db, + session.conversation_id, + session_id.to_string(), + ) + .await; + + if let Some(prompt_text) = session.pending_prompt.take() { + let blocks = vec![PromptInputBlock::Text { text: prompt_text }]; + if let Err(e) = conn_mgr.send_prompt(connection_id, blocks).await { + eprintln!("[SessionEventSub] failed to send pending prompt: {e}"); + let channel_id = session.channel_id; + let msg = RichMessage::error(format!("Failed to send task: {e}")); + let _ = manager.send_to_channel(channel_id, &msg).await; + } + } + } + } + + "content_delta" => { + let text = payload + .get("text") + .and_then(|v| v.as_str()) + .unwrap_or(""); + + let mut guard = bridge.lock().await; + if let Some(session) = guard.get_mut(connection_id) { + session.content_buffer.push_str(text); + + if session.content_buffer.len() >= BUFFER_FLUSH_THRESHOLD + && session.last_flushed.elapsed() >= Duration::from_secs(2) + { + let channel_id = session.channel_id; + let buf_len = session.content_buffer.len(); + let last_tool = session.tool_calls.last().cloned(); + session.last_flushed = Instant::now(); + + let mut status = format!("... ({buf_len} chars)"); + if let Some(tool) = last_tool { + status.push_str(&format!(" | {tool}")); + } + drop(guard); + + let msg = RichMessage::info(status); + let _ = manager.send_to_channel(channel_id, &msg).await; + } + } + } + + "tool_call" => { + let title = payload + .get("title") + .and_then(|v| v.as_str()) + .unwrap_or("tool"); + let status = payload + .get("status") + .and_then(|v| v.as_str()) + .unwrap_or(""); + + let mut guard = bridge.lock().await; + if let Some(session) = guard.get_mut(connection_id) { + session.tool_calls.push(title.to_string()); + let channel_id = session.channel_id; + drop(guard); + + if status != "completed" { + let msg = RichMessage::info(format!(">> {title}")); + let _ = manager.send_to_channel(channel_id, &msg).await; + } + } + } + + "tool_call_update" => { + let title = payload.get("title").and_then(|v| v.as_str()); + let status = payload.get("status").and_then(|v| v.as_str()); + + if let (Some(title), Some("completed")) = (title, status) { + let guard = bridge.lock().await; + if let Some(session) = guard.get(connection_id) { + let channel_id = session.channel_id; + drop(guard); + + let msg = RichMessage::info(format!(">> {title} [done]")); + let _ = manager.send_to_channel(channel_id, &msg).await; + } + } + } + + "permission_request" => { + let request_id = payload + .get("request_id") + .and_then(|v| v.as_str()) + .unwrap_or(""); + let tool_call = payload + .get("tool_call") + .cloned() + .unwrap_or(serde_json::Value::Null); + let options: Vec = payload + .get("options") + .and_then(|v| serde_json::from_value(v.clone()).ok()) + .unwrap_or_default(); + + let mut guard = bridge.lock().await; + if let Some(session) = guard.get_mut(connection_id) { + let channel_id = session.channel_id; + let sender_id = session.sender_id.clone(); + + let auto_approve = sender_context_service::get_or_create( + db, + channel_id, + &sender_id, + ) + .await + .map(|ctx| ctx.auto_approve) + .unwrap_or(false); + + if auto_approve { + let option_id = options + .iter() + .find(|o| o.kind == "allow" || o.kind == "allowForSession") + .or_else(|| options.first()) + .map(|o| o.option_id.clone()); + + drop(guard); + + if let Some(oid) = option_id { + let _ = conn_mgr + .respond_permission(connection_id, request_id, &oid) + .await; + } + return; + } + + let tool_desc = tool_call + .get("title") + .and_then(|v| v.as_str()) + .or_else(|| tool_call.get("tool_name").and_then(|v| v.as_str())) + .unwrap_or("Unknown tool") + .to_string(); + + session.permission_pending = Some(PendingPermission { + request_id: request_id.to_string(), + tool_description: tool_desc.clone(), + options, + sent_message_id: None, + }); + + drop(guard); + + let lang = get_lang(db).await; + let body = match lang { + Lang::ZhCn | Lang::ZhTw => { + format!("Agent 请求权限: {tool_desc}\n\n/approve 批准 | /deny 拒绝 | /approve always 自动批准") + } + _ => { + format!("Agent requests permission: {tool_desc}\n\n/approve | /deny | /approve always") + } + }; + + let msg = RichMessage { + title: Some(match lang { + Lang::ZhCn | Lang::ZhTw => "权限请求".to_string(), + _ => "Permission Request".to_string(), + }), + body, + fields: Vec::new(), + level: MessageLevel::Warning, + }; + let _ = manager.send_to_channel(channel_id, &msg).await; + } + } + + "turn_complete" => { + let stop_reason = payload + .get("stop_reason") + .and_then(|v| v.as_str()) + .unwrap_or("unknown"); + let agent_type = payload + .get("agent_type") + .and_then(|v| v.as_str()) + .unwrap_or("Unknown"); + + let mut guard = bridge.lock().await; + if let Some(session) = guard.get_mut(connection_id) { + let channel_id = session.channel_id; + let conv_id = session.conversation_id; + let content = std::mem::take(&mut session.content_buffer); + let tool_count = session.tool_calls.len(); + session.tool_calls.clear(); + session.last_flushed = Instant::now(); + drop(guard); + + let lang = get_lang(db).await; + let body = format_completion(&content, tool_count, lang); + + let msg = RichMessage::info(body) + .with_title(match lang { + Lang::ZhCn | Lang::ZhTw => "任务完成", + _ => "Turn Complete", + }) + .with_field("Agent", agent_type) + .with_field( + match lang { + Lang::ZhCn | Lang::ZhTw => "结束原因", + _ => "Stop Reason", + }, + stop_reason, + ); + + let _ = manager.send_to_channel(channel_id, &msg).await; + + if stop_reason == "end_turn" { + let _ = conversation_service::update_status( + db, + conv_id, + crate::db::entities::conversation::ConversationStatus::Completed, + ) + .await; + } + } + } + + "error" => { + let message = payload + .get("message") + .and_then(|v| v.as_str()) + .unwrap_or("Unknown error"); + let agent_type = payload + .get("agent_type") + .and_then(|v| v.as_str()) + .unwrap_or("Unknown"); + + let mut guard = bridge.lock().await; + if let Some(session) = guard.remove(connection_id) { + let channel_id = session.channel_id; + let sender_id = session.sender_id.clone(); + let conv_id = session.conversation_id; + drop(guard); + + let lang = get_lang(db).await; + let msg = RichMessage { + title: Some(match lang { + Lang::ZhCn | Lang::ZhTw => "Agent 错误".to_string(), + _ => "Agent Error".to_string(), + }), + body: format!("[{agent_type}] {message}"), + fields: Vec::new(), + level: MessageLevel::Error, + }; + let _ = manager.send_to_channel(channel_id, &msg).await; + + let _ = conversation_service::update_status( + db, + conv_id, + crate::db::entities::conversation::ConversationStatus::Cancelled, + ) + .await; + let _ = + sender_context_service::clear_session(db, channel_id, &sender_id).await; + } + } + + "status_changed" => { + let status = payload + .get("status") + .and_then(|v| v.as_str()) + .unwrap_or(""); + + if status == "disconnected" || status == "error" { + let mut guard = bridge.lock().await; + if let Some(session) = guard.remove(connection_id) { + let channel_id = session.channel_id; + let sender_id = session.sender_id.clone(); + drop(guard); + + let _ = + sender_context_service::clear_session(db, channel_id, &sender_id).await; + } + } + } + + _ => {} + } +} + +async fn flush_progress(bridge: &Arc>, manager: &ChatChannelManager) { + let updates: Vec<(i32, String)> = { + let mut guard = bridge.lock().await; + let mut out = Vec::new(); + for session in guard.all_sessions_mut() { + if !session.content_buffer.is_empty() + && session.last_flushed.elapsed() >= Duration::from_secs(FLUSH_INTERVAL_SECS) + { + let buf_len = session.content_buffer.len(); + let tool_count = session.tool_calls.len(); + session.last_flushed = Instant::now(); + out.push(( + session.channel_id, + format!("... ({buf_len} chars, {tool_count} tools)"), + )); + } + } + out + }; + + for (channel_id, text) in updates { + let msg = RichMessage::info(text); + let _ = manager.send_to_channel(channel_id, &msg).await; + } +} + +fn format_completion(content: &str, tool_count: usize, lang: Lang) -> String { + if content.is_empty() { + return match lang { + Lang::ZhCn | Lang::ZhTw => format!("(无文本输出, {tool_count} 次工具调用)"), + _ => format!("(No text output, {tool_count} tool calls)"), + }; + } + + if content.len() <= MAX_MESSAGE_LEN { + let mut body = content.to_string(); + if tool_count > 0 { + body.push_str(&format!( + "\n\n[{} {}]", + tool_count, + match lang { + Lang::ZhCn | Lang::ZhTw => "次工具调用", + _ => "tool calls", + } + )); + } + return body; + } + + // Truncate long content + let head = &content[..500.min(content.len())]; + let tail_start = content.len().saturating_sub(500); + let tail = &content[tail_start..]; + + match lang { + Lang::ZhCn | Lang::ZhTw => { + format!( + "{head}\n\n...\n\n{tail}\n\n[完整回复: {} 字符, {tool_count} 次工具调用]", + content.len() + ) + } + _ => { + format!( + "{head}\n\n...\n\n{tail}\n\n[Full response: {} chars, {tool_count} tool calls]", + content.len() + ) + } + } +} diff --git a/src-tauri/src/chat_channel/types.rs b/src-tauri/src/chat_channel/types.rs index fe8533c..edc4fcf 100644 --- a/src-tauri/src/chat_channel/types.rs +++ b/src-tauri/src/chat_channel/types.rs @@ -74,6 +74,15 @@ impl RichMessage { } } + pub fn error(body: impl Into) -> Self { + Self { + title: None, + body: body.into(), + fields: Vec::new(), + level: MessageLevel::Error, + } + } + pub fn with_title(mut self, title: impl Into) -> Self { self.title = Some(title.into()); self diff --git a/src-tauri/src/db/entities/chat_channel_sender_context.rs b/src-tauri/src/db/entities/chat_channel_sender_context.rs new file mode 100644 index 0000000..fdd99bf --- /dev/null +++ b/src-tauri/src/db/entities/chat_channel_sender_context.rs @@ -0,0 +1,35 @@ +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] +#[sea_orm(table_name = "chat_channel_sender_context")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub channel_id: i32, + pub sender_id: String, + pub current_folder_id: Option, + pub current_agent_type: Option, + pub current_conversation_id: Option, + pub current_connection_id: Option, + pub auto_approve: bool, + pub created_at: DateTimeUtc, + pub updated_at: DateTimeUtc, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::chat_channel::Entity", + from = "Column::ChannelId", + to = "super::chat_channel::Column::Id" + )] + ChatChannel, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::ChatChannel.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/src-tauri/src/db/entities/mod.rs b/src-tauri/src/db/entities/mod.rs index b826120..66b4c61 100644 --- a/src-tauri/src/db/entities/mod.rs +++ b/src-tauri/src/db/entities/mod.rs @@ -2,6 +2,7 @@ pub mod agent_setting; pub mod app_metadata; pub mod chat_channel; pub mod chat_channel_message_log; +pub mod chat_channel_sender_context; pub mod conversation; pub mod folder; pub mod folder_command; diff --git a/src-tauri/src/db/entities/prelude.rs b/src-tauri/src/db/entities/prelude.rs index a8a775d..ae0d71f 100644 --- a/src-tauri/src/db/entities/prelude.rs +++ b/src-tauri/src/db/entities/prelude.rs @@ -4,6 +4,7 @@ pub use super::agent_setting::Entity as AgentSetting; pub use super::app_metadata::Entity as AppMetadata; pub use super::chat_channel::Entity as ChatChannel; pub use super::chat_channel_message_log::Entity as ChatChannelMessageLog; +pub use super::chat_channel_sender_context::Entity as ChatChannelSenderContext; pub use super::conversation::Entity as Conversation; pub use super::folder::Entity as Folder; pub use super::folder_command::Entity as FolderCommand; diff --git a/src-tauri/src/db/migration/m20260401_000001_chat_channel_sender_context.rs b/src-tauri/src/db/migration/m20260401_000001_chat_channel_sender_context.rs new file mode 100644 index 0000000..7956741 --- /dev/null +++ b/src-tauri/src/db/migration/m20260401_000001_chat_channel_sender_context.rs @@ -0,0 +1,124 @@ +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(ChatChannelSenderContext::Table) + .if_not_exists() + .col( + ColumnDef::new(ChatChannelSenderContext::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col( + ColumnDef::new(ChatChannelSenderContext::ChannelId) + .integer() + .not_null(), + ) + .col( + ColumnDef::new(ChatChannelSenderContext::SenderId) + .string() + .not_null(), + ) + .col( + ColumnDef::new(ChatChannelSenderContext::CurrentFolderId) + .integer() + .null(), + ) + .col( + ColumnDef::new(ChatChannelSenderContext::CurrentAgentType) + .string() + .null(), + ) + .col( + ColumnDef::new(ChatChannelSenderContext::CurrentConversationId) + .integer() + .null(), + ) + .col( + ColumnDef::new(ChatChannelSenderContext::CurrentConnectionId) + .string() + .null(), + ) + .col( + ColumnDef::new(ChatChannelSenderContext::AutoApprove) + .boolean() + .not_null() + .default(false), + ) + .col( + ColumnDef::new(ChatChannelSenderContext::CreatedAt) + .timestamp_with_time_zone() + .not_null(), + ) + .col( + ColumnDef::new(ChatChannelSenderContext::UpdatedAt) + .timestamp_with_time_zone() + .not_null(), + ) + .foreign_key( + ForeignKey::create() + .name("fk_ccsc_channel_id") + .from( + ChatChannelSenderContext::Table, + ChatChannelSenderContext::ChannelId, + ) + .to(ChatChannel::Table, ChatChannel::Id) + .on_delete(ForeignKeyAction::Cascade), + ) + .to_owned(), + ) + .await?; + + manager + .create_index( + Index::create() + .name("idx_ccsc_channel_sender") + .table(ChatChannelSenderContext::Table) + .col(ChatChannelSenderContext::ChannelId) + .col(ChatChannelSenderContext::SenderId) + .unique() + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table( + Table::drop() + .table(ChatChannelSenderContext::Table) + .to_owned(), + ) + .await + } +} + +#[derive(DeriveIden)] +enum ChatChannelSenderContext { + Table, + Id, + ChannelId, + SenderId, + CurrentFolderId, + CurrentAgentType, + CurrentConversationId, + CurrentConnectionId, + AutoApprove, + CreatedAt, + UpdatedAt, +} + +#[derive(DeriveIden)] +enum ChatChannel { + Table, + Id, +} diff --git a/src-tauri/src/db/migration/mod.rs b/src-tauri/src/db/migration/mod.rs index 6e2fa02..12b10e0 100644 --- a/src-tauri/src/db/migration/mod.rs +++ b/src-tauri/src/db/migration/mod.rs @@ -6,6 +6,7 @@ mod m20260221_000001_folder_is_open; mod m20260226_000001_agent_setting; mod m20260227_000001_folder_parent_branch; mod m20260330_000001_chat_channel; +mod m20260401_000001_chat_channel_sender_context; pub struct Migrator; #[async_trait::async_trait] @@ -18,6 +19,7 @@ impl MigratorTrait for Migrator { Box::new(m20260226_000001_agent_setting::Migration), Box::new(m20260227_000001_folder_parent_branch::Migration), Box::new(m20260330_000001_chat_channel::Migration), + Box::new(m20260401_000001_chat_channel_sender_context::Migration), ] } } diff --git a/src-tauri/src/db/service/mod.rs b/src-tauri/src/db/service/mod.rs index 97949ec..a23c6d1 100644 --- a/src-tauri/src/db/service/mod.rs +++ b/src-tauri/src/db/service/mod.rs @@ -6,3 +6,4 @@ pub mod conversation_service; pub mod folder_command_service; pub mod folder_service; pub mod import_service; +pub mod sender_context_service; diff --git a/src-tauri/src/db/service/sender_context_service.rs b/src-tauri/src/db/service/sender_context_service.rs new file mode 100644 index 0000000..97aae58 --- /dev/null +++ b/src-tauri/src/db/service/sender_context_service.rs @@ -0,0 +1,101 @@ +use chrono::Utc; +use sea_orm::{ + ActiveModelTrait, ActiveValue::NotSet, ColumnTrait, DatabaseConnection, EntityTrait, + IntoActiveModel, QueryFilter, Set, +}; + +use crate::db::entities::chat_channel_sender_context; +use crate::db::error::DbError; + +pub async fn get_or_create( + conn: &DatabaseConnection, + channel_id: i32, + sender_id: &str, +) -> Result { + let existing = chat_channel_sender_context::Entity::find() + .filter(chat_channel_sender_context::Column::ChannelId.eq(channel_id)) + .filter(chat_channel_sender_context::Column::SenderId.eq(sender_id)) + .one(conn) + .await?; + + if let Some(model) = existing { + return Ok(model); + } + + let now = Utc::now(); + let active = chat_channel_sender_context::ActiveModel { + id: NotSet, + channel_id: Set(channel_id), + sender_id: Set(sender_id.to_string()), + current_folder_id: Set(None), + current_agent_type: Set(None), + current_conversation_id: Set(None), + current_connection_id: Set(None), + auto_approve: Set(false), + created_at: Set(now), + updated_at: Set(now), + }; + Ok(active.insert(conn).await?) +} + +pub async fn update_folder( + conn: &DatabaseConnection, + channel_id: i32, + sender_id: &str, + folder_id: Option, +) -> Result { + let model = get_or_create(conn, channel_id, sender_id).await?; + let mut active = model.into_active_model(); + active.current_folder_id = Set(folder_id); + active.updated_at = Set(Utc::now()); + Ok(active.update(conn).await?) +} + +pub async fn update_agent( + conn: &DatabaseConnection, + channel_id: i32, + sender_id: &str, + agent_type: Option, +) -> Result { + let model = get_or_create(conn, channel_id, sender_id).await?; + let mut active = model.into_active_model(); + active.current_agent_type = Set(agent_type); + active.updated_at = Set(Utc::now()); + Ok(active.update(conn).await?) +} + +pub async fn update_session( + conn: &DatabaseConnection, + channel_id: i32, + sender_id: &str, + conversation_id: Option, + connection_id: Option, +) -> Result { + let model = get_or_create(conn, channel_id, sender_id).await?; + let mut active = model.into_active_model(); + active.current_conversation_id = Set(conversation_id); + active.current_connection_id = Set(connection_id); + active.updated_at = Set(Utc::now()); + Ok(active.update(conn).await?) +} + +pub async fn clear_session( + conn: &DatabaseConnection, + channel_id: i32, + sender_id: &str, +) -> Result { + update_session(conn, channel_id, sender_id, None, None).await +} + +pub async fn update_auto_approve( + conn: &DatabaseConnection, + channel_id: i32, + sender_id: &str, + auto_approve: bool, +) -> Result { + let model = get_or_create(conn, channel_id, sender_id).await?; + let mut active = model.into_active_model(); + active.auto_approve = Set(auto_approve); + active.updated_at = Set(Utc::now()); + Ok(active.update(conn).await?) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 59e7b17..ab22db5 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -90,8 +90,10 @@ mod tauri_app { let db_conn = app.state::().conn.clone(); let ccm_ref = ccm.clone_ref(); let br = broadcaster.inner().clone(); + let cm = app.state::().clone_ref(); + let emitter = web::event_bridge::EventEmitter::Tauri(app.handle().clone()); tauri::async_runtime::spawn(async move { - ccm_ref.start_background(br, db_conn).await; + ccm_ref.start_background(br, db_conn, cm, emitter).await; }); } diff --git a/src/components/settings/channel-commands-tab.tsx b/src/components/settings/channel-commands-tab.tsx index 412f71d..bf8cc6f 100644 --- a/src/components/settings/channel-commands-tab.tsx +++ b/src/components/settings/channel-commands-tab.tsx @@ -10,6 +10,14 @@ import { Input } from "@/components/ui/input" import { getChatCommandPrefix, setChatCommandPrefix } from "@/lib/api" const BUILT_IN_COMMANDS = [ + { name: "folder [n|path]", descKey: "folderDesc" }, + { name: "agent [n|name]", descKey: "agentDesc" }, + { name: "task ", descKey: "taskDesc" }, + { name: "sessions", descKey: "sessionsDesc" }, + { name: "resume ", descKey: "resumeDesc" }, + { name: "cancel", descKey: "cancelDesc" }, + { name: "approve [always]", descKey: "approveDesc" }, + { name: "deny", descKey: "denyDesc" }, { name: "recent", descKey: "recentDesc" }, { name: "search ", descKey: "searchDesc" }, { name: "detail ", descKey: "detailDesc" }, diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index 152eb51..a0497e1 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -1728,6 +1728,14 @@ "prefixSaveFailed": "فشل حفظ بادئة الأمر.", "prefixInvalid": "يجب أن تكون البادئة 1-3 أحرف غير أبجدية رقمية.", "save": "حفظ", + "folderDesc": "اختيار مجلد العمل", + "agentDesc": "اختيار وكيل الذكاء الاصطناعي", + "taskDesc": "إنشاء جلسة وتنفيذ المهمة", + "sessionsDesc": "عرض الجلسات النشطة في المجلد", + "resumeDesc": "استئناف جلسة موجودة", + "cancelDesc": "إلغاء المهمة الحالية", + "approveDesc": "الموافقة على طلب إذن الوكيل", + "denyDesc": "رفض طلب إذن الوكيل", "recentDesc": "عرض آخر 5 محادثات", "searchDesc": "البحث في المحادثات حسب الكلمة المفتاحية", "detailDesc": "عرض تفاصيل المحادثة", diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index d44c854..f916deb 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -1055,7 +1055,7 @@ "pushWindow": { "title": "Code pushen", "noUnpushedCommits": "Keine ungepushten Commits", - "noRemoteConfigured": "Kein Git-Remote konfiguriert\nFüge einen unter \u201ERemotes verwalten\u201C hinzu", + "noRemoteConfigured": "Kein Git-Remote konfiguriert\nFüge einen unter Remotes verwalten hinzu", "newBranchNoPushedCommits": "Neuer Branch — pushen, um Remote-Tracking-Branch zu erstellen", "unpushed": "Nicht gepusht", "selectFileToViewDiff": "Datei auswählen, um Unterschiede anzuzeigen", @@ -1728,6 +1728,14 @@ "prefixSaveFailed": "Fehler beim Speichern des Präfixes.", "prefixInvalid": "Das Präfix muss 1-3 nicht-alphanumerische Zeichen sein.", "save": "Speichern", + "folderDesc": "Arbeitsordner auswählen", + "agentDesc": "KI-Agent auswählen", + "taskDesc": "Sitzung erstellen und Aufgabe ausführen", + "sessionsDesc": "Aktive Sitzungen im Ordner anzeigen", + "resumeDesc": "Bestehende Sitzung fortsetzen", + "cancelDesc": "Aktuelle Aufgabe abbrechen", + "approveDesc": "Berechtigungsanfrage des Agenten genehmigen", + "denyDesc": "Berechtigungsanfrage des Agenten ablehnen", "recentDesc": "Die 5 neuesten Konversationen anzeigen", "searchDesc": "Konversationen nach Stichwort suchen", "detailDesc": "Konversationsdetails anzeigen", diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 1dbd71f..528b24c 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -1728,6 +1728,14 @@ "prefixSaveFailed": "Failed to save command prefix.", "prefixInvalid": "Prefix must be 1-3 non-alphanumeric characters.", "save": "Save", + "folderDesc": "Select working folder", + "agentDesc": "Select AI agent", + "taskDesc": "Create session and run task", + "sessionsDesc": "List active sessions in folder", + "resumeDesc": "Resume an existing session", + "cancelDesc": "Cancel current task", + "approveDesc": "Approve agent permission request", + "denyDesc": "Deny agent permission request", "recentDesc": "Show 5 most recent conversations", "searchDesc": "Search conversations by keyword", "detailDesc": "Show conversation details", diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json index 349b1f6..9ddee42 100644 --- a/src/i18n/messages/es.json +++ b/src/i18n/messages/es.json @@ -1728,6 +1728,14 @@ "prefixSaveFailed": "Error al guardar el prefijo.", "prefixInvalid": "El prefijo debe ser de 1-3 caracteres no alfanuméricos.", "save": "Guardar", + "folderDesc": "Seleccionar carpeta de trabajo", + "agentDesc": "Seleccionar agente de IA", + "taskDesc": "Crear sesión y ejecutar tarea", + "sessionsDesc": "Listar sesiones activas en la carpeta", + "resumeDesc": "Reanudar una sesión existente", + "cancelDesc": "Cancelar tarea actual", + "approveDesc": "Aprobar solicitud de permiso del agente", + "denyDesc": "Denegar solicitud de permiso del agente", "recentDesc": "Mostrar las 5 conversaciones más recientes", "searchDesc": "Buscar conversaciones por palabra clave", "detailDesc": "Mostrar detalles de la conversación", diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index 7f78fe6..cd9d272 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -1728,6 +1728,14 @@ "prefixSaveFailed": "Échec de l'enregistrement du préfixe.", "prefixInvalid": "Le préfixe doit être de 1-3 caractères non alphanumériques.", "save": "Enregistrer", + "folderDesc": "Sélectionner le dossier de travail", + "agentDesc": "Sélectionner l'agent IA", + "taskDesc": "Créer une session et exécuter la tâche", + "sessionsDesc": "Lister les sessions actives du dossier", + "resumeDesc": "Reprendre une session existante", + "cancelDesc": "Annuler la tâche en cours", + "approveDesc": "Approuver la demande de permission de l'agent", + "denyDesc": "Refuser la demande de permission de l'agent", "recentDesc": "Afficher les 5 conversations les plus récentes", "searchDesc": "Rechercher des conversations par mot-clé", "detailDesc": "Afficher les détails de la conversation", diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json index 39533eb..e4b75ea 100644 --- a/src/i18n/messages/ja.json +++ b/src/i18n/messages/ja.json @@ -1728,6 +1728,14 @@ "prefixSaveFailed": "コマンドプレフィックスの保存に失敗しました。", "prefixInvalid": "プレフィックスは1-3文字の英数字以外の文字である必要があります。", "save": "保存", + "folderDesc": "作業フォルダを選択", + "agentDesc": "AIエージェントを選択", + "taskDesc": "セッションを作成してタスクを実行", + "sessionsDesc": "フォルダ内のアクティブなセッション一覧", + "resumeDesc": "既存のセッションを再開", + "cancelDesc": "現在のタスクをキャンセル", + "approveDesc": "エージェントの権限リクエストを承認", + "denyDesc": "エージェントの権限リクエストを拒否", "recentDesc": "最近の会話5件を表示", "searchDesc": "キーワードで会話を検索", "detailDesc": "会話の詳細を表示", diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json index 14406bd..e162892 100644 --- a/src/i18n/messages/ko.json +++ b/src/i18n/messages/ko.json @@ -1728,6 +1728,14 @@ "prefixSaveFailed": "명령어 접두사 저장에 실패했습니다.", "prefixInvalid": "접두사는 1-3개의 영숫자가 아닌 문자여야 합니다.", "save": "저장", + "folderDesc": "작업 폴더 선택", + "agentDesc": "AI 에이전트 선택", + "taskDesc": "세션 생성 및 작업 실행", + "sessionsDesc": "폴더 내 활성 세션 목록", + "resumeDesc": "기존 세션 재개", + "cancelDesc": "현재 작업 취소", + "approveDesc": "에이전트 권한 요청 승인", + "denyDesc": "에이전트 권한 요청 거부", "recentDesc": "최근 대화 5개 표시", "searchDesc": "키워드로 대화 검색", "detailDesc": "대화 상세 정보 표시", diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index 27413d8..135664b 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -1728,6 +1728,14 @@ "prefixSaveFailed": "Falha ao salvar o prefixo.", "prefixInvalid": "O prefixo deve ser de 1-3 caracteres não alfanuméricos.", "save": "Salvar", + "folderDesc": "Selecionar pasta de trabalho", + "agentDesc": "Selecionar agente de IA", + "taskDesc": "Criar sessão e executar tarefa", + "sessionsDesc": "Listar sessões ativas na pasta", + "resumeDesc": "Retomar uma sessão existente", + "cancelDesc": "Cancelar tarefa atual", + "approveDesc": "Aprovar solicitação de permissão do agente", + "denyDesc": "Negar solicitação de permissão do agente", "recentDesc": "Mostrar as 5 conversas mais recentes", "searchDesc": "Pesquisar conversas por palavra-chave", "detailDesc": "Mostrar detalhes da conversa", diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index d62b451..16de7e1 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -1728,6 +1728,14 @@ "prefixSaveFailed": "保存指令前缀失败。", "prefixInvalid": "前缀必须是 1-3 个非字母数字字符。", "save": "保存", + "folderDesc": "选择工作目录", + "agentDesc": "选择 AI Agent", + "taskDesc": "创建会话并执行任务", + "sessionsDesc": "列出当前目录的活跃会话", + "resumeDesc": "恢复已有会话", + "cancelDesc": "取消当前任务", + "approveDesc": "批准 Agent 权限请求", + "denyDesc": "拒绝 Agent 权限请求", "recentDesc": "最近 5 条会话", "searchDesc": "按关键词搜索会话", "detailDesc": "会话详情", diff --git a/src/i18n/messages/zh-TW.json b/src/i18n/messages/zh-TW.json index d008b63..6648265 100644 --- a/src/i18n/messages/zh-TW.json +++ b/src/i18n/messages/zh-TW.json @@ -1728,6 +1728,14 @@ "prefixSaveFailed": "儲存指令前綴失敗。", "prefixInvalid": "前綴必須是 1-3 個非字母數字字元。", "save": "儲存", + "folderDesc": "選擇工作目錄", + "agentDesc": "選擇 AI Agent", + "taskDesc": "建立會話並執行任務", + "sessionsDesc": "列出當前目錄的活躍會話", + "resumeDesc": "恢復已有會話", + "cancelDesc": "取消當前任務", + "approveDesc": "批准 Agent 權限請求", + "denyDesc": "拒絕 Agent 權限請求", "recentDesc": "最近 5 筆對話", "searchDesc": "依關鍵字搜尋對話", "detailDesc": "對話詳情", From adb5829613c301ebf2dc8c20607d430746dfba18 Mon Sep 17 00:00:00 2001 From: xintaofei Date: Wed, 1 Apr 2026 16:22:54 +0800 Subject: [PATCH 2/8] =?UTF-8?q?optimize:=20channel=20Message=20Commands=20?= =?UTF-8?q?=E2=80=94=20Multilingual=20Support=20and=20Prefixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/chat_channel/command_dispatcher.rs | 12 +- src-tauri/src/chat_channel/i18n.rs | 185 ++++++++++++++---- .../src/chat_channel/session_commands.rs | 107 ++++++---- .../chat_channel/session_event_subscriber.rs | 42 ++-- 4 files changed, 247 insertions(+), 99 deletions(-) diff --git a/src-tauri/src/chat_channel/command_dispatcher.rs b/src-tauri/src/chat_channel/command_dispatcher.rs index dd1f841..a1a5a29 100644 --- a/src-tauri/src/chat_channel/command_dispatcher.rs +++ b/src-tauri/src/chat_channel/command_dispatcher.rs @@ -156,7 +156,7 @@ async fn dispatch_command( }; if has_session { return session_commands::handle_followup( - db, text, channel_id, sender_id, conn_mgr, bridge, lang, + db, text, channel_id, sender_id, conn_mgr, bridge, lang, prefix, ) .await; } @@ -193,23 +193,23 @@ async fn dispatch_command( // Session commands "folder" => { - session_commands::handle_folder(db, args, channel_id, sender_id, lang).await + session_commands::handle_folder(db, args, channel_id, sender_id, lang, prefix).await } "agent" => { - session_commands::handle_agent(db, args, channel_id, sender_id, lang).await + session_commands::handle_agent(db, args, channel_id, sender_id, lang, prefix).await } "task" | "do" => { session_commands::handle_task( - db, args, channel_id, sender_id, conn_mgr, emitter, bridge, lang, + db, args, channel_id, sender_id, conn_mgr, emitter, bridge, lang, prefix, ) .await } "sessions" => { - session_commands::handle_sessions(db, channel_id, sender_id, lang).await + session_commands::handle_sessions(db, channel_id, sender_id, lang, prefix).await } "resume" => { session_commands::handle_resume( - db, args, channel_id, sender_id, conn_mgr, emitter, bridge, lang, + db, args, channel_id, sender_id, conn_mgr, emitter, bridge, lang, prefix, ) .await } diff --git a/src-tauri/src/chat_channel/i18n.rs b/src-tauri/src/chat_channel/i18n.rs index f944e20..ac3008f 100644 --- a/src-tauri/src/chat_channel/i18n.rs +++ b/src-tauri/src/chat_channel/i18n.rs @@ -610,14 +610,14 @@ pub fn help_title(lang: Lang) -> &'static str { pub fn help_body(lang: Lang, prefix: &str) -> String { match lang { Lang::ZhCn => format!( - "📂 {prefix}folder - 选择工作目录\n\ - 🤖 {prefix}agent - 选择 Agent\n\ - 🚀 {prefix}task <描述> - 创建会话并执行任务\n\ - 📋 {prefix}sessions - 当前目录的活跃会话\n\ - ▶️ {prefix}resume - 恢复已有会话\n\ - ⏹️ {prefix}cancel - 取消当前任务\n\ - ✅ {prefix}approve [always] - 批准权限请求\n\ - ❌ {prefix}deny - 拒绝权限请求\n\ + "{prefix}folder - 选择工作目录\n\ + {prefix}agent - 选择 Agent\n\ + {prefix}task <描述> - 创建会话并执行任务\n\ + {prefix}sessions - 当前目录的活跃会话\n\ + {prefix}resume - 恢复已有会话\n\ + {prefix}cancel - 取消当前任务\n\ + {prefix}approve [always] - 批准权限请求\n\ + {prefix}deny - 拒绝权限请求\n\ \n\ {prefix}recent - 最近 5 条会话\n\ {prefix}search <关键词> - 搜索会话\n\ @@ -626,81 +626,169 @@ pub fn help_body(lang: Lang, prefix: &str) -> String { {prefix}status - 渠道连接状态\n\ {prefix}help - 显示帮助\n\ \n\ - 💡 有活跃会话时,直接发文本即可继续对话" + 有活跃会话时,直接发文本即可继续对话" ), Lang::ZhTw => format!( - "{prefix}recent - 最近 5 條對話\n\ + "{prefix}folder - 選擇工作目錄\n\ + {prefix}agent - 選擇 Agent\n\ + {prefix}task <描述> - 建立對話並執行任務\n\ + {prefix}sessions - 當前目錄的活躍對話\n\ + {prefix}resume - 恢復已有對話\n\ + {prefix}cancel - 取消當前任務\n\ + {prefix}approve [always] - 批准權限請求\n\ + {prefix}deny - 拒絕權限請求\n\ + \n\ + {prefix}recent - 最近 5 條對話\n\ {prefix}search <關鍵字> - 搜尋對話\n\ {prefix}detail - 對話詳情\n\ {prefix}today - 今日活動匯總\n\ {prefix}status - 頻道連線狀態\n\ - {prefix}help - 顯示幫助" + {prefix}help - 顯示幫助\n\ + \n\ + 有活躍對話時,直接發文字即可繼續對話" ), Lang::Ja => format!( - "{prefix}recent - 最新5件のセッション\n\ + "{prefix}folder - 作業フォルダを選択\n\ + {prefix}agent - エージェントを選択\n\ + {prefix}task <説明> - セッションを作成してタスクを実行\n\ + {prefix}sessions - フォルダ内のアクティブセッション\n\ + {prefix}resume - セッションを再開\n\ + {prefix}cancel - 現在のタスクをキャンセル\n\ + {prefix}approve [always] - 権限を承認\n\ + {prefix}deny - 権限を拒否\n\ + \n\ + {prefix}recent - 最新5件のセッション\n\ {prefix}search <キーワード> - セッション検索\n\ {prefix}detail - セッション詳細\n\ {prefix}today - 本日の活動まとめ\n\ {prefix}status - チャンネル接続状況\n\ - {prefix}help - ヘルプを表示" + {prefix}help - ヘルプを表示\n\ + \n\ + セッションがアクティブな場合、テキストを送信するだけで会話を続けられます" ), Lang::Ko => format!( - "{prefix}recent - 최근 5개 대화\n\ + "{prefix}folder - 작업 폴더 선택\n\ + {prefix}agent - 에이전트 선택\n\ + {prefix}task <설명> - 세션 생성 및 작업 실행\n\ + {prefix}sessions - 폴더 내 활성 세션\n\ + {prefix}resume - 세션 재개\n\ + {prefix}cancel - 현재 작업 취소\n\ + {prefix}approve [always] - 권한 승인\n\ + {prefix}deny - 권한 거부\n\ + \n\ + {prefix}recent - 최근 5개 대화\n\ {prefix}search <키워드> - 대화 검색\n\ {prefix}detail - 대화 상세\n\ {prefix}today - 오늘의 활동 요약\n\ {prefix}status - 채널 연결 상태\n\ - {prefix}help - 도움말 표시" + {prefix}help - 도움말 표시\n\ + \n\ + 세션이 활성화된 경우 텍스트를 보내면 대화를 계속할 수 있습니다" ), Lang::Es => format!( - "{prefix}recent - 5 conversaciones más recientes\n\ + "{prefix}folder - Seleccionar carpeta de trabajo\n\ + {prefix}agent - Seleccionar agente\n\ + {prefix}task - Crear sesion y ejecutar tarea\n\ + {prefix}sessions - Sesiones activas en la carpeta\n\ + {prefix}resume - Reanudar una sesion\n\ + {prefix}cancel - Cancelar tarea actual\n\ + {prefix}approve [always] - Aprobar permiso\n\ + {prefix}deny - Denegar permiso\n\ + \n\ + {prefix}recent - 5 conversaciones mas recientes\n\ {prefix}search - Buscar conversaciones\n\ - {prefix}detail - Detalles de conversación\n\ + {prefix}detail - Detalles de conversacion\n\ {prefix}today - Resumen de hoy\n\ {prefix}status - Estado de canales\n\ - {prefix}help - Mostrar ayuda" + {prefix}help - Mostrar ayuda\n\ + \n\ + Cuando hay una sesion activa, simplemente escriba texto para continuar" ), Lang::De => format!( - "{prefix}recent - 5 neueste Sitzungen\n\ + "{prefix}folder - Arbeitsordner auswahlen\n\ + {prefix}agent - Agent auswahlen\n\ + {prefix}task - Sitzung erstellen und Aufgabe ausfuhren\n\ + {prefix}sessions - Aktive Sitzungen im Ordner\n\ + {prefix}resume - Sitzung fortsetzen\n\ + {prefix}cancel - Aktuelle Aufgabe abbrechen\n\ + {prefix}approve [always] - Berechtigung genehmigen\n\ + {prefix}deny - Berechtigung verweigern\n\ + \n\ + {prefix}recent - 5 neueste Sitzungen\n\ {prefix}search - Sitzungen suchen\n\ {prefix}detail - Sitzungsdetails\n\ {prefix}today - Heutige Zusammenfassung\n\ {prefix}status - Kanalstatus\n\ - {prefix}help - Hilfe anzeigen" + {prefix}help - Hilfe anzeigen\n\ + \n\ + Bei aktiver Sitzung einfach Text eingeben, um das Gesprach fortzusetzen" ), Lang::Fr => format!( - "{prefix}recent - 5 dernières sessions\n\ - {prefix}search - Rechercher des sessions\n\ - {prefix}detail - Détails de la session\n\ - {prefix}today - Résumé du jour\n\ + "{prefix}folder - Selectionner le dossier de travail\n\ + {prefix}agent - Selectionner l'agent\n\ + {prefix}task - Creer une session et executer une tache\n\ + {prefix}sessions - Sessions actives dans le dossier\n\ + {prefix}resume - Reprendre une session\n\ + {prefix}cancel - Annuler la tache en cours\n\ + {prefix}approve [always] - Approuver la permission\n\ + {prefix}deny - Refuser la permission\n\ + \n\ + {prefix}recent - 5 dernieres sessions\n\ + {prefix}search - Rechercher des sessions\n\ + {prefix}detail - Details de la session\n\ + {prefix}today - Resume du jour\n\ {prefix}status - Statut des canaux\n\ - {prefix}help - Afficher l'aide" + {prefix}help - Afficher l'aide\n\ + \n\ + Lorsqu'une session est active, envoyez du texte pour continuer la conversation" ), Lang::Pt => format!( - "{prefix}recent - 5 sessões mais recentes\n\ - {prefix}search - Buscar sessões\n\ - {prefix}detail - Detalhes da sessão\n\ + "{prefix}folder - Selecionar pasta de trabalho\n\ + {prefix}agent - Selecionar agente\n\ + {prefix}task - Criar sessao e executar tarefa\n\ + {prefix}sessions - Sessoes ativas na pasta\n\ + {prefix}resume - Retomar uma sessao\n\ + {prefix}cancel - Cancelar tarefa atual\n\ + {prefix}approve [always] - Aprovar permissao\n\ + {prefix}deny - Negar permissao\n\ + \n\ + {prefix}recent - 5 sessoes mais recentes\n\ + {prefix}search - Buscar sessoes\n\ + {prefix}detail - Detalhes da sessao\n\ {prefix}today - Resumo de hoje\n\ {prefix}status - Status dos canais\n\ - {prefix}help - Mostrar ajuda" + {prefix}help - Mostrar ajuda\n\ + \n\ + Quando uma sessao esta ativa, basta digitar texto para continuar a conversa" ), Lang::Ar => format!( - "{prefix}recent - أحدث 5 جلسات\n\ + "{prefix}folder - اختيار مجلد العمل\n\ + {prefix}agent - اختيار الوكيل\n\ + {prefix}task <وصف> - انشاء جلسة وتنفيذ مهمة\n\ + {prefix}sessions - الجلسات النشطة في المجلد\n\ + {prefix}resume - استئناف جلسة\n\ + {prefix}cancel - الغاء المهمة الحالية\n\ + {prefix}approve [always] - الموافقة على الاذن\n\ + {prefix}deny - رفض الاذن\n\ + \n\ + {prefix}recent - احدث 5 جلسات\n\ {prefix}search <كلمة> - البحث في الجلسات\n\ {prefix}detail - تفاصيل الجلسة\n\ {prefix}today - ملخص اليوم\n\ {prefix}status - حالة القنوات\n\ - {prefix}help - عرض المساعدة" + {prefix}help - عرض المساعدة\n\ + \n\ + عندما تكون الجلسة نشطة، ارسل نصا لمتابعة المحادثة" ), Lang::En => format!( - "📂 {prefix}folder - Select working folder\n\ - 🤖 {prefix}agent - Select agent\n\ - 🚀 {prefix}task - Create session & run task\n\ - 📋 {prefix}sessions - Active sessions in folder\n\ - ▶️ {prefix}resume - Resume a session\n\ - ⏹️ {prefix}cancel - Cancel current task\n\ - ✅ {prefix}approve [always] - Approve permission\n\ - ❌ {prefix}deny - Deny permission\n\ + "{prefix}folder - Select working folder\n\ + {prefix}agent - Select agent\n\ + {prefix}task - Create session & run task\n\ + {prefix}sessions - Active sessions in folder\n\ + {prefix}resume - Resume a session\n\ + {prefix}cancel - Cancel current task\n\ + {prefix}approve [always] - Approve permission\n\ + {prefix}deny - Deny permission\n\ \n\ {prefix}recent - 5 most recent conversations\n\ {prefix}search - Search conversations\n\ @@ -709,7 +797,7 @@ pub fn help_body(lang: Lang, prefix: &str) -> String { {prefix}status - Channel connection status\n\ {prefix}help - Show help\n\ \n\ - 💡 When a session is active, just type text to continue the conversation" + When a session is active, just type text to continue the conversation" ), } } @@ -810,3 +898,20 @@ pub fn unknown_command_title(lang: Lang) -> &'static str { Lang::En => "Unknown Command", } } + +// ── Session progress messages ── + +pub fn agent_responding(lang: Lang) -> &'static str { + match lang { + Lang::ZhCn => "Claude Code 正在响应中...", + Lang::ZhTw => "Claude Code 正在回應中...", + Lang::Ja => "Claude Code が応答中...", + Lang::Ko => "Claude Code 응답 중...", + Lang::Es => "Claude Code respondiendo...", + Lang::De => "Claude Code antwortet...", + Lang::Fr => "Claude Code en cours de reponse...", + Lang::Pt => "Claude Code respondendo...", + Lang::Ar => "...Claude Code يستجيب", + Lang::En => "Claude Code is responding...", + } +} diff --git a/src-tauri/src/chat_channel/session_commands.rs b/src-tauri/src/chat_channel/session_commands.rs index b73ba02..11822af 100644 --- a/src-tauri/src/chat_channel/session_commands.rs +++ b/src-tauri/src/chat_channel/session_commands.rs @@ -24,14 +24,15 @@ pub async fn handle_folder( channel_id: i32, sender_id: &str, lang: Lang, + prefix: &str, ) -> RichMessage { if args.is_empty() { - return list_folders(db, channel_id, sender_id, lang).await; + return list_folders(db, channel_id, sender_id, lang, prefix).await; } // Try parse as index (1-based) if let Ok(idx) = args.parse::() { - return select_folder_by_index(db, idx, channel_id, sender_id, lang).await; + return select_folder_by_index(db, idx, channel_id, sender_id, lang, prefix).await; } // Treat as path @@ -43,6 +44,7 @@ async fn list_folders( channel_id: i32, sender_id: &str, lang: Lang, + prefix: &str, ) -> RichMessage { let folders = match folder_service::list_folders(db).await { Ok(f) => f, @@ -77,10 +79,11 @@ async fn list_folders( body.push_str(&format!( "\n{}", - t( + tp( lang, - "Reply /folder to select.", - "回复 /folder <数字> 选择目录。" + prefix, + "Reply {prefix}folder to select.", + "回复 {prefix}folder <数字> 选择目录。" ) )); @@ -94,6 +97,7 @@ async fn select_folder_by_index( channel_id: i32, sender_id: &str, lang: Lang, + prefix: &str, ) -> RichMessage { if idx == 0 { return RichMessage::info(t(lang, "Index starts from 1.", "序号从 1 开始。")); @@ -105,10 +109,11 @@ async fn select_folder_by_index( }; let Some(folder) = folders.get(idx - 1) else { - return RichMessage::info(t( + return RichMessage::info(tp( lang, - "Index out of range. Use /folder to list.", - "序号超出范围,请使用 /folder 查看列表。", + prefix, + "Index out of range. Use {prefix}folder to list.", + "序号超出范围,请使用 {prefix}folder 查看列表。", )); }; @@ -146,14 +151,15 @@ pub async fn handle_agent( channel_id: i32, sender_id: &str, lang: Lang, + prefix: &str, ) -> RichMessage { if args.is_empty() { - return list_agents(db, channel_id, sender_id, lang).await; + return list_agents(db, channel_id, sender_id, lang, prefix).await; } // Try parse as index if let Ok(idx) = args.parse::() { - return select_agent_by_index(db, idx, channel_id, sender_id, lang).await; + return select_agent_by_index(db, idx, channel_id, sender_id, lang, prefix).await; } // Try parse as agent type name @@ -165,6 +171,7 @@ async fn list_agents( channel_id: i32, sender_id: &str, lang: Lang, + prefix: &str, ) -> RichMessage { let agents = all_acp_agents(); let ctx = sender_context_service::get_or_create(db, channel_id, sender_id) @@ -185,10 +192,11 @@ async fn list_agents( body.push_str(&format!( "\n{}", - t( + tp( lang, - "Reply /agent or /agent to select.", - "回复 /agent <数字> 或 /agent <名称> 选择。" + prefix, + "Reply {prefix}agent or {prefix}agent to select.", + "回复 {prefix}agent <数字> 或 {prefix}agent <名称> 选择。" ) )); @@ -202,13 +210,15 @@ async fn select_agent_by_index( channel_id: i32, sender_id: &str, lang: Lang, + prefix: &str, ) -> RichMessage { let agents = all_acp_agents(); if idx == 0 || idx > agents.len() { - return RichMessage::info(t( + return RichMessage::info(tp( lang, - "Index out of range. Use /agent to list.", - "序号超出范围,请使用 /agent 查看列表。", + prefix, + "Index out of range. Use {prefix}agent to list.", + "序号超出范围,请使用 {prefix}agent 查看列表。", )); } @@ -257,12 +267,14 @@ pub async fn handle_task( emitter: &EventEmitter, bridge: &Arc>, lang: Lang, + prefix: &str, ) -> RichMessage { if task_description.is_empty() { - return RichMessage::info(t( + return RichMessage::info(tp( lang, - "Usage: /task ", - "用法: /task <任务描述>", + prefix, + "Usage: {prefix}task ", + "用法: {prefix}task <任务描述>", )); } @@ -275,10 +287,11 @@ pub async fn handle_task( let folder_id = match ctx.current_folder_id { Some(id) => id, None => { - return RichMessage::info(t( + return RichMessage::info(tp( lang, - "No folder selected. Use /folder first.", - "未选择工作目录,请先使用 /folder 选择。", + prefix, + "No folder selected. Use {prefix}folder first.", + "未选择工作目录,请先使用 {prefix}folder 选择。", )); } }; @@ -287,10 +300,11 @@ pub async fn handle_task( let folder = match folder_service::get_folder_by_id(db, folder_id).await { Ok(Some(f)) => f, _ => { - return RichMessage::info(t( + return RichMessage::info(tp( lang, - "Folder not found. Use /folder to select.", - "目录不存在,请使用 /folder 重新选择。", + prefix, + "Folder not found. Use {prefix}folder to select.", + "目录不存在,请使用 {prefix}folder 重新选择。", )); } }; @@ -381,6 +395,7 @@ pub async fn handle_sessions( channel_id: i32, sender_id: &str, lang: Lang, + prefix: &str, ) -> RichMessage { let ctx = match sender_context_service::get_or_create(db, channel_id, sender_id).await { Ok(c) => c, @@ -390,10 +405,11 @@ pub async fn handle_sessions( let folder_id = match ctx.current_folder_id { Some(id) => id, None => { - return RichMessage::info(t( + return RichMessage::info(tp( lang, - "No folder selected. Use /folder first.", - "未选择工作目录,请先使用 /folder 选择。", + prefix, + "No folder selected. Use {prefix}folder first.", + "未选择工作目录,请先使用 {prefix}folder 选择。", )); } }; @@ -456,10 +472,11 @@ pub async fn handle_sessions( body.push_str(&format!( "\n{}", - t( + tp( lang, - "Reply /resume to continue.", - "回复 /resume 继续会话。" + prefix, + "Reply {prefix}resume to continue.", + "回复 {prefix}resume 继续会话。" ) )); @@ -482,14 +499,16 @@ pub async fn handle_resume( emitter: &EventEmitter, bridge: &Arc>, lang: Lang, + prefix: &str, ) -> RichMessage { let conversation_id: i32 = match args.parse() { Ok(id) => id, Err(_) => { - return RichMessage::info(t( + return RichMessage::info(tp( lang, - "Usage: /resume ", - "用法: /resume <会话ID>", + prefix, + "Usage: {prefix}resume ", + "用法: {prefix}resume <会话ID>", )); } }; @@ -753,6 +772,7 @@ pub async fn handle_followup( conn_mgr: &ConnectionManager, bridge: &Arc>, lang: Lang, + prefix: &str, ) -> RichMessage { let ctx = match sender_context_service::get_or_create(db, channel_id, sender_id).await { Ok(c) => c, @@ -762,10 +782,11 @@ pub async fn handle_followup( let connection_id = match &ctx.current_connection_id { Some(id) => id.clone(), None => { - return RichMessage::info(t( + return RichMessage::info(tp( lang, - "No active session. Use /task to start one.", - "没有活跃的会话,请使用 /task 开始新任务。", + prefix, + "No active session. Use {prefix}task to start one.", + "没有活跃的会话,请使用 {prefix}task 开始新任务。", )); } }; @@ -777,10 +798,11 @@ pub async fn handle_followup( // Connection lost, clear context drop(bridge_guard); let _ = sender_context_service::clear_session(db, channel_id, sender_id).await; - return RichMessage::info(t( + return RichMessage::info(tp( lang, - "Session connection lost. Use /task to start a new one.", - "会话连接已断开,请使用 /task 开始新任务。", + prefix, + "Session connection lost. Use {prefix}task to start a new one.", + "会话连接已断开,请使用 {prefix}task 开始新任务。", )); } } @@ -812,6 +834,11 @@ fn t(lang: Lang, en: &str, zh: &str) -> String { } } +/// Like `t()` but replaces `{prefix}` placeholders with the actual command prefix. +fn tp(lang: Lang, prefix: &str, en: &str, zh: &str) -> String { + t(lang, en, zh).replace("{prefix}", prefix) +} + fn agent_type_to_string(at: AgentType) -> String { serde_json::to_value(at) .ok() diff --git a/src-tauri/src/chat_channel/session_event_subscriber.rs b/src-tauri/src/chat_channel/session_event_subscriber.rs index 3343629..ac8cf1e 100644 --- a/src-tauri/src/chat_channel/session_event_subscriber.rs +++ b/src-tauri/src/chat_channel/session_event_subscriber.rs @@ -17,10 +17,12 @@ use crate::web::event_bridge::WebEventBroadcaster; use super::manager::ChatChannelManager; -const FLUSH_INTERVAL_SECS: u64 = 5; +const FLUSH_INTERVAL_SECS: u64 = 10; const BUFFER_FLUSH_THRESHOLD: usize = 500; const MAX_MESSAGE_LEN: usize = 2000; const MESSAGE_LANGUAGE_KEY: &str = "chat_message_language"; +const COMMAND_PREFIX_KEY: &str = "chat_command_prefix"; +const DEFAULT_COMMAND_PREFIX: &str = "/"; pub fn spawn_session_event_subscriber( broadcaster: Arc, @@ -59,7 +61,7 @@ pub fn spawn_session_event_subscriber( } _ = tokio::time::sleep(Duration::from_secs(FLUSH_INTERVAL_SECS)) => { if last_heartbeat.elapsed() >= Duration::from_secs(FLUSH_INTERVAL_SECS) { - flush_progress(&bridge, &manager).await; + flush_progress(&bridge, &manager, &db_conn).await; last_heartbeat = Instant::now(); } } @@ -77,6 +79,14 @@ async fn get_lang(db: &DatabaseConnection) -> Lang { .unwrap_or_default() } +async fn get_prefix(db: &DatabaseConnection) -> String { + app_metadata_service::get_value(db, COMMAND_PREFIX_KEY) + .await + .ok() + .flatten() + .unwrap_or_else(|| DEFAULT_COMMAND_PREFIX.to_string()) +} + async fn handle_acp_event_payload( payload: &serde_json::Value, bridge: &Arc>, @@ -135,11 +145,11 @@ async fn handle_acp_event_payload( && session.last_flushed.elapsed() >= Duration::from_secs(2) { let channel_id = session.channel_id; - let buf_len = session.content_buffer.len(); let last_tool = session.tool_calls.last().cloned(); session.last_flushed = Instant::now(); - let mut status = format!("... ({buf_len} chars)"); + let lang = get_lang(db).await; + let mut status = super::i18n::agent_responding(lang).to_string(); if let Some(tool) = last_tool { status.push_str(&format!(" | {tool}")); } @@ -252,12 +262,13 @@ async fn handle_acp_event_payload( drop(guard); let lang = get_lang(db).await; + let prefix = get_prefix(db).await; let body = match lang { Lang::ZhCn | Lang::ZhTw => { - format!("Agent 请求权限: {tool_desc}\n\n/approve 批准 | /deny 拒绝 | /approve always 自动批准") + format!("Agent 请求权限: {tool_desc}\n\n{prefix}approve 批准 | {prefix}deny 拒绝 | {prefix}approve always 自动批准") } _ => { - format!("Agent requests permission: {tool_desc}\n\n/approve | /deny | /approve always") + format!("Agent requests permission: {tool_desc}\n\n{prefix}approve | {prefix}deny | {prefix}approve always") } }; @@ -387,7 +398,11 @@ async fn handle_acp_event_payload( } } -async fn flush_progress(bridge: &Arc>, manager: &ChatChannelManager) { +async fn flush_progress( + bridge: &Arc>, + manager: &ChatChannelManager, + db: &DatabaseConnection, +) { let updates: Vec<(i32, String)> = { let mut guard = bridge.lock().await; let mut out = Vec::new(); @@ -395,13 +410,14 @@ async fn flush_progress(bridge: &Arc>, manager: &ChatChanne if !session.content_buffer.is_empty() && session.last_flushed.elapsed() >= Duration::from_secs(FLUSH_INTERVAL_SECS) { - let buf_len = session.content_buffer.len(); - let tool_count = session.tool_calls.len(); session.last_flushed = Instant::now(); - out.push(( - session.channel_id, - format!("... ({buf_len} chars, {tool_count} tools)"), - )); + let last_tool = session.tool_calls.last().cloned(); + let lang = get_lang(db).await; + let mut status = super::i18n::agent_responding(lang).to_string(); + if let Some(tool) = last_tool { + status.push_str(&format!(" | {tool}")); + } + out.push((session.channel_id, status)); } } out From d76dc716e4ff3918222e842a1159cd4044219d3c Mon Sep 17 00:00:00 2001 From: xintaofei Date: Wed, 1 Apr 2026 17:22:56 +0800 Subject: [PATCH 3/8] optimize: Enhanced session command processing logic --- .../src/chat_channel/command_dispatcher.rs | 9 - .../src/chat_channel/command_handlers.rs | 92 +------- src-tauri/src/chat_channel/i18n.rs | 204 +----------------- .../src/chat_channel/session_commands.rs | 81 ++++++- .../settings/channel-commands-tab.tsx | 4 +- src/i18n/messages/ar.json | 6 +- src/i18n/messages/de.json | 6 +- src/i18n/messages/en.json | 6 +- src/i18n/messages/es.json | 6 +- src/i18n/messages/fr.json | 6 +- src/i18n/messages/ja.json | 6 +- src/i18n/messages/ko.json | 6 +- src/i18n/messages/pt.json | 6 +- src/i18n/messages/zh-CN.json | 6 +- src/i18n/messages/zh-TW.json | 6 +- 15 files changed, 106 insertions(+), 344 deletions(-) diff --git a/src-tauri/src/chat_channel/command_dispatcher.rs b/src-tauri/src/chat_channel/command_dispatcher.rs index a1a5a29..5d354bf 100644 --- a/src-tauri/src/chat_channel/command_dispatcher.rs +++ b/src-tauri/src/chat_channel/command_dispatcher.rs @@ -170,7 +170,6 @@ async fn dispatch_command( match command.as_str() { // Existing commands - "recent" => command_handlers::handle_recent(db, lang).await, "search" => { if args.is_empty() { super::types::RichMessage::info(i18n::search_usage(lang, prefix)) @@ -179,14 +178,6 @@ async fn dispatch_command( command_handlers::handle_search(db, args, lang).await } } - "detail" => { - if let Ok(id) = args.parse::() { - command_handlers::handle_detail(db, id, lang).await - } else { - super::types::RichMessage::info(i18n::detail_usage(lang, prefix)) - .with_title(i18n::invalid_args_title(lang)) - } - } "today" => command_handlers::handle_today(db, lang).await, "status" => command_handlers::handle_status(manager, lang).await, "help" | "start" => command_handlers::handle_help(prefix, lang), diff --git a/src-tauri/src/chat_channel/command_handlers.rs b/src-tauri/src/chat_channel/command_handlers.rs index 70d4a6f..3740cf9 100644 --- a/src-tauri/src/chat_channel/command_handlers.rs +++ b/src-tauri/src/chat_channel/command_handlers.rs @@ -6,48 +6,6 @@ use super::manager::ChatChannelManager; use super::types::{MessageLevel, RichMessage}; use crate::db::entities::conversation; -pub async fn handle_recent(db: &DatabaseConnection, lang: Lang) -> RichMessage { - let recent = match conversation::Entity::find() - .filter(conversation::Column::DeletedAt.is_null()) - .order_by_desc(conversation::Column::CreatedAt) - .limit(5) - .all(db) - .await - { - Ok(rows) => rows, - Err(e) => { - return RichMessage { - title: Some(i18n::query_failed_title(lang).to_string()), - body: e.to_string(), - fields: Vec::new(), - level: MessageLevel::Error, - }; - } - }; - - if recent.is_empty() { - return RichMessage::info(i18n::no_conversations(lang)) - .with_title(i18n::recent_conversations_title(lang)); - } - - let mut body = String::new(); - for (i, conv) in recent.iter().enumerate() { - let title = conv.title.as_deref().unwrap_or(i18n::untitled(lang)); - let agent = &conv.agent_type; - let time = conv.created_at.format("%m-%d %H:%M"); - body.push_str(&format!( - "{}. [{}] {} ({})\n", - i + 1, - agent, - title, - time - )); - } - - RichMessage::info(body.trim_end()) - .with_title(i18n::recent_n_conversations_title(lang, recent.len())) -} - pub async fn handle_search( db: &DatabaseConnection, keyword: &str, @@ -78,15 +36,13 @@ pub async fn handle_search( } let mut body = String::new(); - for (i, conv) in matched.iter().enumerate() { + for conv in &matched { let title = conv.title.as_deref().unwrap_or(i18n::untitled(lang)); let agent = &conv.agent_type; + let time = conv.created_at.format("%m-%d %H:%M"); body.push_str(&format!( - "{}. [{}] {} (ID:{})\n", - i + 1, - agent, - title, - conv.id + "#{} [{}] {} ({})\n", + conv.id, agent, title, time, )); } @@ -97,46 +53,6 @@ pub async fn handle_search( )) } -pub async fn handle_detail( - db: &DatabaseConnection, - conversation_id: i32, - lang: Lang, -) -> RichMessage { - let conv = match conversation::Entity::find_by_id(conversation_id) - .filter(conversation::Column::DeletedAt.is_null()) - .one(db) - .await - { - Ok(Some(c)) => c, - Ok(None) => { - return RichMessage::info(i18n::conversation_not_found(lang, conversation_id)) - .with_title(i18n::not_found_title(lang)); - } - Err(e) => { - return RichMessage { - title: Some(i18n::query_failed_title(lang).to_string()), - body: e.to_string(), - fields: Vec::new(), - level: MessageLevel::Error, - }; - } - }; - - let title = conv.title.as_deref().unwrap_or(i18n::untitled(lang)); - RichMessage::info(title) - .with_title(i18n::conversation_detail_title(lang, conv.id)) - .with_field(i18n::field_agent(lang), &conv.agent_type) - .with_field(i18n::field_status(lang), format!("{:?}", conv.status)) - .with_field( - i18n::field_message_count(lang), - conv.message_count.to_string(), - ) - .with_field( - i18n::field_created_at(lang), - conv.created_at.format("%Y-%m-%d %H:%M").to_string(), - ) -} - pub async fn handle_today(db: &DatabaseConnection, lang: Lang) -> RichMessage { let now = Utc::now(); let today_start = now diff --git a/src-tauri/src/chat_channel/i18n.rs b/src-tauri/src/chat_channel/i18n.rs index ac3008f..96e12cd 100644 --- a/src-tauri/src/chat_channel/i18n.rs +++ b/src-tauri/src/chat_channel/i18n.rs @@ -280,36 +280,6 @@ pub fn query_failed_title(lang: Lang) -> &'static str { } } -pub fn no_conversations(lang: Lang) -> &'static str { - match lang { - Lang::ZhCn => "暂无会话记录", - Lang::ZhTw => "暫無對話記錄", - Lang::Ja => "セッション履歴なし", - Lang::Ko => "대화 기록 없음", - Lang::Es => "Sin conversaciones", - Lang::De => "Keine Sitzungen", - Lang::Fr => "Aucune session", - Lang::Pt => "Nenhuma sessão", - Lang::Ar => "لا توجد جلسات", - Lang::En => "No conversations found", - } -} - -pub fn recent_conversations_title(lang: Lang) -> &'static str { - match lang { - Lang::ZhCn => "最近会话", - Lang::ZhTw => "最近對話", - Lang::Ja => "最近のセッション", - Lang::Ko => "최근 대화", - Lang::Es => "Conversaciones recientes", - Lang::De => "Letzte Sitzungen", - Lang::Fr => "Sessions récentes", - Lang::Pt => "Sessões recentes", - Lang::Ar => "الجلسات الأخيرة", - Lang::En => "Recent Conversations", - } -} - pub fn untitled(lang: Lang) -> &'static str { match lang { Lang::ZhCn => "(无标题)", @@ -325,21 +295,6 @@ pub fn untitled(lang: Lang) -> &'static str { } } -pub fn recent_n_conversations_title(lang: Lang, n: usize) -> String { - match lang { - Lang::ZhCn => format!("最近 {n} 条会话"), - Lang::ZhTw => format!("最近 {n} 條對話"), - Lang::Ja => format!("最新 {n} セッション"), - Lang::Ko => format!("최근 {n}개 대화"), - Lang::Es => format!("{n} conversaciones más recientes"), - Lang::De => format!("Letzte {n} Sitzungen"), - Lang::Fr => format!("{n} dernières sessions"), - Lang::Pt => format!("{n} sessões mais recentes"), - Lang::Ar => format!("أحدث {n} جلسات"), - Lang::En => format!("{n} Most Recent Conversations"), - } -} - pub fn search_no_results(lang: Lang, keyword: &str) -> String { match lang { Lang::ZhCn => format!("未找到包含 \"{keyword}\" 的会话"), @@ -385,110 +340,6 @@ pub fn search_results_count_title(lang: Lang, keyword: &str, count: usize) -> St } } -pub fn conversation_not_found(lang: Lang, id: i32) -> String { - match lang { - Lang::ZhCn => format!("会话 {id} 不存在"), - Lang::ZhTw => format!("對話 {id} 不存在"), - Lang::Ja => format!("セッション {id} が見つかりません"), - Lang::Ko => format!("대화 {id}를 찾을 수 없습니다"), - Lang::Es => format!("Conversación {id} no encontrada"), - Lang::De => format!("Sitzung {id} nicht gefunden"), - Lang::Fr => format!("Session {id} introuvable"), - Lang::Pt => format!("Sessão {id} não encontrada"), - Lang::Ar => format!("الجلسة {id} غير موجودة"), - Lang::En => format!("Conversation {id} not found"), - } -} - -pub fn not_found_title(lang: Lang) -> &'static str { - match lang { - Lang::ZhCn => "未找到", - Lang::ZhTw => "未找到", - Lang::Ja => "見つかりません", - Lang::Ko => "찾을 수 없음", - Lang::Es => "No encontrado", - Lang::De => "Nicht gefunden", - Lang::Fr => "Introuvable", - Lang::Pt => "Não encontrado", - Lang::Ar => "غير موجود", - Lang::En => "Not Found", - } -} - -pub fn conversation_detail_title(lang: Lang, id: i32) -> String { - match lang { - Lang::ZhCn => format!("会话详情 #{id}"), - Lang::ZhTw => format!("對話詳情 #{id}"), - Lang::Ja => format!("セッション詳細 #{id}"), - Lang::Ko => format!("대화 상세 #{id}"), - Lang::Es => format!("Detalles #{id}"), - Lang::De => format!("Sitzungsdetails #{id}"), - Lang::Fr => format!("Détails #{id}"), - Lang::Pt => format!("Detalhes #{id}"), - Lang::Ar => format!("تفاصيل الجلسة #{id}"), - Lang::En => format!("Conversation Details #{id}"), - } -} - -pub fn field_agent(lang: Lang) -> &'static str { - match lang { - Lang::ZhCn => "代理", - Lang::ZhTw => "代理", - Lang::Ja => "エージェント", - Lang::Ko => "에이전트", - Lang::Es => "Agente", - Lang::De => "Agent", - Lang::Fr => "Agent", - Lang::Pt => "Agente", - Lang::Ar => "الوكيل", - Lang::En => "Agent", - } -} - -pub fn field_status(lang: Lang) -> &'static str { - match lang { - Lang::ZhCn => "状态", - Lang::ZhTw => "狀態", - Lang::Ja => "ステータス", - Lang::Ko => "상태", - Lang::Es => "Estado", - Lang::De => "Status", - Lang::Fr => "Statut", - Lang::Pt => "Status", - Lang::Ar => "الحالة", - Lang::En => "Status", - } -} - -pub fn field_message_count(lang: Lang) -> &'static str { - match lang { - Lang::ZhCn => "消息数", - Lang::ZhTw => "訊息數", - Lang::Ja => "メッセージ数", - Lang::Ko => "메시지 수", - Lang::Es => "Mensajes", - Lang::De => "Nachrichten", - Lang::Fr => "Messages", - Lang::Pt => "Mensagens", - Lang::Ar => "عدد الرسائل", - Lang::En => "Messages", - } -} - -pub fn field_created_at(lang: Lang) -> &'static str { - match lang { - Lang::ZhCn => "创建时间", - Lang::ZhTw => "建立時間", - Lang::Ja => "作成日時", - Lang::Ko => "생성 시간", - Lang::Es => "Creado", - Lang::De => "Erstellt", - Lang::Fr => "Créé", - Lang::Pt => "Criado", - Lang::Ar => "تاريخ الإنشاء", - Lang::En => "Created", - } -} pub fn no_activity_today(lang: Lang) -> &'static str { match lang { @@ -614,14 +465,12 @@ pub fn help_body(lang: Lang, prefix: &str) -> String { {prefix}agent - 选择 Agent\n\ {prefix}task <描述> - 创建会话并执行任务\n\ {prefix}sessions - 当前目录的活跃会话\n\ - {prefix}resume - 恢复已有会话\n\ + {prefix}resume [ID] - 最近会话 / 恢复指定会话\n\ {prefix}cancel - 取消当前任务\n\ {prefix}approve [always] - 批准权限请求\n\ {prefix}deny - 拒绝权限请求\n\ \n\ - {prefix}recent - 最近 5 条会话\n\ {prefix}search <关键词> - 搜索会话\n\ - {prefix}detail - 会话详情\n\ {prefix}today - 今日活动汇总\n\ {prefix}status - 渠道连接状态\n\ {prefix}help - 显示帮助\n\ @@ -633,14 +482,12 @@ pub fn help_body(lang: Lang, prefix: &str) -> String { {prefix}agent - 選擇 Agent\n\ {prefix}task <描述> - 建立對話並執行任務\n\ {prefix}sessions - 當前目錄的活躍對話\n\ - {prefix}resume - 恢復已有對話\n\ + {prefix}resume [ID] - 最近對話 / 恢復指定對話\n\ {prefix}cancel - 取消當前任務\n\ {prefix}approve [always] - 批准權限請求\n\ {prefix}deny - 拒絕權限請求\n\ \n\ - {prefix}recent - 最近 5 條對話\n\ {prefix}search <關鍵字> - 搜尋對話\n\ - {prefix}detail - 對話詳情\n\ {prefix}today - 今日活動匯總\n\ {prefix}status - 頻道連線狀態\n\ {prefix}help - 顯示幫助\n\ @@ -652,14 +499,12 @@ pub fn help_body(lang: Lang, prefix: &str) -> String { {prefix}agent - エージェントを選択\n\ {prefix}task <説明> - セッションを作成してタスクを実行\n\ {prefix}sessions - フォルダ内のアクティブセッション\n\ - {prefix}resume - セッションを再開\n\ + {prefix}resume [ID] - 最近のセッション / セッションを再開\n\ {prefix}cancel - 現在のタスクをキャンセル\n\ {prefix}approve [always] - 権限を承認\n\ {prefix}deny - 権限を拒否\n\ \n\ - {prefix}recent - 最新5件のセッション\n\ {prefix}search <キーワード> - セッション検索\n\ - {prefix}detail - セッション詳細\n\ {prefix}today - 本日の活動まとめ\n\ {prefix}status - チャンネル接続状況\n\ {prefix}help - ヘルプを表示\n\ @@ -671,14 +516,12 @@ pub fn help_body(lang: Lang, prefix: &str) -> String { {prefix}agent - 에이전트 선택\n\ {prefix}task <설명> - 세션 생성 및 작업 실행\n\ {prefix}sessions - 폴더 내 활성 세션\n\ - {prefix}resume - 세션 재개\n\ + {prefix}resume [ID] - 최근 대화 / 세션 재개\n\ {prefix}cancel - 현재 작업 취소\n\ {prefix}approve [always] - 권한 승인\n\ {prefix}deny - 권한 거부\n\ \n\ - {prefix}recent - 최근 5개 대화\n\ {prefix}search <키워드> - 대화 검색\n\ - {prefix}detail - 대화 상세\n\ {prefix}today - 오늘의 활동 요약\n\ {prefix}status - 채널 연결 상태\n\ {prefix}help - 도움말 표시\n\ @@ -690,14 +533,12 @@ pub fn help_body(lang: Lang, prefix: &str) -> String { {prefix}agent - Seleccionar agente\n\ {prefix}task - Crear sesion y ejecutar tarea\n\ {prefix}sessions - Sesiones activas en la carpeta\n\ - {prefix}resume - Reanudar una sesion\n\ + {prefix}resume [ID] - Recientes / reanudar una sesion\n\ {prefix}cancel - Cancelar tarea actual\n\ {prefix}approve [always] - Aprobar permiso\n\ {prefix}deny - Denegar permiso\n\ \n\ - {prefix}recent - 5 conversaciones mas recientes\n\ {prefix}search - Buscar conversaciones\n\ - {prefix}detail - Detalles de conversacion\n\ {prefix}today - Resumen de hoy\n\ {prefix}status - Estado de canales\n\ {prefix}help - Mostrar ayuda\n\ @@ -709,14 +550,12 @@ pub fn help_body(lang: Lang, prefix: &str) -> String { {prefix}agent - Agent auswahlen\n\ {prefix}task - Sitzung erstellen und Aufgabe ausfuhren\n\ {prefix}sessions - Aktive Sitzungen im Ordner\n\ - {prefix}resume - Sitzung fortsetzen\n\ + {prefix}resume [ID] - Neueste Sitzungen / Sitzung fortsetzen\n\ {prefix}cancel - Aktuelle Aufgabe abbrechen\n\ {prefix}approve [always] - Berechtigung genehmigen\n\ {prefix}deny - Berechtigung verweigern\n\ \n\ - {prefix}recent - 5 neueste Sitzungen\n\ {prefix}search - Sitzungen suchen\n\ - {prefix}detail - Sitzungsdetails\n\ {prefix}today - Heutige Zusammenfassung\n\ {prefix}status - Kanalstatus\n\ {prefix}help - Hilfe anzeigen\n\ @@ -728,14 +567,12 @@ pub fn help_body(lang: Lang, prefix: &str) -> String { {prefix}agent - Selectionner l'agent\n\ {prefix}task - Creer une session et executer une tache\n\ {prefix}sessions - Sessions actives dans le dossier\n\ - {prefix}resume - Reprendre une session\n\ + {prefix}resume [ID] - Sessions recentes / reprendre une session\n\ {prefix}cancel - Annuler la tache en cours\n\ {prefix}approve [always] - Approuver la permission\n\ {prefix}deny - Refuser la permission\n\ \n\ - {prefix}recent - 5 dernieres sessions\n\ {prefix}search - Rechercher des sessions\n\ - {prefix}detail - Details de la session\n\ {prefix}today - Resume du jour\n\ {prefix}status - Statut des canaux\n\ {prefix}help - Afficher l'aide\n\ @@ -747,14 +584,12 @@ pub fn help_body(lang: Lang, prefix: &str) -> String { {prefix}agent - Selecionar agente\n\ {prefix}task - Criar sessao e executar tarefa\n\ {prefix}sessions - Sessoes ativas na pasta\n\ - {prefix}resume - Retomar uma sessao\n\ + {prefix}resume [ID] - Recentes / retomar uma sessao\n\ {prefix}cancel - Cancelar tarefa atual\n\ {prefix}approve [always] - Aprovar permissao\n\ {prefix}deny - Negar permissao\n\ \n\ - {prefix}recent - 5 sessoes mais recentes\n\ {prefix}search - Buscar sessoes\n\ - {prefix}detail - Detalhes da sessao\n\ {prefix}today - Resumo de hoje\n\ {prefix}status - Status dos canais\n\ {prefix}help - Mostrar ajuda\n\ @@ -766,14 +601,12 @@ pub fn help_body(lang: Lang, prefix: &str) -> String { {prefix}agent - اختيار الوكيل\n\ {prefix}task <وصف> - انشاء جلسة وتنفيذ مهمة\n\ {prefix}sessions - الجلسات النشطة في المجلد\n\ - {prefix}resume - استئناف جلسة\n\ + {prefix}resume [ID] - الجلسات الاخيرة / استئناف جلسة\n\ {prefix}cancel - الغاء المهمة الحالية\n\ {prefix}approve [always] - الموافقة على الاذن\n\ {prefix}deny - رفض الاذن\n\ \n\ - {prefix}recent - احدث 5 جلسات\n\ {prefix}search <كلمة> - البحث في الجلسات\n\ - {prefix}detail - تفاصيل الجلسة\n\ {prefix}today - ملخص اليوم\n\ {prefix}status - حالة القنوات\n\ {prefix}help - عرض المساعدة\n\ @@ -785,14 +618,12 @@ pub fn help_body(lang: Lang, prefix: &str) -> String { {prefix}agent - Select agent\n\ {prefix}task - Create session & run task\n\ {prefix}sessions - Active sessions in folder\n\ - {prefix}resume - Resume a session\n\ + {prefix}resume [ID] - Recent conversations / resume a session\n\ {prefix}cancel - Cancel current task\n\ {prefix}approve [always] - Approve permission\n\ {prefix}deny - Deny permission\n\ \n\ - {prefix}recent - 5 most recent conversations\n\ {prefix}search - Search conversations\n\ - {prefix}detail - Conversation details\n\ {prefix}today - Today's activity summary\n\ {prefix}status - Channel connection status\n\ {prefix}help - Show help\n\ @@ -834,21 +665,6 @@ pub fn search_usage(lang: Lang, prefix: &str) -> String { } } -pub fn detail_usage(lang: Lang, prefix: &str) -> String { - match lang { - Lang::ZhCn => format!("用法: {prefix}detail <会话ID>"), - Lang::ZhTw => format!("用法: {prefix}detail <對話ID>"), - Lang::Ja => format!("使い方: {prefix}detail <セッションID>"), - Lang::Ko => format!("사용법: {prefix}detail <대화ID>"), - Lang::Es => format!("Uso: {prefix}detail "), - Lang::De => format!("Verwendung: {prefix}detail "), - Lang::Fr => format!("Utilisation : {prefix}detail "), - Lang::Pt => format!("Uso: {prefix}detail "), - Lang::Ar => format!("الاستخدام: {prefix}detail "), - Lang::En => format!("Usage: {prefix}detail "), - } -} - pub fn unknown_command(lang: Lang, prefix: &str, command: &str) -> String { match lang { Lang::ZhCn => format!( diff --git a/src-tauri/src/chat_channel/session_commands.rs b/src-tauri/src/chat_channel/session_commands.rs index 11822af..04923dd 100644 --- a/src-tauri/src/chat_channel/session_commands.rs +++ b/src-tauri/src/chat_channel/session_commands.rs @@ -2,12 +2,12 @@ use std::collections::BTreeMap; use std::sync::Arc; use std::time::Instant; -use sea_orm::DatabaseConnection; +use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, QueryOrder, QuerySelect}; use tokio::sync::Mutex; -use super::i18n::Lang; +use super::i18n::{self, Lang}; use super::session_bridge::{ActiveSession, SessionBridge}; -use super::types::RichMessage; +use super::types::{MessageLevel, RichMessage}; use crate::acp::manager::ConnectionManager; use crate::acp::registry::all_acp_agents; use crate::acp::types::PromptInputBlock; @@ -476,7 +476,7 @@ pub async fn handle_sessions( lang, prefix, "Reply {prefix}resume to continue.", - "回复 {prefix}resume 继续会话。" + "回复 {prefix}resume <会话ID> 继续会话。" ) )); @@ -501,15 +501,14 @@ pub async fn handle_resume( lang: Lang, prefix: &str, ) -> RichMessage { + if args.is_empty() { + return list_recent_sessions(db, lang, prefix).await; + } + let conversation_id: i32 = match args.parse() { Ok(id) => id, Err(_) => { - return RichMessage::info(tp( - lang, - prefix, - "Usage: {prefix}resume ", - "用法: {prefix}resume <会话ID>", - )); + return list_recent_sessions(db, lang, prefix).await; } }; @@ -825,6 +824,68 @@ pub async fn handle_followup( RichMessage::info(t(lang, "Message sent.", "消息已发送。")) } +// ── /resume (list recent) ── + +async fn list_recent_sessions( + db: &DatabaseConnection, + lang: Lang, + prefix: &str, +) -> RichMessage { + let recent = match conversation::Entity::find() + .filter(conversation::Column::DeletedAt.is_null()) + .order_by_desc(conversation::Column::CreatedAt) + .limit(10) + .all(db) + .await + { + Ok(rows) => rows, + Err(e) => { + return RichMessage { + title: Some(i18n::query_failed_title(lang).to_string()), + body: e.to_string(), + fields: Vec::new(), + level: MessageLevel::Error, + }; + } + }; + + if recent.is_empty() { + return RichMessage::info(t( + lang, + "No conversations found.", + "暂无会话记录。", + )) + .with_title(t(lang, "Recent Conversations", "最近会话")); + } + + let mut body = String::new(); + for conv in &recent { + let title = conv.title.as_deref().unwrap_or(i18n::untitled(lang)); + let agent = &conv.agent_type; + let time = conv.created_at.format("%m-%d %H:%M"); + body.push_str(&format!( + "#{} [{}] {} ({})\n", + conv.id, agent, title, time, + )); + } + + body.push_str(&format!( + "\n{}", + tp( + lang, + prefix, + "Reply {prefix}resume to resume a session.", + "回复 {prefix}resume <会话ID> 恢复会话。" + ) + )); + + RichMessage::info(body.trim_end()).with_title(t( + lang, + "Recent Conversations", + "最近会话", + )) +} + // ── Helpers ── fn t(lang: Lang, en: &str, zh: &str) -> String { diff --git a/src/components/settings/channel-commands-tab.tsx b/src/components/settings/channel-commands-tab.tsx index bf8cc6f..bb83d6d 100644 --- a/src/components/settings/channel-commands-tab.tsx +++ b/src/components/settings/channel-commands-tab.tsx @@ -14,13 +14,11 @@ const BUILT_IN_COMMANDS = [ { name: "agent [n|name]", descKey: "agentDesc" }, { name: "task ", descKey: "taskDesc" }, { name: "sessions", descKey: "sessionsDesc" }, - { name: "resume ", descKey: "resumeDesc" }, + { name: "resume [id]", descKey: "resumeDesc" }, { name: "cancel", descKey: "cancelDesc" }, { name: "approve [always]", descKey: "approveDesc" }, { name: "deny", descKey: "denyDesc" }, - { name: "recent", descKey: "recentDesc" }, { name: "search ", descKey: "searchDesc" }, - { name: "detail ", descKey: "detailDesc" }, { name: "today", descKey: "todayDesc" }, { name: "status", descKey: "statusDesc" }, { name: "help", descKey: "helpDesc" }, diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index 8a7430c..41ddcd8 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -1724,7 +1724,7 @@ }, "commands": { "title": "الأوامر المدمجة", - "description": "أوامر البوت المتاحة في قنوات المحادثة.", + "description": "أوامر البوت المتاحة في قنوات المحادثة. في المحادثات الجماعية، يلزم @Bot لمعالجة الرسائل.", "prefixLabel": "بادئة الأمر", "prefixDescription": "1-3 أحرف غير أبجدية رقمية لتشغيل أوامر البوت (الافتراضي /).", "prefixSaved": "تم حفظ بادئة الأمر.", @@ -1735,13 +1735,11 @@ "agentDesc": "اختيار وكيل الذكاء الاصطناعي", "taskDesc": "إنشاء جلسة وتنفيذ المهمة", "sessionsDesc": "عرض الجلسات النشطة في المجلد", - "resumeDesc": "استئناف جلسة موجودة", + "resumeDesc": "المحادثات الأخيرة / استئناف جلسة", "cancelDesc": "إلغاء المهمة الحالية", "approveDesc": "الموافقة على طلب إذن الوكيل", "denyDesc": "رفض طلب إذن الوكيل", - "recentDesc": "عرض آخر 5 محادثات", "searchDesc": "البحث في المحادثات حسب الكلمة المفتاحية", - "detailDesc": "عرض تفاصيل المحادثة", "todayDesc": "ملخص نشاط اليوم", "statusDesc": "حالة اتصال القناة", "helpDesc": "عرض المساعدة" diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index 5d73198..573e24b 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -1724,7 +1724,7 @@ }, "commands": { "title": "Integrierte Befehle", - "description": "Im Chat-Kanal verfügbare Bot-Befehle.", + "description": "Im Chat-Kanal verfügbare Bot-Befehle. In Gruppenchats ist @Bot erforderlich, um Nachrichten zu verarbeiten.", "prefixLabel": "Befehlspräfix", "prefixDescription": "1-3 nicht-alphanumerische Zeichen zum Auslösen von Bot-Befehlen (Standard /).", "prefixSaved": "Befehlspräfix gespeichert.", @@ -1735,13 +1735,11 @@ "agentDesc": "KI-Agent auswählen", "taskDesc": "Sitzung erstellen und Aufgabe ausführen", "sessionsDesc": "Aktive Sitzungen im Ordner anzeigen", - "resumeDesc": "Bestehende Sitzung fortsetzen", + "resumeDesc": "Neueste Konversationen / Sitzung fortsetzen", "cancelDesc": "Aktuelle Aufgabe abbrechen", "approveDesc": "Berechtigungsanfrage des Agenten genehmigen", "denyDesc": "Berechtigungsanfrage des Agenten ablehnen", - "recentDesc": "Die 5 neuesten Konversationen anzeigen", "searchDesc": "Konversationen nach Stichwort suchen", - "detailDesc": "Konversationsdetails anzeigen", "todayDesc": "Heutige Aktivitätsübersicht", "statusDesc": "Kanal-Verbindungsstatus", "helpDesc": "Hilfe anzeigen" diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 9ae3a14..846d68a 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -1724,7 +1724,7 @@ }, "commands": { "title": "Built-in Commands", - "description": "Bot commands available in chat channels.", + "description": "Bot commands available in chat channels. In group chats, @Bot is required to process messages.", "prefixLabel": "Command Prefix", "prefixDescription": "1-3 non-alphanumeric characters used to trigger bot commands (default /).", "prefixSaved": "Command prefix saved.", @@ -1735,13 +1735,11 @@ "agentDesc": "Select AI agent", "taskDesc": "Create session and run task", "sessionsDesc": "List active sessions in folder", - "resumeDesc": "Resume an existing session", + "resumeDesc": "Recent conversations / resume a session", "cancelDesc": "Cancel current task", "approveDesc": "Approve agent permission request", "denyDesc": "Deny agent permission request", - "recentDesc": "Show 5 most recent conversations", "searchDesc": "Search conversations by keyword", - "detailDesc": "Show conversation details", "todayDesc": "Today's activity summary", "statusDesc": "Channel connection status", "helpDesc": "Show help message" diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json index f06b9df..83e5817 100644 --- a/src/i18n/messages/es.json +++ b/src/i18n/messages/es.json @@ -1724,7 +1724,7 @@ }, "commands": { "title": "Comandos integrados", - "description": "Comandos de bot disponibles en los canales de chat.", + "description": "Comandos de bot disponibles en los canales de chat. En chats grupales, se requiere @Bot para procesar mensajes.", "prefixLabel": "Prefijo de comando", "prefixDescription": "1-3 caracteres no alfanuméricos para activar comandos del bot (por defecto /).", "prefixSaved": "Prefijo de comando guardado.", @@ -1735,13 +1735,11 @@ "agentDesc": "Seleccionar agente de IA", "taskDesc": "Crear sesión y ejecutar tarea", "sessionsDesc": "Listar sesiones activas en la carpeta", - "resumeDesc": "Reanudar una sesión existente", + "resumeDesc": "Conversaciones recientes / reanudar una sesión", "cancelDesc": "Cancelar tarea actual", "approveDesc": "Aprobar solicitud de permiso del agente", "denyDesc": "Denegar solicitud de permiso del agente", - "recentDesc": "Mostrar las 5 conversaciones más recientes", "searchDesc": "Buscar conversaciones por palabra clave", - "detailDesc": "Mostrar detalles de la conversación", "todayDesc": "Resumen de actividad de hoy", "statusDesc": "Estado de conexión del canal", "helpDesc": "Mostrar ayuda" diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index 1ea4efd..aba2fea 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -1724,7 +1724,7 @@ }, "commands": { "title": "Commandes intégrées", - "description": "Commandes bot disponibles dans les canaux de chat.", + "description": "Commandes bot disponibles dans les canaux de chat. Dans les chats de groupe, @Bot est requis pour traiter les messages.", "prefixLabel": "Préfixe de commande", "prefixDescription": "1-3 caractères non alphanumériques pour déclencher les commandes du bot (par défaut /).", "prefixSaved": "Préfixe de commande enregistré.", @@ -1735,13 +1735,11 @@ "agentDesc": "Sélectionner l'agent IA", "taskDesc": "Créer une session et exécuter la tâche", "sessionsDesc": "Lister les sessions actives du dossier", - "resumeDesc": "Reprendre une session existante", + "resumeDesc": "Conversations récentes / reprendre une session", "cancelDesc": "Annuler la tâche en cours", "approveDesc": "Approuver la demande de permission de l'agent", "denyDesc": "Refuser la demande de permission de l'agent", - "recentDesc": "Afficher les 5 conversations les plus récentes", "searchDesc": "Rechercher des conversations par mot-clé", - "detailDesc": "Afficher les détails de la conversation", "todayDesc": "Résumé de l'activité du jour", "statusDesc": "État de connexion du canal", "helpDesc": "Afficher l'aide" diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json index bae7468..10ac49c 100644 --- a/src/i18n/messages/ja.json +++ b/src/i18n/messages/ja.json @@ -1724,7 +1724,7 @@ }, "commands": { "title": "組み込みコマンド", - "description": "チャットチャンネルで使用可能な Bot コマンド。", + "description": "チャットチャンネルで使用可能な Bot コマンド。グループチャットではメッセージを処理するために @Bot が必要です。", "prefixLabel": "コマンドプレフィックス", "prefixDescription": "Bot コマンドを起動するプレフィックス、1-3 文字の英数字以外の文字(デフォルト /)。", "prefixSaved": "コマンドプレフィックスを保存しました。", @@ -1735,13 +1735,11 @@ "agentDesc": "AIエージェントを選択", "taskDesc": "セッションを作成してタスクを実行", "sessionsDesc": "フォルダ内のアクティブなセッション一覧", - "resumeDesc": "既存のセッションを再開", + "resumeDesc": "最近の会話 / セッションを再開", "cancelDesc": "現在のタスクをキャンセル", "approveDesc": "エージェントの権限リクエストを承認", "denyDesc": "エージェントの権限リクエストを拒否", - "recentDesc": "最近の会話5件を表示", "searchDesc": "キーワードで会話を検索", - "detailDesc": "会話の詳細を表示", "todayDesc": "本日のアクティビティ概要", "statusDesc": "チャンネル接続状態", "helpDesc": "ヘルプを表示" diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json index 702109c..3df750b 100644 --- a/src/i18n/messages/ko.json +++ b/src/i18n/messages/ko.json @@ -1724,7 +1724,7 @@ }, "commands": { "title": "내장 명령어", - "description": "채팅 채널에서 사용 가능한 Bot 명령어입니다.", + "description": "채팅 채널에서 사용 가능한 Bot 명령어입니다. 그룹 채팅에서는 메시지를 처리하려면 @Bot이 필요합니다.", "prefixLabel": "명령어 접두사", "prefixDescription": "Bot 명령어를 실행하는 접두사, 1-3개의 영숫자가 아닌 문자 (기본값 /).", "prefixSaved": "명령어 접두사가 저장되었습니다.", @@ -1735,13 +1735,11 @@ "agentDesc": "AI 에이전트 선택", "taskDesc": "세션 생성 및 작업 실행", "sessionsDesc": "폴더 내 활성 세션 목록", - "resumeDesc": "기존 세션 재개", + "resumeDesc": "최근 대화 / 세션 재개", "cancelDesc": "현재 작업 취소", "approveDesc": "에이전트 권한 요청 승인", "denyDesc": "에이전트 권한 요청 거부", - "recentDesc": "최근 대화 5개 표시", "searchDesc": "키워드로 대화 검색", - "detailDesc": "대화 상세 정보 표시", "todayDesc": "오늘의 활동 요약", "statusDesc": "채널 연결 상태", "helpDesc": "도움말 표시" diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index 9bdb787..923655d 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -1724,7 +1724,7 @@ }, "commands": { "title": "Comandos integrados", - "description": "Comandos de bot disponíveis nos canais de chat.", + "description": "Comandos de bot disponíveis nos canais de chat. Em chats em grupo, @Bot é necessário para processar mensagens.", "prefixLabel": "Prefixo de comando", "prefixDescription": "1-3 caracteres não alfanuméricos para acionar comandos do bot (padrão /).", "prefixSaved": "Prefixo de comando salvo.", @@ -1735,13 +1735,11 @@ "agentDesc": "Selecionar agente de IA", "taskDesc": "Criar sessão e executar tarefa", "sessionsDesc": "Listar sessões ativas na pasta", - "resumeDesc": "Retomar uma sessão existente", + "resumeDesc": "Conversas recentes / retomar uma sessão", "cancelDesc": "Cancelar tarefa atual", "approveDesc": "Aprovar solicitação de permissão do agente", "denyDesc": "Negar solicitação de permissão do agente", - "recentDesc": "Mostrar as 5 conversas mais recentes", "searchDesc": "Pesquisar conversas por palavra-chave", - "detailDesc": "Mostrar detalhes da conversa", "todayDesc": "Resumo da atividade de hoje", "statusDesc": "Status da conexão do canal", "helpDesc": "Mostrar ajuda" diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index 16a9474..40b0d4c 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -1724,7 +1724,7 @@ }, "commands": { "title": "内置指令", - "description": "消息渠道中可用的 Bot 指令。", + "description": "消息渠道中可用的 Bot 指令。群聊中需 @Bot 才会处理消息。", "prefixLabel": "指令前缀", "prefixDescription": "触发 Bot 指令的前缀,1-3 个非字母数字字符(默认 /)。", "prefixSaved": "指令前缀已保存。", @@ -1735,13 +1735,11 @@ "agentDesc": "选择 AI Agent", "taskDesc": "创建会话并执行任务", "sessionsDesc": "列出当前目录的活跃会话", - "resumeDesc": "恢复已有会话", + "resumeDesc": "最近会话 / 恢复指定会话", "cancelDesc": "取消当前任务", "approveDesc": "批准 Agent 权限请求", "denyDesc": "拒绝 Agent 权限请求", - "recentDesc": "最近 5 条会话", "searchDesc": "按关键词搜索会话", - "detailDesc": "会话详情", "todayDesc": "今日活动汇总", "statusDesc": "渠道连接状态", "helpDesc": "显示帮助" diff --git a/src/i18n/messages/zh-TW.json b/src/i18n/messages/zh-TW.json index 03062cd..52cbf7b 100644 --- a/src/i18n/messages/zh-TW.json +++ b/src/i18n/messages/zh-TW.json @@ -1724,7 +1724,7 @@ }, "commands": { "title": "內建指令", - "description": "訊息頻道中可用的 Bot 指令。", + "description": "訊息頻道中可用的 Bot 指令。群組聊天中需 @Bot 才會處理訊息。", "prefixLabel": "指令前綴", "prefixDescription": "觸發 Bot 指令的前綴,1-3 個非字母數字字元(預設 /)。", "prefixSaved": "指令前綴已儲存。", @@ -1735,13 +1735,11 @@ "agentDesc": "選擇 AI Agent", "taskDesc": "建立會話並執行任務", "sessionsDesc": "列出當前目錄的活躍會話", - "resumeDesc": "恢復已有會話", + "resumeDesc": "最近對話 / 恢復指定對話", "cancelDesc": "取消當前任務", "approveDesc": "批准 Agent 權限請求", "denyDesc": "拒絕 Agent 權限請求", - "recentDesc": "最近 5 筆對話", "searchDesc": "依關鍵字搜尋對話", - "detailDesc": "對話詳情", "todayDesc": "今日活動摘要", "statusDesc": "頻道連線狀態", "helpDesc": "顯示說明" From ce95148232d5cd9c5cf8acba7cae7689851f6326 Mon Sep 17 00:00:00 2001 From: xintaofei Date: Wed, 1 Apr 2026 17:29:01 +0800 Subject: [PATCH 4/8] fix: truncate_title panic on multi-byte UTF-8 strings Co-Authored-By: Claude Opus 4.6 (1M context) --- src-tauri/src/chat_channel/session_commands.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/chat_channel/session_commands.rs b/src-tauri/src/chat_channel/session_commands.rs index 04923dd..5545c36 100644 --- a/src-tauri/src/chat_channel/session_commands.rs +++ b/src-tauri/src/chat_channel/session_commands.rs @@ -928,9 +928,10 @@ fn resolve_agent_type( } fn truncate_title(s: &str) -> String { - if s.len() <= 80 { + if s.chars().count() <= 80 { s.to_string() } else { - format!("{}...", &s[..77]) + let truncated: String = s.chars().take(77).collect(); + format!("{truncated}...") } } From b23f6a5aaad0301428ab506e1f1f328842b3123d Mon Sep 17 00:00:00 2001 From: xintaofei Date: Wed, 1 Apr 2026 17:48:46 +0800 Subject: [PATCH 5/8] fix: enable inline math formula rendering with single dollar signs Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/ai-elements/message.tsx | 3 ++- src/components/ai-elements/reasoning.tsx | 3 ++- src/components/files/file-workspace-panel.tsx | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/ai-elements/message.tsx b/src/components/ai-elements/message.tsx index a2f15b6..3d5c9ad 100644 --- a/src/components/ai-elements/message.tsx +++ b/src/components/ai-elements/message.tsx @@ -15,7 +15,7 @@ import { cn } from "@/lib/utils" import { useTranslations } from "next-intl" import { cjk } from "@streamdown/cjk" import { code } from "@streamdown/code" -import { math } from "@streamdown/math" +import { createMathPlugin } from "@streamdown/math" import { mermaid } from "@streamdown/mermaid" import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react" import { @@ -326,6 +326,7 @@ export const MessageBranchPage = ({ export type MessageResponseProps = ComponentProps +const math = createMathPlugin({ singleDollarTextMath: true }) const streamdownPlugins = { cjk, code, math, mermaid } function MessageResponseImpl({ className, ...props }: MessageResponseProps) { diff --git a/src/components/ai-elements/reasoning.tsx b/src/components/ai-elements/reasoning.tsx index 7a9c2d0..4410fd0 100644 --- a/src/components/ai-elements/reasoning.tsx +++ b/src/components/ai-elements/reasoning.tsx @@ -12,7 +12,7 @@ import { import { cn } from "@/lib/utils" import { cjk } from "@streamdown/cjk" import { code } from "@streamdown/code" -import { math } from "@streamdown/math" +import { createMathPlugin } from "@streamdown/math" import { mermaid } from "@streamdown/mermaid" import { BrainIcon, ChevronDownIcon } from "lucide-react" import { @@ -212,6 +212,7 @@ export type ReasoningContentProps = ComponentProps< children: string } +const math = createMathPlugin({ singleDollarTextMath: true }) const streamdownPlugins = { cjk, code, math, mermaid } export const ReasoningContent = memo( diff --git a/src/components/files/file-workspace-panel.tsx b/src/components/files/file-workspace-panel.tsx index 7d81137..1da34c3 100644 --- a/src/components/files/file-workspace-panel.tsx +++ b/src/components/files/file-workspace-panel.tsx @@ -18,13 +18,14 @@ import { } from "@/components/ui/context-menu" import { cjk } from "@streamdown/cjk" import { code } from "@streamdown/code" -import { math } from "@streamdown/math" +import { createMathPlugin } from "@streamdown/math" import { mermaid } from "@streamdown/mermaid" import { Streamdown } from "streamdown" import { readFileBase64 } from "@/lib/api" import { defineMonacoThemes, useMonacoThemeSync } from "@/lib/monaco-themes" import "@/lib/monaco-local" +const math = createMathPlugin({ singleDollarTextMath: true }) const previewPlugins = { cjk, code, math, mermaid } function resolveRelativePath(base: string, relative: string): string { From 098c9adb80937c7fd230f79e95917dfd95158394 Mon Sep 17 00:00:00 2001 From: xintaofei Date: Wed, 1 Apr 2026 18:28:34 +0800 Subject: [PATCH 6/8] optimize: channel tool call details, permission context and stop reason i18n - Display specific file paths, commands, and patterns in tool call messages instead of generic titles (e.g. ">> Read: src/main.rs") - Show tool call details only on completion to ensure raw_input availability - Enrich permission request messages with tool details from rawInput - Localize stop_reason in turn_complete messages for all 10 languages - Fix UTF-8 byte-slice panic in format_completion for multi-byte content Co-Authored-By: Claude Opus 4.6 (1M context) --- src-tauri/src/chat_channel/session_bridge.rs | 2 + .../src/chat_channel/session_commands.rs | 2 + .../chat_channel/session_event_subscriber.rs | 290 ++++++++++++++++-- 3 files changed, 275 insertions(+), 19 deletions(-) diff --git a/src-tauri/src/chat_channel/session_bridge.rs b/src-tauri/src/chat_channel/session_bridge.rs index d0edaf3..fb9cf15 100644 --- a/src-tauri/src/chat_channel/session_bridge.rs +++ b/src-tauri/src/chat_channel/session_bridge.rs @@ -18,6 +18,8 @@ pub struct ActiveSession { pub connection_id: String, pub content_buffer: String, pub tool_calls: Vec, + /// Stores raw_input by tool_call_id for detail extraction on completion. + pub tool_call_inputs: HashMap, pub last_flushed: Instant, pub pending_prompt: Option, pub permission_pending: Option, diff --git a/src-tauri/src/chat_channel/session_commands.rs b/src-tauri/src/chat_channel/session_commands.rs index 5545c36..0ac8c5f 100644 --- a/src-tauri/src/chat_channel/session_commands.rs +++ b/src-tauri/src/chat_channel/session_commands.rs @@ -364,6 +364,7 @@ pub async fn handle_task( connection_id: connection_id.clone(), content_buffer: String::new(), tool_calls: Vec::new(), + tool_call_inputs: std::collections::HashMap::new(), last_flushed: Instant::now(), pending_prompt: Some(task_description.to_string()), permission_pending: None, @@ -561,6 +562,7 @@ pub async fn handle_resume( connection_id: connection_id.clone(), content_buffer: String::new(), tool_calls: Vec::new(), + tool_call_inputs: std::collections::HashMap::new(), last_flushed: Instant::now(), pending_prompt: None, permission_pending: None, diff --git a/src-tauri/src/chat_channel/session_event_subscriber.rs b/src-tauri/src/chat_channel/session_event_subscriber.rs index ac8cf1e..c8ca1a4 100644 --- a/src-tauri/src/chat_channel/session_event_subscriber.rs +++ b/src-tauri/src/chat_channel/session_event_subscriber.rs @@ -166,20 +166,20 @@ async fn handle_acp_event_payload( .get("title") .and_then(|v| v.as_str()) .unwrap_or("tool"); - let status = payload - .get("status") + let tool_call_id = payload + .get("tool_call_id") .and_then(|v| v.as_str()) .unwrap_or(""); + let raw_input = payload.get("raw_input").and_then(|v| v.as_str()); let mut guard = bridge.lock().await; if let Some(session) = guard.get_mut(connection_id) { + // Store title for progress indicator; store raw_input for later session.tool_calls.push(title.to_string()); - let channel_id = session.channel_id; - drop(guard); - - if status != "completed" { - let msg = RichMessage::info(format!(">> {title}")); - let _ = manager.send_to_channel(channel_id, &msg).await; + if let Some(input) = raw_input { + session + .tool_call_inputs + .insert(tool_call_id.to_string(), input.to_string()); } } } @@ -187,14 +187,30 @@ async fn handle_acp_event_payload( "tool_call_update" => { let title = payload.get("title").and_then(|v| v.as_str()); let status = payload.get("status").and_then(|v| v.as_str()); + let tool_call_id = payload + .get("tool_call_id") + .and_then(|v| v.as_str()) + .unwrap_or(""); + let raw_input = payload.get("raw_input").and_then(|v| v.as_str()); - if let (Some(title), Some("completed")) = (title, status) { - let guard = bridge.lock().await; - if let Some(session) = guard.get(connection_id) { + let mut guard = bridge.lock().await; + if let Some(session) = guard.get_mut(connection_id) { + // Accumulate raw_input if newly available + if let Some(input) = raw_input { + session + .tool_call_inputs + .insert(tool_call_id.to_string(), input.to_string()); + } + + if status == Some("completed") { + let stored_input = session.tool_call_inputs.remove(tool_call_id); + let effective_title = title.unwrap_or("tool"); + let input_ref = stored_input.as_deref().or(raw_input); + let detail = format_tool_call_detail(effective_title, input_ref); let channel_id = session.channel_id; drop(guard); - let msg = RichMessage::info(format!(">> {title} [done]")); + let msg = RichMessage::info(format!(">> {detail}")); let _ = manager.send_to_channel(channel_id, &msg).await; } } @@ -245,12 +261,23 @@ async fn handle_acp_event_payload( return; } - let tool_desc = tool_call + let tool_title = tool_call .get("title") .and_then(|v| v.as_str()) .or_else(|| tool_call.get("tool_name").and_then(|v| v.as_str())) - .unwrap_or("Unknown tool") - .to_string(); + .unwrap_or("Unknown tool"); + + // Extract detail from rawInput / raw_input in the tool_call object + let raw_input_str = tool_call + .get("rawInput") + .or_else(|| tool_call.get("raw_input")) + .and_then(|v| match v { + serde_json::Value::String(s) => Some(s.clone()), + serde_json::Value::Null => None, + other => Some(other.to_string()), + }); + let tool_desc = + format_tool_call_detail(tool_title, raw_input_str.as_deref()); session.permission_pending = Some(PendingPermission { request_id: request_id.to_string(), @@ -319,7 +346,7 @@ async fn handle_acp_event_payload( Lang::ZhCn | Lang::ZhTw => "结束原因", _ => "Stop Reason", }, - stop_reason, + localize_stop_reason(stop_reason, lang), ); let _ = manager.send_to_channel(channel_id, &msg).await; @@ -452,9 +479,19 @@ fn format_completion(content: &str, tool_count: usize, lang: Lang) -> String { return body; } - // Truncate long content - let head = &content[..500.min(content.len())]; - let tail_start = content.len().saturating_sub(500); + // Truncate long content (use char boundaries to avoid panic on multi-byte) + let head_end = content + .char_indices() + .nth(500) + .map(|(i, _)| i) + .unwrap_or(content.len()); + let head = &content[..head_end]; + let tail_start = content + .char_indices() + .rev() + .nth(499) + .map(|(i, _)| i) + .unwrap_or(0); let tail = &content[tail_start..]; match lang { @@ -472,3 +509,218 @@ fn format_completion(content: &str, tool_count: usize, lang: Lang) -> String { } } } + +fn localize_stop_reason(reason: &str, lang: Lang) -> String { + match lang { + Lang::ZhCn => match reason { + "end_turn" => "正常结束", + "cancelled" => "已取消", + "max_tokens" => "达到最大长度", + "stop_sequence" => "遇到停止序列", + "error" => "错误", + "timeout" => "超时", + other => other, + }, + Lang::ZhTw => match reason { + "end_turn" => "正常結束", + "cancelled" => "已取消", + "max_tokens" => "達到最大長度", + "stop_sequence" => "遇到停止序列", + "error" => "錯誤", + "timeout" => "逾時", + other => other, + }, + Lang::Ja => match reason { + "end_turn" => "正常終了", + "cancelled" => "キャンセル", + "max_tokens" => "最大トークン数到達", + "stop_sequence" => "停止シーケンス", + "error" => "エラー", + "timeout" => "タイムアウト", + other => other, + }, + Lang::Ko => match reason { + "end_turn" => "정상 종료", + "cancelled" => "취소됨", + "max_tokens" => "최대 길이 도달", + "stop_sequence" => "정지 시퀀스", + "error" => "오류", + "timeout" => "시간 초과", + other => other, + }, + Lang::Es => match reason { + "end_turn" => "Finalizado", + "cancelled" => "Cancelado", + "max_tokens" => "Longitud máxima alcanzada", + "error" => "Error", + "timeout" => "Tiempo agotado", + other => other, + }, + Lang::De => match reason { + "end_turn" => "Abgeschlossen", + "cancelled" => "Abgebrochen", + "max_tokens" => "Maximale Länge erreicht", + "error" => "Fehler", + "timeout" => "Zeitüberschreitung", + other => other, + }, + Lang::Fr => match reason { + "end_turn" => "Terminé", + "cancelled" => "Annulé", + "max_tokens" => "Longueur maximale atteinte", + "error" => "Erreur", + "timeout" => "Délai dépassé", + other => other, + }, + Lang::Pt => match reason { + "end_turn" => "Concluído", + "cancelled" => "Cancelado", + "max_tokens" => "Comprimento máximo atingido", + "error" => "Erro", + "timeout" => "Tempo esgotado", + other => other, + }, + Lang::Ar => match reason { + "end_turn" => "اكتمل", + "cancelled" => "ملغى", + "max_tokens" => "تم بلوغ الحد الأقصى", + "error" => "خطأ", + "timeout" => "انتهت المهلة", + other => other, + }, + Lang::En => match reason { + "end_turn" => "Completed", + "cancelled" => "Cancelled", + "max_tokens" => "Max length reached", + "stop_sequence" => "Stop sequence", + "error" => "Error", + "timeout" => "Timeout", + other => other, + }, + } + .to_string() +} + +/// Extract a concise detail string from a tool call's `raw_input` JSON. +/// +/// Returns a formatted string like `"Read: src/main.rs"` or `"Bash: npm test"`. +/// Falls back to the original title if no detail can be extracted. +fn format_tool_call_detail(title: &str, raw_input: Option<&str>) -> String { + let parsed = raw_input.and_then(|s| serde_json::from_str::(s).ok()); + + let normalized_title = title.to_lowercase().replace([' ', '-'], "_"); + + if let Some(ref obj) = parsed { + // File operations: read, edit, write, delete + if let Some(path) = obj + .get("file_path") + .or_else(|| obj.get("path")) + .or_else(|| obj.get("notebook_path")) + .and_then(|v| v.as_str()) + { + let short = short_path(path); + let label = match normalized_title.as_str() { + s if s.contains("write") => "Write", + s if s.contains("edit") || s.contains("change") || s.contains("update") => "Edit", + s if s.contains("delete") => "Delete", + _ => "Read", + }; + return format!("{label}: {short}"); + } + + // Bash / shell commands + if let Some(cmd) = obj + .get("command") + .or_else(|| obj.get("cmd")) + .and_then(|v| v.as_str()) + { + let short = truncate_str(cmd.lines().next().unwrap_or(cmd), 80); + return format!("Bash: {short}"); + } + + // Grep / search + if let Some(pattern) = obj.get("pattern").and_then(|v| v.as_str()) { + let path = obj.get("path").and_then(|v| v.as_str()); + return if let Some(p) = path { + format!("Grep: \"{}\" in {}", truncate_str(pattern, 40), short_path(p)) + } else { + format!("Grep: \"{}\"", truncate_str(pattern, 60)) + }; + } + + // Glob + if let Some(pat) = obj.get("glob").and_then(|v| v.as_str()) { + return format!("Glob: {pat}"); + } + + // Agent / task + if obj.get("subagent_type").is_some() + || obj.get("task_id").is_some() + || obj.get("subject").is_some() + { + let desc = obj + .get("description") + .or_else(|| obj.get("subject")) + .or_else(|| obj.get("prompt")) + .and_then(|v| v.as_str()); + if let Some(d) = desc { + return format!("Agent: {}", truncate_str(d, 60)); + } + } + + // Web fetch + if let Some(url) = obj.get("url").and_then(|v| v.as_str()) { + return format!("Fetch: {}", truncate_str(url, 80)); + } + + // Web search + if let Some(query) = obj.get("query").and_then(|v| v.as_str()) { + return format!("Search: {}", truncate_str(query, 60)); + } + + // TodoWrite + if obj.get("todos").is_some() { + return "TodoWrite".to_string(); + } + } + + // Fallback: if raw_input is a plain string (e.g. a bare command), use it directly + if let Some(raw) = raw_input { + if !raw.starts_with('{') && !raw.starts_with('[') { + let short = truncate_str(raw.lines().next().unwrap_or(raw), 80); + if normalized_title.contains("bash") + || normalized_title.contains("shell") + || normalized_title.contains("exec") + { + return format!("Bash: {short}"); + } + } + } + + title.to_string() +} + +fn short_path(path: &str) -> &str { + // Show last 2 path components at most, or the full path if short enough + if path.len() <= 60 { + return path; + } + let parts: Vec<&str> = path.rsplitn(3, '/').collect(); + if parts.len() >= 2 { + // e.g. "src/main.rs" from "/very/long/path/src/main.rs" + let tail = &path[path.len() - parts[0].len() - parts[1].len() - 1..]; + if tail.len() < path.len() { + return tail; + } + } + path +} + +fn truncate_str(s: &str, max: usize) -> String { + if s.chars().count() <= max { + s.to_string() + } else { + let truncated: String = s.chars().take(max.saturating_sub(3)).collect(); + format!("{truncated}...") + } +} From b4aaebeb54f773c6552f52a634c3def9b7460375 Mon Sep 17 00:00:00 2001 From: xintaofei Date: Wed, 1 Apr 2026 18:55:44 +0800 Subject: [PATCH 7/8] docs: update chat channels section with session interaction and new commands Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 10 ++++++---- docs/readme/README.ar.md | 10 ++++++---- docs/readme/README.de.md | 10 ++++++---- docs/readme/README.es.md | 12 +++++++----- docs/readme/README.fr.md | 10 ++++++---- docs/readme/README.ja.md | 10 ++++++---- docs/readme/README.ko.md | 10 ++++++---- docs/readme/README.pt.md | 10 ++++++---- docs/readme/README.zh-CN.md | 10 ++++++---- docs/readme/README.zh-TW.md | 10 ++++++---- 10 files changed, 61 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 8f6f27b..03c77db 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ and integrated Git/file/terminal workflows. - Local conversation ingestion with structured rendering - Parallel development with built-in `git worktree` flows - **Project Boot** — visually scaffold new projects with live preview -- **Chat Channels** — connect Telegram, Lark (Feishu) and more to your coding agents for real-time notifications and interactive commands +- **Chat Channels** — connect Telegram, Lark (Feishu) and more to your coding agents for real-time notifications, full session interaction, and remote task control - MCP management (local scan + registry search/install) - Skills management (global and project scope) - Git remote account management (GitHub and other Git servers) @@ -69,7 +69,7 @@ Currently supports **shadcn/ui** project scaffolding, with a tab-based design re ## Chat Channels -Connect your favorite messaging apps — Telegram, Lark (Feishu), and more — to your AI coding agents. Receive real-time notifications when agents complete tasks or encounter errors, query conversation history from your phone, and get automated daily reports — all without leaving your chat app. +Connect your favorite messaging apps — Telegram, Lark (Feishu), and more — to your AI coding agents. Create tasks, send follow-up messages, approve permissions, resume sessions, and monitor activity — all from your chat app. Receive real-time agent responses with tool-call details, permission prompts, and completion summaries without ever opening a browser. ### Supported Channels @@ -82,8 +82,10 @@ Connect your favorite messaging apps — Telegram, Lark (Feishu), and more — t ### Key Features -- **Event Notifications** — agent turn completions and errors are pushed to all enabled channels in real time -- **Interactive Commands** — send commands (`/recent`, `/search`, `/detail`, `/today`, `/status`, `/help`) from your chat app and get instant responses; configurable command prefix. Conversation-related commands (e.g. start, stop, approve) are planned for upcoming releases +- **Session Interaction** — run full agent sessions from your chat app: `/folder` to pick a project, `/agent` to choose an agent, `/task ` to start a task, then send follow-up messages as plain text. `/resume` to continue a previous session, `/cancel` to abort, `/sessions` to list active sessions +- **Permission Control** — agents can request tool-execution permissions in-chat; respond with `/approve` (or `/approve always` for auto-approve) and `/deny` without switching context +- **Event Notifications** — agent turn completions, tool calls, and errors are pushed to all enabled channels in real time with rich formatting +- **Query Commands** — `/search `, `/today`, `/status`, `/help` for quick lookups; configurable command prefix - **Daily Reports** — automated daily summary at a scheduled time, including conversation counts, agent-type breakdown, and project activity - **Multi-Language** — message templates in 10 languages (English, Simplified/Traditional Chinese, Japanese, Korean, Spanish, German, French, Portuguese, Arabic) - **Secure Credentials** — tokens stored in the OS keyring, never exposed in config files or logs diff --git a/docs/readme/README.ar.md b/docs/readme/README.ar.md index 2d1f5d0..02e25f7 100644 --- a/docs/readme/README.ar.md +++ b/docs/readme/README.ar.md @@ -39,7 +39,7 @@ OpenClaw، وغيرها) في تطبيق سطح مكتب أو خادم مستق - استيعاب محلي للمحادثات مع عرض منظّم - تطوير متوازي مع تدفقات `git worktree` مدمجة - **مُنشئ المشروع** — إنشاء مشاريع جديدة بصريًا مع معاينة حية -- **قنوات الدردشة** — ربط Telegram وLark (Feishu) والمزيد بوكلاء البرمجة للحصول على إشعارات فورية وأوامر تفاعلية +- **قنوات الدردشة** — ربط Telegram وLark (Feishu) والمزيد بوكلاء البرمجة للتفاعل الكامل مع الجلسات والتحكم عن بُعد في المهام - إدارة MCP (فحص محلي + بحث/تثبيت من السجل) - إدارة Skills (نطاق عام ونطاق المشروع) - إدارة حسابات Git البعيدة (GitHub وخوادم Git الأخرى) @@ -67,7 +67,7 @@ OpenClaw، وغيرها) في تطبيق سطح مكتب أو خادم مستق ## قنوات الدردشة -اربط تطبيقات المراسلة المفضلة لديك — Telegram وLark (Feishu) والمزيد — بوكلاء البرمجة بالذكاء الاصطناعي. استقبل إشعارات فورية عند إكمال الوكلاء للمهام أو مواجهة أخطاء، واستعلم عن سجل المحادثات من هاتفك، واحصل على تقارير يومية تلقائية — كل ذلك دون مغادرة تطبيق الدردشة. +اربط تطبيقات المراسلة المفضلة لديك — Telegram وLark (Feishu) والمزيد — بوكلاء البرمجة بالذكاء الاصطناعي. أنشئ مهامًا، وأرسل رسائل متابعة، ووافق على الأذونات، واستأنف الجلسات، وراقب النشاط من تطبيق الدردشة — واستقبل ردود الوكلاء الفورية مع تفاصيل استدعاءات الأدوات وطلبات الأذونات وملخصات الإنجاز دون الحاجة لفتح المتصفح. ### القنوات المدعومة @@ -80,8 +80,10 @@ OpenClaw، وغيرها) في تطبيق سطح مكتب أو خادم مستق ### الميزات الرئيسية -- **إشعارات الأحداث** — يتم دفع إتمام أدوار الوكلاء والأخطاء إلى جميع القنوات المُفعَّلة في الوقت الفعلي -- **أوامر تفاعلية** — أرسل أوامر (`/recent`، `/search`، `/detail`، `/today`، `/status`، `/help`) من تطبيق الدردشة واحصل على ردود فورية؛ بادئة الأمر قابلة للتكوين. الأوامر المتعلقة بالمحادثات (البدء، الإيقاف، الموافقة) مخططة للإصدارات القادمة +- **تفاعل الجلسات** — شغّل جلسات وكيل كاملة: `/folder` لاختيار المشروع، `/agent` لاختيار الوكيل، `/task <الوصف>` لبدء مهمة، وأرسل رسائل المتابعة كنص عادي. `/resume` لاستئناف جلسة سابقة، `/cancel` للإلغاء، `/sessions` لعرض الجلسات النشطة +- **التحكم في الأذونات** — يطلب الوكلاء أذونات تنفيذ الأدوات داخل الدردشة؛ `/approve` (أو `/approve always` للموافقة التلقائية) و`/deny` +- **إشعارات الأحداث** — يتم دفع إتمام أدوار الوكلاء واستدعاءات الأدوات والأخطاء في الوقت الفعلي بتنسيق غني +- **أوامر الاستعلام** — `/search <كلمة مفتاحية>`، `/today`، `/status`، `/help` للبحث السريع؛ بادئة الأمر قابلة للتكوين - **التقارير اليومية** — ملخص يومي تلقائي في وقت مجدول، يشمل عدد المحادثات وتوزيع أنواع الوكلاء ونشاط المشروع - **متعدد اللغات** — قوالب رسائل بـ 10 لغات (الإنجليزية، الصينية المبسطة/التقليدية، اليابانية، الكورية، الإسبانية، الألمانية، الفرنسية، البرتغالية، العربية) - **بيانات اعتماد آمنة** — يتم تخزين الرموز في حلقة مفاتيح نظام التشغيل، ولا تُكشف أبدًا في ملفات التكوين أو السجلات diff --git a/docs/readme/README.de.md b/docs/readme/README.de.md index 2738fbd..705defe 100644 --- a/docs/readme/README.de.md +++ b/docs/readme/README.de.md @@ -42,7 +42,7 @@ paralleler `git worktree`-Entwicklung, MCP/Skills-Verwaltung, Chat-Kanal-Integra - Lokale Konversationserfassung mit strukturierter Darstellung - Parallele Entwicklung mit integrierten `git worktree`-Abläufen - **Projekt-Starter** — neue Projekte visuell erstellen mit Live-Vorschau -- **Chat-Kanäle** — Telegram, Lark (Feishu) und mehr mit Ihren Coding-Agenten verbinden für Echtzeit-Benachrichtigungen und interaktive Befehle +- **Chat-Kanäle** — Telegram, Lark (Feishu) und mehr mit Ihren Coding-Agenten verbinden für vollständige Sitzungsinteraktion und Remote-Aufgabensteuerung - MCP-Verwaltung (lokaler Scan + Registry-Suche/Installation) - Skills-Verwaltung (global und projektbezogen) - Git-Remote-Kontoverwaltung (GitHub und andere Git-Server) @@ -70,7 +70,7 @@ Unterstützt derzeit **shadcn/ui**-Projekt-Scaffolding, mit einem Tab-basierten ## Chat-Kanäle -Verbinden Sie Ihre bevorzugten Messaging-Apps — Telegram, Lark (Feishu) und mehr — mit Ihren KI-Coding-Agenten. Erhalten Sie Echtzeit-Benachrichtigungen, wenn Agenten Aufgaben abschließen oder auf Fehler stoßen, fragen Sie den Konversationsverlauf von Ihrem Smartphone ab und erhalten Sie automatisierte Tagesberichte — alles ohne Ihre Chat-App zu verlassen. +Verbinden Sie Ihre bevorzugten Messaging-Apps — Telegram, Lark (Feishu) und mehr — mit Ihren KI-Coding-Agenten. Erstellen Sie Aufgaben, senden Sie Folgenachrichten, genehmigen Sie Berechtigungen, setzen Sie Sitzungen fort und überwachen Sie die Aktivität direkt aus dem Chat — empfangen Sie Echtzeit-Antworten der Agenten mit Tool-Call-Details, Berechtigungsanfragen und Abschlusszusammenfassungen, ohne einen Browser zu öffnen. ### Unterstützte Kanäle @@ -83,8 +83,10 @@ Verbinden Sie Ihre bevorzugten Messaging-Apps — Telegram, Lark (Feishu) und me ### Hauptfunktionen -- **Ereignisbenachrichtigungen** — Agenten-Rundenvervollständigungen und Fehler werden in Echtzeit an alle aktivierten Kanäle gepusht -- **Interaktive Befehle** — senden Sie Befehle (`/recent`, `/search`, `/detail`, `/today`, `/status`, `/help`) aus Ihrer Chat-App und erhalten Sie sofortige Antworten; konfigurierbarer Befehlspräfix. Konversationsbezogene Befehle (Start, Stopp, Genehmigung) sind für kommende Releases geplant +- **Sitzungs-Interaktion** — vollständige Agenten-Sitzungen ausführen: `/folder` Projekt wählen, `/agent` Agent auswählen, `/task ` Aufgabe starten, Folgenachrichten als Klartext senden. `/resume` vorherige Sitzung fortsetzen, `/cancel` abbrechen, `/sessions` aktive Sitzungen auflisten +- **Berechtigungssteuerung** — Agenten fordern Tool-Ausführungsberechtigungen im Chat an; `/approve` (oder `/approve always` für automatische Genehmigung) und `/deny` +- **Ereignisbenachrichtigungen** — Agenten-Rundenvervollständigungen, Tool-Calls und Fehler werden in Echtzeit mit Rich-Formatierung gepusht +- **Abfragebefehle** — `/search `, `/today`, `/status`, `/help` für schnelle Abfragen; konfigurierbarer Befehlspräfix - **Tagesberichte** — automatisierte tägliche Zusammenfassung zu einer geplanten Zeit, einschließlich Konversationszählung, Aufschlüsselung nach Agent-Typ und Projektaktivität - **Mehrsprachig** — Nachrichtenvorlagen in 10 Sprachen (Englisch, vereinfachtes/traditionelles Chinesisch, Japanisch, Koreanisch, Spanisch, Deutsch, Französisch, Portugiesisch, Arabisch) - **Sichere Anmeldedaten** — Token werden im OS-Schlüsselbund gespeichert, nie in Konfigurationsdateien oder Logs exponiert diff --git a/docs/readme/README.es.md b/docs/readme/README.es.md index d17ad27..bff0863 100644 --- a/docs/readme/README.es.md +++ b/docs/readme/README.es.md @@ -42,7 +42,7 @@ interacción con canales de chat (Telegram, Lark, etc.) y flujos integrados de G - Ingesta local de conversaciones con renderizado estructurado - Desarrollo paralelo con flujos integrados de `git worktree` - **Inicio de Proyecto** — crea nuevos proyectos visualmente con vista previa en tiempo real -- **Canales de Chat** — conecta Telegram, Lark (Feishu) y más a tus agentes de codificación para notificaciones en tiempo real y comandos interactivos +- **Canales de Chat** — conecta Telegram, Lark (Feishu) y más a tus agentes de codificación para interacción completa con sesiones y control remoto de tareas - Gestión de MCP (escaneo local + búsqueda/instalación desde registro) - Gestión de Skills (ámbito global y por proyecto) - Gestión de cuentas remotas de Git (GitHub y otros servidores Git) @@ -70,7 +70,7 @@ Actualmente soporta scaffolding de proyectos **shadcn/ui**, con un diseño basad ## Canales de Chat -Conecta tus aplicaciones de mensajería favoritas — Telegram, Lark (Feishu) y más — a tus agentes de codificación IA. Recibe notificaciones en tiempo real cuando los agentes completen tareas o encuentren errores, consulta el historial de conversaciones desde tu teléfono y obtén informes diarios automatizados — todo sin salir de tu aplicación de chat. +Conecta tus aplicaciones de mensajería favoritas — Telegram, Lark (Feishu) y más — a tus agentes de codificación IA. Crea tareas, envía mensajes de seguimiento, aprueba permisos, reanuda sesiones y monitorea la actividad directamente desde el chat — recibe respuestas del agente en tiempo real con detalles de llamadas a herramientas, solicitudes de permisos y resúmenes de finalización sin necesidad de abrir un navegador. ### Canales soportados @@ -83,12 +83,14 @@ Conecta tus aplicaciones de mensajería favoritas — Telegram, Lark (Feishu) y ### Características principales -- **Notificaciones de eventos** — las finalizaciones de turno y errores de los agentes se envían a todos los canales habilitados en tiempo real -- **Comandos interactivos** — envía comandos (`/recent`, `/search`, `/detail`, `/today`, `/status`, `/help`) desde tu app de chat y obtén respuestas instantáneas; prefijo de comando configurable. Los comandos relacionados con conversaciones (inicio, parada, aprobación) están planificados para próximas versiones +- **Interacción de sesiones** — ejecuta sesiones completas de agente: `/folder` para elegir proyecto, `/agent` para seleccionar agente, `/task ` para iniciar una tarea, envía mensajes de seguimiento como texto plano. `/resume` para continuar una sesión anterior, `/cancel` para cancelar, `/sessions` para listar las sesiones activas +- **Control de permisos** — los agentes solicitan permisos de ejecución de herramientas directamente en el chat; `/approve` (o `/approve always` para aprobación automática) y `/deny` +- **Notificaciones de eventos** — las finalizaciones de turno, llamadas a herramientas y errores de los agentes se envían en tiempo real con formato enriquecido +- **Comandos de consulta** — `/search `, `/today`, `/status`, `/help` para búsquedas rápidas; prefijo de comando configurable - **Informes diarios** — resumen diario automatizado a una hora programada, incluyendo recuento de conversaciones, desglose por tipo de agente y actividad del proyecto - **Multi-idioma** — plantillas de mensajes en 10 idiomas (inglés, chino simplificado/tradicional, japonés, coreano, español, alemán, francés, portugués, árabe) - **Credenciales seguras** — los tokens se almacenan en el llavero del SO, nunca se exponen en archivos de configuración ni logs -- **Mensajes enriquecidos** — formato Markdown para Telegram, diseño basado en tarjetas para Lark; respaldo en texto plano para todas las plataformas +- **Mensajes enriquecidos** — formato Markdown para Telegram, diseño de tarjetas para Lark; respaldo en texto plano para todas las plataformas ### Configuración diff --git a/docs/readme/README.fr.md b/docs/readme/README.fr.md index 0415a02..006c6a6 100644 --- a/docs/readme/README.fr.md +++ b/docs/readme/README.fr.md @@ -41,7 +41,7 @@ et workflows intégrés Git/fichiers/terminal. - Ingestion locale des conversations avec rendu structuré - Développement parallèle avec flux `git worktree` intégré - **Lanceur de projet** — créez visuellement de nouveaux projets avec aperçu en temps réel -- **Canaux de chat** — connectez Telegram, Lark (Feishu) et plus à vos agents de codage pour des notifications en temps réel et des commandes interactives +- **Canaux de chat** — connectez Telegram, Lark (Feishu) et plus à vos agents de codage pour une interaction complète avec les sessions et le contrôle à distance des tâches - Gestion MCP (scan local + recherche/installation depuis le registre) - Gestion des Skills (portée globale et projet) - Gestion des comptes distants Git (GitHub et autres serveurs Git) @@ -69,7 +69,7 @@ Prend actuellement en charge le scaffolding de projets **shadcn/ui**, avec un de ## Canaux de chat -Connectez vos applications de messagerie préférées — Telegram, Lark (Feishu) et plus — à vos agents de codage IA. Recevez des notifications en temps réel lorsque les agents terminent des tâches ou rencontrent des erreurs, consultez l'historique des conversations depuis votre téléphone et obtenez des rapports quotidiens automatisés — le tout sans quitter votre application de chat. +Connectez vos applications de messagerie préférées — Telegram, Lark (Feishu) et plus — à vos agents de codage IA. Créez des tâches, envoyez des messages de suivi, approuvez les permissions, reprenez des sessions et surveillez l'activité directement depuis votre chat — recevez les réponses des agents en temps réel avec les détails des appels d'outils, les demandes de permissions et les résumés de complétion, le tout sans ouvrir de navigateur. ### Canaux pris en charge @@ -82,8 +82,10 @@ Connectez vos applications de messagerie préférées — Telegram, Lark (Feishu ### Fonctionnalités clés -- **Notifications d'événements** — les complétions de tour et les erreurs des agents sont poussées vers tous les canaux activés en temps réel -- **Commandes interactives** — envoyez des commandes (`/recent`, `/search`, `/detail`, `/today`, `/status`, `/help`) depuis votre application de chat et obtenez des réponses instantanées ; préfixe de commande configurable. Les commandes liées aux conversations (démarrage, arrêt, approbation) sont prévues pour les prochaines versions +- **Interaction de session** — exécutez des sessions complètes avec les agents : `/folder` pour choisir le projet, `/agent` pour sélectionner l'agent, `/task ` pour lancer une tâche, envoyez des messages de suivi en texte libre. `/resume` pour reprendre une session précédente, `/cancel` pour annuler, `/sessions` pour lister les sessions actives +- **Contrôle des permissions** — les agents demandent les permissions d'exécution d'outils directement dans le chat ; `/approve` (ou `/approve always` pour l'approbation automatique) et `/deny` +- **Notifications d'événements** — les complétions de tour, les appels d'outils et les erreurs des agents sont poussés en temps réel avec un formatage enrichi +- **Commandes de requête** — `/search `, `/today`, `/status`, `/help` pour des consultations rapides ; préfixe de commande configurable - **Rapports quotidiens** — résumé quotidien automatisé à une heure programmée, incluant le nombre de conversations, la répartition par type d'agent et l'activité du projet - **Multi-langue** — modèles de messages en 10 langues (anglais, chinois simplifié/traditionnel, japonais, coréen, espagnol, allemand, français, portugais, arabe) - **Identifiants sécurisés** — les tokens sont stockés dans le trousseau du système d'exploitation, jamais exposés dans les fichiers de configuration ou les logs diff --git a/docs/readme/README.ja.md b/docs/readme/README.ja.md index b811633..90878e2 100644 --- a/docs/readme/README.ja.md +++ b/docs/readme/README.ja.md @@ -39,7 +39,7 @@ Claude Code、Codex CLI、OpenCode、Gemini CLI、OpenClaw などのローカル - ローカル会話の取り込みと構造化レンダリング - 内蔵 `git worktree` フローによる並列開発 - **プロジェクトブート** — ビジュアル設定とライブプレビューで新規プロジェクトを作成 -- **チャットチャンネル** — Telegram、Lark(Feishu)などをコーディング Agent に接続し、リアルタイム通知やインタラクティブコマンドを利用 +- **チャットチャンネル** — Telegram、Lark(Feishu)などをコーディング Agent に接続し、チャットからフルセッション操作やリモートタスク制御を実行 - MCP 管理(ローカルスキャン + レジストリ検索/インストール) - Skills 管理(グローバルおよびプロジェクトスコープ) - Git リモートアカウント管理(GitHub およびその他の Git サーバー) @@ -67,7 +67,7 @@ Claude Code、Codex CLI、OpenCode、Gemini CLI、OpenClaw などのローカル ## チャットチャンネル -お気に入りのメッセージングアプリ — Telegram、Lark(Feishu)など — を AI コーディング Agent に接続。Agent がタスクを完了したりエラーが発生した際にリアルタイム通知を受信し、スマートフォンから会話履歴を検索し、自動日次レポートを取得 — チャットアプリを離れることなくすべて対応可能。 +お気に入りのメッセージングアプリ — Telegram、Lark(Feishu)など — を AI コーディング Agent に接続。チャットからタスクの作成、フォローアップメッセージの送信、権限の承認、セッションの再開、アクティビティの監視が可能です。Agent のレスポンスはツールコール詳細、権限プロンプト、完了サマリーとともにリアルタイムで受信 — ブラウザを開くことなくすべて対応可能。 ### 対応チャンネル @@ -80,8 +80,10 @@ Claude Code、Codex CLI、OpenCode、Gemini CLI、OpenClaw などのローカル ### 主な機能 -- **イベント通知** — Agent のターン完了やエラーがすべての有効なチャンネルにリアルタイムでプッシュ -- **インタラクティブコマンド** — チャットアプリからコマンド(`/recent`、`/search`、`/detail`、`/today`、`/status`、`/help`)を送信し即座に応答を取得。コマンドプレフィックスの設定が可能。会話関連コマンド(開始、停止、承認など)は今後のリリースで対応予定 +- **セッション操作** — チャットからフル Agent セッションを実行:`/folder` でプロジェクト選択、`/agent` で Agent 選択、`/task <説明>` でタスク開始、プレーンテキストでフォローアップ送信。`/resume` で前回セッションを継続、`/cancel` で中止、`/sessions` でアクティブセッション一覧を表示 +- **権限制御** — Agent がツール実行権限をチャット内でリクエスト。`/approve`(または `/approve always` で自動承認)と `/deny` で応答 +- **イベント通知** — Agent のターン完了、ツールコール、エラーがリッチフォーマットでリアルタイムにプッシュ +- **クエリコマンド** — `/search <キーワード>`、`/today`、`/status`、`/help` でクイック検索。コマンドプレフィックスの設定が可能 - **日次レポート** — 予定された時刻に自動日次サマリーを生成(会話数、Agent タイプ別内訳、プロジェクトアクティビティを含む) - **多言語対応** — メッセージテンプレートは 10 言語に対応(英語、簡体字/繁体字中国語、日本語、韓国語、スペイン語、ドイツ語、フランス語、ポルトガル語、アラビア語) - **セキュアな認証情報** — トークンは OS キーリングに保存され、設定ファイルやログに公開されません diff --git a/docs/readme/README.ko.md b/docs/readme/README.ko.md index 2a1ef59..3325e46 100644 --- a/docs/readme/README.ko.md +++ b/docs/readme/README.ko.md @@ -39,7 +39,7 @@ Claude Code, Codex CLI, OpenCode, Gemini CLI, OpenClaw 등 로컬 AI 코딩 Agen - 로컬 대화 수집 및 구조화 렌더링 - 내장 `git worktree` 플로를 통한 병렬 개발 - **프로젝트 부트** — 시각적 설정과 실시간 미리보기로 새 프로젝트 생성 -- **채팅 채널** — Telegram, Lark(Feishu) 등을 코딩 에이전트에 연결하여 실시간 알림 및 대화형 명령 사용 +- **채팅 채널** — Telegram, Lark(Feishu) 등을 코딩 에이전트에 연결하여 전체 세션 상호작용 및 원격 작업 제어 - MCP 관리 (로컬 스캔 + 레지스트리 검색/설치) - Skills 관리 (글로벌 및 프로젝트 범위) - Git 원격 계정 관리 (GitHub 및 기타 Git 서버) @@ -67,7 +67,7 @@ Claude Code, Codex CLI, OpenCode, Gemini CLI, OpenClaw 등 로컬 AI 코딩 Agen ## 채팅 채널 -즐겨 사용하는 메신저 앱 — Telegram, Lark(Feishu) 등 — 을 AI 코딩 에이전트에 연결하세요. 에이전트가 작업을 완료하거나 오류가 발생하면 실시간 알림을 받고, 스마트폰에서 대화 기록을 조회하고, 자동화된 일일 리포트를 받을 수 있습니다 — 채팅 앱을 떠나지 않고도 모든 것이 가능합니다. +즐겨 사용하는 메신저 앱 — Telegram, Lark(Feishu) 등 — 을 AI 코딩 에이전트에 연결하세요. 채팅에서 직접 작업을 생성하고, 후속 메시지를 보내고, 권한을 승인하고, 세션을 재개하고, 활동을 모니터링할 수 있습니다 — 도구 호출 상세 정보, 권한 프롬프트, 완료 요약이 포함된 실시간 에이전트 응답을 브라우저를 열지 않고도 받을 수 있습니다. ### 지원 채널 @@ -80,8 +80,10 @@ Claude Code, Codex CLI, OpenCode, Gemini CLI, OpenClaw 등 로컬 AI 코딩 Agen ### 주요 기능 -- **이벤트 알림** — 에이전트의 턴 완료 및 오류가 활성화된 모든 채널에 실시간으로 푸시 -- **대화형 명령** — 채팅 앱에서 명령(`/recent`, `/search`, `/detail`, `/today`, `/status`, `/help`)을 보내고 즉시 응답을 받을 수 있으며, 명령 접두사 설정 가능. 대화 관련 명령(시작, 중지, 승인 등)은 향후 릴리스에서 지원 예정 +- **세션 상호작용** — 전체 에이전트 세션 실행: `/folder`로 프로젝트 선택, `/agent`로 에이전트 선택, `/task <설명>`으로 작업 시작, 일반 텍스트로 후속 메시지 전송. `/resume`으로 이전 세션 재개, `/cancel`로 중단, `/sessions`로 활성 세션 목록 확인 +- **권한 제어** — 에이전트가 채팅에서 도구 실행 권한을 요청; `/approve`(또는 `/approve always`로 자동 승인) 및 `/deny`로 응답 +- **이벤트 알림** — 에이전트 턴 완료, 도구 호출, 오류가 리치 포맷으로 실시간 푸시 +- **조회 명령어** — `/search <키워드>`, `/today`, `/status`, `/help`로 빠른 조회; 명령 접두사 설정 가능 - **일일 리포트** — 예약된 시간에 자동 일일 요약 생성(대화 수, 에이전트 유형별 분석, 프로젝트 활동 포함) - **다국어 지원** — 10개 언어의 메시지 템플릿(영어, 간체/번체 중국어, 일본어, 한국어, 스페인어, 독일어, 프랑스어, 포르투갈어, 아랍어) - **보안 자격증명** — 토큰은 OS 키링에 저장되며 설정 파일이나 로그에 노출되지 않음 diff --git a/docs/readme/README.pt.md b/docs/readme/README.pt.md index 2b25dcf..7a3394a 100644 --- a/docs/readme/README.pt.md +++ b/docs/readme/README.pt.md @@ -41,7 +41,7 @@ e fluxos integrados de Git/arquivos/terminal. - Ingestão local de conversas com renderização estruturada - Desenvolvimento paralelo com fluxos `git worktree` integrados - **Inicializador de Projeto** — crie novos projetos visualmente com pré-visualização em tempo real -- **Canais de Chat** — conecte Telegram, Lark (Feishu) e mais aos seus agentes de codificação para notificações em tempo real e comandos interativos +- **Canais de Chat** — conecte Telegram, Lark (Feishu) e mais aos seus agentes de codificação para interação completa de sessão e controle remoto de tarefas - Gerenciamento de MCP (varredura local + busca/instalação no registro) - Gerenciamento de Skills (escopo global e por projeto) - Gerenciamento de contas remotas Git (GitHub e outros servidores Git) @@ -69,7 +69,7 @@ Atualmente suporta scaffolding de projetos **shadcn/ui**, com um design baseado ## Canais de Chat -Conecte seus aplicativos de mensagens favoritos — Telegram, Lark (Feishu) e mais — aos seus agentes de codificação IA. Receba notificações em tempo real quando os agentes concluírem tarefas ou encontrarem erros, consulte o histórico de conversas pelo celular e receba relatórios diários automatizados — tudo sem sair do seu app de chat. +Conecte seus aplicativos de mensagens favoritos — Telegram, Lark (Feishu) e mais — aos seus agentes de codificação IA. Crie tarefas, envie mensagens de acompanhamento, aprove permissões, retome sessões e monitore a atividade diretamente do chat — recebendo respostas do agente em tempo real com detalhes de chamadas de ferramentas, prompts de permissão e resumos de conclusão, tudo sem abrir o navegador. ### Canais suportados @@ -82,8 +82,10 @@ Conecte seus aplicativos de mensagens favoritos — Telegram, Lark (Feishu) e ma ### Recursos principais -- **Notificações de eventos** — conclusões de turno e erros dos agentes são enviados a todos os canais habilitados em tempo real -- **Comandos interativos** — envie comandos (`/recent`, `/search`, `/detail`, `/today`, `/status`, `/help`) do seu app de chat e receba respostas instantâneas; prefixo de comando configurável. Comandos relacionados a conversas (iniciar, parar, aprovar) estão planejados para próximas versões +- **Interação de sessão** — execute sessões completas de agente: `/folder` para selecionar o projeto, `/agent` para escolher o agente, `/task ` para iniciar uma tarefa, envie mensagens de acompanhamento como texto simples. `/resume` para continuar uma sessão anterior, `/cancel` para cancelar, `/sessions` para listar sessões ativas +- **Controle de permissões** — os agentes solicitam permissões de execução de ferramentas diretamente no chat; `/approve` (ou `/approve always` para aprovação automática) e `/deny` +- **Notificações de eventos** — conclusões de turno, chamadas de ferramentas e erros dos agentes são enviados em tempo real com formatação rica +- **Comandos de consulta** — `/search `, `/today`, `/status`, `/help` para consultas rápidas; prefixo de comando configurável - **Relatórios diários** — resumo diário automatizado em um horário programado, incluindo contagem de conversas, divisão por tipo de agente e atividade do projeto - **Multi-idioma** — templates de mensagens em 10 idiomas (inglês, chinês simplificado/tradicional, japonês, coreano, espanhol, alemão, francês, português, árabe) - **Credenciais seguras** — tokens armazenados no chaveiro do SO, nunca expostos em arquivos de configuração ou logs diff --git a/docs/readme/README.zh-CN.md b/docs/readme/README.zh-CN.md index cf5564e..8e40475 100644 --- a/docs/readme/README.zh-CN.md +++ b/docs/readme/README.zh-CN.md @@ -39,7 +39,7 @@ OpenClaw、Cline 等)统一到桌面应用、独立服务器或 Docker 容器 - 本地对话解析与结构化渲染 - 内置 `git worktree` 并行开发流程 - **项目启动器** — 可视化创建新项目,实时预览效果 -- **消息渠道** — 连接 Telegram、飞书等即时通讯应用到编码代理,实时接收通知并进行交互式命令操作 +- **消息渠道** — 连接 Telegram、飞书等即时通讯应用到编码代理,实时接收通知、完整会话交互、远程任务控制 - MCP 管理(本地扫描 + 市场搜索/安装) - Skills 管理(全局与项目级) - Git 远程账号管理(支持 GitHub 及其它 Git 服务器) @@ -67,7 +67,7 @@ OpenClaw、Cline 等)统一到桌面应用、独立服务器或 Docker 容器 ## 消息渠道 -连接你喜爱的即时通讯应用——Telegram、飞书等——到 AI 编码代理。当代理完成任务或遇到错误时接收实时通知,通过手机查询对话历史,获取自动化日报——无需离开聊天应用。 +连接你喜爱的即时通讯应用——Telegram、飞书等——到 AI 编码代理。直接在聊天中创建任务、发送后续消息、审批权限、恢复会话、监控活动。实时接收代理响应(包含工具调用详情、权限提示和完成摘要),无需打开浏览器。 ### 支持的渠道 @@ -80,8 +80,10 @@ OpenClaw、Cline 等)统一到桌面应用、独立服务器或 Docker 容器 ### 主要功能 -- **事件通知** — 代理的回合完成和错误事件实时推送到所有已启用的渠道 -- **交互式命令** — 从聊天应用发送命令(`/recent`、`/search`、`/detail`、`/today`、`/status`、`/help`)并获得即时响应;支持自定义命令前缀。对话相关命令(如启动、停止、审批)将在后续版本中支持 +- **会话交互** — 在聊天应用中运行完整代理会话:`/folder` 选择项目、`/agent` 选择代理、`/task <描述>` 启动任务,然后直接发送纯文本作为后续消息。`/resume` 恢复历史会话、`/cancel` 取消任务、`/sessions` 查看活跃会话 +- **权限控制** — 代理可在聊天中请求工具执行权限;使用 `/approve`(或 `/approve always` 自动审批)和 `/deny` 响应,无需切换上下文 +- **事件通知** — 代理回合完成、工具调用和错误事件实时推送到所有已启用渠道,支持富文本格式 +- **查询命令** — `/search <关键词>`、`/today`、`/status`、`/help` 快速查询;支持自定义命令前缀 - **每日报告** — 在预设时间自动生成每日摘要,包括对话数量、代理类型分布和项目活跃度 - **多语言** — 消息模板支持 10 种语言(英语、简体中文/繁体中文、日语、韩语、西班牙语、德语、法语、葡萄牙语、阿拉伯语) - **安全凭据** — 令牌存储在操作系统密钥环中,不会暴露在配置文件或日志中 diff --git a/docs/readme/README.zh-TW.md b/docs/readme/README.zh-TW.md index 0feb7d6..c1c3075 100644 --- a/docs/readme/README.zh-TW.md +++ b/docs/readme/README.zh-TW.md @@ -39,7 +39,7 @@ OpenClaw、Cline 等)整合到桌面應用、獨立伺服器或 Docker 容器 - 本地對話解析與結構化渲染 - 內建 `git worktree` 並行開發流程 - **專案啟動器** — 視覺化建立新專案,即時預覽效果 -- **訊息渠道** — 連接 Telegram、飛書等即時通訊應用到編碼代理,即時接收通知並進行互動式命令操作 +- **訊息渠道** — 連接 Telegram、飛書等即時通訊應用到編碼代理,完整會話交互、遠端任務控制 - MCP 管理(本地掃描 + 市場搜尋/安裝) - Skills 管理(全域與專案級) - Git 遠端帳號管理(支援 GitHub 及其他 Git 伺服器) @@ -67,7 +67,7 @@ OpenClaw、Cline 等)整合到桌面應用、獨立伺服器或 Docker 容器 ## 訊息渠道 -連接你喜愛的即時通訊應用——Telegram、飛書等——到 AI 編碼代理。當代理完成任務或遇到錯誤時接收即時通知,透過手機查詢對話歷史,取得自動化日報——無需離開聊天應用。 +連接你喜愛的即時通訊應用——Telegram、飛書等——到 AI 編碼代理。直接在聊天中建立任務、發送後續訊息、審批權限請求、恢復會話、監控代理活動——即時接收代理回應,包含工具呼叫詳情、權限提示和完成摘要。 ### 支援的渠道 @@ -80,8 +80,10 @@ OpenClaw、Cline 等)整合到桌面應用、獨立伺服器或 Docker 容器 ### 主要功能 -- **事件通知** — 代理的回合完成和錯誤事件即時推送到所有已啟用的渠道 -- **互動式命令** — 從聊天應用發送命令(`/recent`、`/search`、`/detail`、`/today`、`/status`、`/help`)並獲得即時回應;支援自訂命令前綴。對話相關命令(如啟動、停止、審批)將在後續版本中支援 +- **會話交互** — 執行完整的代理會話:`/folder` 選擇專案、`/agent` 選擇代理、`/task <描述>` 啟動任務,直接發送純文字作為後續訊息。`/resume` 繼續上次會話、`/cancel` 中止任務、`/sessions` 列出活躍會話 +- **權限控制** — 代理在聊天中請求工具執行權限;使用 `/approve`(或 `/approve always` 啟用自動審批)和 `/deny` 進行回應 +- **事件通知** — 代理回合完成、工具呼叫和錯誤事件即時推送,支援豐富格式展示 +- **查詢命令** — `/search <關鍵字>`、`/today`、`/status`、`/help` 快速查詢;支援自訂命令前綴 - **每日報告** — 在預設時間自動產生每日摘要,包括對話數量、代理類型分佈和專案活躍度 - **多語言** — 訊息範本支援 10 種語言(英語、簡體中文/繁體中文、日語、韓語、西班牙語、德語、法語、葡萄牙語、阿拉伯語) - **安全憑據** — 令牌儲存在作業系統密鑰環中,不會暴露在設定檔或日誌中 From a34d14bf59500749446d2635bc7cfd5a6cda8e34 Mon Sep 17 00:00:00 2001 From: xintaofei Date: Wed, 1 Apr 2026 19:04:39 +0800 Subject: [PATCH 8/8] Release versioin 0.6.3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Supports conversational Interaction — run full agent sessions from your chat app: /folder to pick a project, /agent to choose an agent, /task to start a task, then send follow-up messages as plain text. /resume to continue a previous session, /cancel to abort, /sessions to list active sessions - feature: messaging channels now support session-related commands. - fix: enable inline math formula rendering with single dollar signs. ----------------------------- 发布版本 0.6.3 支持会话交互 — 在聊天应用中运行完整代理会话:/folder 选择项目、/agent 选择代理、/task <描述> 启动任务,然后直接发送纯文本作为后续消息。/resume 恢复历史会话、/cancel 取消任务、/sessions 查看活跃会话 - 功能:消息通道现已支持会话相关指令; - 修复:启用单美元符号行内数学公式渲染。 --- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 624a8ac..4c31d44 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "codeg", "private": true, - "version": "0.6.2", + "version": "0.6.3", "scripts": { "dev": "next dev --turbopack", "build": "next build", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 41ec920..0c75eb4 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -847,7 +847,7 @@ checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" [[package]] name = "codeg" -version = "0.6.2" +version = "0.6.3" dependencies = [ "agent-client-protocol-schema", "async-trait", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 6fe951e..f3df4eb 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "codeg" -version = "0.6.2" +version = "0.6.3" description = "Agent Code Generation App" authors = ["feitao"] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 97a04c0..289ab1c 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "codeg", - "version": "0.6.2", + "version": "0.6.3", "identifier": "app.codeg", "build": { "beforeDevCommand": "pnpm dev",