forked from 1b-agent/fieldcraft-upstream
ui: Help › Crash Reports lists the saved reports to report, copy or delete (#300)
diagnostics.crash_reports.delete deletes one report or all. The window lists each report (when, what, how many times) with Report…, Copy Path and Delete, and Delete All; ui.prompt answers it over the control channel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
4711f9474a
commit
441f03d093
11 files changed
+231
-7
No files matched your search
@@ -349,10 +349,13 @@ fn help_menu(app: &mut ModelApp, ui: &mut egui::Ui) {
|
||||
.on_hover_text("Download this version again, verify it and swap it in, to repair a damaged install")
|
||||
.clicked();
|
||||
}
|
||||
let crashes = ui.button("Crash Reports…").on_hover_text("The crash reports saved on this computer: report, copy or delete them").clicked();
|
||||
let settings = ui.button("Settings…").on_hover_text("Privacy and updates").clicked();
|
||||
ui.label(egui::RichText::new(format!("FieldCraft {}", crate::VERSION)).color(t.text_dim));
|
||||
if report {
|
||||
app.report_bug(None);
|
||||
} else if crashes {
|
||||
app.open_crash_reports();
|
||||
} else if check {
|
||||
app.check_for_updates();
|
||||
} else if reinstall {
|
||||
|
||||
@@ -256,6 +256,7 @@ pub fn handle(app: &mut ModelApp, ctx: &egui::Context, req: &ControlRequest) ->
|
||||
crate::files::Prompt::ReplaceMesh(r) => json!({"replace_mesh": r.question, "warnings": r.warnings}),
|
||||
crate::files::Prompt::Delete(d) => json!({"delete": d.question(), "kind": d.kind, "name": d.name, "users": d.users, "allowed": d.allowed()}),
|
||||
crate::files::Prompt::Crashed(r) => json!({"crashed": r}),
|
||||
crate::files::Prompt::CrashReports(r) => json!({"crash_reports": r}),
|
||||
crate::files::Prompt::ReportBug => json!({"report_bug": app.report.as_ref().and_then(|f| f.drafted_json())}),
|
||||
crate::files::Prompt::Consent => json!({"consent": crate::privacy::QUESTION}),
|
||||
crate::files::Prompt::Settings => json!("settings"),
|
||||
@@ -449,7 +450,19 @@ pub fn handle(app: &mut ModelApp, ctx: &egui::Context, req: &ControlRequest) ->
|
||||
app.report_bug(r.first().map(|c| c.id.clone()));
|
||||
ok(json!({"prompt": "report_bug"}))
|
||||
}
|
||||
(crate::files::Prompt::Crashed(_), Some("close" | "cancel")) => {
|
||||
(crate::files::Prompt::CrashReports(_), Some("report")) => match s("id") {
|
||||
Some(id) => {
|
||||
app.report_bug(Some(id.to_string()));
|
||||
ok(json!({"prompt": "report_bug"}))
|
||||
}
|
||||
None => err("missing `id` (a crash report)"),
|
||||
},
|
||||
(crate::files::Prompt::CrashReports(_), Some("delete")) => match s("id") {
|
||||
Some(id) => wrap(app.delete_crash_reports(Some(id))),
|
||||
None => err("missing `id` (a crash report)"),
|
||||
},
|
||||
(crate::files::Prompt::CrashReports(_), Some("delete_all")) => wrap(app.delete_crash_reports(None)),
|
||||
(crate::files::Prompt::Crashed(_) | crate::files::Prompt::CrashReports(_), Some("close" | "cancel")) => {
|
||||
app.prompt = None;
|
||||
ok(Value::Null)
|
||||
}
|
||||
|
||||
@@ -402,6 +402,9 @@ pub enum Prompt {
|
||||
Crashed(Vec<fieldcraft_engine::crash::Summary>),
|
||||
/// Help › Report a Bug ([`crate::report`]; the form is [`ModelApp::report`]).
|
||||
ReportBug,
|
||||
/// Help › Crash Reports: the saved reports, newest first, each to report, copy or delete
|
||||
/// ([`crate::report`]).
|
||||
CrashReports(Vec<fieldcraft_engine::crash::Summary>),
|
||||
}
|
||||
|
||||
/// The answer to [`Prompt::Design`].
|
||||
@@ -940,7 +943,8 @@ impl ModelApp {
|
||||
"file.recover" | "file.recovery.list" => self.prompt = Some(Prompt::Recover),
|
||||
"file.sample" => self.prompt = Some(Prompt::Samples),
|
||||
"file.new" => self.new_model(),
|
||||
"diagnostics.bug_report" | "diagnostics.crash_reports" | "diagnostics.crash_report" => self.report_bug(None),
|
||||
"diagnostics.bug_report" => self.report_bug(None),
|
||||
"diagnostics.crash_reports" | "diagnostics.crash_report" | "diagnostics.crash_reports.delete" => self.open_crash_reports(),
|
||||
"solve.run" => self.solve_all(),
|
||||
"deck.write" if can_save => match preset.get("format").and_then(Value::as_str) {
|
||||
Some(f) => self.write_deck(f),
|
||||
@@ -1208,7 +1212,7 @@ pub fn show(app: &mut ModelApp, ctx: &egui::Context) {
|
||||
}
|
||||
}
|
||||
// Drawn by `crate::report::show` and `crate::privacy::show`.
|
||||
Some(Prompt::Crashed(_) | Prompt::ReportBug | Prompt::Consent | Prompt::Settings | Prompt::Update) | None => {}
|
||||
Some(Prompt::Crashed(_) | Prompt::CrashReports(_) | Prompt::ReportBug | Prompt::Consent | Prompt::Settings | Prompt::Update) | None => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -93,6 +93,33 @@ impl ModelApp {
|
||||
self.ui.menu = None;
|
||||
}
|
||||
|
||||
/// Help › Crash Reports: the saved reports (`diagnostics.crash_reports`), newest first.
|
||||
pub fn open_crash_reports(&mut self) {
|
||||
// Read directly: a listing changes nothing, and the window shows it instead of the reply.
|
||||
let reports = match self.session.execute("diagnostics.crash_reports", &json!({})) {
|
||||
Ok(r) => r.get("reports").and_then(Value::as_array).map_or_else(Vec::new, |a| a.iter().filter_map(summary_of).collect()),
|
||||
Err(e) => {
|
||||
self.set_status(format!("Crash reports: {e}"), true);
|
||||
return;
|
||||
}
|
||||
};
|
||||
self.prompt = Some(Prompt::CrashReports(reports));
|
||||
self.ui.menu = None;
|
||||
}
|
||||
|
||||
/// Delete one crash report, or all (`diagnostics.crash_reports.delete`), and list them again.
|
||||
pub fn delete_crash_reports(&mut self, id: Option<&str>) -> Result<Value, String> {
|
||||
let params = match id {
|
||||
Some(id) => json!({"id": id}),
|
||||
None => json!({"all": true}),
|
||||
};
|
||||
let r = self.run("diagnostics.crash_reports.delete", params)?;
|
||||
let n = r.get("deleted").and_then(Value::as_u64).unwrap_or(0);
|
||||
self.set_status(format!("Deleted {n} crash report{}", if n == 1 { "" } else { "s" }), false);
|
||||
self.open_crash_reports();
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
/// The host found these crash reports at startup: ask about the real crashes.
|
||||
pub fn announce_crashes(&mut self, fresh: &[Summary]) {
|
||||
let crashes: Vec<Summary> = fresh.iter().filter(|r| r.kind != "recovered_panic").cloned().collect();
|
||||
@@ -178,6 +205,22 @@ impl ModelApp {
|
||||
}
|
||||
}
|
||||
|
||||
/// A crash report as `diagnostics.crash_reports` lists it.
|
||||
fn summary_of(v: &Value) -> Option<Summary> {
|
||||
let s = |k: &str| v.get(k).and_then(Value::as_str).unwrap_or_default().to_string();
|
||||
Some(Summary {
|
||||
id: v.get("id").and_then(Value::as_str)?.to_string(),
|
||||
kind: s("kind"),
|
||||
time: s("time"),
|
||||
message: s("message"),
|
||||
command: v.get("command").and_then(Value::as_str).map(str::to_string),
|
||||
count: v.get("count").and_then(Value::as_u64).unwrap_or(1),
|
||||
version: s("version"),
|
||||
path: s("path"),
|
||||
replayable: v.get("replayable").and_then(Value::as_bool).unwrap_or(false),
|
||||
})
|
||||
}
|
||||
|
||||
fn size(bytes: usize) -> String {
|
||||
if bytes < 1024 { format!("{bytes} B") } else { format!("{} KB", bytes.div_ceil(1024)) }
|
||||
}
|
||||
@@ -228,6 +271,74 @@ pub fn show(app: &mut ModelApp, ctx: &egui::Context) {
|
||||
app.prompt = None;
|
||||
}
|
||||
}
|
||||
Some(Prompt::CrashReports(reports)) => {
|
||||
enum Act {
|
||||
Report(String),
|
||||
Copy(String),
|
||||
Delete(String),
|
||||
DeleteAll,
|
||||
Close,
|
||||
}
|
||||
let mut act = None;
|
||||
crate::frame::window(ctx, "Crash Reports", crate::frame::Width::Wide).id(egui::Id::new("fc_crash_reports")).show(ctx, |ui| {
|
||||
if reports.is_empty() {
|
||||
ui.label("No crash reports are saved on this computer.");
|
||||
} else {
|
||||
ui.label(
|
||||
RichText::new("Saved on this computer, newest first. Reporting one opens a GitHub issue you review first; nothing is sent until you submit it.")
|
||||
.color(t.text_dim),
|
||||
);
|
||||
ui.add_space(4.0);
|
||||
egui::ScrollArea::vertical().max_height((ctx.content_rect().height() * 0.6).max(160.0)).show(ui, |ui| {
|
||||
for r in &reports {
|
||||
ui.separator();
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(RichText::new(kind_label(&r.kind)).strong());
|
||||
let when = if r.count > 1 { format!("{} · {} times", r.time, r.count) } else { r.time.clone() };
|
||||
ui.label(RichText::new(when).color(t.text_dim));
|
||||
});
|
||||
ui.label(RichText::new(&r.message).color(t.text_dim).size(11.0));
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("Report…").clicked() {
|
||||
act = Some(Act::Report(r.id.clone()));
|
||||
}
|
||||
if ui.button("Copy Path").on_hover_text(&r.path).clicked() {
|
||||
act = Some(Act::Copy(r.path.clone()));
|
||||
}
|
||||
if ui.button("Delete").clicked() {
|
||||
act = Some(Act::Delete(r.id.clone()));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
ui.horizontal(|ui| {
|
||||
if ui.add_enabled(!reports.is_empty(), egui::Button::new("Delete All")).clicked() {
|
||||
act = Some(Act::DeleteAll);
|
||||
}
|
||||
if ui.button("Close").clicked() {
|
||||
act = Some(Act::Close);
|
||||
}
|
||||
});
|
||||
});
|
||||
match act {
|
||||
Some(Act::Report(id)) => app.report_bug(Some(id)),
|
||||
Some(Act::Copy(path)) => {
|
||||
ctx.copy_text(path);
|
||||
app.set_status("Copied the crash report's folder", false);
|
||||
}
|
||||
Some(Act::Delete(id)) => {
|
||||
let _ = app.delete_crash_reports(Some(&id));
|
||||
}
|
||||
Some(Act::DeleteAll) => {
|
||||
let _ = app.delete_crash_reports(None);
|
||||
}
|
||||
Some(Act::Close) => app.prompt = None,
|
||||
None if ctx.input(|i| i.key_pressed(egui::Key::Escape)) => app.prompt = None,
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
Some(Prompt::ReportBug) => {
|
||||
let Some(mut form) = app.report.take() else {
|
||||
app.prompt = None;
|
||||
@@ -336,6 +447,24 @@ mod tests {
|
||||
assert_eq!(d["attachments"], json!([]), "{d}");
|
||||
}
|
||||
|
||||
/// Help › Crash Reports reads each report as `diagnostics.crash_reports` lists it.
|
||||
#[test]
|
||||
fn crash_reports_read_the_listing() {
|
||||
let r = Summary {
|
||||
id: "20261014-101500-0000000000000001".into(),
|
||||
kind: "unclean_exit".into(),
|
||||
time: "2026-10-14T10:15:00Z".into(),
|
||||
message: "ended".into(),
|
||||
command: Some("solve.run".into()),
|
||||
count: 2,
|
||||
version: "0.5.9".into(),
|
||||
path: "/x".into(),
|
||||
replayable: true,
|
||||
};
|
||||
assert_eq!(summary_of(&serde_json::to_value(&r).unwrap()), Some(r));
|
||||
assert_eq!(summary_of(&json!({"kind": "panic"})), None, "a report without an id is left out");
|
||||
}
|
||||
|
||||
fn bug_report_text(d: &Draft) -> String {
|
||||
d.fields.iter().map(|(_, v)| v.as_str()).collect::<Vec<_>>().join("\n")
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user