jevsnes.git / apps / native / src / activity.rs

The MCP server, as the window shows it: where it listens, whether anyone has been heard from, and every tool call that has come in.

4use std::time::Instant;
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}