use eframe::egui::{self, Color32};
use super::action_log::{ActionLog, Kind};
use super::remote;
#[derive(Clone)]
pub struct Server {
pub endpoint: String,
pub token: String,
}
fn local_hint(ui: &mut egui::Ui, what: &str) {
ui.add_space(20.0);
ui.label(format!(
"{what} is server-backed β launch the viz against a running nornir-server \
(NORNIR_SERVER=β¦) to use it."
));
}
#[derive(Default)]
pub struct SearchState {
query: String,
corpus: String,
sym_repo: String,
sym_query: String,
vec_repo: String,
vec_query: String,
call_repo: String,
call_name: String,
call_to: String,
hits: Option<Result<Vec<remote::Hit>, String>>,
syms: Option<Result<Vec<remote::KnownSym>, String>>,
vhits: Option<Result<Vec<remote::VecHit>, String>>,
stats: Option<Result<(u64, Vec<(String, String)>), String>>,
calls: Option<Result<(String, Vec<String>), String>>,
}
impl SearchState {
pub fn draw(&mut self, ui: &mut egui::Ui, srv: Option<&Server>, workspace: &str, log: &ActionLog) {
let Some(srv) = srv else { return local_hint(ui, "Search") };
ui.horizontal(|ui| {
ui.heading("π Search");
if ui.button("index stats").on_hover_text("Index.Stats β corpus doc counts").clicked() {
log.push(Kind::Rpc, "Index.Stats");
self.stats = Some(remote::index_stats(&srv.endpoint, &srv.token, workspace).map_err(|e| format!("{e:#}")));
}
if let Some(Ok((total, by))) = &self.stats {
ui.label(format!("index: {total} docs [{}]",
by.iter().map(|(k, v)| format!("{k}={v}")).collect::<Vec<_>>().join(", ")));
} else if let Some(Err(e)) = &self.stats {
ui.colored_label(Color32::RED, e);
}
});
ui.group(|ui| {
ui.label("Full-text (BM25 over docs/code/bench/changelog/config)");
ui.horizontal(|ui| {
let go = ui.text_edit_singleline(&mut self.query).lost_focus()
&& ui.input(|i| i.key_pressed(egui::Key::Enter));
ui.label("corpus:");
egui::ComboBox::from_id_salt("search_corpus")
.selected_text(if self.corpus.is_empty() { "all".into() } else { self.corpus.clone() })
.show_ui(ui, |ui| {
for c in ["", "docs", "code", "bench_history", "changelog", "config"] {
ui.selectable_value(&mut self.corpus, c.to_string(), if c.is_empty() { "all" } else { c });
}
});
if (ui.button("Search").clicked() || go) && !self.query.trim().is_empty() {
log.push(Kind::Query, format!("Search.Query q={:?} corpus={:?}", self.query, self.corpus));
self.hits = Some(
remote::search(&srv.endpoint, &srv.token, &self.query, &self.corpus, "", 25, workspace)
.map_err(|e| format!("{e:#}")),
);
}
});
});
match &self.hits {
Some(Ok(hits)) => {
ui.label(format!("{} hit(s)", hits.len()));
egui::ScrollArea::vertical().max_height(220.0).auto_shrink([false, false]).id_salt("hits").show(ui, |ui| {
for h in hits {
ui.horizontal(|ui| {
ui.colored_label(Color32::from_gray(150), format!("[{}/{}] {:.2}", h.corpus, h.repo, h.score));
ui.strong(if h.title.is_empty() { &h.path } else { &h.title });
});
if !h.snippet.is_empty() {
ui.label(egui::RichText::new(&h.snippet).size(11.0).color(Color32::from_gray(180)));
}
ui.separator();
}
});
}
Some(Err(e)) => { ui.colored_label(Color32::RED, e); }
None => {}
}
ui.add_space(8.0);
ui.group(|ui| {
ui.label("Symbol lookup (item-name substring over symbol_facts)");
ui.horizontal(|ui| {
ui.label("repo:");
ui.add(egui::TextEdit::singleline(&mut self.sym_repo).desired_width(120.0).hint_text("blank=all"));
let go = ui.text_edit_singleline(&mut self.sym_query).lost_focus()
&& ui.input(|i| i.key_pressed(egui::Key::Enter));
if (ui.button("Lookup").clicked() || go) && !self.sym_query.trim().is_empty() {
log.push(Kind::Query, format!("Knowledge.SymbolLookup q={:?} repo={:?}", self.sym_query, self.sym_repo));
self.syms = Some(
remote::knowledge_lookup(&srv.endpoint, &srv.token, &self.sym_repo, &self.sym_query, 50, workspace)
.map_err(|e| format!("{e:#}")),
);
}
});
});
match &self.syms {
Some(Ok(syms)) => {
ui.label(format!("{} symbol(s)", syms.len()));
egui::ScrollArea::vertical().max_height(220.0).auto_shrink([false, false]).id_salt("syms").show(ui, |ui| {
for s in syms {
ui.horizontal(|ui| {
ui.colored_label(Color32::from_gray(150), format!("{} {}", s.visibility, s.item_kind));
ui.strong(&s.item_name);
ui.label(egui::RichText::new(format!("{}:{}", s.file, s.line)).monospace().size(11.0));
});
if !s.signature.is_empty() {
ui.label(egui::RichText::new(&s.signature).monospace().size(11.0).color(Color32::from_gray(170)));
}
ui.separator();
}
});
}
Some(Err(e)) => { ui.colored_label(Color32::RED, e); }
None => {}
}
ui.add_space(8.0);
ui.group(|ui| {
ui.label("Semantic (vector embeddings β meaning, not keywords)");
ui.horizontal(|ui| {
ui.label("repo:");
ui.add(egui::TextEdit::singleline(&mut self.vec_repo).desired_width(120.0).hint_text("repo name"));
let go = ui.text_edit_singleline(&mut self.vec_query).lost_focus()
&& ui.input(|i| i.key_pressed(egui::Key::Enter));
if (ui.button("Semantic search").clicked() || go)
&& !self.vec_query.trim().is_empty()
&& !self.vec_repo.trim().is_empty()
{
log.push(Kind::Query, format!("Vector.Search q={:?} repo={:?}", self.vec_query, self.vec_repo));
self.vhits = Some(
remote::vector_search(&srv.endpoint, &srv.token, &self.vec_repo, &self.vec_query, 15, workspace)
.map_err(|e| format!("{e:#}")),
);
}
});
});
match &self.vhits {
Some(Ok(vh)) => {
ui.label(format!("{} hit(s)", vh.len()));
egui::ScrollArea::vertical().max_height(220.0).auto_shrink([false, false]).id_salt("vhits").show(ui, |ui| {
for h in vh {
ui.horizontal(|ui| {
ui.colored_label(Color32::from_rgb(120, 180, 120), format!("{:.3}", h.score));
ui.label(egui::RichText::new(format!("{}:{}-{}", h.file, h.start_line, h.end_line)).monospace().size(11.0));
});
}
});
}
Some(Err(e)) => { ui.colored_label(Color32::RED, e); }
None => {}
}
ui.add_space(8.0);
ui.group(|ui| {
ui.label("Call relationships (callers / callees / path between)");
ui.horizontal(|ui| {
ui.label("repo:");
ui.add(egui::TextEdit::singleline(&mut self.call_repo).desired_width(110.0).hint_text("blank=all"));
ui.label("fn:");
ui.add(egui::TextEdit::singleline(&mut self.call_name).desired_width(140.0).hint_text("function"));
if ui.button("callers").clicked() && !self.call_name.trim().is_empty() {
log.push(Kind::Query, format!("Knowledge.Callers fn={:?} repo={:?}", self.call_name, self.call_repo));
self.calls = Some(self.run_calls(srv, workspace, true));
}
if ui.button("callees").clicked() && !self.call_name.trim().is_empty() {
log.push(Kind::Query, format!("Knowledge.Callees fn={:?} repo={:?}", self.call_name, self.call_repo));
self.calls = Some(self.run_calls(srv, workspace, false));
}
});
ui.horizontal(|ui| {
ui.label("β¦β to:");
ui.add(egui::TextEdit::singleline(&mut self.call_to).desired_width(140.0).hint_text("target fn"));
if ui.button("path between").clicked()
&& !self.call_name.trim().is_empty()
&& !self.call_to.trim().is_empty()
{
log.push(Kind::Query, format!("Knowledge.CallPath {:?} β {:?}", self.call_name, self.call_to));
self.calls = Some(
remote::knowledge_call_path(&srv.endpoint, &srv.token, &self.call_repo, &self.call_name, &self.call_to, workspace)
.map(|p| (
if p.is_empty() { "no path found".into() } else { format!("path ({} hops)", p.len()) },
p,
))
.map_err(|e| format!("{e:#}")),
);
}
});
});
match &self.calls {
Some(Ok((label, rows))) => {
ui.label(format!("{label}: {}", rows.len()));
egui::ScrollArea::vertical().max_height(220.0).auto_shrink([false, false]).id_salt("calls").show(ui, |ui| {
for row in rows {
ui.label(egui::RichText::new(row).monospace().size(11.0));
}
});
}
Some(Err(e)) => { ui.colored_label(Color32::RED, e); }
None => {}
}
}
fn run_calls(&self, srv: &Server, workspace: &str, callers: bool) -> Result<(String, Vec<String>), String> {
remote::knowledge_calls(&srv.endpoint, &srv.token, &self.call_repo, &self.call_name, callers, 100, workspace)
.map(|rows| {
let label = if callers { "callers" } else { "callees" };
let lines = rows
.into_iter()
.map(|(caller, callee, file, line)| format!("{caller} β {callee} ({file}:{line})"))
.collect();
(label.to_string(), lines)
})
.map_err(|e| format!("{e:#}"))
}
}
#[derive(Default)]
pub struct GatesState {
repo: String,
report: Option<Result<remote::GateReport, String>>,
trace: Option<Result<String, String>>,
}
impl GatesState {
pub fn draw(&mut self, ui: &mut egui::Ui, srv: Option<&Server>, workspace: &str, repos: &[String], log: &ActionLog) {
let Some(srv) = srv else { return local_hint(ui, "Gates") };
ui.heading("π¦ Release gates");
if self.repo.is_empty() {
self.repo = repos.first().cloned().unwrap_or_default();
}
ui.horizontal(|ui| {
ui.label("repo:");
egui::ComboBox::from_id_salt("gate_repo")
.selected_text(if self.repo.is_empty() { "β".into() } else { self.repo.clone() })
.show_ui(ui, |ui| {
for r in repos {
ui.selectable_value(&mut self.repo, r.clone(), r);
}
});
ui.add(egui::TextEdit::singleline(&mut self.repo).desired_width(160.0).hint_text("repo name"));
if ui.button("Run all gates").clicked() && !self.repo.trim().is_empty() {
log.push(Kind::Rpc, format!("Release.GateAll repo={}", self.repo));
self.report = Some(
remote::gate_all(&srv.endpoint, &srv.token, &self.repo, workspace).map_err(|e| format!("{e:#}")),
);
}
if ui.button("Trace regression").clicked() && !self.repo.trim().is_empty() {
log.push(Kind::Rpc, format!("Release.Trace repo={}", self.repo));
self.trace = Some(
remote::trace(&srv.endpoint, &srv.token, &self.repo, workspace).map_err(|e| format!("{e:#}")),
);
}
});
ui.separator();
match &self.report {
Some(Ok(r)) => {
ui.colored_label(Color32::from_rgb(80, 180, 120), format!("β passed ({})", r.passed.len()));
for p in &r.passed {
ui.label(format!(" β {p}"));
}
if !r.failed.is_empty() {
ui.colored_label(Color32::RED, format!("β failed ({})", r.failed.len()));
for (name, err) in &r.failed {
ui.colored_label(Color32::from_rgb(220, 120, 100), format!(" β {name}: {err}"));
}
} else {
ui.colored_label(Color32::from_rgb(80, 180, 120), "all gates green β releasable");
}
}
Some(Err(e)) => { ui.colored_label(Color32::RED, e); }
None => {}
}
if let Some(tr) = &self.trace {
ui.separator();
ui.label("regression trace:");
egui::ScrollArea::vertical().max_height(240.0).auto_shrink([false, false]).id_salt("trace").show(ui, |ui| {
match tr {
Ok(json) => {
let pretty = serde_json::from_str::<serde_json::Value>(json)
.ok()
.and_then(|v| serde_json::to_string_pretty(&v).ok())
.unwrap_or_else(|| json.clone());
ui.add(egui::Label::new(egui::RichText::new(pretty).monospace().size(11.0)));
}
Err(e) => { ui.colored_label(Color32::RED, e); }
}
});
}
}
}
#[derive(Default)]
pub struct BenchState {
repo: String,
points: Option<Result<Vec<remote::BenchPoint>, String>>,
metric: String,
}
impl BenchState {
pub fn draw(&mut self, ui: &mut egui::Ui, srv: Option<&Server>, workspace: &str, repos: &[String], log: &ActionLog) {
let Some(srv) = srv else { return local_hint(ui, "Bench") };
ui.heading("π Bench history");
if self.repo.is_empty() {
self.repo = repos.first().cloned().unwrap_or_default();
}
ui.horizontal(|ui| {
ui.label("repo:");
egui::ComboBox::from_id_salt("bench_repo")
.selected_text(if self.repo.is_empty() { "β".into() } else { self.repo.clone() })
.show_ui(ui, |ui| {
for r in repos {
ui.selectable_value(&mut self.repo, r.clone(), r);
}
});
ui.add(egui::TextEdit::singleline(&mut self.repo).desired_width(160.0).hint_text("repo name"));
if ui.button("Load history").clicked() && !self.repo.trim().is_empty() {
log.push(Kind::Rpc, format!("Bench.History repo={}", self.repo));
self.points = Some(
remote::bench_history(&srv.endpoint, &srv.token, &self.repo, workspace).map_err(|e| format!("{e:#}")),
);
self.metric.clear();
}
});
ui.separator();
let points = match &self.points {
Some(Ok(p)) => p,
Some(Err(e)) => { ui.colored_label(Color32::RED, e); return; }
None => { ui.label("pick a repo and load its bench history"); return; }
};
if points.is_empty() {
ui.label("no bench history recorded for this repo");
return;
}
let metrics: Vec<String> = {
let mut m: Vec<String> = points.iter().map(|p| p.metric.clone()).collect();
m.sort();
m.dedup();
m
};
if self.metric.is_empty() {
self.metric = metrics.first().cloned().unwrap_or_default();
}
ui.horizontal(|ui| {
ui.label("metric:");
egui::ComboBox::from_id_salt("bench_metric")
.selected_text(&self.metric)
.show_ui(ui, |ui| {
for m in &metrics {
ui.selectable_value(&mut self.metric, m.clone(), m);
}
});
});
let series: Vec<&remote::BenchPoint> = points.iter().filter(|p| p.metric == self.metric).collect();
if series.is_empty() {
ui.label("no points for this metric");
return;
}
let max = series.iter().map(|p| p.value.abs()).fold(f64::EPSILON, f64::max);
egui::ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| {
for p in &series {
ui.horizontal(|ui| {
ui.add_sized([150.0, 16.0], egui::Label::new(
egui::RichText::new(format!("{} {}", p.date, p.version)).monospace().size(11.0),
));
let frac = (p.value.abs() / max) as f32;
let (rect, _) = ui.allocate_exact_size(egui::Vec2::new(ui.available_width() - 90.0, 14.0), egui::Sense::hover());
ui.painter().rect_filled(
egui::Rect::from_min_size(rect.min, egui::Vec2::new((rect.width() * frac).max(1.0), 12.0)),
2.0,
Color32::from_rgb(80, 150, 220),
);
ui.label(egui::RichText::new(format!("{}", p.value)).monospace().size(11.0));
});
}
});
}
}
#[derive(Default)]
pub struct WorkspacePanel {
open: bool,
info: Option<Result<remote::WorkspaceInfo, String>>,
info_for: String,
sync_result: Option<Result<(u32, Vec<String>, Vec<String>, String), String>>,
}
impl WorkspacePanel {
#[must_use]
pub fn draw_controls(&mut self, ui: &mut egui::Ui, srv: Option<&Server>, workspace: &str) -> bool {
let Some(srv) = srv else { return false };
if ui.selectable_label(self.open, "βΉ info").clicked() {
self.open = !self.open;
if self.open {
self.refresh(srv, workspace);
}
}
let mut synced = false;
if ui
.button("β³ Sync now")
.on_hover_text(
"Workspaces.Fetch (force) β poll the remote(s) and rebuild this \
workspace's warehouse now, then reload the view",
)
.clicked()
{
self.sync_result = Some(
remote::fetch_workspace(&srv.endpoint, &srv.token, workspace, true)
.map_err(|e| format!("{e:#}")),
);
self.refresh(srv, workspace); synced = true;
}
synced
}
fn refresh(&mut self, srv: &Server, workspace: &str) {
self.info = Some(remote::get_workspace(&srv.endpoint, &srv.token, workspace).map_err(|e| format!("{e:#}")));
self.info_for = workspace.to_string();
}
pub fn draw_panel(&mut self, ui: &mut egui::Ui, srv: Option<&Server>, workspace: &str) {
if !self.open || srv.is_none() {
return;
}
if self.info_for != workspace {
self.refresh(srv.unwrap(), workspace);
}
ui.separator();
ui.strong("workspace");
match &self.info {
Some(Ok(i)) => {
ui.label(format!("{} Β· {}", i.name, i.mode));
ui.label(format!("poll: {}", i.poll));
ui.label(format!("snapshot: {}", short(&i.current_snapshot)));
ui.label(format!("updated: {}", i.updated_at));
ui.label(format!("members ({}):", i.members.len()));
for (name, summary) in &i.members {
ui.label(egui::RichText::new(format!(" {name}: {summary}")).size(11.0));
}
}
Some(Err(e)) => { ui.colored_label(Color32::RED, e); }
None => {}
}
if let Some(sync) = &self.sync_result {
ui.separator();
match sync {
Ok((fetched, changed, errors, snapshot)) => {
let snap = if snapshot.is_empty() {
String::new()
} else {
format!(" β snapshot {}", short(snapshot))
};
ui.colored_label(
Color32::from_rgb(80, 180, 120),
format!("synced: {fetched} fetched, {} changed{snap}", changed.len()),
);
for e in errors {
ui.colored_label(Color32::from_rgb(220, 120, 100), format!(" {e}"));
}
}
Err(e) => { ui.colored_label(Color32::RED, e); }
}
}
}
}
fn short(s: &str) -> String {
if s.chars().count() > 12 {
let head: String = s.chars().take(12).collect();
format!("{head}β¦")
} else {
s.to_string()
}
}
#[cfg(test)]
mod short_tests {
use super::short;
#[test]
fn short_passes_through_when_within_limit() {
assert_eq!(short("abc123"), "abc123");
assert_eq!(short("0123456789ab"), "0123456789ab"); }
#[test]
fn short_truncates_ascii() {
assert_eq!(short("0123456789abcdef"), "0123456789abβ¦");
}
#[test]
fn short_never_panics_on_multibyte_boundary() {
let s = "ÀâüÀâüÀâüÀâüÀâü"; let out = short(s);
assert_eq!(out.chars().take(12).collect::<String>(), "ÀâüÀâüÀâüÀâüβ¦".chars().take(12).collect::<String>());
assert_eq!(out.chars().count(), 13);
let s = "π¦π¦π¦π¦π¦π¦π¦π¦π¦π¦π¦π¦π¦π¦";
let out = short(s);
assert_eq!(out.chars().count(), 13); }
}