mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-31 16:49:34 +00:00
Add Chat Completions API support and expand REPL debugging
- Add NearAiChatProvider using /v1/chat/completions endpoint with API key auth - Add NEARAI_API_KEY and NEARAI_API_MODE config options - Auto-detect API mode from presence of API key - Keep existing Responses API (NearAiProvider) for session-based auth - Fix response parsing to accept input_text/output_text/text content types - Expand REPL with /help, /debug toggle, colored output - Better tool status display (dots vs verbose based on debug mode) Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
74d94d33b0
commit
7ef6362d83
@@ -228,8 +228,10 @@ impl AppState {
|
||||
|
||||
/// 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.messages.push(
|
||||
ChatMessage::system(format!("Error: {}", content.into()))
|
||||
.with_status(MessageStatus::Error),
|
||||
);
|
||||
self.scroll_to_bottom();
|
||||
}
|
||||
|
||||
|
||||
@@ -163,7 +163,9 @@ fn render_model_selector_inline(frame: &mut Frame, app: &AppState, area: Rect) {
|
||||
if models.is_empty() {
|
||||
spans.push(Span::styled(
|
||||
"Loading models...",
|
||||
Style::default().fg(Color::DarkGray).add_modifier(Modifier::ITALIC),
|
||||
Style::default()
|
||||
.fg(Color::DarkGray)
|
||||
.add_modifier(Modifier::ITALIC),
|
||||
));
|
||||
} else {
|
||||
for (i, model) in models.iter().enumerate() {
|
||||
@@ -189,12 +191,14 @@ fn render_model_selector_inline(frame: &mut Frame, app: &AppState, area: Rect) {
|
||||
}
|
||||
|
||||
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))));
|
||||
.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);
|
||||
}
|
||||
@@ -335,4 +339,3 @@ fn render_approval_overlay(frame: &mut Frame, app: &AppState) {
|
||||
|
||||
frame.render_widget(content, overlay_area);
|
||||
}
|
||||
|
||||
|
||||
+119
-15
@@ -1,8 +1,22 @@
|
||||
//! Simple REPL channel for testing without TUI.
|
||||
//! Interactive REPL channel for debugging and testing.
|
||||
//!
|
||||
//! Provides a basic stdin/stdout interface for testing the agent.
|
||||
//! Provides a command-line interface for interacting with the agent.
|
||||
//!
|
||||
//! ## Commands
|
||||
//!
|
||||
//! - `/help` - Show available commands
|
||||
//! - `/quit` or `/exit` - Exit the REPL
|
||||
//! - `/debug` - Toggle debug mode (verbose tool output)
|
||||
//! - `/undo` - Undo the last turn
|
||||
//! - `/redo` - Redo an undone turn
|
||||
//! - `/clear` - Clear the conversation
|
||||
//! - `/compact` - Compact the context
|
||||
//! - `/new` - Start a new thread
|
||||
//! - `yes`/`no`/`always` - Respond to tool approval prompts
|
||||
|
||||
use std::io::{self, BufRead, Write};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use tokio::sync::mpsc;
|
||||
@@ -11,10 +25,12 @@ use tokio_stream::wrappers::ReceiverStream;
|
||||
use crate::channels::{Channel, IncomingMessage, MessageStream, OutgoingResponse, StatusUpdate};
|
||||
use crate::error::ChannelError;
|
||||
|
||||
/// Simple REPL channel using stdin/stdout.
|
||||
/// REPL channel for interactive agent debugging.
|
||||
pub struct ReplChannel {
|
||||
/// Optional single message to send (for -m flag).
|
||||
single_message: Option<String>,
|
||||
/// Debug mode flag (shared with input thread).
|
||||
debug_mode: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl ReplChannel {
|
||||
@@ -22,6 +38,7 @@ impl ReplChannel {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
single_message: None,
|
||||
debug_mode: Arc::new(AtomicBool::new(false)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,8 +46,13 @@ impl ReplChannel {
|
||||
pub fn with_message(message: String) -> Self {
|
||||
Self {
|
||||
single_message: Some(message),
|
||||
debug_mode: Arc::new(AtomicBool::new(false)),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_debug(&self) -> bool {
|
||||
self.debug_mode.load(Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ReplChannel {
|
||||
@@ -39,6 +61,35 @@ impl Default for ReplChannel {
|
||||
}
|
||||
}
|
||||
|
||||
fn print_help() {
|
||||
println!(
|
||||
r#"
|
||||
NEAR Agent REPL - Interactive debugging mode
|
||||
|
||||
Commands:
|
||||
/help Show this help message
|
||||
/quit, /exit Exit the REPL
|
||||
/debug Toggle debug mode (verbose output)
|
||||
/undo Undo the last turn
|
||||
/redo Redo an undone turn
|
||||
/clear Clear the conversation
|
||||
/compact Compact the context window
|
||||
/new Start a new conversation thread
|
||||
/interrupt Stop the current operation
|
||||
|
||||
Approval responses (when prompted):
|
||||
yes, y Approve the tool execution
|
||||
no, n Deny the tool execution
|
||||
always Approve and auto-approve this tool for the session
|
||||
|
||||
Tips:
|
||||
- Tool calls requiring approval will pause and wait for your response
|
||||
- Use /debug to see detailed tool inputs and outputs
|
||||
- Press Ctrl+C to interrupt a long-running operation
|
||||
"#
|
||||
);
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Channel for ReplChannel {
|
||||
fn name(&self) -> &str {
|
||||
@@ -48,6 +99,7 @@ impl Channel for ReplChannel {
|
||||
async fn start(&self) -> Result<MessageStream, ChannelError> {
|
||||
let (tx, rx) = mpsc::channel(32);
|
||||
let single_message = self.single_message.clone();
|
||||
let debug_mode = Arc::clone(&self.debug_mode);
|
||||
|
||||
std::thread::spawn(move || {
|
||||
// If single message mode, send it and exit
|
||||
@@ -64,9 +116,17 @@ impl Channel for ReplChannel {
|
||||
let stdin = io::stdin();
|
||||
let mut stdout = io::stdout();
|
||||
|
||||
println!("NEAR Agent REPL - Type /help for commands, /quit to exit");
|
||||
println!();
|
||||
|
||||
loop {
|
||||
// Print prompt
|
||||
print!("> ");
|
||||
let prompt = if debug_mode.load(Ordering::Relaxed) {
|
||||
"[debug] > "
|
||||
} else {
|
||||
"> "
|
||||
};
|
||||
print!("{}", prompt);
|
||||
let _ = stdout.flush();
|
||||
|
||||
// Read line
|
||||
@@ -78,8 +138,25 @@ impl Channel for ReplChannel {
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if line == "/quit" || line == "/exit" {
|
||||
break;
|
||||
|
||||
// Handle local REPL commands
|
||||
match line.to_lowercase().as_str() {
|
||||
"/quit" | "/exit" => break,
|
||||
"/help" | "/?" => {
|
||||
print_help();
|
||||
continue;
|
||||
}
|
||||
"/debug" => {
|
||||
let current = debug_mode.load(Ordering::Relaxed);
|
||||
debug_mode.store(!current, Ordering::Relaxed);
|
||||
if !current {
|
||||
println!("Debug mode ON - showing verbose tool output");
|
||||
} else {
|
||||
println!("Debug mode OFF");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let msg = IncomingMessage::new("repl", "user", line);
|
||||
@@ -100,26 +177,51 @@ impl Channel for ReplChannel {
|
||||
_msg: &IncomingMessage,
|
||||
response: OutgoingResponse,
|
||||
) -> Result<(), ChannelError> {
|
||||
println!("\n{}\n", response.content);
|
||||
println!();
|
||||
println!("{}", response.content);
|
||||
println!();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_status(&self, status: StatusUpdate) -> Result<(), ChannelError> {
|
||||
let debug = self.is_debug();
|
||||
|
||||
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);
|
||||
StatusUpdate::Thinking(msg) => {
|
||||
if debug {
|
||||
eprintln!("\x1b[90m[thinking] {}\x1b[0m", msg);
|
||||
} else {
|
||||
eprintln!("[tool] Failed: {}", name);
|
||||
eprint!(".");
|
||||
let _ = io::stderr().flush();
|
||||
}
|
||||
}
|
||||
StatusUpdate::ToolStarted { name } => {
|
||||
if debug {
|
||||
eprintln!("\x1b[33m[tool:start] {}\x1b[0m", name);
|
||||
} else {
|
||||
eprintln!("\x1b[33m⚡ {}\x1b[0m", name);
|
||||
}
|
||||
}
|
||||
StatusUpdate::ToolCompleted { name, success } => {
|
||||
if debug {
|
||||
if success {
|
||||
eprintln!("\x1b[32m[tool:done] {} ✓\x1b[0m", name);
|
||||
} else {
|
||||
eprintln!("\x1b[31m[tool:fail] {} ✗\x1b[0m", name);
|
||||
}
|
||||
} else if !success {
|
||||
eprintln!("\x1b[31m✗ {} failed\x1b[0m", name);
|
||||
}
|
||||
}
|
||||
StatusUpdate::StreamChunk(chunk) => {
|
||||
print!("{}", chunk);
|
||||
let _ = io::stdout().flush();
|
||||
}
|
||||
StatusUpdate::Status(msg) => eprintln!("[status] {}", msg),
|
||||
StatusUpdate::Status(msg) => {
|
||||
if debug || msg.contains("approval") || msg.contains("Approval") {
|
||||
eprintln!("\x1b[90m[status] {}\x1b[0m", msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -129,7 +231,9 @@ impl Channel for ReplChannel {
|
||||
_user_id: &str,
|
||||
response: OutgoingResponse,
|
||||
) -> Result<(), ChannelError> {
|
||||
println!("\n[broadcast] {}\n", response.content);
|
||||
println!();
|
||||
println!("\x1b[36m[notification]\x1b[0m {}", response.content);
|
||||
println!();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user