show background stream error

This commit is contained in:
easong-openai
2025-08-19 01:13:58 -07:00
parent 37e5b087a7
commit 19bd3513fe
5 changed files with 22478 additions and 1 deletions

View File

@@ -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,

View File

@@ -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.

View File

@@ -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}"
);
}

View File

@@ -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 userfriendly plan update styled like a checkbox todo list.
pub(crate) fn new_plan_update(update: UpdatePlanArgs) -> PlainHistoryCell {
let UpdatePlanArgs { explanation, plan } = update;

File diff suppressed because one or more lines are too long