mirror of
https://github.com/openai/codex.git
synced 2026-02-01 22:47:52 +00:00
Compare commits
1 Commits
pr5657
...
h8pjl1-cod
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de786fad0a |
@@ -59,6 +59,9 @@ pub(crate) struct App<'a> {
|
||||
|
||||
pending_history_lines: Vec<Line<'static>>,
|
||||
|
||||
/// Pending replacement for the most recently inserted history line.
|
||||
pending_history_update: Option<Line<'static>>,
|
||||
|
||||
/// Stored parameters needed to instantiate the ChatWidget later, e.g.,
|
||||
/// after dismissing the Git-repo warning.
|
||||
chat_args: Option<ChatWidgetArgs>,
|
||||
@@ -164,6 +167,7 @@ impl App<'_> {
|
||||
Self {
|
||||
app_event_tx,
|
||||
pending_history_lines: Vec::new(),
|
||||
pending_history_update: None,
|
||||
app_event_rx,
|
||||
app_state,
|
||||
config,
|
||||
@@ -213,6 +217,10 @@ impl App<'_> {
|
||||
self.pending_history_lines.extend(lines);
|
||||
self.app_event_tx.send(AppEvent::RequestRedraw);
|
||||
}
|
||||
AppEvent::UpdateHistoryLastLine(line) => {
|
||||
self.pending_history_update = Some(line);
|
||||
self.app_event_tx.send(AppEvent::RequestRedraw);
|
||||
}
|
||||
AppEvent::RequestRedraw => {
|
||||
self.schedule_redraw();
|
||||
}
|
||||
@@ -431,6 +439,9 @@ impl App<'_> {
|
||||
);
|
||||
self.pending_history_lines.clear();
|
||||
}
|
||||
if let Some(line) = self.pending_history_update.take() {
|
||||
crate::insert_history::overwrite_last_history_line(terminal, line);
|
||||
}
|
||||
match &mut self.app_state {
|
||||
AppState::Chat { widget } => {
|
||||
terminal.draw(|frame| frame.render_widget_ref(&**widget, frame.area()))?;
|
||||
|
||||
@@ -48,4 +48,7 @@ pub(crate) enum AppEvent {
|
||||
},
|
||||
|
||||
InsertHistory(Vec<Line<'static>>),
|
||||
/// Replace the most recently inserted history line without adding a new
|
||||
/// newline. Used for streaming updates of the agent's response.
|
||||
UpdateHistoryLastLine(Line<'static>),
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::widgets::Widget;
|
||||
use ratatui::widgets::WidgetRef;
|
||||
use ratatui::text::Line;
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
use tokio::sync::mpsc::unbounded_channel;
|
||||
|
||||
@@ -45,6 +46,8 @@ use crate::history_cell::HistoryCell;
|
||||
use crate::history_cell::PatchEventType;
|
||||
use crate::user_approval_widget::ApprovalRequest;
|
||||
use codex_file_search::FileMatch;
|
||||
use crate::markdown::append_markdown;
|
||||
use ratatui::style::Stylize;
|
||||
|
||||
struct RunningCommand {
|
||||
command: Vec<String>,
|
||||
@@ -59,10 +62,15 @@ pub(crate) struct ChatWidget<'a> {
|
||||
config: Config,
|
||||
initial_user_message: Option<UserMessage>,
|
||||
token_usage: TokenUsage,
|
||||
/// Buffer for streaming assistant reasoning text.
|
||||
///
|
||||
/// `reasoning_streamed_lines` tracks lines already emitted for streaming.
|
||||
reasoning_streamed_lines: Vec<Line<'static>>,
|
||||
reasoning_buffer: String,
|
||||
// Buffer for streaming assistant answer text; we do not surface partial
|
||||
// We wait for the final AgentMessage event and then emit the full text
|
||||
// at once into scrollback so the history contains a single message.
|
||||
/// Buffer for streaming assistant answer text.
|
||||
///
|
||||
/// `answer_streamed_lines` tracks lines already emitted for streaming.
|
||||
answer_streamed_lines: Vec<Line<'static>>,
|
||||
answer_buffer: String,
|
||||
running_commands: HashMap<String, RunningCommand>,
|
||||
}
|
||||
@@ -149,7 +157,9 @@ impl ChatWidget<'_> {
|
||||
initial_images,
|
||||
),
|
||||
token_usage: TokenUsage::default(),
|
||||
reasoning_streamed_lines: Vec::new(),
|
||||
reasoning_buffer: String::new(),
|
||||
answer_streamed_lines: Vec::new(),
|
||||
answer_buffer: String::new(),
|
||||
running_commands: HashMap::new(),
|
||||
}
|
||||
@@ -237,45 +247,109 @@ impl ChatWidget<'_> {
|
||||
}
|
||||
EventMsg::AgentMessage(AgentMessageEvent { message }) => {
|
||||
// Final assistant answer. Prefer the fully provided message
|
||||
// from the event; if it is empty fall back to any accumulated
|
||||
// delta buffer (some providers may only stream deltas and send
|
||||
// an empty final message).
|
||||
let full = if message.is_empty() {
|
||||
std::mem::take(&mut self.answer_buffer)
|
||||
} else {
|
||||
self.answer_buffer.clear();
|
||||
self.answer_buffer = message.clone();
|
||||
message
|
||||
};
|
||||
if !full.is_empty() {
|
||||
self.add_to_history(HistoryCell::new_agent_message(&self.config, full));
|
||||
if self.answer_streamed_lines.is_empty() {
|
||||
if !full.is_empty() {
|
||||
self.add_to_history(HistoryCell::new_agent_message(&self.config, full));
|
||||
}
|
||||
} else {
|
||||
let mut lines: Vec<Line<'static>> = Vec::new();
|
||||
lines.push(Line::from("codex".magenta().bold()));
|
||||
append_markdown(&full, &mut lines, &self.config);
|
||||
let old_len = self.answer_streamed_lines.len();
|
||||
if lines.len() > old_len {
|
||||
let to_insert = lines[old_len..].to_vec();
|
||||
self.app_event_tx.send(AppEvent::InsertHistory(to_insert));
|
||||
} else if let Some(last) = lines.last() {
|
||||
self.app_event_tx
|
||||
.send(AppEvent::UpdateHistoryLastLine(last.clone()));
|
||||
}
|
||||
self.app_event_tx
|
||||
.send(AppEvent::InsertHistory(vec![Line::from("")]))
|
||||
;
|
||||
self.answer_streamed_lines.clear();
|
||||
}
|
||||
self.answer_buffer.clear();
|
||||
self.request_redraw();
|
||||
}
|
||||
EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { delta }) => {
|
||||
// Buffer only – do not emit partial lines. This avoids cases
|
||||
// where long responses appear truncated if the terminal
|
||||
// wrapped early. The full message is emitted on
|
||||
// AgentMessage.
|
||||
self.answer_buffer.push_str(&delta);
|
||||
let mut lines: Vec<Line<'static>> = Vec::new();
|
||||
if self.answer_streamed_lines.is_empty() {
|
||||
lines.push(Line::from("codex".magenta().bold()));
|
||||
}
|
||||
append_markdown(&self.answer_buffer, &mut lines, &self.config);
|
||||
if self.answer_streamed_lines.is_empty() {
|
||||
self.app_event_tx.send(AppEvent::InsertHistory(lines.clone()));
|
||||
} else {
|
||||
if lines.len() > self.answer_streamed_lines.len() {
|
||||
let new_lines = lines[self.answer_streamed_lines.len()..].to_vec();
|
||||
self.app_event_tx.send(AppEvent::InsertHistory(new_lines));
|
||||
} else if let Some(last) = lines.last() {
|
||||
self.app_event_tx
|
||||
.send(AppEvent::UpdateHistoryLastLine(last.clone()));
|
||||
}
|
||||
}
|
||||
self.answer_streamed_lines = lines;
|
||||
}
|
||||
EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent { delta }) => {
|
||||
// Buffer only – disable incremental reasoning streaming so we
|
||||
// avoid truncated intermediate lines. Full text emitted on
|
||||
// AgentReasoning.
|
||||
self.reasoning_buffer.push_str(&delta);
|
||||
let mut lines: Vec<Line<'static>> = Vec::new();
|
||||
if self.reasoning_streamed_lines.is_empty() {
|
||||
lines.push(Line::from("thinking".magenta().italic()));
|
||||
}
|
||||
append_markdown(&self.reasoning_buffer, &mut lines, &self.config);
|
||||
if self.reasoning_streamed_lines.is_empty() {
|
||||
self.app_event_tx.send(AppEvent::InsertHistory(lines.clone()));
|
||||
} else {
|
||||
if lines.len() > self.reasoning_streamed_lines.len() {
|
||||
let new_lines =
|
||||
lines[self.reasoning_streamed_lines.len()..].to_vec();
|
||||
self.app_event_tx.send(AppEvent::InsertHistory(new_lines));
|
||||
} else if let Some(last) = lines.last() {
|
||||
self.app_event_tx
|
||||
.send(AppEvent::UpdateHistoryLastLine(last.clone()));
|
||||
}
|
||||
}
|
||||
self.reasoning_streamed_lines = lines;
|
||||
}
|
||||
EventMsg::AgentReasoning(AgentReasoningEvent { text }) => {
|
||||
// Emit full reasoning text once. Some providers might send
|
||||
// final event with empty text if only deltas were used.
|
||||
let full = if text.is_empty() {
|
||||
std::mem::take(&mut self.reasoning_buffer)
|
||||
} else {
|
||||
self.reasoning_buffer.clear();
|
||||
self.reasoning_buffer = text.clone();
|
||||
text
|
||||
};
|
||||
if !full.is_empty() {
|
||||
self.add_to_history(HistoryCell::new_agent_reasoning(&self.config, full));
|
||||
if self.reasoning_streamed_lines.is_empty() {
|
||||
if !full.is_empty() {
|
||||
self.add_to_history(HistoryCell::new_agent_reasoning(
|
||||
&self.config,
|
||||
full,
|
||||
));
|
||||
}
|
||||
} else {
|
||||
let mut lines: Vec<Line<'static>> = Vec::new();
|
||||
lines.push(Line::from("thinking".magenta().italic()));
|
||||
append_markdown(&full, &mut lines, &self.config);
|
||||
let old_len = self.reasoning_streamed_lines.len();
|
||||
if lines.len() > old_len {
|
||||
let to_insert = lines[old_len..].to_vec();
|
||||
self.app_event_tx.send(AppEvent::InsertHistory(to_insert));
|
||||
} else if let Some(last) = lines.last() {
|
||||
self.app_event_tx
|
||||
.send(AppEvent::UpdateHistoryLastLine(last.clone()));
|
||||
}
|
||||
self.app_event_tx
|
||||
.send(AppEvent::InsertHistory(vec![Line::from("")]))
|
||||
;
|
||||
self.reasoning_streamed_lines.clear();
|
||||
}
|
||||
self.reasoning_buffer.clear();
|
||||
self.request_redraw();
|
||||
}
|
||||
EventMsg::TaskStarted => {
|
||||
|
||||
@@ -13,6 +13,7 @@ use crossterm::style::SetAttribute;
|
||||
use crossterm::style::SetBackgroundColor;
|
||||
use crossterm::style::SetColors;
|
||||
use crossterm::style::SetForegroundColor;
|
||||
use crossterm::terminal::{Clear, ClearType};
|
||||
use ratatui::layout::Size;
|
||||
use ratatui::prelude::Backend;
|
||||
use ratatui::style::Color;
|
||||
@@ -79,6 +80,31 @@ pub(crate) fn insert_history_lines(terminal: &mut tui::Tui, lines: Vec<Line>) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Overwrite the most recently inserted history line with `line` without
|
||||
/// inserting a newline. Used for streaming updates where the last line is
|
||||
/// still being written.
|
||||
pub(crate) fn overwrite_last_history_line(terminal: &mut tui::Tui, line: Line) {
|
||||
let cursor_pos = terminal.get_cursor_position().ok();
|
||||
let mut area = terminal.get_frame().area();
|
||||
|
||||
// Limit the scroll region to everything above the viewport so that
|
||||
// rewriting the last line does not affect the bottom pane.
|
||||
queue!(std::io::stdout(), SetScrollRegion(1..area.top())).ok();
|
||||
|
||||
// Move to the start of the last history line, clear it, and write the new content.
|
||||
let y = area.top().saturating_sub(1);
|
||||
queue!(std::io::stdout(), MoveTo(0, y)).ok();
|
||||
queue!(std::io::stdout(), Clear(ClearType::CurrentLine)).ok();
|
||||
write_spans(&mut std::io::stdout(), line.iter()).ok();
|
||||
|
||||
queue!(std::io::stdout(), ResetScrollRegion).ok();
|
||||
|
||||
// Restore cursor position.
|
||||
if let Some(cursor_pos) = cursor_pos {
|
||||
queue!(std::io::stdout(), MoveTo(cursor_pos.x, cursor_pos.y)).ok();
|
||||
}
|
||||
}
|
||||
|
||||
fn wrapped_line_count(lines: &[Line], width: u16) -> u16 {
|
||||
let mut count = 0;
|
||||
for line in lines {
|
||||
|
||||
Reference in New Issue
Block a user