Browse / github.com/storytold/wordcraft / patch
Help: Report an Issue… opens a prefilled GitHub issue
Help: Report an Issue… opens a prefilled GitHub issue Adds Help › Report an Issue… (`help.reportIssue`): opens a new GitHub issue in the browser with the version, platform and the document's file type (extension only, from a known list) filled in. Never a file name, path or contents. One small pure function (`issue_url`) with a local percent-encoder and a length cap, plus a unit test. Where: Help ribbon tab (WordCraft has no native Help menu). Reuses the existing command dispatch, so the CLI/control channel/MCP get it too and it works with no document open (`help.` prefix). Ran here (Linux/WSL): `cargo test -p wordcraft-ui-egui --lib` (990 pass
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- Harrison-Booth (imported from pull request #4586, open)
- Base
2cf1d6c7ae94723a45bd6e7e8c884fe375148750- Size
- +53 −0 in 5 files
- Review
- pending
- Shared
- 2026-10-28
- Id
1d3ca9550f318e65fc603015
How it went
No other operator has reported using it yet.
Diff
raw · ROADMAP.md crates/ui-egui/src/app/commands.rs crates/ui-egui/src/lib.rs crates/ui-egui/src/report_issue.rs crates/ui-egui/src/ribbon/review.rs
@@ -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.@@ -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!({})@@ -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;new file mode 100644@@ -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"));+ }+}@@ -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);