From e980e8a3133d53ab9293ae47ad6cf2cea927bf64 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 30 Apr 2026 09:58:39 -0700 Subject: [PATCH 01/10] Add a debug modal for failed auto-updates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When an update install throws (e.g. macOS Gatekeeper translocation, permission denied on /Applications, signature verify failure), the banner now reads "Update failed. [Click here to debug]" and the link opens a modal that helps the user (a) search existing GitHub issues and (b) copy a pre-filled bug report — version, platform, error, and the last ~10 KB of mouseterm.log — and open the new-issue page. A new read_update_log Tauri command exposes default_log_path() to the JS side so the report includes the sidecar/setup output that explains why pendingUpdate.install() rejected. Co-Authored-By: Claude Opus 4.7 (1M context) --- standalone/src-tauri/Cargo.lock | 2 +- standalone/src-tauri/src/lib.rs | 21 +++++ standalone/src/UpdateBanner.tsx | 29 +++++-- standalone/src/UpdateDebugDialog.tsx | 124 +++++++++++++++++++++++++++ standalone/src/main.tsx | 48 ++++++++++- standalone/src/updater.test.ts | 39 ++++++++- standalone/src/updater.ts | 72 +++++++++++++++- 7 files changed, 320 insertions(+), 15 deletions(-) create mode 100644 standalone/src/UpdateDebugDialog.tsx diff --git a/standalone/src-tauri/Cargo.lock b/standalone/src-tauri/Cargo.lock index f64ed23..3062329 100644 --- a/standalone/src-tauri/Cargo.lock +++ b/standalone/src-tauri/Cargo.lock @@ -1938,7 +1938,7 @@ dependencies = [ [[package]] name = "mouseterm" -version = "0.7.0" +version = "0.8.0" dependencies = [ "libc", "serde", diff --git a/standalone/src-tauri/src/lib.rs b/standalone/src-tauri/src/lib.rs index 44904ff..21c63da 100644 --- a/standalone/src-tauri/src/lib.rs +++ b/standalone/src-tauri/src/lib.rs @@ -88,6 +88,21 @@ fn append_log(message: impl AsRef) { } } +fn read_log_tail(max_bytes: usize) -> Result { + let path = default_log_path(); + let contents = std::fs::read_to_string(&path) + .map_err(|e| format!("read {}: {e}", path.display()))?; + if contents.len() <= max_bytes { + return Ok(contents); + } + // Slice on a char boundary so we never split a multi-byte sequence. + let start = contents.len() - max_bytes; + let start = (start..contents.len()) + .find(|&i| contents.is_char_boundary(i)) + .unwrap_or(contents.len()); + Ok(contents[start..].to_string()) +} + #[derive(Serialize, Deserialize, Clone)] struct PtySpawnOptions { cols: Option, @@ -239,6 +254,11 @@ fn read_clipboard_image_as_file_path( .and_then(|path| path.as_str().map(String::from))) } +#[tauri::command] +fn read_update_log() -> Result { + read_log_tail(10_000) +} + #[tauri::command] fn shutdown_sidecar(state: tauri::State<'_, SidecarState>) { let _ = state.tx.send(SidecarMsg::Shutdown); @@ -532,6 +552,7 @@ pub fn run() { get_available_shells, read_clipboard_file_paths, read_clipboard_image_as_file_path, + read_update_log, ]) .build(tauri::generate_context!()) .expect("error while building MouseTerm") diff --git a/standalone/src/UpdateBanner.tsx b/standalone/src/UpdateBanner.tsx index ad67b5a..34da0aa 100644 --- a/standalone/src/UpdateBanner.tsx +++ b/standalone/src/UpdateBanner.tsx @@ -5,38 +5,40 @@ export type UpdateBannerState = | { status: 'downloaded'; version: string } | { status: 'dismissed' } | { status: 'post-update-success'; from: string; to: string } - | { status: 'post-update-failure'; version: string }; + | { status: 'post-update-failure'; version: string; error?: string }; interface UpdateBannerProps { state: UpdateBannerState; onDismiss: () => void; onOpenChangelog: () => void; + onOpenDebug: () => void; } -export function UpdateBanner({ state, onDismiss, onOpenChangelog }: UpdateBannerProps) { +export function UpdateBanner({ state, onDismiss, onOpenChangelog, onOpenDebug }: UpdateBannerProps) { if (state.status === 'idle' || state.status === 'dismissed') return null; let message: string; - let showChangelog = false; + let action: 'changelog' | 'debug' | null = null; switch (state.status) { case 'downloaded': - message = `Update downloaded (v${state.version}) \u2014 will install when you quit.`; - showChangelog = true; + message = `Update downloaded (v${state.version}) — will install when you quit.`; + action = 'changelog'; break; case 'post-update-success': - message = `Updated to v${state.to} \u2014 from v${state.from}.`; - showChangelog = true; + message = `Updated to v${state.to} — from v${state.from}.`; + action = 'changelog'; break; case 'post-update-failure': - message = `Update to v${state.version} failed \u2014 will retry next launch.`; + message = 'Update failed.'; + action = 'debug'; break; } return ( {message} - {showChangelog && ( + {action === 'changelog' && ( + )} + + +
+
+

+ We couldn't install v{targetVersion}. The error was: +

+
+              {errorPreview || '(no error captured)'}
+            
+
+ +
+

1. Search existing reports

+

+ Someone may have already hit this — a quick search saves a duplicate report. +

+ +
+ +
+

2. File a new bug

+

+ Copy this report, then paste it into the new issue page. +

+