mirror of
https://github.com/aaif-goose/goose.git
synced 2026-07-03 14:10:03 +02:00
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 (<persona-instructions>, <user-message>, etc.) because the goose binary stores the full wrapped prompt in its session history. Add extract_user_message() to parse out just the <user-message> content before emitting it to the frontend during session replay. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: move dispatcher tests to separate file to stay under line limit Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style: apply cargo fmt and trim to file size limit Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: remove blank line to satisfy file size check Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: anchor XML close tag to end of string to avoid truncating user input Addresses review feedback: if a user's message literally contains "</user-message>", 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) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,17 @@ use crate::services::acp::payloads::{
|
||||
ToolResultPayload, ToolTitlePayload,
|
||||
};
|
||||
|
||||
fn extract_user_message(raw: &str) -> &str {
|
||||
const OPEN: &str = "<user-message>\n";
|
||||
const CLOSE: &str = "\n</user-message>";
|
||||
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<ModelOption> {
|
||||
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;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn extract_user_message_strips_xml_wrapper() {
|
||||
let wrapped = "<persona-instructions>\nYou are a helpful assistant.\n</persona-instructions>\n\n<user-message>\nhello\n</user-message>";
|
||||
assert_eq!(extract_user_message(wrapped), "hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_user_message_multiline() {
|
||||
let wrapped = "<persona-instructions>\nstuff\n</persona-instructions>\n\n<user-message>\nline one\nline two\n</user-message>";
|
||||
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 "</user-message>" in their message — must not truncate.
|
||||
let wrapped = "<persona-instructions>\nstuff\n</persona-instructions>\n\n<user-message>\ncheck this tag: </user-message> cool right?\n</user-message>";
|
||||
assert_eq!(
|
||||
extract_user_message(wrapped),
|
||||
"check this tag: </user-message> cool right?"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user