From 42d6e8718656c82ae4354092716feecdaefdee80 Mon Sep 17 00:00:00 2001 From: Illia Polosukhin Date: Tue, 3 Feb 2026 00:17:40 -0800 Subject: [PATCH] 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 --- src/channels/cli/app.rs | 30 +++++++++++++++++++++++++++++- src/channels/cli/events.rs | 9 ++++++--- src/channels/cli/mod.rs | 4 ++-- src/llm/nearai.rs | 15 +++++++++++++++ 4 files changed, 52 insertions(+), 6 deletions(-) diff --git a/src/channels/cli/app.rs b/src/channels/cli/app.rs index 6ad6f08c..50281765 100644 --- a/src/channels/cli/app.rs +++ b/src/channels/cli/app.rs @@ -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) { + 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()); diff --git a/src/channels/cli/events.rs b/src/channels/cli/events.rs index d140e7ce..f6b21ec2 100644 --- a/src/channels/cli/events.rs +++ b/src/channels/cli/events.rs @@ -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); + } } } diff --git a/src/channels/cli/mod.rs b/src/channels/cli/mod.rs index f99554e5..e976129a 100644 --- a/src/channels/cli/mod.rs +++ b/src/channels/cli/mod.rs @@ -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) diff --git a/src/llm/nearai.rs b/src/llm/nearai.rs index 86f52b3a..00516911 100644 --- a/src/llm/nearai.rs +++ b/src/llm/nearai.rs @@ -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::>() .join(""); + if text.is_empty() { + tracing::warn!( + "Empty response from NEAR AI. Raw output: {:?}", + response.output + ); + } + Ok(CompletionResponse { content: text, finish_reason: FinishReason::Stop,