From 86cd8ab975d6b477566830b67a7244e6a8a60e95 Mon Sep 17 00:00:00 2001 From: Taylor Ho Date: Fri, 10 Apr 2026 11:10:29 -1000 Subject: [PATCH] fix: strip XML wrapper from replayed user messages (#150) * fix: strip XML wrapper from replayed user messages When loading a previous session, user messages were displayed with raw XML tags (, , etc.) because the goose binary stores the full wrapped prompt in its session history. Add extract_user_message() to parse out just the content before emitting it to the frontend during session replay. Co-Authored-By: Claude Opus 4.6 (1M context) * refactor: move dispatcher tests to separate file to stay under line limit Co-Authored-By: Claude Opus 4.6 (1M context) * style: apply cargo fmt and trim to file size limit Co-Authored-By: Claude Opus 4.6 (1M context) * fix: remove blank line to satisfy file size check Co-Authored-By: Claude Opus 4.6 (1M context) * fix: anchor XML close tag to end of string to avoid truncating user input Addresses review feedback: if a user's message literally contains "", the previous find()-based approach would truncate. Now we require the close tag to be at the end of the string, matching the backend's wrapping format exactly. Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- .../src/services/acp/manager/dispatcher.rs | 17 ++++++++++- .../services/acp/manager/dispatcher_tests.rs | 28 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 ui/goose2/src-tauri/src/services/acp/manager/dispatcher_tests.rs diff --git a/ui/goose2/src-tauri/src/services/acp/manager/dispatcher.rs b/ui/goose2/src-tauri/src/services/acp/manager/dispatcher.rs index db0bd9d1cf..0e6155f184 100644 --- a/ui/goose2/src-tauri/src/services/acp/manager/dispatcher.rs +++ b/ui/goose2/src-tauri/src/services/acp/manager/dispatcher.rs @@ -19,6 +19,17 @@ use crate::services::acp::payloads::{ ToolResultPayload, ToolTitlePayload, }; +fn extract_user_message(raw: &str) -> &str { + const OPEN: &str = "\n"; + const CLOSE: &str = "\n"; + if let Some(start) = raw.find(OPEN) { + let inner = start + OPEN.len(); + if raw[inner..].ends_with(CLOSE) { + return &raw[inner..raw.len() - CLOSE.len()]; + } + } + raw +} fn model_options_from_select_options(options: &SessionConfigSelectOptions) -> Vec { match options { SessionConfigSelectOptions::Ungrouped(values) => values @@ -386,13 +397,14 @@ impl Client for SessionEventDispatcher { } } + let display_text = extract_user_message(&text.text); let message_id = uuid::Uuid::new_v4().to_string(); let _ = self.app_handle.emit( "acp:replay_user_message", serde_json::json!({ "sessionId": local_session_id, "messageId": message_id, - "text": text.text, + "text": display_text, }), ); } @@ -502,3 +514,6 @@ impl Client for SessionEventDispatcher { Ok(()) } } +#[cfg(test)] +#[path = "dispatcher_tests.rs"] +mod tests; diff --git a/ui/goose2/src-tauri/src/services/acp/manager/dispatcher_tests.rs b/ui/goose2/src-tauri/src/services/acp/manager/dispatcher_tests.rs new file mode 100644 index 0000000000..7b684780d0 --- /dev/null +++ b/ui/goose2/src-tauri/src/services/acp/manager/dispatcher_tests.rs @@ -0,0 +1,28 @@ +use super::*; + +#[test] +fn extract_user_message_strips_xml_wrapper() { + let wrapped = "\nYou are a helpful assistant.\n\n\n\nhello\n"; + assert_eq!(extract_user_message(wrapped), "hello"); +} + +#[test] +fn extract_user_message_multiline() { + let wrapped = "\nstuff\n\n\n\nline one\nline two\n"; + assert_eq!(extract_user_message(wrapped), "line one\nline two"); +} + +#[test] +fn extract_user_message_no_wrapper() { + assert_eq!(extract_user_message("plain text"), "plain text"); +} + +#[test] +fn extract_user_message_preserves_inner_delimiter() { + // User literally typed "" in their message — must not truncate. + let wrapped = "\nstuff\n\n\n\ncheck this tag: cool right?\n"; + assert_eq!( + extract_user_message(wrapped), + "check this tag: cool right?" + ); +}