重构: 添加斜杠命令支持和命令菜单功能
This commit is contained in:
parent
0c356e7ac4
commit
98259a7770
@ -5,7 +5,7 @@ use uuid::Uuid;
|
|||||||
|
|
||||||
use crate::bus::{ControlMessage, InboundMessage, MessageBus, OutboundMessage};
|
use crate::bus::{ControlMessage, InboundMessage, MessageBus, OutboundMessage};
|
||||||
use crate::session::{SessionCommand, SessionEvent, UnifiedSessionId};
|
use crate::session::{SessionCommand, SessionEvent, UnifiedSessionId};
|
||||||
use crate::protocol::{parse_inbound, WsInbound, WsOutbound};
|
use crate::protocol::{parse_inbound, WsInbound, WsOutbound, SlashCommandInfo};
|
||||||
|
|
||||||
use super::base::{Channel, ChannelError};
|
use super::base::{Channel, ChannelError};
|
||||||
use super::slash_command::parse_slash_command;
|
use super::slash_command::parse_slash_command;
|
||||||
@ -435,6 +435,43 @@ impl CliChatChannel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
WsInbound::GetSlashCommands => {
|
||||||
|
// Get commands from session manager via control message
|
||||||
|
let (reply_tx, mut reply_rx) = mpsc::channel(1);
|
||||||
|
bus.publish_control(ControlMessage {
|
||||||
|
op: SessionCommand::GetSlashCommands {
|
||||||
|
channel: "cli_chat".to_string(),
|
||||||
|
chat_id: "".to_string(),
|
||||||
|
},
|
||||||
|
reply_tx,
|
||||||
|
}).await?;
|
||||||
|
|
||||||
|
if let Some(result) = reply_rx.recv().await {
|
||||||
|
match result {
|
||||||
|
Ok(SessionEvent::SlashCommandsList { commands }) => {
|
||||||
|
// Convert to SlashCommand to SlashCommandInfo
|
||||||
|
let command_infos: Vec<SlashCommandInfo> = commands.into_iter().map(|cmd| {
|
||||||
|
SlashCommandInfo {
|
||||||
|
name: cmd.name.to_string(),
|
||||||
|
description: cmd.description.to_string(),
|
||||||
|
aliases: cmd.aliases.iter().map(|&a| a.to_string()).collect(),
|
||||||
|
}
|
||||||
|
}).collect();
|
||||||
|
let _ = client.sender.send(WsOutbound::SlashCommandsList { commands: command_infos }).await;
|
||||||
|
}
|
||||||
|
Ok(SessionEvent::Error { code, message }) => {
|
||||||
|
let _ = client.sender.send(WsOutbound::Error { code, message }).await;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let _ = client.sender.send(WsOutbound::Error {
|
||||||
|
code: "GET_COMMANDS_ERROR".to_string(),
|
||||||
|
message: e.to_string()
|
||||||
|
}).await;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
WsInbound::Ping => {
|
WsInbound::Ping => {
|
||||||
let _ = client.sender.send(WsOutbound::Pong).await;
|
let _ = client.sender.send(WsOutbound::Pong).await;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,7 @@ use crossterm::{
|
|||||||
execute,
|
execute,
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||||
};
|
};
|
||||||
use futures_util::StreamExt;
|
use futures_util::{SinkExt, StreamExt};
|
||||||
use ratatui::{prelude::CrosstermBackend, Terminal};
|
use ratatui::{prelude::CrosstermBackend, Terminal};
|
||||||
use std::io;
|
use std::io;
|
||||||
use tokio_tungstenite::{connect_async, tungstenite::Message};
|
use tokio_tungstenite::{connect_async, tungstenite::Message};
|
||||||
@ -25,7 +25,6 @@ pub async fn run(gateway_url: &str) -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
app.ws_sender = Some(ws_sender);
|
app.ws_sender = Some(ws_sender);
|
||||||
app.ws_receiver = Some(ws_receiver);
|
app.ws_receiver = Some(ws_receiver);
|
||||||
|
|
||||||
// 初始化终端
|
|
||||||
enable_raw_mode()?;
|
enable_raw_mode()?;
|
||||||
let mut stdout = io::stdout();
|
let mut stdout = io::stdout();
|
||||||
execute!(stdout, EnterAlternateScreen)?;
|
execute!(stdout, EnterAlternateScreen)?;
|
||||||
@ -35,7 +34,7 @@ pub async fn run(gateway_url: &str) -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
let result = run_app(&mut terminal, app).await;
|
let result = run_app(&mut terminal, app).await;
|
||||||
|
|
||||||
// 清理终端 - 确保正确的顺序,忽略错误
|
// Cleanup terminal, ignore errors
|
||||||
let _ = execute!(terminal.backend_mut(), LeaveAlternateScreen);
|
let _ = execute!(terminal.backend_mut(), LeaveAlternateScreen);
|
||||||
let _ = disable_raw_mode();
|
let _ = disable_raw_mode();
|
||||||
let _ = terminal.show_cursor();
|
let _ = terminal.show_cursor();
|
||||||
@ -50,6 +49,14 @@ async fn run_app(
|
|||||||
let mut ws_receiver = app.ws_receiver.take().unwrap();
|
let mut ws_receiver = app.ws_receiver.take().unwrap();
|
||||||
let mut event_reader = event::EventStream::new();
|
let mut event_reader = event::EventStream::new();
|
||||||
|
|
||||||
|
// Request command list on startup
|
||||||
|
if let Some(sender) = &mut app.ws_sender {
|
||||||
|
let inbound = WsInbound::GetSlashCommands;
|
||||||
|
if let Ok(text) = serialize_inbound(&inbound) {
|
||||||
|
let _ = sender.send(Message::Text(text.into())).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
terminal.draw(|f| render_ui(f, &app))?;
|
terminal.draw(|f| render_ui(f, &app))?;
|
||||||
|
|
||||||
@ -58,7 +65,7 @@ async fn run_app(
|
|||||||
match msg {
|
match msg {
|
||||||
Some(Ok(Message::Text(text))) => {
|
Some(Ok(Message::Text(text))) => {
|
||||||
if let Ok(outbound) = serde_json::from_str::<WsOutbound>(&text) {
|
if let Ok(outbound) = serde_json::from_str::<WsOutbound>(&text) {
|
||||||
handle_ws_message(&mut app, outbound);
|
handle_ws_message(&mut app, outbound).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(Ok(Message::Close(_))) | None => {
|
Some(Ok(Message::Close(_))) | None => {
|
||||||
@ -83,7 +90,7 @@ async fn run_app(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_ws_message(app: &mut App, outbound: WsOutbound) {
|
async fn handle_ws_message(app: &mut App, outbound: WsOutbound) {
|
||||||
match outbound {
|
match outbound {
|
||||||
WsOutbound::AssistantResponse { content, .. } => {
|
WsOutbound::AssistantResponse { content, .. } => {
|
||||||
app.add_message(MessageRole::Assistant, content);
|
app.add_message(MessageRole::Assistant, content);
|
||||||
@ -116,6 +123,12 @@ fn handle_ws_message(app: &mut App, outbound: WsOutbound) {
|
|||||||
WsOutbound::HistoryCleared { .. } => {
|
WsOutbound::HistoryCleared { .. } => {
|
||||||
app.messages.clear();
|
app.messages.clear();
|
||||||
}
|
}
|
||||||
_ => {}
|
WsOutbound::SlashCommandsList { commands } => {
|
||||||
|
app.set_commands(commands);
|
||||||
|
}
|
||||||
|
WsOutbound::Pong => {}
|
||||||
|
WsOutbound::CommandExecuted { message } => {
|
||||||
|
app.add_message(MessageRole::System, message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
use crate::protocol::SessionSummary;
|
use crate::protocol::{SessionSummary, SlashCommandInfo};
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use tokio_tungstenite::tungstenite::Message;
|
use tokio_tungstenite::tungstenite::Message;
|
||||||
|
|
||||||
@ -53,6 +53,11 @@ pub struct App {
|
|||||||
pub chat_scroll_offset: u16,
|
pub chat_scroll_offset: u16,
|
||||||
pub session_scroll_offset: u16,
|
pub session_scroll_offset: u16,
|
||||||
pub should_quit: bool,
|
pub should_quit: bool,
|
||||||
|
|
||||||
|
// Command menu state
|
||||||
|
pub commands: Vec<SlashCommandInfo>,
|
||||||
|
pub show_command_menu: bool,
|
||||||
|
pub selected_command_idx: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
@ -71,6 +76,9 @@ impl App {
|
|||||||
chat_scroll_offset: 0,
|
chat_scroll_offset: 0,
|
||||||
session_scroll_offset: 0,
|
session_scroll_offset: 0,
|
||||||
should_quit: false,
|
should_quit: false,
|
||||||
|
commands: Vec::new(),
|
||||||
|
show_command_menu: false,
|
||||||
|
selected_command_idx: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,4 +160,57 @@ impl App {
|
|||||||
pub fn quit(&mut self) {
|
pub fn quit(&mut self) {
|
||||||
self.should_quit = true;
|
self.should_quit = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Command menu methods
|
||||||
|
pub fn set_commands(&mut self, commands: Vec<SlashCommandInfo>) {
|
||||||
|
self.commands = commands;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_filtered_commands(&self) -> Vec<&SlashCommandInfo> {
|
||||||
|
let input_lower = self.input.to_lowercase();
|
||||||
|
self.commands
|
||||||
|
.iter()
|
||||||
|
.filter(|cmd| {
|
||||||
|
cmd.name.to_lowercase().contains(&input_lower)
|
||||||
|
|| cmd.description.to_lowercase().contains(&input_lower)
|
||||||
|
|| cmd
|
||||||
|
.aliases
|
||||||
|
.iter()
|
||||||
|
.any(|a| a.to_lowercase().contains(&input_lower))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_next_command(&mut self) {
|
||||||
|
let filtered = self.get_filtered_commands();
|
||||||
|
if !filtered.is_empty() {
|
||||||
|
self.selected_command_idx = (self.selected_command_idx + 1) % filtered.len() as u16;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_prev_command(&mut self) {
|
||||||
|
let filtered = self.get_filtered_commands();
|
||||||
|
if !filtered.is_empty() {
|
||||||
|
self.selected_command_idx = if self.selected_command_idx == 0 {
|
||||||
|
filtered.len() as u16 - 1
|
||||||
|
} else {
|
||||||
|
self.selected_command_idx - 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_selected_command(&self) -> Option<&SlashCommandInfo> {
|
||||||
|
let filtered = self.get_filtered_commands();
|
||||||
|
filtered.get(self.selected_command_idx as usize).copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert_command(&mut self) {
|
||||||
|
if let Some(cmd) = self.get_selected_command() {
|
||||||
|
// Use the first alias as the command to insert
|
||||||
|
if let Some(alias) = cmd.aliases.first() {
|
||||||
|
self.input = alias.clone();
|
||||||
|
self.input_cursor_pos = self.input.len();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
52
src/client/tui/components/command_menu.rs
Normal file
52
src/client/tui/components/command_menu.rs
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
use crate::client::tui::app::App;
|
||||||
|
use ratatui::{
|
||||||
|
layout::Rect,
|
||||||
|
style::{Color, Modifier, Style},
|
||||||
|
text::{Line, Span},
|
||||||
|
widgets::{Block, Borders, List, ListItem},
|
||||||
|
Frame,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||||
|
let filtered = app.get_filtered_commands();
|
||||||
|
|
||||||
|
if filtered.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let items: Vec<ListItem> = filtered
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, cmd)| {
|
||||||
|
let is_selected = i == app.selected_command_idx as usize;
|
||||||
|
let style = if is_selected {
|
||||||
|
Style::default()
|
||||||
|
.fg(Color::White)
|
||||||
|
.bg(Color::Blue)
|
||||||
|
.add_modifier(Modifier::BOLD)
|
||||||
|
} else {
|
||||||
|
Style::default().fg(Color::White)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Show first alias as the command
|
||||||
|
let alias = cmd.aliases.first().map(|a| a.as_str()).unwrap_or(&cmd.name);
|
||||||
|
|
||||||
|
ListItem::new(Line::from(vec![
|
||||||
|
Span::styled(alias, style.clone()),
|
||||||
|
Span::styled(" - ", Style::default().fg(Color::Gray)),
|
||||||
|
Span::styled(&cmd.description, style),
|
||||||
|
]))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let list = List::new(items)
|
||||||
|
.block(
|
||||||
|
Block::default()
|
||||||
|
.title("Commands")
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(Style::default().fg(Color::Cyan)),
|
||||||
|
)
|
||||||
|
.highlight_style(Style::default().add_modifier(Modifier::BOLD));
|
||||||
|
|
||||||
|
f.render_widget(list, area);
|
||||||
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
pub mod chat_history;
|
pub mod chat_history;
|
||||||
|
pub mod command_menu;
|
||||||
pub mod help_popup;
|
pub mod help_popup;
|
||||||
pub mod input_area;
|
pub mod input_area;
|
||||||
pub mod session_list;
|
pub mod session_list;
|
||||||
|
|||||||
@ -14,7 +14,39 @@ pub async fn handle_key_event(app: &mut App, key: KeyEvent) {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if app.show_command_menu {
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Esc => {
|
||||||
|
app.show_command_menu = false;
|
||||||
|
app.selected_command_idx = 0;
|
||||||
|
}
|
||||||
|
KeyCode::Up => {
|
||||||
|
app.select_prev_command();
|
||||||
|
}
|
||||||
|
KeyCode::Down => {
|
||||||
|
app.select_next_command();
|
||||||
|
}
|
||||||
|
KeyCode::Enter => {
|
||||||
|
app.insert_command();
|
||||||
|
app.show_command_menu = false;
|
||||||
|
app.selected_command_idx = 0;
|
||||||
|
}
|
||||||
|
KeyCode::Tab => {
|
||||||
|
app.insert_command();
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Handle normal input and check if menu should stay open
|
||||||
|
handle_normal_input(app, key).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_normal_input(app, key).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_normal_input(app: &mut App, key: KeyEvent) {
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Esc | KeyCode::Char('q') => {
|
KeyCode::Esc | KeyCode::Char('q') => {
|
||||||
app.quit();
|
app.quit();
|
||||||
@ -24,9 +56,23 @@ pub async fn handle_key_event(app: &mut App, key: KeyEvent) {
|
|||||||
}
|
}
|
||||||
KeyCode::Char(c) => {
|
KeyCode::Char(c) => {
|
||||||
app.input_insert_char(c);
|
app.input_insert_char(c);
|
||||||
|
|
||||||
|
// Show command menu when input starts with /
|
||||||
|
if !app.show_command_menu && (app.input == "/" || (app.input.len() > 1 && app.input.starts_with('/'))) {
|
||||||
|
app.show_command_menu = true;
|
||||||
|
app.selected_command_idx = 0;
|
||||||
|
} else if app.show_command_menu && !app.input.starts_with('/') {
|
||||||
|
app.show_command_menu = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Backspace => {
|
KeyCode::Backspace => {
|
||||||
app.input_delete_char();
|
app.input_delete_char();
|
||||||
|
|
||||||
|
// Hide menu if input no longer starts with /
|
||||||
|
if app.show_command_menu && !app.input.starts_with('/') {
|
||||||
|
app.show_command_menu = false;
|
||||||
|
app.selected_command_idx = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Left => {
|
KeyCode::Left => {
|
||||||
app.input_move_cursor_left();
|
app.input_move_cursor_left();
|
||||||
@ -48,6 +94,8 @@ pub async fn handle_key_event(app: &mut App, key: KeyEvent) {
|
|||||||
}
|
}
|
||||||
KeyCode::Enter => {
|
KeyCode::Enter => {
|
||||||
let input = app.take_input();
|
let input = app.take_input();
|
||||||
|
app.show_command_menu = false;
|
||||||
|
app.selected_command_idx = 0;
|
||||||
if !input.is_empty() {
|
if !input.is_empty() {
|
||||||
process_input(app, input).await;
|
process_input(app, input).await;
|
||||||
}
|
}
|
||||||
@ -57,140 +105,16 @@ pub async fn handle_key_event(app: &mut App, key: KeyEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn process_input(app: &mut App, input: String) {
|
async fn process_input(app: &mut App, input: String) {
|
||||||
let trimmed = input.trim();
|
app.add_message(MessageRole::User, input.clone());
|
||||||
|
if let Some(sender) = &mut app.ws_sender {
|
||||||
if let Some(cmd) = parse_command(trimmed) {
|
let inbound = WsInbound::UserInput {
|
||||||
match cmd {
|
content: input,
|
||||||
InputCommand::Quit => {
|
chat_id: app.current_session_id.clone(),
|
||||||
app.quit();
|
channel: None,
|
||||||
}
|
sender_id: None,
|
||||||
InputCommand::Help => {
|
};
|
||||||
app.toggle_help();
|
if let Ok(text) = serialize_inbound(&inbound) {
|
||||||
}
|
let _ = sender.send(tokio_tungstenite::tungstenite::Message::Text(text.into())).await;
|
||||||
InputCommand::Clear => {
|
|
||||||
if let Some(session_id) = &app.current_session_id {
|
|
||||||
if let Some(sender) = &mut app.ws_sender {
|
|
||||||
let inbound = WsInbound::ClearHistory {
|
|
||||||
chat_id: None,
|
|
||||||
session_id: Some(session_id.clone()),
|
|
||||||
};
|
|
||||||
if let Ok(text) = serialize_inbound(&inbound) {
|
|
||||||
let _ = sender.send(tokio_tungstenite::tungstenite::Message::Text(text.into())).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
InputCommand::New(title) => {
|
|
||||||
if let Some(sender) = &mut app.ws_sender {
|
|
||||||
let inbound = WsInbound::CreateSession { title };
|
|
||||||
if let Ok(text) = serialize_inbound(&inbound) {
|
|
||||||
let _ = sender.send(tokio_tungstenite::tungstenite::Message::Text(text.into())).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
InputCommand::Sessions => {
|
|
||||||
if let Some(sender) = &mut app.ws_sender {
|
|
||||||
let inbound = WsInbound::ListSessions {
|
|
||||||
include_archived: true,
|
|
||||||
};
|
|
||||||
if let Ok(text) = serialize_inbound(&inbound) {
|
|
||||||
let _ = sender.send(tokio_tungstenite::tungstenite::Message::Text(text.into())).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
InputCommand::Use(session_id) => {
|
|
||||||
if let Some(sender) = &mut app.ws_sender {
|
|
||||||
let inbound = WsInbound::LoadSession { session_id };
|
|
||||||
if let Ok(text) = serialize_inbound(&inbound) {
|
|
||||||
let _ = sender.send(tokio_tungstenite::tungstenite::Message::Text(text.into())).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
InputCommand::Rename(title) => {
|
|
||||||
if let Some(session_id) = &app.current_session_id {
|
|
||||||
if let Some(sender) = &mut app.ws_sender {
|
|
||||||
let inbound = WsInbound::RenameSession {
|
|
||||||
session_id: Some(session_id.clone()),
|
|
||||||
title,
|
|
||||||
};
|
|
||||||
if let Ok(text) = serialize_inbound(&inbound) {
|
|
||||||
let _ = sender.send(tokio_tungstenite::tungstenite::Message::Text(text.into())).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
InputCommand::Archive => {
|
|
||||||
if let Some(session_id) = &app.current_session_id {
|
|
||||||
if let Some(sender) = &mut app.ws_sender {
|
|
||||||
let inbound = WsInbound::ArchiveSession {
|
|
||||||
session_id: Some(session_id.clone()),
|
|
||||||
};
|
|
||||||
if let Ok(text) = serialize_inbound(&inbound) {
|
|
||||||
let _ = sender.send(tokio_tungstenite::tungstenite::Message::Text(text.into())).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
InputCommand::Delete => {
|
|
||||||
if let Some(session_id) = &app.current_session_id {
|
|
||||||
if let Some(sender) = &mut app.ws_sender {
|
|
||||||
let inbound = WsInbound::DeleteSession {
|
|
||||||
session_id: Some(session_id.clone()),
|
|
||||||
};
|
|
||||||
if let Ok(text) = serialize_inbound(&inbound) {
|
|
||||||
let _ = sender.send(tokio_tungstenite::tungstenite::Message::Text(text.into())).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
app.add_message(MessageRole::User, input.clone());
|
|
||||||
if let Some(sender) = &mut app.ws_sender {
|
|
||||||
let inbound = WsInbound::UserInput {
|
|
||||||
content: input,
|
|
||||||
chat_id: app.current_session_id.clone(),
|
|
||||||
channel: None,
|
|
||||||
sender_id: None,
|
|
||||||
};
|
|
||||||
if let Ok(text) = serialize_inbound(&inbound) {
|
|
||||||
let _ = sender.send(tokio_tungstenite::tungstenite::Message::Text(text.into())).await;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum InputCommand {
|
|
||||||
Quit,
|
|
||||||
Help,
|
|
||||||
Clear,
|
|
||||||
New(Option<String>),
|
|
||||||
Sessions,
|
|
||||||
Use(String),
|
|
||||||
Rename(String),
|
|
||||||
Archive,
|
|
||||||
Delete,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_command(input: &str) -> Option<InputCommand> {
|
|
||||||
if !input.starts_with('/') {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let parts: Vec<&str> = input.splitn(2, char::is_whitespace).collect();
|
|
||||||
let cmd = parts[0];
|
|
||||||
let arg = parts.get(1).map(|s| s.trim().to_string()).filter(|s| !s.is_empty());
|
|
||||||
|
|
||||||
match cmd {
|
|
||||||
"/quit" | "/exit" | "/q" => Some(InputCommand::Quit),
|
|
||||||
"/help" | "/?" => Some(InputCommand::Help),
|
|
||||||
"/clear" | "/reset" => Some(InputCommand::Clear),
|
|
||||||
"/new" => Some(InputCommand::New(arg)),
|
|
||||||
"/sessions" => Some(InputCommand::Sessions),
|
|
||||||
"/use" => arg.map(InputCommand::Use),
|
|
||||||
"/rename" => arg.map(InputCommand::Rename),
|
|
||||||
"/archive" => Some(InputCommand::Archive),
|
|
||||||
"/delete" => Some(InputCommand::Delete),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,12 +1,10 @@
|
|||||||
use crate::client::tui::app::App;
|
use crate::client::tui::app::App;
|
||||||
|
use crate::client::tui::components::*;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::{Constraint, Direction, Layout, Rect},
|
layout::{Constraint, Direction, Layout, Rect},
|
||||||
widgets::{Block, Borders, Clear, List, ListItem, Paragraph, Wrap},
|
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::components::*;
|
|
||||||
|
|
||||||
pub fn render_ui(f: &mut Frame, app: &App) {
|
pub fn render_ui(f: &mut Frame, app: &App) {
|
||||||
let size = f.size();
|
let size = f.size();
|
||||||
let chunks = Layout::default()
|
let chunks = Layout::default()
|
||||||
@ -30,9 +28,27 @@ pub fn render_ui(f: &mut Frame, app: &App) {
|
|||||||
|
|
||||||
input_area::render(f, chunks[2], app);
|
input_area::render(f, chunks[2], app);
|
||||||
|
|
||||||
|
// Render command menu if needed - position above input area
|
||||||
|
if app.show_command_menu && !app.get_filtered_commands().is_empty() {
|
||||||
|
let menu_area = menu_above_input(chunks[2]);
|
||||||
|
command_menu::render(f, menu_area, app);
|
||||||
|
}
|
||||||
|
|
||||||
if app.show_help {
|
if app.show_help {
|
||||||
let area = centered_rect(60, 60, size);
|
let help_area = centered_rect(60, 60, size);
|
||||||
help_popup::render(f, area);
|
help_popup::render(f, help_area);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn menu_above_input(input_area: Rect) -> Rect {
|
||||||
|
let max_commands = 6; // Show up to 6 commands
|
||||||
|
let menu_height = max_commands + 2; // +2 for borders
|
||||||
|
|
||||||
|
Rect {
|
||||||
|
x: input_area.x + 1,
|
||||||
|
y: input_area.y.saturating_sub(menu_height),
|
||||||
|
width: input_area.width.saturating_sub(2),
|
||||||
|
height: menu_height,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -12,6 +12,13 @@ pub struct SessionSummary {
|
|||||||
pub archived_at: Option<i64>,
|
pub archived_at: Option<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct SlashCommandInfo {
|
||||||
|
pub name: String,
|
||||||
|
pub description: String,
|
||||||
|
pub aliases: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(tag = "type")]
|
#[serde(tag = "type")]
|
||||||
pub enum WsInbound {
|
pub enum WsInbound {
|
||||||
@ -43,9 +50,7 @@ pub enum WsInbound {
|
|||||||
include_archived: bool,
|
include_archived: bool,
|
||||||
},
|
},
|
||||||
#[serde(rename = "load_session")]
|
#[serde(rename = "load_session")]
|
||||||
LoadSession {
|
LoadSession { session_id: String },
|
||||||
session_id: String,
|
|
||||||
},
|
|
||||||
#[serde(rename = "rename_session")]
|
#[serde(rename = "rename_session")]
|
||||||
RenameSession {
|
RenameSession {
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
@ -62,6 +67,8 @@ pub enum WsInbound {
|
|||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
session_id: Option<String>,
|
session_id: Option<String>,
|
||||||
},
|
},
|
||||||
|
#[serde(rename = "get_slash_commands")]
|
||||||
|
GetSlashCommands,
|
||||||
#[serde(rename = "ping")]
|
#[serde(rename = "ping")]
|
||||||
Ping,
|
Ping,
|
||||||
}
|
}
|
||||||
@ -70,7 +77,11 @@ pub enum WsInbound {
|
|||||||
#[serde(tag = "type")]
|
#[serde(tag = "type")]
|
||||||
pub enum WsOutbound {
|
pub enum WsOutbound {
|
||||||
#[serde(rename = "assistant_response")]
|
#[serde(rename = "assistant_response")]
|
||||||
AssistantResponse { id: String, content: String, role: String },
|
AssistantResponse {
|
||||||
|
id: String,
|
||||||
|
content: String,
|
||||||
|
role: String,
|
||||||
|
},
|
||||||
#[serde(rename = "error")]
|
#[serde(rename = "error")]
|
||||||
Error { code: String, message: String },
|
Error { code: String, message: String },
|
||||||
#[serde(rename = "session_established")]
|
#[serde(rename = "session_established")]
|
||||||
@ -97,6 +108,8 @@ pub enum WsOutbound {
|
|||||||
SessionDeleted { session_id: String },
|
SessionDeleted { session_id: String },
|
||||||
#[serde(rename = "history_cleared")]
|
#[serde(rename = "history_cleared")]
|
||||||
HistoryCleared { session_id: String },
|
HistoryCleared { session_id: String },
|
||||||
|
#[serde(rename = "slash_commands_list")]
|
||||||
|
SlashCommandsList { commands: Vec<SlashCommandInfo> },
|
||||||
#[serde(rename = "pong")]
|
#[serde(rename = "pong")]
|
||||||
Pong,
|
Pong,
|
||||||
#[serde(rename = "command_executed")]
|
#[serde(rename = "command_executed")]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user