diff --git a/ROADMAP.md b/ROADMAP.md
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -140,6 +140,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm
 
 ## Progress log
 
+- 2026-10-27: Help › Report an Issue… (`help.reportIssue`) opens a new GitHub issue with the version, platform and the document's file type filled in (never its name, path or contents).
 - 2026-10-22: View › Window › New Window opens a second window on the document (desktop). The windows are titled `Name:1` and `Name:2` and each keeps its own caret, scroll position, zoom and view; edits and Undo are the document's and show in both. Closing one of them asks nothing; the last one asks about unsaved changes, and quitting asks once per document.
 - 2026-10-22: One native window per open document on the desktop. File › New and Open (from the ribbon, File, shortcuts, the file manager, the command line or the control channel) open the document in a window of its own, focused and placed a step below and right of the last one; opening a file replaces a blank document nobody has used in its own window, and a file that is already open brings its window to the front. Each window keeps its own ribbon tab, File page, dialogs, panes, scroll position and composition. View › Window › Switch Windows lists the windows and brings the chosen one forward. File › Close and a window's close button close that window, asking about unsaved changes first; the last window stays open with no document. Quitting asks about each document with unsaved changes in its own window, and Cancel keeps everything open. The control channel's input and screenshots go to the window focused last. The web app keeps one window.
 - 2026-10-22: Recovery covers every open document. Each dirty document keeps its own copy, written on the same ten-second cadence whether it is shown or not; closing a document without saving deletes its copy, as saving does, and a clean exit deletes the copies of every document saved or closed since its last copy. File › Recover lists only copies no open document keeps, and Restore opens a copy as another document (or in place of an untouched blank one), so it never replaces work. After a crash with several unsaved documents, the next start lists all of them.
diff --git a/crates/ui-egui/src/app/commands.rs b/crates/ui-egui/src/app/commands.rs
--- a/crates/ui-egui/src/app/commands.rs
+++ b/crates/ui-egui/src/app/commands.rs
@@ -737,6 +737,7 @@ impl WordApp {
                         .into(),
                 ));
             }
+            "help.reportIssue" => crate::report_issue::run(self),
             "ui.discord" => {
                 self.canvas.open_url = Some("https://discord.gg/artcraft".into());
                 json!({})
diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs
--- a/crates/ui-egui/src/lib.rs
+++ b/crates/ui-egui/src/lib.rs
@@ -53,6 +53,7 @@ pub mod previews;
 pub mod print_page;
 pub mod printing;
 pub mod read_aloud;
+mod report_issue;
 pub mod ribbon;
 pub mod ribbon_fit;
 pub mod ruler_tabs;
diff --git a/crates/ui-egui/src/report_issue.rs b/crates/ui-egui/src/report_issue.rs
new file mode 100644
--- /dev/null
+++ b/crates/ui-egui/src/report_issue.rs
@@ -0,0 +1,49 @@
+//! Help ▸ Report an Issue…: opens a new GitHub issue with the version and system filled in. Nothing
+//! is created until the user submits it.
+
+use serde_json::{Value, json};
+
+use crate::WordApp;
+
+/// Extensions WordCraft opens; any other one could be private text and is left out of the report.
+const KNOWN: [&str; 11] = ["docx", "doc", "dotx", "dotm", "docm", "rtf", "odt", "txt", "md", "html", "htm"];
+
+/// The new-issue address. The document is described by its file extension alone, never a name,
+/// path or contents. Plain strings in, so it can move to a shared crate.
+pub(crate) fn issue_url(version: &str, os: &str, document_ext: Option<&str>) -> String {
+    let short = |s: &str| s.chars().filter(|c| !c.is_control()).take(16).collect::<String>();
+    let ext: String = document_ext.unwrap_or("none").chars().filter(char::is_ascii_alphanumeric).take(8).collect();
+    let (version, environment) = (short(version), format!("{}; document: {ext}", short(os)));
+    let encode = |s: &str| {
+        let keep = |b: &u8| b.is_ascii_alphanumeric() || b"-_.~".contains(b);
+        s.bytes().map(|b| if keep(&b) { char::from(b).to_string() } else { format!("%{b:02X}") }).collect::<String>()
+    };
+    let body = format!("WordCraft {version}\n{environment}");
+    let (v, e, b) = (encode(&version), encode(&environment), encode(&body));
+    format!("https://github.com/storytold/wordcraft/issues/new?version={v}&environment={e}&os={e}&body={b}")
+}
+
+/// `help.reportIssue`: opens the form in the browser. Returns `{url}` and never opens a dialog.
+pub(crate) fn run(app: &mut WordApp) -> Value {
+    let ext = app.session.path.as_deref().and_then(|p| p.extension()?.to_str()).map(str::to_ascii_lowercase).filter(|e| KNOWN.contains(&e.as_str()));
+    let os = if cfg!(target_arch = "wasm32") { "Web".into() } else { format!("{} ({})", std::env::consts::OS, std::env::consts::ARCH) };
+    let url = issue_url(env!("CARGO_PKG_VERSION"), &os, ext.as_deref());
+    app.canvas.open_url = Some(url.clone());
+    json!({"url": url})
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn the_address_carries_version_and_system_only_and_stays_short() {
+        let url = issue_url("0.1.0", "linux (x86_64)", Some("docx"));
+        assert!(url.contains("?version=0.1.0&environment=linux%20%28x86_64%29%3B%20document%3A%20docx&os="), "{url}");
+        // Hostile text and an unusual extension can't add a parameter, leak, or grow the address.
+        let url = issue_url(&"\u{1F600}".repeat(99), &"&#=".repeat(99), Some("x.y/Z"));
+        assert_eq!(url.matches('?').count() + url.matches('#').count(), 1, "{url}");
+        assert!(url.is_ascii() && url.contains("document%3A%20xyZ") && url.len() < 2048, "{} bytes: {url}", url.len());
+        assert!(issue_url("1", "w", None).contains("document%3A%20none"));
+    }
+}
diff --git a/crates/ui-egui/src/ribbon/review.rs b/crates/ui-egui/src/ribbon/review.rs
--- a/crates/ui-egui/src/ribbon/review.rs
+++ b/crates/ui-egui/src/ribbon/review.rs
@@ -125,6 +125,7 @@ pub(super) fn help(app: &mut WordApp, ui: &mut Ui) {
         big(ui, app, "help", "Help", "ui.dialog", json!({"name": "about"}), false);
         big(ui, app, "checkForUpdates", "Check for\nUpdates…", "ui.checkForUpdates", json!({}), false);
         big(ui, app, "discord", "Community\nDiscord", "ui.discord", json!({}), false);
+        big(ui, app, "info", "Report an\nIssue…", "help.reportIssue", json!({}), false);
     });
     group(ui, "Agents", None, app, |ui, app| {
         big(ui, app, "macros", "Commands", "ui.dialog", json!({"name": "commands"}), false);
