Skip to content

Commit be29aff

Browse files
committed
Crash reporting: post-review fixes
- Fix `serde(rename_all = "camelCase")` on `CrashReport` and `ActiveSettings` — frontend field access was silently broken - Settings loaded via `settings::load_settings()` instead of hardcoded defaults - Add `ai_provider` and `verbose_logging` to Rust `Settings` struct for crash reports - macOS thread count via Mach `task_threads()` API (was always 0) - Rename `settings/legacy.rs` → `loader.rs` — it's the correct architecture, not legacy - Crash report dialog: selectable text, copy button - Toast uses persistent dismissal (pane navigation was clearing it)
1 parent 33ec2f2 commit be29aff

6 files changed

Lines changed: 98 additions & 31 deletions

File tree

apps/desktop/src-tauri/src/crash_reporter/mod.rs

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ mod symbolicate;
1010
mod tests;
1111

1212
use crate::config;
13+
use crate::settings;
1314
use regex::Regex;
1415
use serde::{Deserialize, Serialize};
1516
use std::path::{Path, PathBuf};
@@ -70,8 +71,8 @@ pub fn init<R: tauri::Runtime>(app: &tauri::AppHandle<R>) {
7071
let crash_path = data_dir.join(CRASH_FILE_NAME);
7172
let raw_crash_path = data_dir.join(RAW_CRASH_FILE_NAME);
7273

73-
// Cache active settings from settings.json for crash reports
74-
cache_active_settings(&data_dir);
74+
// Cache active settings for crash reports, using the same loader as the rest of the app
75+
cache_active_settings(app);
7576

7677
// Process any pending crash file from a previous session
7778
process_pending_crash(&crash_path, &raw_crash_path);
@@ -462,20 +463,18 @@ fn process_pending_crash(crash_json_path: &Path, raw_crash_path: &Path) {
462463
}
463464
}
464465

465-
/// Read active settings from settings.json for crash report metadata.
466-
fn cache_active_settings(data_dir: &Path) {
467-
let settings_path = data_dir.join("settings.json");
468-
let settings = if let Ok(contents) = std::fs::read_to_string(&settings_path)
469-
&& let Ok(json) = serde_json::from_str::<serde_json::Value>(&contents)
470-
{
471-
ActiveSettings {
472-
indexing_enabled: json.get("indexing.enabled").and_then(|v| v.as_bool()),
473-
ai_provider: json.get("ai.provider").and_then(|v| v.as_str()).map(String::from),
474-
mcp_enabled: json.get("developer.mcpEnabled").and_then(|v| v.as_bool()),
475-
verbose_logging: json.get("developer.verboseLogging").and_then(|v| v.as_bool()),
476-
}
477-
} else {
478-
ActiveSettings::default()
466+
/// Cache active settings for crash reports, using the app's settings loader.
467+
/// This piggybacks on `settings::load_settings` so defaults stay in sync.
468+
/// Fields that are `None` in the settings struct mean "user hasn't changed this" —
469+
/// the frontend registry owns the defaults. We pass through `None` as-is; the crash
470+
/// report consumer can interpret null as "default."
471+
fn cache_active_settings<R: tauri::Runtime>(app: &tauri::AppHandle<R>) {
472+
let s = settings::load_settings(app);
473+
let settings = ActiveSettings {
474+
indexing_enabled: s.indexing_enabled,
475+
ai_provider: s.ai_provider,
476+
mcp_enabled: s.developer_mcp_enabled,
477+
verbose_logging: s.verbose_logging,
479478
};
480479
let _ = CACHED_SETTINGS.set(settings);
481480
}
@@ -492,7 +491,33 @@ fn uptime_secs() -> f64 {
492491
}
493492

494493
fn current_thread_count() -> usize {
495-
// Read /proc/self/status on Linux, use sysinfo on macOS as best effort
494+
#[cfg(target_os = "macos")]
495+
{
496+
// Mach API: get thread list for the current task, return the count.
497+
unsafe extern "C" {
498+
fn mach_task_self() -> libc::mach_port_t;
499+
}
500+
unsafe {
501+
let mut thread_list: libc::mach_port_t = 0;
502+
let mut thread_count: u32 = 0;
503+
let kr = libc::task_threads(
504+
mach_task_self(),
505+
std::ptr::addr_of_mut!(thread_list) as *mut *mut libc::mach_port_t,
506+
std::ptr::addr_of_mut!(thread_count) as *mut u32,
507+
);
508+
if kr == libc::KERN_SUCCESS {
509+
// Deallocate the thread list (we only needed the count)
510+
libc::vm_deallocate(
511+
mach_task_self(),
512+
thread_list as libc::vm_address_t,
513+
(thread_count as usize) * size_of::<libc::mach_port_t>(),
514+
);
515+
thread_count as usize
516+
} else {
517+
0
518+
}
519+
}
520+
}
496521
#[cfg(target_os = "linux")]
497522
{
498523
if let Ok(status) = std::fs::read_to_string("/proc/self/status") {
@@ -504,10 +529,8 @@ fn current_thread_count() -> usize {
504529
}
505530
0
506531
}
507-
#[cfg(not(target_os = "linux"))]
532+
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
508533
{
509-
// On macOS, getting thread count without sysinfo is non-trivial.
510-
// Return 0 rather than pulling in a heavy dependency just for this.
511534
0
512535
}
513536
}

apps/desktop/src-tauri/src/settings/CLAUDE.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ Thin read-only settings loader used during Rust startup. The frontend owns all s
66

77
| File | Purpose |
88
|---|---|
9-
| `mod.rs` | Re-exports `load_settings` from `legacy` |
10-
| `legacy.rs` | `Settings` struct + `load_settings`: reads `settings.json`, falls back to defaults |
9+
| `mod.rs` | Re-exports `load_settings` from `loader` |
10+
| `loader.rs` | `Settings` struct + `load_settings`: reads `settings.json`, falls back to defaults |
1111

1212
## Settings struct
1313

@@ -19,6 +19,8 @@ Settings {
1919
developer_mcp_port: Option<u16>,
2020
indexing_enabled: Option<bool>,
2121
crash_reports_enabled: Option<bool>, // from "updates.crashReports"
22+
ai_provider: Option<String>, // from "ai.provider", for crash reports
23+
verbose_logging: Option<bool>, // from "developer.verboseLogging", for crash reports
2224
}
2325
```
2426

@@ -43,7 +45,7 @@ These are top-level keys — the dot is part of the key name, not a nesting sepa
4345
## Key patterns
4446

4547
- **One-way read only.** This module never writes. All writes go through the frontend's settings store.
46-
- Module is named `legacy` because ideally the frontend would push relevant values via IPC at startup rather than requiring Rust to parse the store file directly.
48+
- Direct file reading is the correct design — multiple backend systems (MCP, indexing, crash reporter) need settings before the frontend loads.
4749
- `full_disk_access_choice` is marked `#[allow(dead_code)]` — it is persisted by the frontend but the backend takes no action on it.
4850
- Falls back gracefully: missing file → use `Default`.
4951

apps/desktop/src-tauri/src/settings/legacy.rs renamed to apps/desktop/src-tauri/src/settings/loader.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
//! Settings loading from tauri-plugin-store JSON file.
22
//!
33
//! Reads settings from the settings.json file created by tauri-plugin-store.
4-
//! Used to initialize app state (menu checkboxes, MCP config) on startup.
4+
//! The backend reads the file directly at startup so it can configure itself
5+
//! (MCP server, hidden files, indexing, crash reporter) before the frontend loads.
6+
//! The frontend owns all writes via tauri-plugin-store; this module is read-only.
57
68
use serde::Deserialize;
79
use std::fs;
@@ -38,9 +40,15 @@ pub struct Settings {
3840
#[serde(alias = "updates.crashReports", default)]
3941
#[allow(
4042
dead_code,
41-
reason = "Crash reporter reads settings.json directly via cache_active_settings()"
43+
reason = "Read by frontend; Rust Settings struct preserves it for crash reporter"
4244
)]
4345
pub crash_reports_enabled: Option<bool>,
46+
#[serde(alias = "ai.provider", default)]
47+
#[allow(dead_code, reason = "Included in crash reports for feature correlation")]
48+
pub ai_provider: Option<String>,
49+
#[serde(alias = "developer.verboseLogging", default)]
50+
#[allow(dead_code, reason = "Included in crash reports for feature correlation")]
51+
pub verbose_logging: Option<bool>,
4452
}
4553

4654
fn default_show_hidden() -> bool {
@@ -56,6 +64,8 @@ impl Default for Settings {
5664
developer_mcp_port: None,
5765
indexing_enabled: None,
5866
crash_reports_enabled: None,
67+
ai_provider: None,
68+
verbose_logging: None,
5969
}
6070
}
6171
}
@@ -100,6 +110,8 @@ fn parse_settings(contents: &str) -> Result<Settings, serde_json::Error> {
100110
let indexing_enabled = json.get("indexing.enabled").and_then(|v| v.as_bool());
101111

102112
let crash_reports_enabled = json.get("updates.crashReports").and_then(|v| v.as_bool());
113+
let ai_provider = json.get("ai.provider").and_then(|v| v.as_str()).map(String::from);
114+
let verbose_logging = json.get("developer.verboseLogging").and_then(|v| v.as_bool());
103115

104116
Ok(Settings {
105117
show_hidden_files,
@@ -108,5 +120,7 @@ fn parse_settings(contents: &str) -> Result<Settings, serde_json::Error> {
108120
developer_mcp_port,
109121
indexing_enabled,
110122
crash_reports_enabled,
123+
ai_provider,
124+
verbose_logging,
111125
})
112126
}
Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
//! Settings module for legacy settings loading.
1+
//! Settings module — loads settings from tauri-plugin-store's JSON file at startup.
22
3-
mod legacy;
3+
mod loader;
44

5-
// Re-export only what's used externally
6-
pub use legacy::load_settings;
5+
pub use loader::load_settings;

apps/desktop/src/lib/crash-reporter/CrashReportDialog.svelte

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,16 @@
1818
let detailsExpanded = $state(false)
1919
let alwaysSend = $state(false)
2020
let sending = $state(false)
21+
let copied = $state(false)
2122
2223
const reportJson = $derived(JSON.stringify(report, null, 2))
2324
25+
async function handleCopy() {
26+
await navigator.clipboard.writeText(reportJson)
27+
copied = true
28+
setTimeout(() => (copied = false), 2000)
29+
}
30+
2431
async function handleSend() {
2532
sending = true
2633
try {
@@ -79,7 +86,10 @@
7986

8087
{#if detailsExpanded}
8188
<div class="details-container">
82-
<pre class="details-json">{reportJson}</pre>
89+
<button class="copy-button" onclick={() => void handleCopy()}>
90+
{copied ? 'Copied' : 'Copy'}
91+
</button>
92+
<pre class="details-json" style="user-select: text">{reportJson}</pre>
8393
</div>
8494
{/if}
8595

@@ -142,13 +152,32 @@
142152
}
143153
144154
.details-container {
155+
position: relative;
145156
margin-bottom: var(--spacing-md);
146157
border: 1px solid var(--color-border-strong);
147158
border-radius: var(--radius-md);
148159
max-height: 200px;
149160
overflow-y: auto;
150161
}
151162
163+
.copy-button {
164+
position: sticky;
165+
top: var(--spacing-xs);
166+
float: right;
167+
margin: var(--spacing-xs) var(--spacing-xs) 0 0;
168+
padding: var(--spacing-xxs) var(--spacing-sm);
169+
font-size: var(--font-size-xs);
170+
color: var(--color-text-secondary);
171+
border: 1px solid var(--color-border);
172+
border-radius: var(--radius-sm);
173+
cursor: default;
174+
z-index: 1;
175+
}
176+
177+
.copy-button:hover {
178+
color: var(--color-text-primary);
179+
}
180+
152181
.details-json {
153182
margin: 0;
154183
padding: var(--spacing-sm) var(--spacing-md);

apps/desktop/src/routes/(main)/+layout.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@
119119
addToast(CrashReportToastContent, {
120120
id: 'crash-report-sent',
121121
level: 'info',
122-
timeoutMs: 6000,
122+
dismissal: 'persistent',
123123
})
124124
crashLog.info('Crash report auto-sent')
125125
} catch (e) {

0 commit comments

Comments
 (0)