use ratatui::prelude::*;
use super::super::super::style::ratatui_style_from_inline;
use super::super::super::types::InlineMessageKind;
use super::super::Session;
use super::super::message::TranscriptLine;
use super::super::render;
use super::super::text_utils;
use super::transcript_line_with_detected_links;
fn trim_leading_whitespace(line: &mut Line<'static>) {
while let Some(first) = line.spans.first_mut() {
let trimmed = first.content.trim_start();
let trimmed_len = trimmed.len();
let original_len = first.content.len();
if trimmed_len == 0 {
line.spans.remove(0);
} else if trimmed_len < original_len {
let content = first.content.to_mut();
content.drain(0..original_len - trimmed_len);
break;
} else {
break;
}
}
}
impl Session {
pub(super) fn reflow_thinking_lines(
&self,
index: usize,
width: u16,
) -> Option<Vec<TranscriptLine>> {
if index >= self.lines.len() {
return None;
}
if self.lines[index].kind != InlineMessageKind::Policy {
return None;
}
let run_start = self.thinking_run_start(index);
if run_start != index {
return Some(Vec::new());
}
Some(self.render_thinking_block(run_start, width))
}
pub(crate) fn thinking_run_len(&self, start: usize) -> usize {
let mut len = 0;
for line in self.lines.iter().skip(start) {
if line.kind == InlineMessageKind::Policy {
len += 1;
} else {
break;
}
}
len
}
fn thinking_run_start(&self, index: usize) -> usize {
let mut start = index;
while start > 0 {
let Some(prev) = self.lines.get(start - 1) else {
break;
};
if prev.kind != InlineMessageKind::Policy {
break;
}
start -= 1;
}
start
}
pub(crate) fn thinking_collapsed(&self, start: usize) -> bool {
self.thinking_runs
.is_collapsed(start, self.appearance.thinking_collapsed_by_default())
}
fn render_thinking_block(&self, start: usize, width: u16) -> Vec<TranscriptLine> {
let collapsed = self.thinking_collapsed(start);
let run_len = self.thinking_run_len(start);
let accent =
ratatui_style_from_inline(&self.styles.accent_inline_style(), self.theme.foreground);
let header = Line::from(Span::styled("Thinking", accent));
let mut result = vec![transcript_line_with_detected_links(
header,
self.workspace_root.as_deref(),
)];
if !collapsed {
let content_width = match width {
0 => None,
w => Some(w as usize),
};
let mut combined_spans: Vec<Span<'static>> = Vec::new();
for idx in start..start + run_len {
if self.lines[idx].segments.is_empty() {
continue;
}
let spans = render::render_message_spans(self, idx);
if !combined_spans.is_empty()
&& !spans.is_empty()
&& !combined_spans
.last()
.expect("combined_spans is not empty")
.content
.ends_with(char::is_whitespace)
{
combined_spans.push(Span::raw(" ".to_owned()));
}
combined_spans.extend(spans);
}
if combined_spans.is_empty() {
combined_spans.push(Span::raw(String::new()));
}
let mut wrapped = match content_width {
None => vec![Line::from(combined_spans)],
Some(cw) => text_utils::wrap_line(Line::from(combined_spans), cw),
};
for line in &mut wrapped {
trim_leading_whitespace(line);
}
for line in wrapped {
result.push(transcript_line_with_detected_links(
line,
self.workspace_root.as_deref(),
));
}
}
let run_end = start + run_len;
let next_kind = self.lines.get(run_end).map(|line| line.kind);
if collapsed && next_kind == Some(InlineMessageKind::Agent) {
result.push(TranscriptLine::default());
} else if next_kind != Some(InlineMessageKind::Agent) {
let spacing = self.appearance.message_block_spacing.min(2) as usize;
for _ in 0..spacing {
result.push(TranscriptLine::default());
}
}
result
}
}