Show status/thinking messages in chat window, debug empty responses

- Add ThinkingMessage variant to AppEvent for chat-visible status
- Add set_thinking() and clear_thinking() to AppState
- Thinking messages show as system messages with spinner indicator
- Auto-clear thinking messages when agent response arrives
- Tool started/completed now shown in chat window
- Add debug logging to NEAR AI provider to diagnose empty responses
- Log raw response output when text extraction returns empty

Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
Illia Polosukhin
2026-02-03 00:17:40 -08:00
co-authored by Claude Opus 4.5
parent 3d709eb253
commit 42d6e87186
4 changed files with 52 additions and 6 deletions
+29 -1
View File
@@ -20,8 +20,10 @@ pub enum AppEvent {
ApprovalRequested(ApprovalRequest),
/// Streaming chunk received.
StreamChunk(String),
/// Log message from the application.
/// Log message from the application (shown in status line).
LogMessage(String),
/// Thinking/status message (shown in chat window).
ThinkingMessage(String),
/// Force a redraw.
Redraw,
/// Quit the application.
@@ -153,10 +155,36 @@ impl AppState {
if self.streaming_buffer.is_some() {
self.streaming_buffer = None;
}
// Remove any pending thinking message before adding the response
self.clear_thinking();
self.messages.push(ChatMessage::agent(content));
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();
// Check if last message is a thinking message (system with InProgress status)
if let Some(last) = self.messages.last_mut() {
if last.role == MessageRole::System && last.status == Some(MessageStatus::InProgress) {
last.content = content;
return;
}
}
// Add new thinking message
self.messages
.push(ChatMessage::system(content).with_status(MessageStatus::InProgress));
self.scroll_to_bottom();
}
/// Clear any thinking/status message.
pub fn clear_thinking(&mut self) {
// Remove any thinking messages (system with InProgress status)
self.messages.retain(|msg| {
!(msg.role == MessageRole::System && msg.status == Some(MessageStatus::InProgress))
});
}
/// Start streaming a response.
pub fn start_streaming(&mut self) {
self.streaming_buffer = Some(String::new());
+6 -3
View File
@@ -258,13 +258,13 @@ fn handle_app_event(app: &mut AppState, event: AppEvent) {
app.add_agent_message(content);
}
AppEvent::ToolStarted { name } => {
app.set_status(format!("Running tool: {}...", name));
app.set_thinking(format!("⚙️ Running tool: {}...", name));
}
AppEvent::ToolCompleted { name, success } => {
if success {
app.set_status(format!("Tool {} completed", name));
app.set_thinking(format!("Tool {} completed", name));
} else {
app.set_status(format!("Tool {} failed", name));
app.set_thinking(format!("Tool {} failed", name));
}
}
AppEvent::ApprovalRequested(request) => {
@@ -288,5 +288,8 @@ fn handle_app_event(app: &mut AppState, event: AppEvent) {
AppEvent::LogMessage(msg) => {
app.set_status(msg);
}
AppEvent::ThinkingMessage(msg) => {
app.set_thinking(msg);
}
}
}
+2 -2
View File
@@ -111,13 +111,13 @@ impl Channel for TuiChannel {
async fn send_status(&self, status: StatusUpdate) -> Result<(), ChannelError> {
let event = match status {
StatusUpdate::Thinking(msg) => AppEvent::LogMessage(format!("🤔 {}", msg)),
StatusUpdate::Thinking(msg) => AppEvent::ThinkingMessage(format!("🤔 {}", msg)),
StatusUpdate::ToolStarted { name } => AppEvent::ToolStarted { name },
StatusUpdate::ToolCompleted { name, success } => {
AppEvent::ToolCompleted { name, success }
}
StatusUpdate::StreamChunk(chunk) => AppEvent::StreamChunk(chunk),
StatusUpdate::Status(msg) => AppEvent::LogMessage(msg),
StatusUpdate::Status(msg) => AppEvent::ThinkingMessage(msg),
};
self.event_tx
.send(event)
+15
View File
@@ -118,16 +118,24 @@ impl LlmProvider for NearAiProvider {
let response: NearAiResponse = self.send_request("responses", &request).await?;
tracing::debug!("NEAR AI response: {:?}", response);
// Extract text from response output
let text = response
.output
.iter()
.filter_map(|item| {
tracing::debug!("Processing output item: type={}", item.item_type);
if item.item_type == "message" {
item.content.as_ref().and_then(|contents| {
contents
.iter()
.filter_map(|c| {
tracing::debug!(
"Content item: type={}, text={:?}",
c.content_type,
c.text
);
if c.content_type == "output_text" {
c.text.clone()
} else {
@@ -143,6 +151,13 @@ impl LlmProvider for NearAiProvider {
.collect::<Vec<_>>()
.join("");
if text.is_empty() {
tracing::warn!(
"Empty response from NEAR AI. Raw output: {:?}",
response.output
);
}
Ok(CompletionResponse {
content: text,
finish_reason: FinishReason::Stop,