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,
#[cfg(all(feature = "mcp", not(target_arch = "wasm32")))]
mcp_preferences: Option<McpPreferences>,
}
#[cfg(all(feature = "mcp", not(target_arch = "wasm32")))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum McpPersistence {
Named,
Directory,
Ephemeral,
}
#[cfg(all(feature = "mcp", not(target_arch = "wasm32")))]
impl McpPersistence {
const CHOICES: [(Self, &'static str, &'static str); 3] = [
(Self::Named, "named", "Named instance"),
(Self::Directory, "directory", "Explicit directory"),
(Self::Ephemeral, "ephemeral", "Ephemeral"),
];
}
#[cfg(all(feature = "mcp", not(target_arch = "wasm32")))]
struct McpPreferences {
mode: McpPersistence,
instance: String,
path: String,
message: Option<Result<String, String>>,
}
#[cfg(all(feature = "mcp", not(target_arch = "wasm32")))]
impl McpPreferences {
fn load() -> Self {
let loaded = crate::mcp::state_settings_path().and_then(|path| brep_mcp_core::launch_state::Settings::load(&path));
let error = loaded.as_ref().err().cloned();
let policy = loaded.ok().and_then(|s| s.desktop).unwrap_or_default();
Self {
mode: if policy.ephemeral { McpPersistence::Ephemeral } else if policy.state_dir.is_some() { McpPersistence::Directory } else { McpPersistence::Named },
instance: policy.instance.unwrap_or_else(|| "desktop".into()),
path: policy.state_dir.map(|p| p.to_string_lossy().into_owned()).unwrap_or_default(),
message: error.map(Err),
}
}
}
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());
}
}
}
#[cfg(all(feature = "mcp", not(target_arch = "wasm32")))]
if !matches!(mcp, McpView::Unavailable) { self.mcp_persistence(ui); }
}
#[cfg(all(feature = "mcp", not(target_arch = "wasm32")))]
fn mcp_persistence(&mut self, ui: &mut egui::Ui) {
ui.separator();
ui.label("MCP state persistence");
match crate::mcp::state_configuration() {
Some(Ok(state)) => {
if let Some(path) = &state.state_dir {
ui.label(format!("This launch: persistent ({:?})", state.source));
if let Some(name) = &state.instance { ui.label(format!("Instance: {name}")); }
ui.label(format!("Journal: {}", path.display()));
} else { ui.label(format!("This launch: ephemeral ({:?})", state.source)); }
}
Some(Err(error)) => { ui.colored_label(ui.visuals().error_fg_color, error); }
None => { ui.label("Launch configuration has not been initialized."); }
}
ui.label("Machine-local preference for the next launch. Command-line and environment overrides take precedence. Restart required after saving.");
if let Ok(path) = crate::mcp::state_settings_path() { ui.label(format!("Settings: {}", path.display())); }
let preferences = self.mcp_preferences.get_or_insert_with(McpPreferences::load);
crate::form::enum_dropdown(ui, "mcp:persistence", &McpPersistence::CHOICES, &mut preferences.mode, None, Some(&mut self.hits));
match preferences.mode {
McpPersistence::Named => {
ui.label("Instance name");
let response = ui.text_edit_singleline(&mut preferences.instance);
self.hits.insert("mcp:instance".into(), response.rect);
}
McpPersistence::Directory => {
ui.label("Journal directory");
let response = ui.text_edit_singleline(&mut preferences.path);
self.hits.insert("mcp:state-dir".into(), response.rect);
}
McpPersistence::Ephemeral => {}
}
let save = ui.button("Save MCP preference for next launch");
self.hits.insert("mcp:save-preference".into(), save.rect);
if save.clicked() {
let policy = brep_mcp_core::launch_state::Policy {
instance: (preferences.mode == McpPersistence::Named).then(|| preferences.instance.clone()),
state_dir: (preferences.mode == McpPersistence::Directory).then(|| preferences.path.clone().into()),
ephemeral: preferences.mode == McpPersistence::Ephemeral,
};
preferences.message = Some(crate::mcp::save_desktop_policy(policy).map(|()| "Saved. Restart the application to apply; launch overrides still take precedence.".into()));
}
if let Some(message) = &preferences.message {
match message {
Ok(message) => { ui.label(message); }
Err(error) => { ui.colored_label(ui.visuals().error_fg_color, error); }
}
}
}
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; mcp:persistence is the next-launch state dropdown (mcp:persistence#named/#directory/#ephemeral while open), mcp:instance and mcp:state-dir edit the location, mcp:save-preference saves machine-local settings", command: None },
];