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:
Illia Polosukhin
2026-02-03 21:55:05 -08:00
co-authored by Claude Opus 4.5
parent 74d94d33b0
commit 7ef6362d83
8 changed files with 665 additions and 42 deletions
+4 -2
View File
@@ -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();
}
+11 -8
View File
@@ -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
View File
@@ -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(())
}