use ratatui::text::Line;
use crate::subagent::RunStatus;
use super::super::{
feed_image::DEFAULT_IMAGE_HEIGHT,
render::{entry_lines, tool_entry_lines},
tool_output_ui::tool_output_toggleable,
Entry, ToolEntry,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum ToggleTarget {
Transcript(usize),
Pending(String),
}
pub(super) enum HistoryItem<'a> {
Transcript { index: usize, entry: &'a Entry },
Pending { key: &'a str, tool: &'a ToolEntry },
Ephemeral(Entry),
}
impl HistoryItem<'_> {
pub(super) fn paint_lines(
&self,
width: usize,
max_tool_output_lines: usize,
) -> Vec<Line<'static>> {
match self {
Self::Transcript { entry, .. } => {
entry_lines(entry, width, max_tool_output_lines, DEFAULT_IMAGE_HEIGHT)
}
Self::Ephemeral(entry) => {
entry_lines(entry, width, max_tool_output_lines, DEFAULT_IMAGE_HEIGHT)
}
Self::Pending { tool, .. } => {
tool_entry_lines(tool, width, max_tool_output_lines, DEFAULT_IMAGE_HEIGHT)
}
}
}
fn tool_entry(&self) -> Option<&ToolEntry> {
match self {
Self::Transcript {
entry: Entry::Tool(tool),
..
}
| Self::Ephemeral(Entry::Tool(tool)) => Some(tool),
Self::Pending { tool, .. } => Some(tool),
_ => None,
}
}
fn toggle_target(&self) -> Option<ToggleTarget> {
match self {
Self::Transcript {
index,
entry: Entry::Tool(_),
} => Some(ToggleTarget::Transcript(*index)),
Self::Pending { key, .. } => Some(ToggleTarget::Pending((*key).to_string())),
_ => None,
}
}
fn is_toggleable(&self, width: usize, max_tool_output_lines: usize) -> bool {
self.tool_entry()
.is_some_and(|tool| tool_output_toggleable(tool, max_tool_output_lines, width))
}
}
pub(super) fn status_fallback_items(
status: Option<&RunStatus>,
has_assistant: bool,
) -> Vec<HistoryItem<'static>> {
let mut items = Vec::new();
if !has_assistant {
let fallback = status.and_then(|status| {
status
.result
.as_deref()
.or(status.last_text.as_deref())
.filter(|text| !text.is_empty())
});
if let Some(text) = fallback {
items.push(HistoryItem::Ephemeral(Entry::Assistant(text.to_string())));
}
}
if let Some(error) = status.and_then(|status| status.error.as_deref()) {
items.push(HistoryItem::Ephemeral(Entry::Error(error.to_string())));
}
if let Some(error) = status.and_then(|status| status.attachment_error.as_deref()) {
items.push(HistoryItem::Ephemeral(Entry::Error(error.to_string())));
}
items
}
pub(super) fn tool_card_at_line<'a, I>(
items: I,
line: usize,
width: usize,
max_tool_output_lines: usize,
) -> Option<(ToggleTarget, std::ops::Range<usize>)>
where
I: IntoIterator<Item = HistoryItem<'a>>,
{
let mut start = 0usize;
for item in items {
let height = item.paint_lines(width, max_tool_output_lines).len();
if height == 0 {
continue;
}
let end = start.saturating_add(height);
if (start..end).contains(&line) {
return item
.is_toggleable(width, max_tool_output_lines)
.then(|| item.toggle_target())
.flatten()
.map(|target| (target, start..end));
}
start = end;
}
None
}
pub(super) fn latest_toggle_target<'a, I>(
items: I,
width: usize,
max_tool_output_lines: usize,
) -> Option<ToggleTarget>
where
I: IntoIterator<Item = HistoryItem<'a>>,
{
let mut last_pending = None;
let mut last_toggleable = None;
for item in items {
if matches!(item, HistoryItem::Pending { .. }) {
last_pending = Some(item);
} else if item.is_toggleable(width, max_tool_output_lines) {
last_toggleable = Some(item);
}
}
let candidate = match last_pending {
Some(pending) => pending,
None => last_toggleable?,
};
candidate
.is_toggleable(width, max_tool_output_lines)
.then(|| candidate.toggle_target())
.flatten()
}
#[cfg(test)]
#[path = "tool_toggle_tests.rs"]
mod tests;