feat: message when stream get correctly resumed (#4988)
<img width="366" height="109" alt="Screenshot 2025-10-09 at 17 44 16" src="https://github.com/user-attachments/assets/26bc6f60-11bc-4fc6-a1cc-430ca1203969" />
This commit is contained in:
@@ -2008,9 +2008,7 @@ async fn run_turn(
|
|||||||
// at a seemingly frozen screen.
|
// at a seemingly frozen screen.
|
||||||
sess.notify_stream_error(
|
sess.notify_stream_error(
|
||||||
&sub_id,
|
&sub_id,
|
||||||
format!(
|
format!("Re-connecting... {retries}/{max_retries}"),
|
||||||
"stream error: {e}; retrying {retries}/{max_retries} in {delay:?}…"
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
|||||||
@@ -136,9 +136,7 @@ async fn run_compact_task_inner(
|
|||||||
let delay = backoff(retries);
|
let delay = backoff(retries);
|
||||||
sess.notify_stream_error(
|
sess.notify_stream_error(
|
||||||
&sub_id,
|
&sub_id,
|
||||||
format!(
|
format!("Re-connecting... {retries}/{max_retries}"),
|
||||||
"stream error: {e}; retrying {retries}/{max_retries} in {delay:?}…"
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
tokio::time::sleep(delay).await;
|
tokio::time::sleep(delay).await;
|
||||||
|
|||||||
@@ -1203,6 +1203,11 @@ pub struct StreamErrorEvent {
|
|||||||
pub message: String,
|
pub message: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, TS)]
|
||||||
|
pub struct StreamInfoEvent {
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, TS)]
|
#[derive(Debug, Clone, Deserialize, Serialize, TS)]
|
||||||
pub struct PatchApplyBeginEvent {
|
pub struct PatchApplyBeginEvent {
|
||||||
/// Identifier so this can be paired with the PatchApplyEnd event.
|
/// Identifier so this can be paired with the PatchApplyEnd event.
|
||||||
|
|||||||
@@ -240,6 +240,10 @@ pub(crate) struct ChatWidget {
|
|||||||
reasoning_buffer: String,
|
reasoning_buffer: String,
|
||||||
// Accumulates full reasoning content for transcript-only recording
|
// Accumulates full reasoning content for transcript-only recording
|
||||||
full_reasoning_buffer: String,
|
full_reasoning_buffer: String,
|
||||||
|
// Current status header shown in the status indicator.
|
||||||
|
current_status_header: String,
|
||||||
|
// Previous status header to restore after a transient stream retry.
|
||||||
|
retry_status_header: Option<String>,
|
||||||
conversation_id: Option<ConversationId>,
|
conversation_id: Option<ConversationId>,
|
||||||
frame_requester: FrameRequester,
|
frame_requester: FrameRequester,
|
||||||
// Whether to include the initial welcome banner on session configured
|
// Whether to include the initial welcome banner on session configured
|
||||||
@@ -303,6 +307,14 @@ impl ChatWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_status_header(&mut self, header: String) {
|
||||||
|
if self.current_status_header == header {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.current_status_header = header.clone();
|
||||||
|
self.bottom_pane.update_status_header(header);
|
||||||
|
}
|
||||||
|
|
||||||
// --- Small event handlers ---
|
// --- Small event handlers ---
|
||||||
fn on_session_configured(&mut self, event: codex_core::protocol::SessionConfiguredEvent) {
|
fn on_session_configured(&mut self, event: codex_core::protocol::SessionConfiguredEvent) {
|
||||||
self.bottom_pane
|
self.bottom_pane
|
||||||
@@ -352,7 +364,7 @@ impl ChatWidget {
|
|||||||
|
|
||||||
if let Some(header) = extract_first_bold(&self.reasoning_buffer) {
|
if let Some(header) = extract_first_bold(&self.reasoning_buffer) {
|
||||||
// Update the shimmer header to the extracted reasoning chunk header.
|
// Update the shimmer header to the extracted reasoning chunk header.
|
||||||
self.bottom_pane.update_status_header(header);
|
self.set_status_header(header);
|
||||||
} else {
|
} else {
|
||||||
// Fallback while we don't yet have a bold header: leave existing header as-is.
|
// Fallback while we don't yet have a bold header: leave existing header as-is.
|
||||||
}
|
}
|
||||||
@@ -386,6 +398,8 @@ impl ChatWidget {
|
|||||||
fn on_task_started(&mut self) {
|
fn on_task_started(&mut self) {
|
||||||
self.bottom_pane.clear_ctrl_c_quit_hint();
|
self.bottom_pane.clear_ctrl_c_quit_hint();
|
||||||
self.bottom_pane.set_task_running(true);
|
self.bottom_pane.set_task_running(true);
|
||||||
|
self.retry_status_header = None;
|
||||||
|
self.set_status_header(String::from("Working"));
|
||||||
self.full_reasoning_buffer.clear();
|
self.full_reasoning_buffer.clear();
|
||||||
self.reasoning_buffer.clear();
|
self.reasoning_buffer.clear();
|
||||||
self.request_redraw();
|
self.request_redraw();
|
||||||
@@ -621,9 +635,10 @@ impl ChatWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn on_stream_error(&mut self, message: String) {
|
fn on_stream_error(&mut self, message: String) {
|
||||||
// Show stream errors in the transcript so users see retry/backoff info.
|
if self.retry_status_header.is_none() {
|
||||||
self.add_to_history(history_cell::new_stream_error_event(message));
|
self.retry_status_header = Some(self.current_status_header.clone());
|
||||||
self.request_redraw();
|
}
|
||||||
|
self.set_status_header(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Periodic tick to commit at most one queued line to history with a small delay,
|
/// Periodic tick to commit at most one queued line to history with a small delay,
|
||||||
@@ -928,6 +943,8 @@ impl ChatWidget {
|
|||||||
interrupts: InterruptManager::new(),
|
interrupts: InterruptManager::new(),
|
||||||
reasoning_buffer: String::new(),
|
reasoning_buffer: String::new(),
|
||||||
full_reasoning_buffer: String::new(),
|
full_reasoning_buffer: String::new(),
|
||||||
|
current_status_header: String::from("Working"),
|
||||||
|
retry_status_header: None,
|
||||||
conversation_id: None,
|
conversation_id: None,
|
||||||
queued_user_messages: VecDeque::new(),
|
queued_user_messages: VecDeque::new(),
|
||||||
show_welcome_banner: true,
|
show_welcome_banner: true,
|
||||||
@@ -991,6 +1008,8 @@ impl ChatWidget {
|
|||||||
interrupts: InterruptManager::new(),
|
interrupts: InterruptManager::new(),
|
||||||
reasoning_buffer: String::new(),
|
reasoning_buffer: String::new(),
|
||||||
full_reasoning_buffer: String::new(),
|
full_reasoning_buffer: String::new(),
|
||||||
|
current_status_header: String::from("Working"),
|
||||||
|
retry_status_header: None,
|
||||||
conversation_id: None,
|
conversation_id: None,
|
||||||
queued_user_messages: VecDeque::new(),
|
queued_user_messages: VecDeque::new(),
|
||||||
show_welcome_banner: true,
|
show_welcome_banner: true,
|
||||||
|
|||||||
@@ -276,6 +276,8 @@ fn make_chatwidget_manual() -> (
|
|||||||
interrupts: InterruptManager::new(),
|
interrupts: InterruptManager::new(),
|
||||||
reasoning_buffer: String::new(),
|
reasoning_buffer: String::new(),
|
||||||
full_reasoning_buffer: String::new(),
|
full_reasoning_buffer: String::new(),
|
||||||
|
current_status_header: String::from("Working"),
|
||||||
|
retry_status_header: None,
|
||||||
conversation_id: None,
|
conversation_id: None,
|
||||||
frame_requester: FrameRequester::test_dummy(),
|
frame_requester: FrameRequester::test_dummy(),
|
||||||
show_welcome_banner: true,
|
show_welcome_banner: true,
|
||||||
@@ -2044,9 +2046,10 @@ fn plan_update_renders_history_cell() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn stream_error_is_rendered_to_history() {
|
fn stream_error_updates_status_indicator() {
|
||||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
|
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
|
||||||
let msg = "stream error: stream disconnected before completion: idle timeout waiting for SSE; retrying 1/5 in 211ms…";
|
chat.bottom_pane.set_task_running(true);
|
||||||
|
let msg = "Re-connecting... 2/5";
|
||||||
chat.handle_codex_event(Event {
|
chat.handle_codex_event(Event {
|
||||||
id: "sub-1".into(),
|
id: "sub-1".into(),
|
||||||
msg: EventMsg::StreamError(StreamErrorEvent {
|
msg: EventMsg::StreamError(StreamErrorEvent {
|
||||||
@@ -2055,11 +2058,15 @@ fn stream_error_is_rendered_to_history() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let cells = drain_insert_history(&mut rx);
|
let cells = drain_insert_history(&mut rx);
|
||||||
assert!(!cells.is_empty(), "expected a history cell for StreamError");
|
assert!(
|
||||||
let blob = lines_to_single_string(cells.last().unwrap());
|
cells.is_empty(),
|
||||||
assert!(blob.contains('⚠'));
|
"expected no history cell for StreamError event"
|
||||||
assert!(blob.contains("stream error:"));
|
);
|
||||||
assert!(blob.contains("idle timeout waiting for SSE"));
|
let status = chat
|
||||||
|
.bottom_pane
|
||||||
|
.status_widget()
|
||||||
|
.expect("status indicator should be visible");
|
||||||
|
assert_eq!(status.header(), msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -1062,11 +1062,6 @@ pub(crate) fn new_error_event(message: String) -> PlainHistoryCell {
|
|||||||
PlainHistoryCell { lines }
|
PlainHistoryCell { lines }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn new_stream_error_event(message: String) -> PlainHistoryCell {
|
|
||||||
let lines: Vec<Line<'static>> = vec![vec![padded_emoji("⚠️").into(), message.dim()].into()];
|
|
||||||
PlainHistoryCell { lines }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Render a user‑friendly plan update styled like a checkbox todo list.
|
/// Render a user‑friendly plan update styled like a checkbox todo list.
|
||||||
pub(crate) fn new_plan_update(update: UpdatePlanArgs) -> PlanUpdateCell {
|
pub(crate) fn new_plan_update(update: UpdatePlanArgs) -> PlanUpdateCell {
|
||||||
let UpdatePlanArgs { explanation, plan } = update;
|
let UpdatePlanArgs { explanation, plan } = update;
|
||||||
|
|||||||
@@ -103,6 +103,11 @@ impl StatusIndicatorWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) fn header(&self) -> &str {
|
||||||
|
&self.header
|
||||||
|
}
|
||||||
|
|
||||||
/// Replace the queued messages displayed beneath the header.
|
/// Replace the queued messages displayed beneath the header.
|
||||||
pub(crate) fn set_queued_messages(&mut self, queued: Vec<String>) {
|
pub(crate) fn set_queued_messages(&mut self, queued: Vec<String>) {
|
||||||
self.queued_messages = queued;
|
self.queued_messages = queued;
|
||||||
|
|||||||
Reference in New Issue
Block a user