Add streaming to exec and tui (#1594)

Added support for streaming in `tui`
Added support for streaming in `exec`


https://github.com/user-attachments/assets/4215892e-d940-452c-a1d0-416ed0cf14eb
This commit is contained in:
aibrahim-oai
2025-07-16 22:26:31 -07:00
committed by GitHub
parent d3dbc10479
commit 643ab1f582
5 changed files with 149 additions and 32 deletions

View File

@@ -297,6 +297,8 @@ impl<'a> App<'a> {
}
fn draw_next_frame(&mut self, terminal: &mut tui::Tui) -> Result<()> {
// TODO: add a throttle to avoid redrawing too often
match &mut self.app_state {
AppState::Chat { widget } => {
terminal.draw(|frame| frame.render_widget_ref(&**widget, frame.area()))?;

View File

@@ -51,6 +51,8 @@ pub(crate) struct ChatWidget<'a> {
config: Config,
initial_user_message: Option<UserMessage>,
token_usage: TokenUsage,
reasoning_buffer: String,
answer_buffer: String,
}
#[derive(Clone, Copy, Eq, PartialEq)]
@@ -137,6 +139,8 @@ impl ChatWidget<'_> {
initial_images,
),
token_usage: TokenUsage::default(),
reasoning_buffer: String::new(),
answer_buffer: String::new(),
}
}
@@ -242,16 +246,51 @@ impl ChatWidget<'_> {
self.request_redraw();
}
EventMsg::AgentMessage(AgentMessageEvent { message }) => {
// if the answer buffer is empty, this means we haven't received any
// delta. Thus, we need to print the message as a new answer.
if self.answer_buffer.is_empty() {
self.conversation_history
.add_agent_message(&self.config, message);
} else {
self.conversation_history
.replace_prev_agent_message(&self.config, message);
}
self.answer_buffer.clear();
self.request_redraw();
}
EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { delta }) => {
if self.answer_buffer.is_empty() {
self.conversation_history
.add_agent_message(&self.config, "".to_string());
}
self.answer_buffer.push_str(&delta.clone());
self.conversation_history
.add_agent_message(&self.config, message);
.replace_prev_agent_message(&self.config, self.answer_buffer.clone());
self.request_redraw();
}
EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent { delta }) => {
if self.reasoning_buffer.is_empty() {
self.conversation_history
.add_agent_reasoning(&self.config, "".to_string());
}
self.reasoning_buffer.push_str(&delta.clone());
self.conversation_history
.replace_prev_agent_reasoning(&self.config, self.reasoning_buffer.clone());
self.request_redraw();
}
EventMsg::AgentReasoning(AgentReasoningEvent { text }) => {
if !self.config.hide_agent_reasoning {
// if the reasoning buffer is empty, this means we haven't received any
// delta. Thus, we need to print the message as a new reasoning.
if self.reasoning_buffer.is_empty() {
self.conversation_history
.add_agent_reasoning(&self.config, text);
self.request_redraw();
.add_agent_reasoning(&self.config, "".to_string());
} else {
// else, we rerender one last time.
self.conversation_history
.replace_prev_agent_reasoning(&self.config, text);
}
self.reasoning_buffer.clear();
self.request_redraw();
}
EventMsg::TaskStarted => {
self.bottom_pane.clear_ctrl_c_quit_hint();
@@ -377,12 +416,6 @@ impl ChatWidget<'_> {
self.bottom_pane
.on_history_entry_response(log_id, offset, entry.map(|e| e.text));
}
EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { delta: _ }) => {
// TODO: think how we want to support this in the TUI
}
EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent { delta: _ }) => {
// TODO: think how we want to support this in the TUI
}
event => {
self.conversation_history
.add_background_event(format!("{event:?}"));

View File

@@ -202,6 +202,14 @@ impl ConversationHistoryWidget {
self.add_to_history(HistoryCell::new_agent_reasoning(config, text));
}
pub fn replace_prev_agent_reasoning(&mut self, config: &Config, text: String) {
self.replace_last_agent_reasoning(config, text);
}
pub fn replace_prev_agent_message(&mut self, config: &Config, text: String) {
self.replace_last_agent_message(config, text);
}
pub fn add_background_event(&mut self, message: String) {
self.add_to_history(HistoryCell::new_background_event(message));
}
@@ -249,6 +257,42 @@ impl ConversationHistoryWidget {
});
}
pub fn replace_last_agent_reasoning(&mut self, config: &Config, text: String) {
if let Some(idx) = self
.entries
.iter()
.rposition(|entry| matches!(entry.cell, HistoryCell::AgentReasoning { .. }))
{
let width = self.cached_width.get();
let entry = &mut self.entries[idx];
entry.cell = HistoryCell::new_agent_reasoning(config, text);
let height = if width > 0 {
entry.cell.height(width)
} else {
0
};
entry.line_count.set(height);
}
}
pub fn replace_last_agent_message(&mut self, config: &Config, text: String) {
if let Some(idx) = self
.entries
.iter()
.rposition(|entry| matches!(entry.cell, HistoryCell::AgentMessage { .. }))
{
let width = self.cached_width.get();
let entry = &mut self.entries[idx];
entry.cell = HistoryCell::new_agent_message(config, text);
let height = if width > 0 {
entry.cell.height(width)
} else {
0
};
entry.line_count.set(height);
}
}
pub fn record_completed_exec_command(
&mut self,
call_id: String,
@@ -454,7 +498,7 @@ impl WidgetRef for ConversationHistoryWidget {
{
// Choose a thumb color that stands out only when this pane has focus so that the
// users attention is naturally drawn to the active viewport. When unfocused we show
// user's attention is naturally drawn to the active viewport. When unfocused we show
// a low-contrast thumb so the scrollbar fades into the background without becoming
// invisible.
let thumb_style = if self.has_input_focus {