#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Segment {
Reasoning(String),
Text(String),
Blocked(String),
}
use super::ledger_scan::find_at_line_start;
const OPEN: &str = "<!-- reasoning -->";
const CLOSE: &str = "<!-- /reasoning -->";
const BLOCKED_OPEN: &str = "<!-- phantom_blocked=1 -->";
const BLOCKED_CLOSE: &str = "<!-- /phantom_blocked=1 -->";
pub(crate) const BLOCKED_LABEL: &str = "Blocked narration (phantom tool calls, never delivered):";
pub(crate) fn split_segments(content: &str) -> Vec<Segment> {
let mut out = Vec::new();
let mut rest = content;
while let Some(start) = find_at_line_start(rest, BLOCKED_OPEN, 0) {
push_reasoning_segments(&mut out, &rest[..start]);
let after_open = &rest[start + BLOCKED_OPEN.len()..];
let (inner, after) = match find_at_line_start(after_open, BLOCKED_CLOSE, 0) {
Some(end) => (&after_open[..end], &after_open[end + BLOCKED_CLOSE.len()..]),
None => (after_open, ""),
};
let folded = fold_blocked(inner);
if !folded.is_empty() {
out.push(Segment::Blocked(folded));
}
rest = after;
}
push_reasoning_segments(&mut out, rest);
out
}
fn fold_blocked(inner: &str) -> String {
let mut parts = Vec::new();
push_reasoning_segments(&mut parts, inner);
parts
.into_iter()
.map(|s| match s {
Segment::Reasoning(r) | Segment::Text(r) | Segment::Blocked(r) => r,
})
.collect::<Vec<_>>()
.join("\n\n")
}
fn push_reasoning_segments(out: &mut Vec<Segment>, content: &str) {
let mut rest = content;
while let Some(start) = find_at_line_start(rest, OPEN, 0) {
push_text(out, &rest[..start]);
let after_open = &rest[start + OPEN.len()..];
match find_at_line_start(after_open, CLOSE, 0) {
Some(end) => {
let inner = after_open[..end].trim();
if !inner.is_empty() {
out.push(Segment::Reasoning(inner.to_string()));
}
rest = &after_open[end + CLOSE.len()..];
}
None => {
let inner = after_open.trim();
if !inner.is_empty() {
out.push(Segment::Reasoning(inner.to_string()));
}
rest = "";
break;
}
}
}
push_text(out, rest);
}
fn push_text(out: &mut Vec<Segment>, text: &str) {
let kept: Vec<&str> = text
.lines()
.filter(|line| {
let t = line.trim();
!(t == OPEN || t == CLOSE || t == BLOCKED_OPEN || t == BLOCKED_CLOSE)
})
.collect();
let joined = kept.join("\n");
let trimmed = joined.trim();
if !trimmed.is_empty() {
out.push(Segment::Text(trimmed.to_string()));
}
}
pub(crate) fn is_intermediate(segments: &[Segment], i: usize) -> bool {
segments
.iter()
.rposition(|s| matches!(s, Segment::Reasoning(_)))
.is_some_and(|last| i < last)
}