mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-31 16:49:34 +00:00
Support non interactive mode and model selection
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::channels::cli::composer::ChatComposer;
|
||||
use crate::channels::cli::model_selector::{ModelSelectorOverlay, ModelSelectorRequest};
|
||||
use crate::channels::cli::overlay::{ApprovalOverlay, ApprovalRequest};
|
||||
|
||||
/// Events that can occur in the TUI.
|
||||
@@ -24,6 +25,10 @@ pub enum AppEvent {
|
||||
LogMessage(String),
|
||||
/// Thinking/status message (shown in chat window).
|
||||
ThinkingMessage(String),
|
||||
/// Error message (shown in chat window).
|
||||
ErrorMessage(String),
|
||||
/// Available models fetched from API.
|
||||
AvailableModels(Vec<String>),
|
||||
/// Force a redraw.
|
||||
Redraw,
|
||||
/// Quit the application.
|
||||
@@ -39,6 +44,8 @@ pub enum InputMode {
|
||||
Editing,
|
||||
/// Approval overlay is active.
|
||||
Approval,
|
||||
/// Model selector overlay is active.
|
||||
ModelSelector,
|
||||
}
|
||||
|
||||
/// Message in the chat history.
|
||||
@@ -110,6 +117,8 @@ pub struct AppState {
|
||||
pub composer: ChatComposer,
|
||||
/// Approval overlay (if active).
|
||||
pub approval: Option<ApprovalOverlay>,
|
||||
/// Model selector overlay (if active).
|
||||
pub model_selector: Option<ModelSelectorOverlay>,
|
||||
/// Scroll offset for messages.
|
||||
pub scroll_offset: u16,
|
||||
/// Whether the app should quit.
|
||||
@@ -122,11 +131,19 @@ pub struct AppState {
|
||||
pub status_message: Option<String>,
|
||||
/// Whether Ctrl+D was pressed (waiting for second press to quit).
|
||||
pub ctrl_d_pending: bool,
|
||||
/// Currently selected model.
|
||||
pub current_model: String,
|
||||
/// Available models (fetched from API).
|
||||
pub available_models: Vec<String>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
/// Create a new app state.
|
||||
pub fn new() -> Self {
|
||||
// Load saved model from settings
|
||||
let settings = crate::settings::Settings::load();
|
||||
let current_model = settings.model_or("claude-3-5-sonnet-20241022");
|
||||
|
||||
Self {
|
||||
mode: InputMode::Editing,
|
||||
messages: vec![ChatMessage::system(
|
||||
@@ -134,12 +151,60 @@ impl AppState {
|
||||
)],
|
||||
composer: ChatComposer::new(),
|
||||
approval: None,
|
||||
model_selector: None,
|
||||
scroll_offset: 0,
|
||||
should_quit: false,
|
||||
pending_approvals: VecDeque::new(),
|
||||
streaming_buffer: None,
|
||||
status_message: None,
|
||||
ctrl_d_pending: false,
|
||||
current_model,
|
||||
available_models: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Show the model selector.
|
||||
pub fn show_model_selector(&mut self) {
|
||||
let request = ModelSelectorRequest {
|
||||
current_model: self.current_model.clone(),
|
||||
available_models: self.available_models.clone(),
|
||||
};
|
||||
self.model_selector = Some(ModelSelectorOverlay::new(request));
|
||||
self.mode = InputMode::ModelSelector;
|
||||
}
|
||||
|
||||
/// Handle model selection.
|
||||
pub fn handle_model_selection(&mut self, selected: Option<String>) {
|
||||
self.model_selector = None;
|
||||
self.mode = InputMode::Editing;
|
||||
|
||||
if let Some(model) = selected {
|
||||
if model != self.current_model {
|
||||
self.current_model = model.clone();
|
||||
// Save to settings
|
||||
let mut settings = crate::settings::Settings::load();
|
||||
if let Err(e) = settings.set_model(&model) {
|
||||
tracing::warn!("Failed to save model setting: {}", e);
|
||||
}
|
||||
self.messages.push(ChatMessage::system(format!(
|
||||
"Switched to model: {}",
|
||||
ModelSelectorOverlay::format_model_name(&model)
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Set available models (also updates selector if open).
|
||||
pub fn set_available_models(&mut self, models: Vec<String>) {
|
||||
self.available_models = models.clone();
|
||||
|
||||
// Update the selector if it's currently open
|
||||
if let Some(ref mut selector) = self.model_selector {
|
||||
selector.request.available_models = models;
|
||||
// Reset selection index if it's out of bounds
|
||||
if selector.selection_index >= selector.request.available_models.len() {
|
||||
selector.selection_index = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,6 +226,13 @@ impl AppState {
|
||||
self.scroll_to_bottom();
|
||||
}
|
||||
|
||||
/// Add an error message to the chat.
|
||||
pub fn add_error_message(&mut self, content: impl Into<String>) {
|
||||
self.messages
|
||||
.push(ChatMessage::system(format!("Error: {}", content.into())).with_status(MessageStatus::Error));
|
||||
self.scroll_to_bottom();
|
||||
}
|
||||
|
||||
/// Add or update a thinking/status message (shown as system message).
|
||||
pub fn set_thinking(&mut self, content: impl Into<String>) {
|
||||
let content = content.into();
|
||||
|
||||
@@ -110,6 +110,7 @@ fn handle_key(
|
||||
InputMode::Normal => handle_normal_mode(app, key),
|
||||
InputMode::Editing => handle_editing_mode(app, key, msg_tx),
|
||||
InputMode::Approval => handle_approval_mode(app, key),
|
||||
InputMode::ModelSelector => handle_model_selector_mode(app, key),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,6 +153,13 @@ fn handle_editing_mode(
|
||||
KeyCode::Enter => {
|
||||
if !app.composer.is_empty() {
|
||||
let input = app.composer.submit();
|
||||
|
||||
// Handle /model command locally (TUI-specific)
|
||||
if input.trim().eq_ignore_ascii_case("/model") {
|
||||
app.show_model_selector();
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
app.add_user_message(&input);
|
||||
|
||||
// Send message to agent
|
||||
@@ -251,6 +259,30 @@ fn handle_approval_mode(app: &mut AppState, key: KeyEvent) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle keys in model selector mode.
|
||||
fn handle_model_selector_mode(app: &mut AppState, key: KeyEvent) -> io::Result<()> {
|
||||
if let Some(ref mut overlay) = app.model_selector {
|
||||
match key.code {
|
||||
KeyCode::Left | KeyCode::Char('h') => {
|
||||
overlay.select_prev();
|
||||
}
|
||||
KeyCode::Right | KeyCode::Char('l') => {
|
||||
overlay.select_next();
|
||||
}
|
||||
KeyCode::Enter | KeyCode::Char(' ') => {
|
||||
let selected = overlay.selected_model().map(|s| s.to_string());
|
||||
app.handle_model_selection(selected);
|
||||
}
|
||||
KeyCode::Esc => {
|
||||
// Cancel without changing model
|
||||
app.handle_model_selection(None);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle an application event.
|
||||
fn handle_app_event(app: &mut AppState, event: AppEvent) {
|
||||
match event {
|
||||
@@ -291,5 +323,11 @@ fn handle_app_event(app: &mut AppState, event: AppEvent) {
|
||||
AppEvent::ThinkingMessage(msg) => {
|
||||
app.set_thinking(msg);
|
||||
}
|
||||
AppEvent::ErrorMessage(msg) => {
|
||||
app.add_error_message(msg);
|
||||
}
|
||||
AppEvent::AvailableModels(models) => {
|
||||
app.set_available_models(models);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
mod app;
|
||||
mod composer;
|
||||
mod events;
|
||||
mod model_selector;
|
||||
mod overlay;
|
||||
mod render;
|
||||
|
||||
@@ -30,6 +31,7 @@ use crate::error::ChannelError;
|
||||
|
||||
pub use app::{AppEvent, AppState, InputMode};
|
||||
pub use composer::ChatComposer;
|
||||
pub use model_selector::{ModelSelectorOverlay, ModelSelectorRequest};
|
||||
pub use overlay::{ApprovalOverlay, ApprovalRequest};
|
||||
|
||||
/// TUI channel for interactive terminal input with Ratatui.
|
||||
@@ -55,6 +57,12 @@ impl TuiChannel {
|
||||
pub fn log_writer(&self) -> TuiLogWriter {
|
||||
TuiLogWriter::new(self.event_tx.clone())
|
||||
}
|
||||
|
||||
/// Get a sender for sending events to the TUI.
|
||||
/// Use this to send available models or other events from outside the channel.
|
||||
pub fn event_sender(&self) -> mpsc::Sender<AppEvent> {
|
||||
self.event_tx.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TuiChannel {
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
//! Model selector overlay for switching LLM models.
|
||||
|
||||
/// Request to show the model selector.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ModelSelectorRequest {
|
||||
/// Currently selected model.
|
||||
pub current_model: String,
|
||||
/// Available models to choose from.
|
||||
pub available_models: Vec<String>,
|
||||
}
|
||||
|
||||
/// Model selector overlay state.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ModelSelectorOverlay {
|
||||
/// The request that triggered this overlay.
|
||||
pub request: ModelSelectorRequest,
|
||||
/// Currently highlighted index.
|
||||
pub selection_index: usize,
|
||||
}
|
||||
|
||||
impl ModelSelectorOverlay {
|
||||
/// Create a new model selector overlay.
|
||||
pub fn new(request: ModelSelectorRequest) -> Self {
|
||||
// Find the current model in the list, default to 0
|
||||
let selection_index = request
|
||||
.available_models
|
||||
.iter()
|
||||
.position(|m| m == &request.current_model)
|
||||
.unwrap_or(0);
|
||||
|
||||
Self {
|
||||
request,
|
||||
selection_index,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the list of available models.
|
||||
pub fn models(&self) -> &[String] {
|
||||
&self.request.available_models
|
||||
}
|
||||
|
||||
/// Move selection up.
|
||||
pub fn select_prev(&mut self) {
|
||||
let len = self.request.available_models.len();
|
||||
if len == 0 {
|
||||
return;
|
||||
}
|
||||
if self.selection_index > 0 {
|
||||
self.selection_index -= 1;
|
||||
} else {
|
||||
// Wrap to bottom
|
||||
self.selection_index = len - 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Move selection down.
|
||||
pub fn select_next(&mut self) {
|
||||
let len = self.request.available_models.len();
|
||||
if len == 0 {
|
||||
return;
|
||||
}
|
||||
if self.selection_index < len - 1 {
|
||||
self.selection_index += 1;
|
||||
} else {
|
||||
// Wrap to top
|
||||
self.selection_index = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the currently selected model name.
|
||||
pub fn selected_model(&self) -> Option<&str> {
|
||||
self.request
|
||||
.available_models
|
||||
.get(self.selection_index)
|
||||
.map(|s| s.as_str())
|
||||
}
|
||||
|
||||
/// Check if the selection is the current model.
|
||||
pub fn is_current(&self) -> bool {
|
||||
self.selected_model() == Some(&self.request.current_model)
|
||||
}
|
||||
|
||||
/// Format a model name for display (shorten long names).
|
||||
pub fn format_model_name(model: &str) -> String {
|
||||
// Shorten fireworks model names
|
||||
if let Some(rest) = model.strip_prefix("fireworks::accounts/fireworks/models/") {
|
||||
return format!("fireworks/{}", rest);
|
||||
}
|
||||
// Shorten other long prefixes
|
||||
if let Some(rest) = model.strip_prefix("accounts/") {
|
||||
return rest.to_string();
|
||||
}
|
||||
model.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_model_selector_navigation() {
|
||||
let request = ModelSelectorRequest {
|
||||
current_model: "gpt-4o".to_string(),
|
||||
available_models: vec![
|
||||
"claude-3-5-sonnet".to_string(),
|
||||
"gpt-4o".to_string(),
|
||||
"gpt-4o-mini".to_string(),
|
||||
],
|
||||
};
|
||||
let mut overlay = ModelSelectorOverlay::new(request);
|
||||
|
||||
// Should start at gpt-4o index (1)
|
||||
assert_eq!(overlay.selected_model(), Some("gpt-4o"));
|
||||
|
||||
// Navigate down
|
||||
overlay.select_next();
|
||||
assert_eq!(overlay.selected_model(), Some("gpt-4o-mini"));
|
||||
|
||||
// Navigate down (wrap)
|
||||
overlay.select_next();
|
||||
assert_eq!(overlay.selected_model(), Some("claude-3-5-sonnet"));
|
||||
|
||||
// Navigate up
|
||||
overlay.select_prev();
|
||||
assert_eq!(overlay.selected_model(), Some("gpt-4o-mini"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_model_name() {
|
||||
assert_eq!(
|
||||
ModelSelectorOverlay::format_model_name("claude-3-5-sonnet-20241022"),
|
||||
"claude-3-5-sonnet-20241022"
|
||||
);
|
||||
assert_eq!(
|
||||
ModelSelectorOverlay::format_model_name(
|
||||
"fireworks::accounts/fireworks/models/llama-v3p1-405b-instruct"
|
||||
),
|
||||
"fireworks/llama-v3p1-405b-instruct"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_models() {
|
||||
let request = ModelSelectorRequest {
|
||||
current_model: "unknown".to_string(),
|
||||
available_models: vec![],
|
||||
};
|
||||
let mut overlay = ModelSelectorOverlay::new(request);
|
||||
assert_eq!(overlay.selected_model(), None);
|
||||
|
||||
// Should not panic
|
||||
overlay.select_next();
|
||||
overlay.select_prev();
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ use ratatui::{
|
||||
};
|
||||
|
||||
use crate::channels::cli::app::{AppState, InputMode, MessageRole, MessageStatus};
|
||||
use crate::channels::cli::model_selector::ModelSelectorOverlay;
|
||||
use crate::channels::cli::overlay::ApprovalSelection;
|
||||
|
||||
/// Render the entire UI.
|
||||
@@ -100,12 +101,18 @@ fn render_messages(frame: &mut Frame, app: &AppState, area: Rect) {
|
||||
frame.render_widget(messages, area);
|
||||
}
|
||||
|
||||
/// Render the input area.
|
||||
/// Render the input area (or model selector when in ModelSelector mode).
|
||||
fn render_input(frame: &mut Frame, app: &AppState, area: Rect) {
|
||||
// In ModelSelector mode, render inline selector instead of input
|
||||
if app.mode == InputMode::ModelSelector {
|
||||
render_model_selector_inline(frame, app, area);
|
||||
return;
|
||||
}
|
||||
|
||||
let input_style = match app.mode {
|
||||
InputMode::Editing => Style::default().fg(Color::Yellow),
|
||||
InputMode::Normal => Style::default(),
|
||||
InputMode::Approval => Style::default().fg(Color::DarkGray),
|
||||
InputMode::Approval | InputMode::ModelSelector => Style::default().fg(Color::DarkGray),
|
||||
};
|
||||
|
||||
let buffer = app.composer.buffer();
|
||||
@@ -142,15 +149,91 @@ fn render_input(frame: &mut Frame, app: &AppState, area: Rect) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Render inline model selector in the input area.
|
||||
fn render_model_selector_inline(frame: &mut Frame, app: &AppState, area: Rect) {
|
||||
let Some(ref overlay) = app.model_selector else {
|
||||
return;
|
||||
};
|
||||
|
||||
let models = overlay.models();
|
||||
|
||||
// Build horizontal list of models
|
||||
let mut spans: Vec<Span> = Vec::new();
|
||||
|
||||
if models.is_empty() {
|
||||
spans.push(Span::styled(
|
||||
"Loading models...",
|
||||
Style::default().fg(Color::DarkGray).add_modifier(Modifier::ITALIC),
|
||||
));
|
||||
} else {
|
||||
for (i, model) in models.iter().enumerate() {
|
||||
if i > 0 {
|
||||
spans.push(Span::raw(" "));
|
||||
}
|
||||
|
||||
let display_name = ModelSelectorOverlay::format_model_name(model);
|
||||
let is_selected = i == overlay.selection_index;
|
||||
let is_current = model == &overlay.request.current_model;
|
||||
|
||||
let style = if is_selected {
|
||||
Style::default().bg(Color::Blue).fg(Color::White)
|
||||
} else if is_current {
|
||||
Style::default().fg(Color::Green)
|
||||
} else {
|
||||
Style::default().fg(Color::White)
|
||||
};
|
||||
|
||||
let prefix = if is_current { "●" } else { " " };
|
||||
spans.push(Span::styled(format!("{}{}", prefix, display_name), style));
|
||||
}
|
||||
}
|
||||
|
||||
let content = Paragraph::new(Line::from(spans))
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title(Span::styled("Select Model", Style::default().fg(Color::Cyan))),
|
||||
)
|
||||
.scroll((0, calculate_model_scroll(overlay, area.width.saturating_sub(2))));
|
||||
|
||||
frame.render_widget(content, area);
|
||||
}
|
||||
|
||||
/// Calculate horizontal scroll offset to keep selected model visible.
|
||||
fn calculate_model_scroll(overlay: &ModelSelectorOverlay, visible_width: u16) -> u16 {
|
||||
let models = overlay.models();
|
||||
if models.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Estimate position of selected model (rough calculation)
|
||||
let mut pos: u16 = 0;
|
||||
for (i, model) in models.iter().enumerate() {
|
||||
let name_len = ModelSelectorOverlay::format_model_name(model).len() as u16 + 3; // +3 for prefix and spacing
|
||||
if i == overlay.selection_index {
|
||||
// Check if selection is beyond visible area
|
||||
if pos > visible_width {
|
||||
return pos.saturating_sub(visible_width / 2);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
pos += name_len;
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
/// Render the status line.
|
||||
fn render_status(frame: &mut Frame, app: &AppState, area: Rect) {
|
||||
let status_text = if let Some(ref msg) = app.status_message {
|
||||
msg.clone()
|
||||
} else {
|
||||
match app.mode {
|
||||
InputMode::Normal => "Press 'i' to edit, 'q' to quit".to_string(),
|
||||
InputMode::Editing => "Type message, Enter to send, Esc to cancel".to_string(),
|
||||
InputMode::Approval => "y=Yes, n=No, a=Always, Ctrl+C=Cancel all".to_string(),
|
||||
InputMode::Normal | InputMode::Editing => {
|
||||
let model = ModelSelectorOverlay::format_model_name(&app.current_model);
|
||||
format!("{} | /model to switch", model)
|
||||
}
|
||||
InputMode::Approval => "y=Yes, n=No, a=Always, Ctrl+C=Cancel".to_string(),
|
||||
InputMode::ModelSelector => "←/→ navigate, Enter=select, Esc=cancel".to_string(),
|
||||
}
|
||||
};
|
||||
|
||||
@@ -252,3 +335,4 @@ fn render_approval_overlay(frame: &mut Frame, app: &AppState) {
|
||||
|
||||
frame.render_widget(content, overlay_area);
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -7,8 +7,10 @@ mod channel;
|
||||
pub mod cli;
|
||||
mod http;
|
||||
mod manager;
|
||||
mod repl;
|
||||
|
||||
pub use channel::{Channel, IncomingMessage, MessageStream, OutgoingResponse, StatusUpdate};
|
||||
pub use cli::TuiChannel;
|
||||
pub use cli::{AppEvent, TuiChannel};
|
||||
pub use http::HttpChannel;
|
||||
pub use manager::ChannelManager;
|
||||
pub use repl::ReplChannel;
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
//! Simple REPL channel for testing without TUI.
|
||||
//!
|
||||
//! Provides a basic stdin/stdout interface for testing the agent.
|
||||
|
||||
use std::io::{self, BufRead, Write};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio_stream::wrappers::ReceiverStream;
|
||||
|
||||
use crate::channels::{Channel, IncomingMessage, MessageStream, OutgoingResponse, StatusUpdate};
|
||||
use crate::error::ChannelError;
|
||||
|
||||
/// Simple REPL channel using stdin/stdout.
|
||||
pub struct ReplChannel {
|
||||
/// Optional single message to send (for -m flag).
|
||||
single_message: Option<String>,
|
||||
}
|
||||
|
||||
impl ReplChannel {
|
||||
/// Create a new REPL channel.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
single_message: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a REPL channel that sends a single message and exits.
|
||||
pub fn with_message(message: String) -> Self {
|
||||
Self {
|
||||
single_message: Some(message),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ReplChannel {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Channel for ReplChannel {
|
||||
fn name(&self) -> &str {
|
||||
"repl"
|
||||
}
|
||||
|
||||
async fn start(&self) -> Result<MessageStream, ChannelError> {
|
||||
let (tx, rx) = mpsc::channel(32);
|
||||
let single_message = self.single_message.clone();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
// If single message mode, send it and exit
|
||||
if let Some(msg) = single_message {
|
||||
let incoming = IncomingMessage::new("repl", "user", &msg);
|
||||
if tx.blocking_send(incoming).is_err() {
|
||||
return;
|
||||
}
|
||||
// Wait a bit for response, then the channel will close
|
||||
return;
|
||||
}
|
||||
|
||||
// Interactive REPL mode
|
||||
let stdin = io::stdin();
|
||||
let mut stdout = io::stdout();
|
||||
|
||||
loop {
|
||||
// Print prompt
|
||||
print!("> ");
|
||||
let _ = stdout.flush();
|
||||
|
||||
// Read line
|
||||
let mut line = String::new();
|
||||
match stdin.lock().read_line(&mut line) {
|
||||
Ok(0) => break, // EOF
|
||||
Ok(_) => {
|
||||
let line = line.trim();
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if line == "/quit" || line == "/exit" {
|
||||
break;
|
||||
}
|
||||
|
||||
let msg = IncomingMessage::new("repl", "user", line);
|
||||
if tx.blocking_send(msg).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(Box::pin(ReceiverStream::new(rx)))
|
||||
}
|
||||
|
||||
async fn respond(
|
||||
&self,
|
||||
_msg: &IncomingMessage,
|
||||
response: OutgoingResponse,
|
||||
) -> Result<(), ChannelError> {
|
||||
println!("\n{}\n", response.content);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_status(&self, status: StatusUpdate) -> Result<(), ChannelError> {
|
||||
match status {
|
||||
StatusUpdate::Thinking(msg) => eprintln!("[thinking] {}", msg),
|
||||
StatusUpdate::ToolStarted { name } => eprintln!("[tool] Starting: {}", name),
|
||||
StatusUpdate::ToolCompleted { name, success } => {
|
||||
if success {
|
||||
eprintln!("[tool] Completed: {}", name);
|
||||
} else {
|
||||
eprintln!("[tool] Failed: {}", name);
|
||||
}
|
||||
}
|
||||
StatusUpdate::StreamChunk(chunk) => {
|
||||
print!("{}", chunk);
|
||||
let _ = io::stdout().flush();
|
||||
}
|
||||
StatusUpdate::Status(msg) => eprintln!("[status] {}", msg),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn broadcast(
|
||||
&self,
|
||||
_user_id: &str,
|
||||
response: OutgoingResponse,
|
||||
) -> Result<(), ChannelError> {
|
||||
println!("\n[broadcast] {}\n", response.content);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn health_check(&self) -> Result<(), ChannelError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn shutdown(&self) -> Result<(), ChannelError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user