diff --git a/crates/focus-tui/src/app.rs b/crates/focus-tui/src/app.rs index 12d14ce..f1132ac 100644 --- a/crates/focus-tui/src/app.rs +++ b/crates/focus-tui/src/app.rs @@ -438,8 +438,22 @@ impl App { self.persist_entries(&to_persist); self.transcript = messages; - if let Some(err) = error { - self.status = format!("⚠ {}", err); + // 错误优先取 job 报告的 error;若错误被编码成消息(如 provider 的 + // HTTP 400 → StreamEvent::Error → assistant error 消息), + // 从最后一条 assistant 消息的 error_message 提取。 + // Prefer the job's reported error; when the error is encoded as + // a message (provider HTTP 400 → StreamEvent::Error → an + // assistant error message), extract it from the last assistant. + let surfaced = error.or_else(|| { + self.transcript.iter().rev().find_map(|m| match m { + Message::Assistant(a) if a.stop_reason == StopReason::Error => { + a.error_message.clone() + } + _ => None, + }) + }); + if let Some(err) = surfaced { + self.status = format!("⚠ {}", crate::format::truncate(&err, 80)); self.push_note(format!("运行出错:{}", err)); } else { self.status = "✓ 完成".into(); diff --git a/crates/focus-tui/src/state.rs b/crates/focus-tui/src/state.rs index e77f8e6..b980b37 100644 --- a/crates/focus-tui/src/state.rs +++ b/crates/focus-tui/src/state.rs @@ -77,6 +77,9 @@ pub enum UiBlock { is_error: bool, expanded: bool, }, + /// 运行错误(assistant 消息的 error_message,红色显示)。 + /// A run error (an assistant message's error_message, shown in red). + Error { id: BlockId, text: String }, /// 工具执行过程(运行中 / 完成,含耗时与输出)。 /// A tool execution (running / done, with duration and output). ToolExec { @@ -382,6 +385,14 @@ pub fn build_blocks( expanded, ); } + // 具体错误文本(如 provider 的 HTTP 400 详情)。 + // The concrete error text (e.g. a provider HTTP 400 detail). + if let Some(err) = &a.error_message { + out.push(UiBlock::Error { + id: BlockId(format!("m{}-err", mi)), + text: err.clone(), + }); + } } Message::ToolResult(t) => { out.push(UiBlock::ToolResult { @@ -414,6 +425,12 @@ pub fn build_blocks( expanded, ); } + if let Some(err) = &partial.error_message { + out.push(UiBlock::Error { + id: BlockId("live-err".into()), + text: err.clone(), + }); + } } for (ei, exec) in rs.tool_execs.iter().enumerate() { out.push(UiBlock::ToolExec { @@ -485,7 +502,8 @@ impl UiBlock { | UiBlock::Thinking { id, .. } | UiBlock::ToolCall { id, .. } | UiBlock::ToolResult { id, .. } - | UiBlock::ToolExec { id, .. } => Some(id), + | UiBlock::ToolExec { id, .. } + | UiBlock::Error { id, .. } => Some(id), UiBlock::Note { .. } => None, } } diff --git a/crates/focus-tui/src/ui.rs b/crates/focus-tui/src/ui.rs index 5abae59..93462d2 100644 --- a/crates/focus-tui/src/ui.rs +++ b/crates/focus-tui/src/ui.rs @@ -271,6 +271,14 @@ fn block_lines(block: &UiBlock, width: usize) -> Vec> { ))); } } + UiBlock::Error { text, .. } => { + for line in wrap_text(text, width) { + out.push(Line::from(Span::styled( + line, + Style::new().fg(Color::Red).bold(), + ))); + } + } UiBlock::ToolCall { name, summary, diff --git a/crates/focus-tui/tests/state_tests.rs b/crates/focus-tui/tests/state_tests.rs index af78311..f7caee8 100644 --- a/crates/focus-tui/tests/state_tests.rs +++ b/crates/focus-tui/tests/state_tests.rs @@ -279,3 +279,26 @@ fn working_phase_detection() { }); assert_eq!(rs.working_phase(), "streaming…"); } + +/// 错误消息必须渲染出具体的错误文本(而不是只有消息头)。 +/// An error message must render its concrete error text (not just a header). +#[test] +fn error_message_renders_as_error_block() { + let transcript = vec![Message::Assistant(AssistantMessage { + content: Vec::new(), + model: "m".into(), + usage: Usage::default(), + stop_reason: StopReason::Error, + error_message: Some("http error 400: bad request detail".into()), + timestamp: 0, + })]; + let blocks = build_blocks(&transcript, &HashMap::new(), None, &HashSet::new()); + let err = blocks + .iter() + .find_map(|b| match b { + UiBlock::Error { text, .. } => Some(text.clone()), + _ => None, + }) + .expect("error block"); + assert_eq!(err, "http error 400: bad request detail"); +}