use crate::automation::hit_keys::HitKeyDoc;
use crate::diagnostics::Diagnostics;
use eframe::egui;
use std::collections::HashMap;
const PROJECT_LICENCE: &str = include_str!("../../LICENSE.md");
const THIRD_PARTY_NOTICES: &str = include_str!("../../THIRD-PARTY-NOTICES.md");
const INVENTORY_URL: &str = "help/licences/third-party-crates.html";
pub const START_MCP_LABEL: &str = "Start MCP server";
pub const NO_MCP_TEXT: &str = "this build has no MCP server (built without the `mcp` feature)";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum McpView {
Unavailable,
Stopped { error: Option<String> },
Running { url: String, instructions: String, note: Option<String> },
}
#[derive(Default)]
pub struct InfoPanel {
pub open: bool,
hits: HashMap<String, egui::Rect>,
mcp_start: bool,
}
impl InfoPanel {
pub fn new() -> Self {
Self::default()
}
pub fn take_mcp_start(&mut self) -> bool {
std::mem::take(&mut self.mcp_start)
}
pub fn show(&mut self, ctx: &egui::Context, diagnostics: &Diagnostics, mcp: &McpView) {
if !self.open {
return;
}
let mut open = true;
egui::Window::new("Info")
.open(&mut open)
.movable(true)
.resizable(true)
.default_size([560.0, 520.0])
.default_pos(super::below_chrome(ctx, 120.0, 0.0))
.show(ctx, |ui| self.body(ui, diagnostics, mcp));
self.open = open;
if crate::automation::registry::enabled() {
crate::automation::registry::publish(
"__brepInfoHit",
"info window widget rects (copy, inventory, licence:*, mcp:*)",
&self.hits_json(),
);
}
}
fn body(&mut self, ui: &mut egui::Ui, diagnostics: &Diagnostics, mcp: &McpView) {
self.hits.clear();
egui::ScrollArea::vertical()
.auto_shrink([false; 2])
.show(ui, |ui| {
self.diagnostics_section(ui, diagnostics);
ui.add_space(12.0);
ui.separator();
ui.add_space(6.0);
self.mcp_section(ui, mcp);
ui.add_space(12.0);
ui.separator();
ui.add_space(6.0);
self.licences_section(ui);
});
}
fn mcp_section(&mut self, ui: &mut egui::Ui, mcp: &McpView) {
ui.heading("Connect an agent (MCP)");
ui.label(
egui::RichText::new(
"Start the MCP server inside this window — what launching with --mcp does — so \
Claude Code or Codex can drive the documents you see here.",
)
.weak()
.small(),
);
ui.add_space(4.0);
let (enabled, label) = match mcp {
McpView::Unavailable => (false, START_MCP_LABEL),
McpView::Stopped { .. } => (true, START_MCP_LABEL),
McpView::Running { .. } => (false, "MCP server running"),
};
let start = ui.add_enabled(enabled, egui::Button::new(label));
self.hit("mcp:start", &start);
if start.clicked() {
self.mcp_start = true;
}
match mcp {
McpView::Unavailable => {
ui.label(egui::RichText::new(NO_MCP_TEXT).weak());
}
McpView::Stopped { error: None } => {}
McpView::Stopped { error: Some(error) } => {
ui.label(egui::RichText::new(error).color(ui.visuals().error_fg_color));
}
McpView::Running { url, instructions, note } => {
ui.label(format!("Listening on {url}"));
if let Some(note) = note {
ui.label(egui::RichText::new(note).color(ui.visuals().warn_fg_color));
}
ui.add_space(4.0);
ui.label(
egui::RichText::new("Paste this into your agent (the same text the terminal prints):")
.weak()
.small(),
);
ui.add(
egui::TextEdit::multiline(&mut instructions.as_str())
.font(egui::TextStyle::Monospace)
.desired_width(f32::INFINITY)
.desired_rows(instructions.lines().count()),
);
let copy = ui.button("Copy agent instructions");
self.hit("mcp:copy", ©);
if copy.clicked() {
ui.ctx().copy_text(instructions.clone());
}
}
}
}
fn diagnostics_section(&mut self, ui: &mut egui::Ui, diagnostics: &Diagnostics) {
ui.heading("Diagnostics");
ui.label(
egui::RichText::new("Sent with every problem report from the Submit Bug button.")
.weak()
.small(),
);
ui.add_space(4.0);
egui::Grid::new("brep-info-diagnostics")
.num_columns(2)
.spacing([12.0, 4.0])
.striped(true)
.show(ui, |ui| {
for (label, value) in diagnostics.rows() {
ui.label(egui::RichText::new(label).strong());
ui.label(egui::RichText::new(value).monospace());
ui.end_row();
}
});
ui.add_space(6.0);
let copy = ui.button("Copy diagnostics");
self.hit("copy", ©);
if copy.clicked() {
ui.ctx().copy_text(diagnostics.report_text());
}
}
fn licences_section(&mut self, ui: &mut egui::Ui) {
ui.heading("Licences");
ui.add_space(4.0);
let project = egui::CollapsingHeader::new("Project licence (LICENSE.md)")
.id_salt("brep-info-licence-project")
.show(ui, |ui| licence_text(ui, PROJECT_LICENCE));
self.hit("licence:project", &project.header_response);
let notices = egui::CollapsingHeader::new("Third-party notices (THIRD-PARTY-NOTICES.md)")
.id_salt("brep-info-licence-notices")
.show(ui, |ui| licence_text(ui, THIRD_PARTY_NOTICES));
self.hit("licence:notices", ¬ices.header_response);
ui.add_space(6.0);
ui.label(
egui::RichText::new(
"Every crate this application ships, grouped by its licence, is generated \
into the help site from the dependency graph itself:",
)
.weak()
.small(),
);
let inventory = ui.button("Open the third-party crate inventory");
self.hit("inventory", &inventory);
if inventory.clicked() {
ui.ctx().open_url(egui::OpenUrl::new_tab(INVENTORY_URL));
}
}
fn hit(&mut self, key: &str, resp: &egui::Response) {
self.hits.insert(key.to_string(), resp.rect);
}
pub fn hits_json(&self) -> String {
let map: serde_json::Map<String, serde_json::Value> = self
.hits
.iter()
.map(|(k, r)| {
(
k.clone(),
serde_json::json!([r.center().x, r.center().y, r.width(), r.height()]),
)
})
.collect();
serde_json::Value::Object(map).to_string()
}
}
fn licence_text(ui: &mut egui::Ui, text: &str) {
ui.add(egui::Label::new(egui::RichText::new(text).monospace().small()).wrap());
}
pub static HIT_KEYS: &[HitKeyDoc] = &[
HitKeyDoc { panel: "info", prefix: "copy", meaning: "copy the diagnostics block to the clipboard (the same rows `diagnostics` returns)", command: None },
HitKeyDoc { panel: "info", prefix: "inventory", meaning: "open the generated third-party crate inventory on the help site", command: None },
HitKeyDoc { panel: "info", prefix: "licence:", meaning: "expand a licence text (licence:project, licence:notices)", command: None },
HitKeyDoc { panel: "info", prefix: "mcp:", meaning: "the Connect an agent section: mcp:start starts the embedded MCP server (disabled while it runs or in a build without one), mcp:copy copies the agent instructions it then shows", command: None },
];