From fc4b22e7db991c9025f453f46c72d839f4ea049f Mon Sep 17 00:00:00 2001 From: Cody De Arkland Date: Tue, 12 May 2026 08:53:59 -0700 Subject: [PATCH] fix(agent): preserve part order in history and surface auth errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two small follow-ups to the --thread-id history replay shipped in #897: - render_history now iterates a message's parts once in source order so text and `[used tool: ...]` / `[attachment: ...]` markers stay interleaved the way the model emitted them, instead of all text appearing before any tool/attachment markers. - If the history fetch fails with an auth error (Unauthorized, expired token, invalid token), propagate it instead of swallowing — otherwise the user gets a confusing dim warning followed by another auth failure on the first message. Non-auth errors (network, rate limit, etc.) still fall through to the dim warning + empty history path. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/commands/agent.rs | 98 ++++++++++++++++++++++++------------------- 1 file changed, 56 insertions(+), 42 deletions(-) diff --git a/src/commands/agent.rs b/src/commands/agent.rs index 184fb7a90..eab146653 100644 --- a/src/commands/agent.rs +++ b/src/commands/agent.rs @@ -31,6 +31,7 @@ use crate::{ project::get_project, service::get_or_prompt_service, }, + errors::RailwayError, interact_or, util::progress::create_spinner, }; @@ -144,12 +145,24 @@ pub async fn command(args: Args) -> Result<()> { let history = match args.thread_id.as_deref() { Some(tid) => { let messages_url = get_thread_messages_url(&configs, tid); - get_thread_messages(&chat_client, &messages_url, Some(10)) - .await - .unwrap_or_else(|e| { + match get_thread_messages(&chat_client, &messages_url, Some(10)).await { + Ok(messages) => messages, + Err(e) => { + if let Some(railway_err) = e.downcast_ref::() + && matches!( + railway_err, + RailwayError::Unauthorized + | RailwayError::UnauthorizedToken(_) + | RailwayError::UnauthorizedLogin + | RailwayError::InvalidRailwayToken(_) + ) + { + return Err(e); + } eprintln!("{}", format!("Could not load thread history: {e}").dimmed()); Vec::new() - }) + } + } } None => Vec::new(), }; @@ -169,6 +182,17 @@ pub async fn command(args: Args) -> Result<()> { } } +fn render_text_block(text: &str, is_assistant: bool, skin: &termimad::MadSkin) { + if text.trim().is_empty() { + return; + } + if is_assistant { + skin.print_text(text); + } else { + println!("{}", text); + } +} + fn render_history(messages: &[ChatMessage], thread_id: Option<&str>) { let banner = match thread_id { Some(id) => format!("── resuming thread {id} ──"), @@ -187,47 +211,37 @@ fn render_history(messages: &[ChatMessage], thread_id: Option<&str>) { }; println!("{}", role_label); - let text_parts: Vec<&str> = message - .parts - .iter() - .filter_map(|part| match part { - ChatMessagePart::Text { content } => Some(content.as_str()), - _ => None, - }) - .collect(); - - let text = if text_parts.is_empty() { - message.content.clone() + if message.parts.is_empty() { + render_text_block(&message.content, is_assistant, &skin); } else { - text_parts.join("") - }; - if !text.trim().is_empty() { - if is_assistant { - skin.print_text(&text); - } else { - println!("{}", text); - } - } - - for part in &message.parts { - match part { - ChatMessagePart::Tool { - tool_name, - is_error, - .. - } => { - let label = if *is_error { - format!("[tool failed: {tool_name}]") - } else { - format!("[used tool: {tool_name}]") - }; - println!("{}", label.dimmed().italic()); - } - ChatMessagePart::Attachment { name, .. } => { - println!("{}", format!("[attachment: {name}]").dimmed().italic()); + let mut text_buf = String::new(); + for part in &message.parts { + match part { + ChatMessagePart::Text { content } => { + text_buf.push_str(content); + } + ChatMessagePart::Tool { + tool_name, + is_error, + .. + } => { + render_text_block(&text_buf, is_assistant, &skin); + text_buf.clear(); + let label = if *is_error { + format!("[tool failed: {tool_name}]") + } else { + format!("[used tool: {tool_name}]") + }; + println!("{}", label.dimmed().italic()); + } + ChatMessagePart::Attachment { name, .. } => { + render_text_block(&text_buf, is_assistant, &skin); + text_buf.clear(); + println!("{}", format!("[attachment: {name}]").dimmed().italic()); + } } - ChatMessagePart::Text { .. } => {} } + render_text_block(&text_buf, is_assistant, &skin); } println!();