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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-15 11:07:46 -04:00
1 parent 4711f9474a
commit 441f03d093
11 files changed
+231 -7

No files matched your search

+66 -1
View File
@@ -30,6 +30,16 @@ pub const CRASH_REPORT: CommandSpec = CommandSpec {
run: crash_report,
};
pub const CRASH_REPORTS_DELETE: CommandSpec = CommandSpec {
id: "diagnostics.crash_reports.delete",
label: "Delete Crash Reports",
params: r#"{"id": "20261014-101500-…"} or {"all": true}; "dir"?: "…" — deletes one crash report, or all of them, from this computer: how many were deleted"#,
form: Some(super::forms::CRASH_REPORTS_DELETE),
kind: CommandKind::Session,
requires: &[],
run: crash_reports_delete,
};
pub const BUG_REPORT: CommandSpec = CommandSpec {
id: "diagnostics.bug_report",
label: "Report a Bug",
@@ -51,8 +61,12 @@ pub const CRASH_TEST: CommandSpec = CommandSpec {
};
fn crash_dir(s: &Session, p: &Value) -> Result<PathBuf> {
crash_dir_for(s, p, Access::Read)
}
fn crash_dir_for(s: &Session, p: &Value, access: Access) -> Result<PathBuf> {
if p.get("dir").is_some() {
return Ok(PathBuf::from(file_param(s, p, "dir", Access::Read)?));
return Ok(PathBuf::from(file_param(s, p, "dir", access)?));
}
crash::default_dir().ok_or_else(|| EngineError::Params("no crash folder: set FIELDCRAFT_CRASH_DIR or give `dir`".into()))
}
@@ -71,6 +85,21 @@ fn crash_report(s: &mut Session, p: &Value) -> Result<Value> {
crash::read(&dir, &id).map_err(EngineError::Params)
}
fn crash_reports_delete(s: &mut Session, p: &Value) -> Result<Value> {
let id = opt::<String>(p, "id")?;
let all = opt::<bool>(p, "all")?.unwrap_or(false);
let dir = crash_dir_for(s, p, Access::Write)?;
let deleted = match (id.as_deref(), all) {
(Some(id), false) => match crash::clear(&dir, Some(id)) {
0 => return Err(EngineError::Params(format!("no crash report `{}`", crash::limited(id, 64)))),
n => n,
},
(None, true) => crash::clear(&dir, None),
_ => return Err(EngineError::Params("give one of `id` (a crash report) or `all: true`".into())),
};
Ok(json!({"deleted": deleted}))
}
/// The draft input for `p` from the session: its commands and model (when shared), or a crash
/// report's replay and model.
pub fn bug_report_input(s: &Session, p: &Value) -> Result<bug_report::Input> {
@@ -131,3 +160,39 @@ fn crash_test(_: &mut Session, p: &Value) -> Result<Value> {
_ => Err(EngineError::invalid("mode", "must be panic, recovered or abort")),
}
}
#[cfg(test)]
mod tests {
use super::*;
/// `diagnostics.crash_reports.delete` deletes one report by id, then the rest; an unknown or
/// hostile id and a missing choice are errors that delete nothing.
#[test]
fn crash_reports_are_deleted_one_or_all() {
let dir = std::env::temp_dir().join(format!("fieldcraft-crash-delete-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let ids = ["20261014-101500-0000000000000001", "20261014-101600-0000000000000002", "20261014-101700-0000000000000003"];
for id in ids {
std::fs::create_dir_all(dir.join(id)).unwrap();
std::fs::write(dir.join(id).join("report.json"), r#"{"kind": "panic", "message": "x"}"#).unwrap();
}
let mut s = Session::default();
let d = dir.to_string_lossy().to_string();
let listed = |s: &mut Session| s.execute("diagnostics.crash_reports", &json!({"dir": d})).unwrap()["reports"].as_array().map_or(0, Vec::len);
assert_eq!(listed(&mut s), 3);
for bad in [
json!({"dir": d}),
json!({"dir": d, "id": "../x"}),
json!({"dir": d, "id": "20261014-101500-00000000000000ff"}),
json!({"dir": d, "id": ids[0], "all": true}),
] {
assert!(s.execute("diagnostics.crash_reports.delete", &bad).is_err(), "{bad}");
}
assert_eq!(listed(&mut s), 3);
assert_eq!(s.execute("diagnostics.crash_reports.delete", &json!({"dir": d, "id": ids[1]})).unwrap()["deleted"], 1);
assert_eq!(listed(&mut s), 2);
assert_eq!(s.execute("diagnostics.crash_reports.delete", &json!({"dir": d, "all": true})).unwrap()["deleted"], 2);
assert_eq!(listed(&mut s), 0);
let _ = std::fs::remove_dir_all(&dir);
}
}
+5
View File
@@ -1518,6 +1518,11 @@ pub const CRASH_REPORT: &[Field] = &[
text("id", "Report").help("The crash report to show.").req(),
path("dir", "Folder", false).help("The crash folder; the default one when empty.").hint("the crash folder"),
];
pub const CRASH_REPORTS_DELETE: &[Field] = &[
text("id", "Report").help("The crash report to delete.").hint("or All"),
flag("all", "All").help("Delete every crash report instead of one."),
path("dir", "Folder", false).help("The crash folder; the default one when empty.").hint("the crash folder"),
];
pub const BUG_REPORT: &[Field] = &[
text("title", "Title").help("The issue's title; one is made up when empty."),
text("observed", "What happened").help("What went wrong; the last error when empty."),
+1
View File
@@ -165,6 +165,7 @@ pub static ALL: &[CommandSpec] = &[
results::CLEAR,
diagnostics::CRASH_REPORTS,
diagnostics::CRASH_REPORT,
diagnostics::CRASH_REPORTS_DELETE,
diagnostics::BUG_REPORT,
diagnostics::CRASH_TEST,
update::CHECK,