use super::fence::strip_optional_json_fence_label;
use super::parse;
use super::types::AgentReplyPlanV1;
pub fn prose_before_first_fence(content: &str) -> String {
if !content.contains("```") {
return String::new();
}
content.split("```").next().unwrap_or("").trim().to_string()
}
pub fn format_plan_steps_markdown(plan: &AgentReplyPlanV1) -> String {
use std::fmt::Write;
let mut out = String::new();
let mut n = 1usize;
for st in &plan.steps {
let desc = crate::cm_agent::text_sanitize::naturalize_plan_step_description(&st.description);
let id = st.id.trim();
if id.is_empty() {
continue;
}
let id_esc = id.replace('`', "'");
let role = st
.executor_kind
.map(|k| format!(" [`executor_kind={}`]", k.as_snake_case_str()))
.unwrap_or_default();
let _ = writeln!(&mut out, "{}. `{}`: {}{}", n, id_esc, desc.trim(), role);
n += 1;
}
out.trim_end().to_string()
}
pub fn augment_agent_reply_plan_goal_for_display(goal: &str, plan: &AgentReplyPlanV1) -> String {
let goal = goal.trim();
let lead = breakdown_lead_line_from_first_step_description(plan);
let Some(lead) = lead else {
return goal.to_string();
};
if goal.contains("以下") && goal.contains("拆解") {
return goal.to_string();
}
if goal.is_empty() {
return lead;
}
if goal.contains(&lead) {
return goal.to_string();
}
format!("{lead}\n\n{goal}")
}
fn breakdown_lead_line_from_first_step_description(plan: &AgentReplyPlanV1) -> Option<String> {
let st = plan.steps.first()?;
let desc = crate::cm_agent::text_sanitize::naturalize_plan_step_description(&st.description);
let desc = desc.trim();
if desc.is_empty() {
return None;
}
for line in desc.lines() {
let t = line.trim();
if t.is_empty() {
continue;
}
if let Some(frag) = extract_task_breakdown_lead_fragment(t) {
return Some(frag);
}
}
None
}
fn extract_task_breakdown_lead_fragment(line: &str) -> Option<String> {
let t = line.trim();
if t.is_empty() {
return None;
}
let candidate = t.contains("以下是任务拆解")
|| (t.contains("以下") && t.contains("拆解") && t.chars().count() <= 72);
if !candidate {
return None;
}
let start = t.find("以下")?;
let tail = t[start..].trim_start();
let clipped = clip_task_breakdown_lead_at_punct(tail);
let clipped = clipped.trim();
if clipped.chars().count() < 4 {
return None;
}
Some(cap_display_fragment(clipped, 80))
}
fn clip_task_breakdown_lead_at_punct(s: &str) -> &str {
for (i, c) in s.char_indices() {
if matches!(c, '。' | '!' | '?') {
return &s[..i + c.len_utf8()];
}
}
for (i, c) in s.char_indices() {
if c == ':' {
return &s[..i + c.len_utf8()];
}
}
s
}
fn cap_display_fragment(s: &str, max_chars: usize) -> String {
if s.chars().count() <= max_chars {
s.to_string()
} else {
let mut out: String = s.chars().take(max_chars).collect();
out.push('…');
out
}
}
pub fn format_agent_reply_plan_for_display(content: &str) -> Option<String> {
let plan = parse::parse_agent_reply_plan_v1(content).ok()?;
let raw_goal = prose_before_first_fence(content);
let goal = crate::cm_agent::text_sanitize::naturalize_assistant_plan_prose_tail(&raw_goal);
let goal_t = augment_agent_reply_plan_goal_for_display(goal.trim(), &plan);
let steps = format_plan_steps_markdown(&plan);
let mut out = String::new();
if !goal_t.is_empty() {
out.push_str(&goal_t);
out.push_str("\n\n");
}
out.push_str(&steps);
Some(out.trim_end().to_string())
}
fn fence_inner_should_hide_agent_reply_plan_json(inner: &str) -> bool {
let raw = inner.trim();
let body = strip_optional_json_fence_label(raw);
if !body.starts_with('{') {
return false;
}
if parse::parse_agent_reply_plan_v1(&body).is_ok() {
return true;
}
let b = body.trim();
if !b.contains("\"agent_reply_plan\"") || !b.contains("\"steps\"") {
return false;
}
serde_json::from_str::<serde_json::Value>(b).is_ok()
}
pub fn strip_agent_reply_plan_fence_blocks_for_display(content: &str) -> String {
let parts: Vec<&str> = content.split("```").collect();
let unclosed_trailing_fence = parts.len().is_multiple_of(2);
let mut out = String::new();
let mut i = 0usize;
while i < parts.len() {
out.push_str(parts[i]);
i += 1;
if i >= parts.len() {
break;
}
let inner = parts[i];
i += 1;
if fence_inner_should_hide_agent_reply_plan_json(inner) {
continue;
}
if unclosed_trailing_fence && i >= parts.len() && inner.trim().is_empty() {
break;
}
out.push_str("```");
out.push_str(inner);
out.push_str("```");
}
out
}