mirror of
https://github.com/openai/codex.git
synced 2026-09-13 11:47:17 +00:00
show background stream error
This commit is contained in:
@@ -11,7 +11,9 @@ use crate::slash_command::SlashCommand;
|
||||
use crate::tui;
|
||||
use codex_core::ConversationManager;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::protocol::BackgroundEventEvent;
|
||||
use codex_core::protocol::Event;
|
||||
use codex_core::protocol::EventMsg;
|
||||
use codex_core::protocol::Op;
|
||||
use color_eyre::eyre::Result;
|
||||
use crossterm::SynchronizedUpdate;
|
||||
@@ -274,6 +276,25 @@ impl App<'_> {
|
||||
}
|
||||
AppEvent::KeyEvent(key_event) => {
|
||||
match key_event {
|
||||
KeyEvent {
|
||||
code: KeyCode::Char('e'),
|
||||
modifiers: crossterm::event::KeyModifiers::CONTROL,
|
||||
kind: KeyEventKind::Press,
|
||||
..
|
||||
} => {
|
||||
let env = std::env::var("MANUALLY_DEBUG_TUI_BACKGROUND_RETRY")
|
||||
.unwrap_or_default();
|
||||
if env == "1" {
|
||||
self.app_event_tx.send(AppEvent::CodexEvent(Event {
|
||||
id: "manual".to_string(),
|
||||
msg: EventMsg::BackgroundEvent(BackgroundEventEvent {
|
||||
message: "stream error: stream disconnected before completion: idle timeout waiting for SSE; retrying 1/5 in 200ms…".to_string(),
|
||||
}),
|
||||
}));
|
||||
} else {
|
||||
self.dispatch_key_event(key_event);
|
||||
}
|
||||
}
|
||||
KeyEvent {
|
||||
code: KeyCode::Char('c'),
|
||||
modifiers: crossterm::event::KeyModifiers::CONTROL,
|
||||
|
||||
@@ -297,6 +297,7 @@ impl ChatWidget<'_> {
|
||||
|
||||
fn on_background_event(&mut self, message: String) {
|
||||
debug!("BackgroundEvent: {message}");
|
||||
self.add_to_history(&history_cell::new_background_event(message));
|
||||
}
|
||||
/// Periodic tick to commit at most one queued line to history with a small delay,
|
||||
/// animating the output.
|
||||
|
||||
@@ -181,7 +181,7 @@ fn open_fixture(name: &str) -> std::fs::File {
|
||||
return f;
|
||||
}
|
||||
}
|
||||
// 2) Fallback to parent (workspace root)
|
||||
// 2) Fallback to parent (workspace crate root)
|
||||
{
|
||||
let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
p.push("..");
|
||||
@@ -979,3 +979,117 @@ fn deltas_then_same_final_message_are_rendered_snapshot() {
|
||||
.collect::<String>();
|
||||
assert_snapshot!(combined);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn timeout_session_transcript_shows_background_errors() {
|
||||
let (mut chat, rx, _op_rx) = make_chatwidget_manual();
|
||||
|
||||
// Set up a VT100 test terminal to capture ANSI visual output
|
||||
let width: u16 = 80;
|
||||
let height: u16 = 2000;
|
||||
let viewport = ratatui::layout::Rect::new(0, height - 1, width, 1);
|
||||
let backend = ratatui::backend::TestBackend::new(width, height);
|
||||
let mut terminal = crate::custom_terminal::Terminal::with_options(backend)
|
||||
.expect("failed to construct terminal");
|
||||
terminal.set_viewport_area(viewport);
|
||||
|
||||
// Replay the recorded session into the widget and collect transcript
|
||||
let file = open_fixture("timeout-session-log.jsonl");
|
||||
let reader = BufReader::new(file);
|
||||
let mut ansi: Vec<u8> = Vec::new();
|
||||
|
||||
for line in reader.lines() {
|
||||
let line = line.expect("read line");
|
||||
if line.trim().is_empty() || line.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
let Ok(v): Result<serde_json::Value, _> = serde_json::from_str(&line) else {
|
||||
continue;
|
||||
};
|
||||
let Some(dir) = v.get("dir").and_then(|d| d.as_str()) else {
|
||||
continue;
|
||||
};
|
||||
if dir != "to_tui" {
|
||||
continue;
|
||||
}
|
||||
let Some(kind) = v.get("kind").and_then(|k| k.as_str()) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
match kind {
|
||||
"codex_event" => {
|
||||
if let Some(payload) = v.get("payload") {
|
||||
let ev: Event = serde_json::from_value(payload.clone()).expect("parse");
|
||||
chat.handle_codex_event(ev);
|
||||
while let Ok(app_ev) = rx.try_recv() {
|
||||
if let AppEvent::InsertHistory(lines) = app_ev {
|
||||
crate::insert_history::insert_history_lines_to_writer(
|
||||
&mut terminal,
|
||||
&mut ansi,
|
||||
lines,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"app_event" => {
|
||||
if let Some(variant) = v.get("variant").and_then(|s| s.as_str()) {
|
||||
if variant == "CommitTick" {
|
||||
chat.on_commit_tick();
|
||||
while let Ok(app_ev) = rx.try_recv() {
|
||||
if let AppEvent::InsertHistory(lines) = app_ev {
|
||||
crate::insert_history::insert_history_lines_to_writer(
|
||||
&mut terminal,
|
||||
&mut ansi,
|
||||
lines,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Build the final VT100 visual by parsing the ANSI stream. Trim trailing spaces per line
|
||||
// and drop trailing empty lines for stable comparisons.
|
||||
let mut parser = vt100::Parser::new(height, width, 0);
|
||||
parser.process(&ansi);
|
||||
let mut lines: Vec<String> = Vec::with_capacity(height as usize);
|
||||
for row in 0..height {
|
||||
let mut s = String::with_capacity(width as usize);
|
||||
for col in 0..width {
|
||||
if let Some(cell) = parser.screen().cell(row, col) {
|
||||
if let Some(ch) = cell.contents().chars().next() {
|
||||
s.push(ch);
|
||||
} else {
|
||||
s.push(' ');
|
||||
}
|
||||
} else {
|
||||
s.push(' ');
|
||||
}
|
||||
}
|
||||
lines.push(s.trim_end().to_string());
|
||||
}
|
||||
while lines.last().is_some_and(|l| l.is_empty()) {
|
||||
lines.pop();
|
||||
}
|
||||
|
||||
let visible_after = lines.join("\n");
|
||||
let visible_flat = visible_after.replace('\n', " ");
|
||||
|
||||
// Assertions: ensure background events are visible and contain timeout info.
|
||||
assert!(
|
||||
visible_flat.contains("stream error:"),
|
||||
"missing 'stream error:' in vt100 output:\n{visible_after}"
|
||||
);
|
||||
assert!(
|
||||
visible_flat.contains("idle timeout waiting for SSE"),
|
||||
"missing timeout detail in vt100 output:\n{visible_after}"
|
||||
);
|
||||
assert!(
|
||||
visible_flat.contains("retrying 1/"),
|
||||
"missing retry indicator in vt100 output:\n{visible_after}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -639,6 +639,18 @@ pub(crate) fn new_error_event(message: String) -> PlainHistoryCell {
|
||||
PlainHistoryCell { lines }
|
||||
}
|
||||
|
||||
pub(crate) fn new_background_event(message: String) -> PlainHistoryCell {
|
||||
let lines: Vec<Line<'static>> = vec![
|
||||
Line::from(vec![
|
||||
"ℹ".magenta(),
|
||||
" ".into(),
|
||||
Span::styled(message, Style::default().add_modifier(Modifier::DIM)),
|
||||
]),
|
||||
Line::from(""),
|
||||
];
|
||||
PlainHistoryCell { lines }
|
||||
}
|
||||
|
||||
/// Render a user‑friendly plan update styled like a checkbox todo list.
|
||||
pub(crate) fn new_plan_update(update: UpdatePlanArgs) -> PlainHistoryCell {
|
||||
let UpdatePlanArgs { explanation, plan } = update;
|
||||
|
||||
22329
codex-rs/tui/tests/fixtures/timeout-session-log.jsonl
vendored
Normal file
22329
codex-rs/tui/tests/fixtures/timeout-session-log.jsonl
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user