Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src-tauri/src/acp/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8439,7 +8439,12 @@ fn normalize_grok_image_blocks(blocks: Vec<PromptInputBlock>) -> Vec<PromptInput
.collect()
}

fn map_prompt_blocks(blocks: Vec<PromptInputBlock>) -> Vec<ContentBlock> {
/// `pub(crate)` for one reader beyond this module: the ACP-native history
/// parser's parity test, which needs the EXACT wire bytes `record_prompt`
/// writes in order to assert its projection equals the live one
/// ([`crate::acp::types::user_blocks_from_prompt`]). Rebuilding those bytes by
/// hand in the test would let the two drift without failing anything.
pub(crate) fn map_prompt_blocks(blocks: Vec<PromptInputBlock>) -> Vec<ContentBlock> {
blocks
.into_iter()
.map(|block| match block {
Expand Down
63 changes: 55 additions & 8 deletions src-tauri/src/acp/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,10 +250,15 @@ fn user_prompt_text_preview(blocks: &[PromptInputBlock]) -> Option<String> {
/// delegating prompt's text blocks (the sub-agent's task). Uses the parser's own
/// `title_from_user_text` (folds reference links, caps at 100 chars) so the value
/// matches what `refresh_auto_title` would later compute from that same first
/// turn — the conditional UPDATE then sees no change and doesn't churn. Returns
/// `None` for a textless prompt, leaving the title unset to be backfilled on
/// first detail load as before. Kept unlocked by the caller so an AI-generated
/// title can still replace it later.
/// turn — the conditional UPDATE then sees no change and doesn't churn. Kept
/// unlocked by the caller so an AI-generated title can still replace it later.
///
/// A prompt with no prose at all — one dropped-in file and nothing else — is
/// named after what it carries instead. That row would otherwise read
/// "Untitled" forever: for an agent with no store parser, ACP has no title
/// channel and the history parse honestly reports no title. This value only
/// ever reaches `seed_auto_title_if_empty` / a fresh row's `title`, never
/// `refresh_auto_title`, so it cannot displace a name the agent published.
fn delegation_child_title_seed(blocks: &[PromptInputBlock]) -> Option<String> {
let joined = blocks
.iter()
Expand All @@ -267,11 +272,11 @@ fn delegation_child_title_seed(blocks: &[PromptInputBlock]) -> Option<String> {
.collect::<Vec<_>>()
.join(" ");
let trimmed = joined.trim();
if trimmed.is_empty() {
None
} else {
Some(crate::parsers::title_from_user_text(trimmed))
if !trimmed.is_empty() {
return Some(crate::parsers::title_from_user_text(trimmed));
}
crate::acp::types::attachment_names_from_prompt(blocks)
.map(|names| crate::parsers::title_from_user_text(&names))
}

/// Composite key identifying a logical agent session for spawn-time dedup.
Expand Down Expand Up @@ -6082,6 +6087,48 @@ mod tests {
assert!(delegation_child_title_seed(&img).is_none());
}

/// A message that is one dropped-in file and no prose. The row would read
/// "Untitled" forever otherwise — for an agent with no store parser, ACP
/// has no title channel and the history parse honestly reports none. The
/// value is a SEED: it reaches `seed_auto_title_if_empty` / a fresh row's
/// `title`, never `refresh_auto_title`, so a name the agent publishes over
/// `session_info_update` still wins.
#[test]
fn delegation_child_title_seed_names_an_attachment_only_prompt() {
let blocks = vec![
PromptInputBlock::ResourceLink {
uri: "file:///tmp/report.pdf".into(),
name: "report.pdf".into(),
mime_type: None,
description: None,
},
PromptInputBlock::Resource {
uri: "clipboard://my%20notes.txt-9f2".into(),
mime_type: Some("text/plain".into()),
text: Some("body".into()),
blob: None,
},
];
assert_eq!(
delegation_child_title_seed(&blocks).as_deref(),
Some("report.pdf, my notes.txt-9f2")
);

// Prose still wins outright, attachments and all — and matches what
// `refresh_auto_title` will later compute, so the row doesn't churn.
let mut with_prose = blocks.clone();
with_prose.insert(
0,
PromptInputBlock::Text {
text: "Review these".into(),
},
);
assert_eq!(
delegation_child_title_seed(&with_prose),
Some(crate::parsers::title_from_user_text("Review these"))
);
}

#[test]
fn delegation_child_title_seed_caps_long_task_text() {
// Mirrors the parser cap (100 chars) so an over-long task doesn't store a
Expand Down
Loading
Loading