use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use super::theme::*;
use super::types::ContextTreeState;
use crate::commands::dashboard::helpers::format_tokens;
use crate::runstate::ContextSnapshot;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum TreeRow {
RegionHeader { region: String },
EntryStub { region: String, index: usize },
}
pub(super) fn rows(
snap: &ContextSnapshot,
tree: &ContextTreeState,
searching: bool,
) -> Vec<TreeRow> {
let mut rows = Vec::new();
for region in &snap.regions {
rows.push(TreeRow::RegionHeader {
region: region.name.clone(),
});
if searching || !tree.collapsed_regions.contains(®ion.name) {
for index in 0..region.entries.len() {
rows.push(TreeRow::EntryStub {
region: region.name.clone(),
index,
});
}
}
}
rows
}
pub(super) fn entry_expanded(
tree: &ContextTreeState,
searching: bool,
region: &str,
index: usize,
) -> bool {
searching || tree.expanded_entries.contains(&(region.to_string(), index))
}
pub(super) struct FlatTree {
pub(super) lines: Vec<Line<'static>>,
pub(super) cursor_lines: Vec<usize>,
}
pub(super) fn flatten(
snap: &ContextSnapshot,
tree: &ContextTreeState,
cursor: usize,
searching: bool,
render_width: u16,
) -> FlatTree {
let mut lines: Vec<Line<'static>> = Vec::new();
let mut cursor_lines: Vec<usize> = Vec::new();
let mut row_idx = 0usize;
for region in &snap.regions {
let folded = !searching && tree.collapsed_regions.contains(®ion.name);
let on_cursor = cursor == row_idx;
cursor_lines.push(lines.len());
row_idx += 1;
let pct = (region.current_tokens * 100)
.checked_div(region.max_tokens)
.unwrap_or(0)
.min(100);
let bar_w = 16usize;
let filled = bar_w * pct / 100;
let bar = format!("{}{}", "█".repeat(filled), "░".repeat(bar_w - filled));
let bar_color = if pct >= 90 {
C_ERROR
} else if pct >= 70 {
C_WARN
} else if pct > 0 {
C_SUCCESS
} else {
C_DIM
};
let kind_color = match region.kind.as_str() {
"pinned" => C_ACCENT,
"sliding" => C_SUCCESS,
"compacting" | "history" => C_WARN,
"temporary" | "clearable" => C_MUTED,
"custom" => C_SCRIPT,
_ => C_DIM,
};
let name_style = if on_cursor {
Style::default()
.fg(C_WHITE)
.add_modifier(Modifier::BOLD | Modifier::REVERSED)
} else {
Style::default().fg(C_WHITE).add_modifier(Modifier::BOLD)
};
lines.push(Line::from(vec![
Span::styled(
if folded { "▸ " } else { "▾ " },
Style::default().fg(C_ACCENT).add_modifier(Modifier::BOLD),
),
Span::styled(format!("{:<16}", region.name), name_style),
Span::styled(
format!("{:<12}", region.kind),
Style::default().fg(kind_color),
),
Span::styled(bar, Style::default().fg(bar_color)),
Span::styled(
format!(
" {}/{}",
format_tokens(region.current_tokens),
format_tokens(region.max_tokens)
),
Style::default().fg(C_DIM),
),
]));
if folded {
if !region.entries.is_empty() {
lines.push(Line::from(Span::styled(
format!(" ({} entries folded)", region.entries.len()),
Style::default().fg(C_DIM),
)));
}
continue;
}
if region.entries.is_empty() {
lines.push(Line::from(Span::styled(
" (empty)",
Style::default().fg(C_DIM),
)));
continue;
}
for (index, entry) in region.entries.iter().enumerate() {
let expanded = entry_expanded(tree, searching, ®ion.name, index);
let on_cursor = cursor == row_idx;
cursor_lines.push(lines.len());
row_idx += 1;
let preview: String = entry
.content
.lines()
.find(|l| !l.trim().is_empty())
.unwrap_or("")
.chars()
.take(60)
.collect();
let stub_style = if on_cursor {
Style::default()
.fg(C_WHITE)
.add_modifier(Modifier::REVERSED)
} else {
Style::default().fg(C_MUTED)
};
let mut spans = vec![
Span::styled(
if expanded { " ▾ " } else { " ▸ " },
Style::default().fg(C_ACCENT),
),
Span::styled(
format!("entry {} · {} tokens", index + 1, entry.tokens),
stub_style,
),
];
if !expanded && !preview.is_empty() {
spans.push(Span::styled(
format!(" {preview}"),
Style::default().fg(C_DIM),
));
}
lines.push(Line::from(spans));
if expanded {
let rendered =
crate::render::markdown_to_text(&entry.content, render_width.saturating_sub(4));
for mut l in rendered.lines {
l.spans.insert(0, Span::raw(" "));
lines.push(l);
}
}
}
lines.push(Line::from(""));
}
FlatTree {
lines,
cursor_lines,
}
}
pub(super) fn nearest_region_row(rows: &[TreeRow], cursor: usize, forward: bool) -> Option<usize> {
if forward {
rows.iter()
.enumerate()
.skip(cursor + 1)
.find(|(_, r)| matches!(r, TreeRow::RegionHeader { .. }))
.map(|(i, _)| i)
} else {
rows.iter()
.enumerate()
.take(cursor)
.rev()
.find(|(_, r)| matches!(r, TreeRow::RegionHeader { .. }))
.map(|(i, _)| i)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::runstate::{ContextSnapshot, RegionSnapshot};
use leviath_core::run_meta::RegionEntrySnapshot;
fn entry(content: &str) -> RegionEntrySnapshot {
RegionEntrySnapshot {
content: content.to_string(),
tokens: 5,
kind: Default::default(),
metadata: None,
key: None,
taint: Default::default(),
}
}
fn snap() -> ContextSnapshot {
ContextSnapshot {
stage_name: "main".to_string(),
total_tokens: 30,
max_tokens: 100,
regions: vec![
RegionSnapshot {
name: "system".to_string(),
kind: "pinned".to_string(),
current_tokens: 10,
max_tokens: 50,
entries: vec![entry("first entry line\nmore"), entry("second")],
},
RegionSnapshot {
name: "conversation".to_string(),
kind: "sliding".to_string(),
current_tokens: 20,
max_tokens: 50,
entries: vec![],
},
],
}
}
#[test]
fn rows_list_headers_and_stubs_and_fold_hides_stubs() {
let s = snap();
let mut tree = ContextTreeState::default();
let all = rows(&s, &tree, false);
assert_eq!(
all.len(),
4,
"two headers + two stubs (the empty region has none)"
);
assert!(matches!(&all[0], TreeRow::RegionHeader { region } if region == "system"));
assert!(matches!(&all[1], TreeRow::EntryStub { region, index: 0 } if region == "system"));
tree.collapsed_regions.insert("system".to_string());
let folded = rows(&s, &tree, false);
assert_eq!(folded.len(), 2, "the folded region's stubs are gone");
assert_eq!(rows(&s, &tree, true).len(), 4);
}
#[test]
fn flatten_marks_folds_previews_and_expansions() {
let s = snap();
let mut tree = ContextTreeState::default();
let text_of = |flat: &FlatTree| -> String {
flat.lines
.iter()
.map(|l| {
l.spans
.iter()
.map(|sp| sp.content.as_ref())
.collect::<String>()
})
.collect::<Vec<_>>()
.join("\n")
};
let flat = flatten(&s, &tree, 0, false, 80);
let text = text_of(&flat);
assert!(text.contains("entry 1 · 5 tokens"));
assert!(text.contains("first entry line"), "preview shown");
assert!(!text.contains("more"), "full content not rendered");
assert!(text.contains("(empty)"));
assert_eq!(flat.cursor_lines.len(), 4);
tree.expanded_entries.insert(("system".to_string(), 0));
let text = text_of(&flatten(&s, &tree, 1, false, 80));
assert!(text.contains("more"), "expanded entry content rendered");
tree.collapsed_regions.insert("system".to_string());
let text = text_of(&flatten(&s, &tree, 0, false, 80));
assert!(text.contains("(2 entries folded)"));
assert!(!text.contains("entry 1"), "stubs folded away");
let text = text_of(&flatten(&s, &tree, 0, true, 80));
assert!(text.contains("more"), "search sees inside entries");
tree.collapsed_regions.insert("conversation".to_string());
let text = text_of(&flatten(&s, &tree, 0, false, 80));
assert!(!text.contains("(empty)"), "folded empty region: {text}");
}
#[test]
fn region_jumps_find_the_neighboring_headers() {
let s = snap();
let tree = ContextTreeState::default();
let all = rows(&s, &tree, false);
assert_eq!(all.len(), 4);
assert_eq!(nearest_region_row(&all, 0, true), Some(3));
assert_eq!(nearest_region_row(&all, 3, false), Some(0));
assert_eq!(nearest_region_row(&all, 1, false), Some(0));
assert_eq!(nearest_region_row(&all, 0, false), None);
assert_eq!(nearest_region_row(&all, 3, true), None);
}
}