BREP_app 0.17.0

The BREP CAD application: an eframe (egui + wgpu) host that draws the brep-render 3D engine into an egui frame — native + wasm from one codebase.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
//! The **Info** window — the licences, and what this build is running on.
//!
//! The toolbar's ℹ button opens it. Two things live here because a user asking
//! either question is in the same place: "what am I allowed to do with this"
//! and "what is it running on".
//!
//! * **Licences.** The project licence and the maintained third-party notices,
//!   compiled into the binary from the very files the help site renders
//!   (`LICENSE.md`, `THIRD-PARTY-NOTICES.md`) — so a build that is not served
//!   beside its `web/help/` still carries them. The GENERATED crate inventory
//!   (every shipped dependency, grouped by licence expression) is built from
//!   `cargo metadata` at docs time and cannot be compiled in, so the window
//!   links to its help page instead.
//! * **Diagnostics.** [`crate::diagnostics::Diagnostics`], rendered row for row.
//!   The window OWNS no diagnostic of its own: it is handed the app's one
//!   instance, the same one the problem report embeds, so what a user reads
//!   here and what a triager reads in their report cannot differ.
//! * **Connect an agent (MCP).** A button that starts the embedded MCP server
//!   at runtime — what `brep-app --mcp` does at launch — and, once it runs,
//!   the very text the console prints for pointing an agent at this window,
//!   selectable and with a Copy button. It is here, beside the diagnostics,
//!   because the URL and the session root are facts about THIS session, like
//!   the renderer: not a setting that persists. The window owns none of the
//!   state: it is handed a [`McpView`] snapshot each frame (the shell reads
//!   `crate::mcp::status()`), and a click is handed back to the shell, which
//!   owns the automation queue the server attaches to.
//!
//! Not to be confused with [`crate::panels::info_windows`], the pinned
//! per-ENTITY inspectors opened from the context bar. This window is about the
//! application; those are about a face, an edge or a solid.

use crate::automation::hit_keys::HitKeyDoc;
use crate::diagnostics::Diagnostics;
use eframe::egui;
use std::collections::HashMap;

/// The project licence, as authored. `BREP_docs` renders this same file as the
/// help site's first Licences page.
///
/// This is the crate's OWN copy, not the repository root's. An `include_str!`
/// that climbs out of the package compiles in a checkout and fails in the
/// tarball `cargo publish` verifies, which is exactly where nobody looks —
/// `build.rs` holds the two copies identical so the distinction stays
/// bookkeeping rather than a second source of truth.
const PROJECT_LICENCE: &str = include_str!("../../LICENSE.md");

/// The maintained third-party notices, as authored — the embedded fonts and the
/// material whose licence requires its notice to travel with it. The crate's own
/// copy, for the reason given above.
const THIRD_PARTY_NOTICES: &str = include_str!("../../THIRD-PARTY-NOTICES.md");

/// The generated crate inventory on the help site: every crate the application
/// ships, grouped under its declared licence expression. Generated from `cargo
/// metadata` by `BREP_docs` (see its `licences` module), so it exists only
/// beside the served page — hence a link rather than an include.
const INVENTORY_URL: &str = "help/licences/third-party-crates.html";

/// The label on the start button — the one a user looks for in the window.
pub const START_MCP_LABEL: &str = "Start MCP server";

/// What this build says when it has no server to start: main.rs's own words
/// for `--mcp` on such a build, so the button and the flag agree.
pub const NO_MCP_TEXT: &str = "this build has no MCP server (built without the `mcp` feature)";

/// What the "Connect an agent" section shows this frame. A SNAPSHOT: the
/// shell builds it from `crate::mcp::status()` (or [`McpView::Unavailable`]
/// when the build has no server) so this module compiles the same with and
/// without the `mcp` feature and on the web.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum McpView {
    /// No server in this build (built without `mcp`, or the browser build).
    Unavailable,
    /// Not running; `error` is the console's line from the last failure.
    Stopped { error: Option<String> },
    /// Serving: `instructions` is EXACTLY `crate::mcp::agent_instructions` for
    /// the live `url`; `note` says a fallback port was used, when one was.
    Running { url: String, instructions: String, note: Option<String> },
}

/// The Info window's state. Its whole model is `open`: everything it draws is
/// either a compiled-in constant or read live from the app's [`Diagnostics`]
/// and the [`McpView`] it is handed.
#[derive(Default)]
pub struct InfoPanel {
    /// Whether the window is showing — the toolbar ℹ toggle's flag, also set by
    /// the `info_window` command.
    pub open: bool,
    /// Per-frame widget screen rects for the headed verifier, like every other
    /// panel.
    hits: HashMap<String, egui::Rect>,
    /// Set by a click on **Start MCP server**; the shell takes it after the
    /// frame ([`Self::take_mcp_start`]) because the shell, not this window,
    /// owns the queue the server attaches to.
    mcp_start: bool,
    #[cfg(all(feature = "mcp", not(target_arch = "wasm32")))]
    mcp_preferences: Option<McpPreferences>,
}

/// Where the next launch keeps its MCP state (the persistence dropdown).
#[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 {
    /// `(value, hit id, label)` for [`crate::form::enum_dropdown`].
    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()
    }

    /// Whether **Start MCP server** was clicked since the last call. The shell
    /// answers it with `crate::mcp::start_from_window`.
    pub fn take_mcp_start(&mut self) -> bool {
        std::mem::take(&mut self.mcp_start)
    }

    /// Draw the floating window (if open) at ctx level, after the panels, so it
    /// floats over the shell. Idempotent while closed.
    pub fn show(&mut self, ctx: &egui::Context, diagnostics: &Diagnostics, mcp: &McpView) {
        if !self.open {
            return;
        }
        // `egui::Window::open` needs its own `&mut bool`; borrow a copy so the
        // draw closure can still take `&mut self`, then fold the close back in
        // (the Part Properties window's rule).
        let mut open = true;
        egui::Window::new("Info")
            .open(&mut open)
            .movable(true)
            .resizable(true)
            // A bounded default plus the filling ScrollArea in `body`: without a
            // filling child egui hugs the window to its content and the user
            // cannot drag it larger.
            .default_size([560.0, 520.0])
            // Clear of the Settings (x 720) and Part Properties (x 660)
            // rest positions, so opening all three does not stack them.
            .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);
            });
    }

    /// The button that starts the MCP server in this window, and — once it
    /// runs — the console's own agent instructions to copy into Claude Code
    /// or Codex. The button is the whole control: disabled with the reason
    /// when there is nothing to start (no server in this build, or it already
    /// runs — started here or by `--mcp`, the section cannot tell and need
    /// not), enabled otherwise. A failure's line sits under it in the words
    /// the console used, so a window launched from a desktop icon, with no
    /// terminal to read, still says what went wrong.
    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(),
                );
                // A read-only text edit: selectable with the pointer, wrapped
                // to the window, and painted as ONE galley so a test reading
                // the frame sees the whole text, not a line of it.
                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", &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); }
            }
        }
    }

    /// The diagnostics grid, then the one button that puts the very same text a
    /// problem report would carry on the clipboard — for a user reporting
    /// somewhere other than the in-app form.
    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", &copy);
        if copy.clicked() {
            ui.ctx().copy_text(diagnostics.report_text());
        }
    }

    /// The licences: the project's own, the maintained notices, and a link to
    /// the generated crate inventory. Each authored file is behind a collapsing
    /// header — several thousand words of licence text opened by default would
    /// bury the diagnostics above it.
    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", &notices.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));
        }
    }

    /// Record a widget's screen rect for the headed verifier.
    fn hit(&mut self, key: &str, resp: &egui::Response) {
        self.hits.insert(key.to_string(), resp.rect);
    }

    /// The published widget hit-rects (egui points) for the headed verifier.
    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()
    }
}

/// One authored licence file, verbatim. Markdown as typed — the app has no
/// markdown renderer, and a licence is one of the few texts where showing
/// exactly the bytes that ship is the right answer anyway. Selectable, so a
/// reader can copy a clause out.
fn licence_text(ui: &mut egui::Ui, text: &str) {
    ui.add(egui::Label::new(egui::RichText::new(text).monospace().small()).wrap());
}

/// The hit keys this panel publishes (see `automation::hit_keys`).
/// Every `command` here is `None`, and honestly so: the registered command is
/// the one that DOES what a click does, and no command copies to a clipboard,
/// opens this particular help page or expands a header. The window itself is
/// reachable without a pointer (`info_window`), and the diagnostics it shows are
/// readable without one (`diagnostics`) — naming either against a button that
/// does something else would put a promise in the generated widget docs that
/// `tools/call` cannot keep.
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 },
    // No command either: a server started from the agent's own tool call
    // would be the server that call came in on, and the copy goes to a
    // clipboard. The instructions are the console's, so a host has them.
    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 },
];