1//! The MCP server, as the window shows it: where it listens, whether anyone 2//! has been heard from, and every tool call that has come in. 3 4use std::time::Instant; 5 6use eframe::egui::{self, Color32, FontId, RichText, Sense, vec2}; 7 8use mcp::{ADDRESS, Activity, Listening, Outcome}; 9 10const GREEN: Color32 = Color32::from_rgb(0x3C, 0xD0, 0x5A); 11const AMBER: Color32 = Color32::from_rgb(0xE0, 0xA0, 0x30); 12const RED: Color32 = Color32::from_rgb(0xE0, 0x50, 0x50); 13const GREY: Color32 = Color32::from_rgb(0x80, 0x80, 0x80); 14 15fn ago(then: Instant) -> String { 16 match then.elapsed().as_secs() { 17 0 => "now".into(), 18 seconds @ 1..60 => format!("{seconds}s ago"), 19 seconds @ 60..3600 => format!("{}m ago", seconds / 60), 20 seconds => format!("{}h ago", seconds / 3600), 21 } 22} 23 24pub fn show(ui: &mut egui::Ui, activity: &Activity) { 25 // Green means a client has been heard from since this window opened; how 26 // long ago is the most current thing there is to say about it. 27 let (light, status) = match (&activity.listening, activity.last_heard) { 28 (Listening::Starting, _) => (GREY, "starting".to_owned()), 29 (Listening::Failed(why), _) => (RED, why.clone()), 30 (Listening::On, None) => (AMBER, "listening; nobody yet".to_owned()), 31 (Listening::On, Some(heard)) => { 32 // A client that connected to an earlier window carries on without 33 // introducing itself to this one. 34 let client = activity.client.as_deref().unwrap_or("a client from before"); 35 (GREEN, format!("{client}, heard {}", ago(heard))) 36 } 37 }; 38 39 ui.horizontal(|ui| { 40 let (dot, painter) = ui.allocate_painter(vec2(12.0, 12.0), Sense::hover()); 41 painter.circle_filled(dot.rect.center(), 5.0, light); 42 ui.monospace(ADDRESS); 43 }); 44 ui.label(status); 45 // Whether a client can change anything, which is the thing to know at a 46 // glance about a window that is supposed to be playing by itself. 47 if activity.dev_mode.is_on() { 48 ui.label(RichText::new("dev mode: clients can drive").color(AMBER)); 49 } else { 50 ui.label(RichText::new("clients watch only").color(GREY)); 51 } 52 53 egui::ScrollArea::vertical() 54 .stick_to_bottom(true) 55 .auto_shrink(false) 56 .show(ui, |ui| { 57 for call in &activity.calls { 58 let line = format!("{:>7} {} {}", ago(call.at), call.tool, call.arguments); 59 let line = RichText::new(line).font(FontId::monospace(10.0)); 60 match &call.outcome { 61 Outcome::Running => ui.label(line.color(AMBER)), 62 Outcome::Done => ui.label(line), 63 Outcome::Failed(why) => { 64 ui.label(line.color(RED)); 65 ui.label(RichText::new(format!(" {why}")).font(FontId::monospace(10.0)).color(RED)) 66 } 67 }; 68 } 69 }); 70}