feat(code): friendly errors, batch history, persistent command history
- Agent errors sanitized: raw hyper/h2/gRPC dumps replaced with
human-readable messages ("sol disconnected", "connection lost", etc.)
- Batch history loading: single viewport rebuild instead of per-entry
- Persistent command history: saved to .sunbeam/history, loaded on start
- Default model: mistral-medium-latest (personality adherence)
This commit is contained in:
@@ -7,10 +7,43 @@
|
||||
//! This module is designed to be usable as a library — nothing here
|
||||
//! depends on ratatui or terminal state.
|
||||
|
||||
use crossbeam_channel::{Receiver, Sender, TrySendError};
|
||||
use crossbeam_channel::{Receiver, Sender};
|
||||
|
||||
use super::client::{self, CodeSession};
|
||||
|
||||
/// Turn raw internal errors into something a human can read.
|
||||
fn friendly_error(e: &str) -> String {
|
||||
let lower = e.to_lowercase();
|
||||
if lower.contains("broken pipe") || lower.contains("stream closed") || lower.contains("h2 protocol") {
|
||||
"sol disconnected — try again or restart with /exit".into()
|
||||
} else if lower.contains("channel closed") || lower.contains("send on closed") {
|
||||
"connection to sol lost".into()
|
||||
} else if lower.contains("timed out") || lower.contains("timeout") {
|
||||
"request timed out — sol may be overloaded".into()
|
||||
} else if lower.contains("connection refused") {
|
||||
"can't reach sol — is it running?".into()
|
||||
} else if lower.contains("not found") && lower.contains("agent") {
|
||||
"sol's agent was reset — reconnect with /exit".into()
|
||||
} else if lower.contains("invalid_request_error") {
|
||||
// Extract the actual message from Mistral API errors
|
||||
if let Some(start) = e.find("\"msg\":\"") {
|
||||
let rest = &e[start + 7..];
|
||||
if let Some(end) = rest.find('"') {
|
||||
return rest[..end].to_string();
|
||||
}
|
||||
}
|
||||
"request error from sol".into()
|
||||
} else {
|
||||
// Truncate long errors and strip Rust debug formatting
|
||||
let clean = e.replace("\\n", " ").replace("\\\"", "'");
|
||||
if clean.len() > 120 {
|
||||
format!("{}…", &clean[..117])
|
||||
} else {
|
||||
clean
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Requests (TUI → Agent) ──────────────────────────────────────────────
|
||||
|
||||
/// A request from the UI to the agent backend.
|
||||
@@ -38,6 +71,8 @@ pub enum AgentEvent {
|
||||
Error { message: String },
|
||||
/// Status update (shown in title bar).
|
||||
Status { message: String },
|
||||
/// Connection health: true = reachable, false = unreachable.
|
||||
Health { connected: bool },
|
||||
/// Session ended.
|
||||
SessionEnded,
|
||||
}
|
||||
@@ -76,11 +111,12 @@ impl AgentHandle {
|
||||
// ── Spawn ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// Spawn the agent background task. Returns a handle for the TUI.
|
||||
pub fn spawn(session: CodeSession) -> AgentHandle {
|
||||
pub fn spawn(session: CodeSession, endpoint: String) -> AgentHandle {
|
||||
let (req_tx, req_rx) = crossbeam_channel::bounded::<AgentRequest>(32);
|
||||
let (evt_tx, evt_rx) = crossbeam_channel::bounded::<AgentEvent>(256);
|
||||
|
||||
tokio::spawn(agent_loop(session, req_rx, evt_tx));
|
||||
tokio::spawn(agent_loop(session, req_rx, evt_tx.clone()));
|
||||
tokio::spawn(heartbeat_loop(endpoint, evt_tx));
|
||||
|
||||
AgentHandle {
|
||||
tx: req_tx,
|
||||
@@ -88,6 +124,25 @@ pub fn spawn(session: CodeSession) -> AgentHandle {
|
||||
}
|
||||
}
|
||||
|
||||
/// Ping the gRPC endpoint every second to check if Sol is reachable.
|
||||
async fn heartbeat_loop(endpoint: String, evt_tx: Sender<AgentEvent>) {
|
||||
use sunbeam_proto::sunbeam_code_v1::code_agent_client::CodeAgentClient;
|
||||
|
||||
let mut last_state = true; // assume connected initially (we just connected)
|
||||
let _ = evt_tx.try_send(AgentEvent::Health { connected: true });
|
||||
|
||||
loop {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
|
||||
let connected = CodeAgentClient::connect(endpoint.clone()).await.is_ok();
|
||||
|
||||
if connected != last_state {
|
||||
let _ = evt_tx.try_send(AgentEvent::Health { connected });
|
||||
last_state = connected;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The background agent loop. Reads requests, calls gRPC, emits events.
|
||||
async fn agent_loop(
|
||||
mut session: CodeSession,
|
||||
@@ -126,7 +181,7 @@ async fn agent_loop(
|
||||
message: msg.clone(),
|
||||
},
|
||||
client::ChatEvent::Error(msg) => AgentEvent::Error {
|
||||
message: msg.clone(),
|
||||
message: friendly_error(msg),
|
||||
},
|
||||
};
|
||||
let _ = evt_tx.try_send(agent_event);
|
||||
@@ -136,7 +191,7 @@ async fn agent_loop(
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = evt_tx.try_send(AgentEvent::Error {
|
||||
message: e.to_string(),
|
||||
message: friendly_error(&e.to_string()),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user