use std::collections::HashSet;
use super::{Doc, StepDetail, StepSummary, StepsView, ToolIo};
use crate::git_tree::Framing;
use crate::theme;
const SHORT_OID: usize = 7;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StepTab {
Meta,
Request,
Staging,
Response,
Tools,
}
pub fn render(
ui: &mut egui::Ui,
view: &StepsView,
selected: Option<usize>,
detail: Option<&StepDetail>,
tab: StepTab,
collapsed: &mut HashSet<String>,
) {
egui::ScrollArea::vertical().show(ui, |ui| {
if view.steps.is_empty() {
ui.label("(no steps yet)");
}
for (i, step) in view.steps.iter().enumerate() {
render_summary(ui, step, Some(i) == selected);
}
if let Some(detail) = detail {
ui.separator();
render_detail(ui, detail, tab, collapsed);
}
});
}
fn render_summary(ui: &mut egui::Ui, step: &StepSummary, selected: bool) {
ui.horizontal(|ui| {
let (glyph, color) = summary_badge(step);
ui.colored_label(color, glyph);
let marker = if selected { "▶" } else { " " };
ui.monospace(format!("{marker} {}", step.seq));
ui.label(format!("{} attempts", step.attempts));
ui.label(format!("{} tok", step.tokens.total_tokens()));
if step.no_response {
ui.colored_label(theme::ICHOR, super::NO_RESPONSE);
}
if let Some(commit) = &step.commit {
ui.monospace(short(commit));
}
if let Some(started) = &step.started_at {
ui.weak(started);
}
if let Some(ended) = &step.ended_at {
ui.weak(format!("→ {ended}"));
}
});
}
fn summary_badge(step: &StepSummary) -> (&'static str, egui::Color32) {
if step.no_response {
("✗", theme::ICHOR)
} else {
framing_badge(step.framing)
}
}
fn framing_badge(framing: Framing) -> (&'static str, egui::Color32) {
match framing {
Framing::Complete => ("✓", theme::HYDRA),
Framing::Failed => ("✗", theme::ICHOR),
Framing::Killed => ("■", theme::ASH),
}
}
fn short(oid: &str) -> String {
oid.chars().take(SHORT_OID).collect()
}
fn render_detail(
ui: &mut egui::Ui,
detail: &StepDetail,
tab: StepTab,
collapsed: &mut HashSet<String>,
) {
ui.horizontal(|ui| {
for (which, label) in [
(StepTab::Meta, "meta"),
(StepTab::Request, "request"),
(StepTab::Staging, "staging"),
(StepTab::Response, "response"),
(StepTab::Tools, "tools"),
] {
let text = egui::RichText::new(label);
ui.label(if which == tab { text.strong() } else { text });
}
});
match tab {
StepTab::Meta => render_doc(ui, &detail.meta, &format!("{}/meta", detail.seq), collapsed),
StepTab::Request => render_doc(
ui,
&detail.request,
&format!("{}/request", detail.seq),
collapsed,
),
StepTab::Staging => render_doc(
ui,
&detail.staging,
&format!("{}/staging", detail.seq),
collapsed,
),
StepTab::Response => render_response(ui, &detail.response, &detail.seq, collapsed),
StepTab::Tools => render_tools(ui, &detail.tools, &detail.seq, collapsed),
}
}
fn render_doc(ui: &mut egui::Ui, doc: &Doc, root: &str, collapsed: &mut HashSet<String>) {
match doc {
Doc::Json(value) => crate::jsonview::render(ui, value, root, collapsed),
Doc::Raw(bytes) if bytes.is_empty() => {
ui.weak("(absent)");
}
Doc::Raw(bytes) => {
ui.monospace(String::from_utf8_lossy(bytes));
}
}
}
fn render_response(ui: &mut egui::Ui, events: &[Doc], seq: &str, collapsed: &mut HashSet<String>) {
if events.is_empty() {
ui.label("(no events)");
}
for (i, event) in events.iter().enumerate() {
render_doc(ui, event, &format!("{seq}/resp/{i}"), collapsed);
}
}
fn render_tools(ui: &mut egui::Ui, tools: &[ToolIo], seq: &str, collapsed: &mut HashSet<String>) {
if tools.is_empty() {
ui.label("(no tool calls)");
}
for tool in tools {
ui.horizontal(|ui| {
let (glyph, color) = if tool.is_error {
("✗", theme::ICHOR)
} else {
("✓", theme::HYDRA)
};
ui.colored_label(color, glyph);
ui.monospace(&tool.tool_id);
});
ui.label("input");
render_doc(
ui,
&tool.input,
&format!("{seq}/{}/in", tool.tool_id),
collapsed,
);
ui.label("output");
render_doc(
ui,
&tool.output,
&format!("{seq}/{}/out", tool.tool_id),
collapsed,
);
}
}