增强斜杠命令功能,支持参数解析和新命令;实现双重 Ctrl+C 退出确认
This commit is contained in:
parent
c11eb348f9
commit
61eea62bfc
@ -123,13 +123,14 @@ impl CliChatChannel {
|
|||||||
let session_id = current_session_guard.clone().unwrap();
|
let session_id = current_session_guard.clone().unwrap();
|
||||||
|
|
||||||
// Check for slash command
|
// Check for slash command
|
||||||
if let Some((cmd_name, _)) = parse_slash_command(&content) {
|
if let Some((cmd_name, cmd_args)) = parse_slash_command(&content) {
|
||||||
// Send ExecuteSlashCommand via control plane
|
// Send ExecuteSlashCommand via control plane
|
||||||
let (reply_tx, mut reply_rx) = mpsc::channel(1);
|
let (reply_tx, mut reply_rx) = mpsc::channel(1);
|
||||||
let unified_id = UnifiedSessionId::parse(&session_id);
|
let unified_id = UnifiedSessionId::parse(&session_id);
|
||||||
bus.publish_control(ControlMessage {
|
bus.publish_control(ControlMessage {
|
||||||
op: SessionCommand::ExecuteSlashCommand {
|
op: SessionCommand::ExecuteSlashCommand {
|
||||||
command: cmd_name.to_string(),
|
command: cmd_name.to_string(),
|
||||||
|
args: if cmd_args.is_empty() { None } else { Some(cmd_args.to_string()) },
|
||||||
channel: self.name().to_string(),
|
channel: self.name().to_string(),
|
||||||
chat_id: chat_id.clone(),
|
chat_id: chat_id.clone(),
|
||||||
current_session_id: unified_id,
|
current_session_id: unified_id,
|
||||||
|
|||||||
@ -54,6 +54,10 @@ pub struct App {
|
|||||||
pub session_scroll_offset: u16,
|
pub session_scroll_offset: u16,
|
||||||
pub should_quit: bool,
|
pub should_quit: bool,
|
||||||
|
|
||||||
|
// Quit confirmation state (double Ctrl+C to exit)
|
||||||
|
pub ctrl_c_count: u8,
|
||||||
|
pub pending_quit: bool,
|
||||||
|
|
||||||
// Command menu state
|
// Command menu state
|
||||||
pub commands: Vec<SlashCommandInfo>,
|
pub commands: Vec<SlashCommandInfo>,
|
||||||
pub show_command_menu: bool,
|
pub show_command_menu: bool,
|
||||||
@ -76,6 +80,8 @@ impl App {
|
|||||||
chat_scroll_offset: 0,
|
chat_scroll_offset: 0,
|
||||||
session_scroll_offset: 0,
|
session_scroll_offset: 0,
|
||||||
should_quit: false,
|
should_quit: false,
|
||||||
|
ctrl_c_count: 0,
|
||||||
|
pending_quit: false,
|
||||||
commands: Vec::new(),
|
commands: Vec::new(),
|
||||||
show_command_menu: false,
|
show_command_menu: false,
|
||||||
selected_command_idx: 0,
|
selected_command_idx: 0,
|
||||||
@ -161,6 +167,28 @@ impl App {
|
|||||||
self.should_quit = true;
|
self.should_quit = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handle Ctrl+C for quit confirmation (requires double press)
|
||||||
|
pub fn handle_ctrl_c_for_quit(&mut self) -> bool {
|
||||||
|
if self.pending_quit {
|
||||||
|
self.ctrl_c_count += 1;
|
||||||
|
if self.ctrl_c_count >= 2 {
|
||||||
|
self.should_quit = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
self.pending_quit = true;
|
||||||
|
self.ctrl_c_count = 1;
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cancel pending quit if user presses any other key
|
||||||
|
pub fn cancel_pending_quit(&mut self) {
|
||||||
|
self.pending_quit = false;
|
||||||
|
self.ctrl_c_count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Command menu methods
|
// Command menu methods
|
||||||
pub fn set_commands(&mut self, commands: Vec<SlashCommandInfo>) {
|
pub fn set_commands(&mut self, commands: Vec<SlashCommandInfo>) {
|
||||||
self.commands = commands;
|
self.commands = commands;
|
||||||
|
|||||||
@ -10,21 +10,21 @@ pub fn render(f: &mut Frame, area: Rect) {
|
|||||||
|
|
||||||
let help_text = vec![
|
let help_text = vec![
|
||||||
ListItem::new("Commands:"),
|
ListItem::new("Commands:"),
|
||||||
ListItem::new(" /new [title] - Create new session"),
|
ListItem::new(" /new [title] - Archive current, start new"),
|
||||||
ListItem::new(" /sessions - List all sessions"),
|
ListItem::new(" /sessions - List all conversations"),
|
||||||
ListItem::new(" /use <id> - Load session"),
|
ListItem::new(" /switch <id> - Switch to conversation"),
|
||||||
ListItem::new(" /rename <title> - Rename session"),
|
ListItem::new(" /rename <t> - Rename current conversation"),
|
||||||
ListItem::new(" /archive - Archive session"),
|
ListItem::new(" /archive - Archive current conversation"),
|
||||||
ListItem::new(" /delete - Delete session"),
|
ListItem::new(" /delete - Delete current conversation"),
|
||||||
ListItem::new(" /clear - Clear history"),
|
ListItem::new(" /compact - Trigger context compression"),
|
||||||
ListItem::new(" /help, /? - Show this help"),
|
ListItem::new(" /info - Show session information"),
|
||||||
ListItem::new(" /quit, /q - Quit"),
|
|
||||||
ListItem::new(""),
|
ListItem::new(""),
|
||||||
ListItem::new("Keyboard:"),
|
ListItem::new("Keyboard:"),
|
||||||
ListItem::new(" Enter - Send message"),
|
ListItem::new(" Enter - Send message"),
|
||||||
ListItem::new(" Esc, q - Quit"),
|
ListItem::new(" Ctrl+C ×2 - Quit"),
|
||||||
ListItem::new(" ? - Show help"),
|
ListItem::new(" ? - Show help"),
|
||||||
ListItem::new(" Arrow keys - Navigate"),
|
ListItem::new(" Arrow keys - Navigate"),
|
||||||
|
ListItem::new(" / - Show command menu"),
|
||||||
];
|
];
|
||||||
|
|
||||||
let list = List::new(help_text).block(
|
let list = List::new(help_text).block(
|
||||||
|
|||||||
@ -7,18 +7,36 @@ use ratatui::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||||
let title = if let Some(session_id) = &app.current_session_id {
|
let (title, style) = if app.pending_quit {
|
||||||
format!("PicoBot | Session: {}", session_id)
|
let msg = if let Some(session_id) = &app.current_session_id {
|
||||||
|
format!("PicoBot | Session: {} | Press Ctrl+C again to quit", session_id)
|
||||||
} else {
|
} else {
|
||||||
"PicoBot".to_string()
|
"PicoBot | Press Ctrl+C again to quit".to_string()
|
||||||
};
|
};
|
||||||
|
(
|
||||||
let paragraph = Paragraph::new(title)
|
msg,
|
||||||
.style(
|
Style::default()
|
||||||
|
.fg(Color::Yellow)
|
||||||
|
.add_modifier(Modifier::BOLD),
|
||||||
|
)
|
||||||
|
} else if let Some(session_id) = &app.current_session_id {
|
||||||
|
(
|
||||||
|
format!("PicoBot | Session: {}", session_id),
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(Color::Cyan)
|
.fg(Color::Cyan)
|
||||||
.add_modifier(Modifier::BOLD),
|
.add_modifier(Modifier::BOLD),
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
"PicoBot".to_string(),
|
||||||
|
Style::default()
|
||||||
|
.fg(Color::Cyan)
|
||||||
|
.add_modifier(Modifier::BOLD),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let paragraph = Paragraph::new(title)
|
||||||
|
.style(style)
|
||||||
.block(Block::default().borders(Borders::ALL));
|
.block(Block::default().borders(Borders::ALL));
|
||||||
|
|
||||||
f.render_widget(paragraph, area);
|
f.render_widget(paragraph, area);
|
||||||
|
|||||||
@ -47,10 +47,17 @@ pub async fn handle_key_event(app: &mut App, key: KeyEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_normal_input(app: &mut App, key: KeyEvent) {
|
async fn handle_normal_input(app: &mut App, key: KeyEvent) {
|
||||||
match key.code {
|
// Handle Ctrl+C for quit (double press to exit)
|
||||||
KeyCode::Esc | KeyCode::Char('q') => {
|
let is_ctrl_c = key.code == KeyCode::Char('c') && key.modifiers.contains(crossterm::event::KeyModifiers::CONTROL);
|
||||||
app.quit();
|
if is_ctrl_c {
|
||||||
|
if app.handle_ctrl_c_for_quit() {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
app.cancel_pending_quit();
|
||||||
|
}
|
||||||
|
|
||||||
|
match key.code {
|
||||||
KeyCode::Char('?') => {
|
KeyCode::Char('?') => {
|
||||||
app.toggle_help();
|
app.toggle_help();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -183,8 +183,8 @@ impl GatewayState {
|
|||||||
let commands = session_manager.get_slash_commands().to_vec();
|
let commands = session_manager.get_slash_commands().to_vec();
|
||||||
Ok(SessionEvent::SlashCommandsList { commands })
|
Ok(SessionEvent::SlashCommandsList { commands })
|
||||||
}
|
}
|
||||||
ExecuteSlashCommand { command, channel, chat_id, current_session_id } => {
|
ExecuteSlashCommand { command, args, channel, chat_id, current_session_id } => {
|
||||||
session_manager.execute_slash_command(&command, &channel, &chat_id, current_session_id.as_ref())
|
session_manager.execute_slash_command(&command, args.as_deref(), &channel, &chat_id, current_session_id.as_ref())
|
||||||
.await
|
.await
|
||||||
.map(|(new_id, msg)| SessionEvent::SlashCommandExecuted { new_session_id: new_id, message: msg })
|
.map(|(new_id, msg)| SessionEvent::SlashCommandExecuted { new_session_id: new_id, message: msg })
|
||||||
.map_err(|e| ChannelError::Other(e.to_string()))
|
.map_err(|e| ChannelError::Other(e.to_string()))
|
||||||
|
|||||||
@ -51,6 +51,7 @@ pub enum SessionCommand {
|
|||||||
/// Execute a slash command
|
/// Execute a slash command
|
||||||
ExecuteSlashCommand {
|
ExecuteSlashCommand {
|
||||||
command: String,
|
command: String,
|
||||||
|
args: Option<String>,
|
||||||
channel: String,
|
channel: String,
|
||||||
chat_id: String,
|
chat_id: String,
|
||||||
current_session_id: Option<UnifiedSessionId>,
|
current_session_id: Option<UnifiedSessionId>,
|
||||||
|
|||||||
@ -59,6 +59,16 @@ pub enum SessionEvent {
|
|||||||
new_session_id: Option<UnifiedSessionId>,
|
new_session_id: Option<UnifiedSessionId>,
|
||||||
message: String,
|
message: String,
|
||||||
},
|
},
|
||||||
|
/// Context compression completed
|
||||||
|
ContextCompressed {
|
||||||
|
original_count: usize,
|
||||||
|
compressed_count: usize,
|
||||||
|
},
|
||||||
|
/// Session information
|
||||||
|
SessionInfo {
|
||||||
|
session_id: String,
|
||||||
|
message_count: usize,
|
||||||
|
},
|
||||||
/// Error occurred
|
/// Error occurred
|
||||||
Error {
|
Error {
|
||||||
code: String,
|
code: String,
|
||||||
|
|||||||
@ -239,9 +239,44 @@ impl SlashCommand {
|
|||||||
/// Session 支持的斜杠命令列表
|
/// Session 支持的斜杠命令列表
|
||||||
pub static SLASH_COMMANDS: &[SlashCommand] = &[
|
pub static SLASH_COMMANDS: &[SlashCommand] = &[
|
||||||
SlashCommand {
|
SlashCommand {
|
||||||
name: "reset",
|
name: "new",
|
||||||
description: "Start a fresh conversation (archives current dialog)",
|
description: "Archive current conversation and start a new one",
|
||||||
aliases: &["/reset", "/new"],
|
aliases: &["/new"],
|
||||||
|
},
|
||||||
|
SlashCommand {
|
||||||
|
name: "sessions",
|
||||||
|
description: "List all conversations",
|
||||||
|
aliases: &["/sessions"],
|
||||||
|
},
|
||||||
|
SlashCommand {
|
||||||
|
name: "switch",
|
||||||
|
description: "Switch to a specific conversation by ID",
|
||||||
|
aliases: &["/switch"],
|
||||||
|
},
|
||||||
|
SlashCommand {
|
||||||
|
name: "rename",
|
||||||
|
description: "Rename current conversation",
|
||||||
|
aliases: &["/rename"],
|
||||||
|
},
|
||||||
|
SlashCommand {
|
||||||
|
name: "archive",
|
||||||
|
description: "Archive current conversation",
|
||||||
|
aliases: &["/archive"],
|
||||||
|
},
|
||||||
|
SlashCommand {
|
||||||
|
name: "delete",
|
||||||
|
description: "Delete current conversation",
|
||||||
|
aliases: &["/delete"],
|
||||||
|
},
|
||||||
|
SlashCommand {
|
||||||
|
name: "compact",
|
||||||
|
description: "Manually trigger context compression",
|
||||||
|
aliases: &["/compact"],
|
||||||
|
},
|
||||||
|
SlashCommand {
|
||||||
|
name: "info",
|
||||||
|
description: "Print current session information",
|
||||||
|
aliases: &["/info"],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -285,6 +320,7 @@ impl SessionManager {
|
|||||||
pub async fn execute_slash_command(
|
pub async fn execute_slash_command(
|
||||||
&self,
|
&self,
|
||||||
command: &str,
|
command: &str,
|
||||||
|
args: Option<&str>,
|
||||||
channel: &str,
|
channel: &str,
|
||||||
chat_id: &str,
|
chat_id: &str,
|
||||||
current_session_id: Option<&UnifiedSessionId>,
|
current_session_id: Option<&UnifiedSessionId>,
|
||||||
@ -295,16 +331,95 @@ impl SessionManager {
|
|||||||
.ok_or_else(|| AgentError::Other(format!("Unknown command: {}", command)))?;
|
.ok_or_else(|| AgentError::Other(format!("Unknown command: {}", command)))?;
|
||||||
|
|
||||||
match cmd.name {
|
match cmd.name {
|
||||||
"reset" => {
|
"new" => {
|
||||||
if let Some(sid) = current_session_id {
|
if let Some(sid) = current_session_id {
|
||||||
let unified_str = sid.to_string();
|
|
||||||
self.store
|
self.store
|
||||||
.archive_session(&unified_str)
|
.archive_session(&sid.to_string())
|
||||||
.map_err(|e| AgentError::Other(format!("archive session error: {}", e)))?;
|
.map_err(|e| AgentError::Other(format!("archive session error: {}", e)))?;
|
||||||
}
|
}
|
||||||
|
let title = args.map(|s| s.to_string());
|
||||||
|
let (new_id, title) = self.create_session(channel, chat_id, title.as_deref()).await?;
|
||||||
|
Ok((Some(new_id), format!("New conversation '{}' created.", title)))
|
||||||
|
}
|
||||||
|
"sessions" => {
|
||||||
|
Ok((None, "Fetching sessions list...".to_string()))
|
||||||
|
}
|
||||||
|
"switch" => {
|
||||||
|
let target_id = args
|
||||||
|
.ok_or_else(|| AgentError::Other("Usage: /switch <session_id>".to_string()))?;
|
||||||
|
let unified_id = UnifiedSessionId::parse(target_id)
|
||||||
|
.ok_or_else(|| AgentError::Other("Invalid session ID format".to_string()))?;
|
||||||
|
Ok((Some(unified_id), format!("Switched to session {}", target_id)))
|
||||||
|
}
|
||||||
|
"rename" => {
|
||||||
|
let title = args
|
||||||
|
.ok_or_else(|| AgentError::Other("Usage: /rename <title>".to_string()))?;
|
||||||
|
if let Some(sid) = current_session_id {
|
||||||
|
self.store
|
||||||
|
.rename_session(&sid.to_string(), title)
|
||||||
|
.map_err(|e| AgentError::Other(format!("rename session error: {}", e)))?;
|
||||||
|
Ok((None, format!("Conversation renamed to '{}'.", title)))
|
||||||
|
} else {
|
||||||
|
Ok((None, "No active conversation to rename.".to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"archive" => {
|
||||||
|
if let Some(sid) = current_session_id {
|
||||||
|
self.store
|
||||||
|
.archive_session(&sid.to_string())
|
||||||
|
.map_err(|e| AgentError::Other(format!("archive session error: {}", e)))?;
|
||||||
|
Ok((None, "Conversation archived.".to_string()))
|
||||||
|
} else {
|
||||||
|
Ok((None, "No active conversation to archive.".to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"delete" => {
|
||||||
|
if let Some(sid) = current_session_id {
|
||||||
|
self.store
|
||||||
|
.delete_session(&sid.to_string())
|
||||||
|
.map_err(|e| AgentError::Other(format!("delete session error: {}", e)))?;
|
||||||
let (new_id, _title) = self.create_session(channel, chat_id, None).await?;
|
let (new_id, _title) = self.create_session(channel, chat_id, None).await?;
|
||||||
Ok((Some(new_id), "Starting a fresh conversation...".to_string()))
|
Ok((Some(new_id), "Conversation deleted. New conversation created.".to_string()))
|
||||||
|
} else {
|
||||||
|
Ok((None, "No active conversation to delete.".to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"compact" => {
|
||||||
|
if let Some(sid) = current_session_id {
|
||||||
|
let session = self.get_or_create_session(sid).await?;
|
||||||
|
let mut session_guard = session.lock().await;
|
||||||
|
session_guard.load_history()?;
|
||||||
|
let original_count = session_guard.get_history().len();
|
||||||
|
let history = session_guard.get_history().to_vec();
|
||||||
|
let compressed = session_guard.compressor
|
||||||
|
.compress_if_needed(history)
|
||||||
|
.await?;
|
||||||
|
let compressed_count = compressed.len();
|
||||||
|
session_guard.clear_history()?;
|
||||||
|
for msg in compressed {
|
||||||
|
session_guard.add_message(msg);
|
||||||
|
}
|
||||||
|
Ok((None, format!(
|
||||||
|
"Context compressed: {} → {} messages.",
|
||||||
|
original_count, compressed_count
|
||||||
|
)))
|
||||||
|
} else {
|
||||||
|
Ok((None, "No active conversation to compress.".to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"info" => {
|
||||||
|
if let Some(sid) = current_session_id {
|
||||||
|
let session = self.get_or_create_session(sid).await?;
|
||||||
|
let session_guard = session.lock().await;
|
||||||
|
let message_count = session_guard.get_history().len();
|
||||||
|
let session_id_str = session_guard.persistent_session_id();
|
||||||
|
Ok((None, format!(
|
||||||
|
"Session ID: {}\nMessage count: {}",
|
||||||
|
session_id_str, message_count
|
||||||
|
)))
|
||||||
|
} else {
|
||||||
|
Ok((None, "No active session.".to_string()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => Err(AgentError::Other(format!("Command not implemented: {}", cmd.name))),
|
_ => Err(AgentError::Other(format!("Command not implemented: {}", cmd.name))),
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user