You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Friendly, provider-aware error pane for listing failures
- Replace raw "I/O error (os error 60)" with structured `ErrorPane` that shows a warm title, plain-language explanation, provider-specific suggestions, and collapsible technical details
- Two-layer Rust mapping: 37 macOS errno codes → `FriendlyError` (Transient/NeedsAction/Serious categories), then path-based enrichment for 19 cloud/mount providers (Dropbox, Google Drive, OneDrive, iCloud, MacDroid, macFUSE/SSHFS, VeraCrypt, and more)
- Provider detection uses `~/Library/CloudStorage/` prefixes, `~/Library/Mobile Documents/`, specific `/Volumes/` paths, and `statfs` `f_fstypename` for FUSE mounts
- `VolumeError::IoError` now carries `raw_os_error: Option<i32>` so errno survives the `From<std::io::Error>` conversion
- `streaming.rs` propagates `VolumeError` directly (instead of wrapping in `std::io::Error`) and builds `FriendlyError` at the emission site
- Frontend `ErrorPane.svelte` replaces both `PermissionDeniedPane` and the raw error div. Category-based visual tone, markdown rendering via `snarkdown`, retry button with attempt history for transient errors, "Open System Settings" for permission denied
- E2E testability: `inject_error()` on `Volume` trait behind `playwright-e2e` feature flag, with Playwright tests for transient/needs-action/a11y
- Removed `@lottiefiles/dotlottie-svelte` (only consumer was deleted `PermissionDeniedPane`)
|`friendly_error.rs`| User-facing error messages. See [Friendly error system](#friendly-error-system) below. |
14
15
|`manager.rs`|`VolumeManager` — thread-safe `RwLock<HashMap>` registry; supports a default volume |
15
16
|`local_posix.rs`|`LocalPosixVolume` — real filesystem; delegates listing to `file_system::listing`, indexing to `indexing::scanner`, watching to `indexing::watcher` (FSEvents), copy scanning via `walkdir`. Uses `libc::statvfs` FFI for space info. |
16
17
|`mtp.rs`|`MtpVolume` — MTP device storage; synchronous `Volume` trait bridged to async MTP calls via `tokio::runtime::Handle::block_on`. Gated with `#[cfg(any(target_os = "macos", target_os = "linux"))]`. |
@@ -71,6 +72,99 @@ Both paths check the `network.directSmbConnection` setting (global `AtomicBool`)
71
72
warning and the volume stays as `LocalPosixVolume`. The "Connect directly" UI action (`upgrade_to_smb_volume` command)
72
73
provides a manual upgrade path.
73
74
75
+
## Friendly error system
76
+
77
+
`friendly_error.rs` turns raw OS errors into warm, actionable messages so the user feels supported when something goes
78
+
wrong. This is one of Cmdr's UX differentiators: where other file managers show "I/O error: Operation timed out (os
79
+
error 60)", we show a friendly title, a plain-language explanation, and provider-specific advice ("This folder is managed
80
+
by **MacDroid**. Here's what to try: ...").
81
+
82
+
### Philosophy
83
+
84
+
**The user should never feel alone with a broken state.** Every error message should feel like the app is putting its
85
+
hand on the user's shoulder and saying "Here's what happened, and here's what you can do." We go above and beyond: we
86
+
detect which cloud provider or mount tool manages the path, and tailor the suggestion to that specific app. A timeout on
87
+
a Dropbox folder gets different advice than a timeout on an SSHFS mount.
88
+
89
+
Power users also need the raw details (errno name, code) for debugging or bug reports. These are available in a
90
+
collapsible "Technical details" section, never hidden but never in your face either.
91
+
92
+
### Architecture
93
+
94
+
Two-layer mapping, both in this file:
95
+
96
+
1.**`friendly_error_from_volume_error(err, path)`** — maps `VolumeError` variants and macOS errno codes (37 codes) to a
97
+
`FriendlyError` with category (Transient/NeedsAction/Serious), title, explanation, suggestion, and raw detail.
98
+
2.**`enrich_with_provider(error, path)`** — detects 19 cloud/mount providers from path patterns and `statfs` filesystem
99
+
type, then overwrites the suggestion with provider-specific advice.
100
+
101
+
The frontend receives the fully-baked `FriendlyError` struct via the `listing-error` Tauri event and renders it with
102
+
category-based visual styling. The frontend never sees errno codes or does OS-specific logic.
103
+
104
+
### Adding a new error message
105
+
106
+
When you need to handle a new errno or `VolumeError` variant:
107
+
108
+
1. Add the match arm in `friendly_error_from_volume_error`
109
+
2. Pick the right `ErrorCategory`: **Transient** (retry might work), **NeedsAction** (user must do something),
110
+
**Serious** (something is genuinely broken)
111
+
3. Write the message following the rules below
112
+
4. Add a unit test asserting the category and that the text follows the style rules
113
+
5. Run the existing `error_messages_never_contain_error_or_failed` test to catch violations
114
+
115
+
### Adding a new provider
116
+
117
+
When a new cloud storage or mount tool becomes popular enough to detect:
118
+
119
+
1. Add a variant to the `Provider` enum with `display_name()` and `app_name()`
120
+
2. Add path detection in `detect_provider` (CloudStorage prefix, specific path, or `statfs` type)
121
+
3. Write provider-specific suggestions in `provider_suggestion` for each `ErrorCategory`
122
+
4. Add a unit test for path detection and suggestion content
123
+
5. Update `volumes/CLAUDE.md` provider table to keep the two lists in sync
124
+
125
+
### Writing rules for error messages
126
+
127
+
These are non-negotiable. The existing test suite enforces some of them automatically.
128
+
129
+
-**NEVER use "error" or "failed"** in titles, explanations, or suggestions. Say "Couldn't read" not "Read error". The
130
+
automated test `error_messages_never_contain_error_or_failed` catches this.
131
+
-**Active voice, contractions**: "Cmdr couldn't..." not "The operation was unable to..."
132
+
-**No trivializing**: no "just", "simply", "easy", "all you have to do"
133
+
-**No permissive language**: "Check your connection" not "You might want to check..."
134
+
-**Direct and warm**: "Here's what to try:" not "Please attempt the following remediation steps:"
135
+
-**No em dashes**: use parentheses, commas, or new sentences
136
+
-**Sentence case in titles**: "Connection timed out" not "Connection Timed Out"
137
+
-**Bold key terms** with `**` only when it helps scanning (for example, provider names)
138
+
-**Platform-native terms**: "System Settings" on macOS, "Finder", "Trash"
139
+
-**Keep it short**: max two sentences for explanation, bullets for suggestions
140
+
141
+
Good example:
142
+
```
143
+
title: "Connection timed out"
144
+
explanation: "Cmdr tried to read this folder but the connection didn't respond in time."
145
+
suggestion: "Here's what to try:\n- Check that the device or server is reachable\n- ..."
146
+
```
147
+
148
+
Bad example (every rule violated):
149
+
```
150
+
title: "I/O Error: Operation Timed Out" // "Error", Title Case
151
+
explanation: "An error occurred while the system attempted to access the directory." // passive, "error"
152
+
suggestion: "You may want to try simply reconnecting the device." // permissive, trivializing
153
+
```
154
+
155
+
### Provider detection strategies
156
+
157
+
| Strategy | Providers covered |
158
+
|---|---|
159
+
|`~/Library/CloudStorage/<Prefix>*`| Dropbox, GoogleDrive, OneDrive, Box, pCloud, Nextcloud, SynologyDrive, Tresorit, ProtonDrive, Sync, Egnyte, MacDroid, plus a generic fallback for unrecognized providers |
The `statfs` check runs only at error time (not on every listing), so the syscall cost is negligible.
167
+
74
168
## Integration status
75
169
76
170
`LocalPosixVolume` is wired into the indexing subsystem. `VolumeManager` is actively used.
@@ -93,7 +187,13 @@ provides a manual upgrade path.
93
187
**Why**: The `Volume` trait is synchronous because local filesystem ops are blocking and shouldn't touch the async executor. MTP operations are inherently async (USB bulk transfers), so `block_on` bridges the gap. This is safe because MTP methods are always called from `spawn_blocking` contexts (separate OS thread pool), avoiding nested-runtime panics.
94
188
95
189
**Decision**: `VolumeError` stores `String` messages, not the original `std::io::Error`
96
-
**Why**: `std::io::Error` is not `Clone`, but `VolumeError` needs to be `Clone` for ergonomic error propagation across thread boundaries and for serialization to the frontend. Storing the formatted message loses the original error type but keeps the information that matters for user-facing error messages.
190
+
**Why**: `std::io::Error` is not `Clone`, but `VolumeError` needs to be `Clone` for ergonomic error propagation across thread boundaries and for serialization to the frontend. Storing the formatted message loses the original error type but keeps the information that matters for user-facing error messages. The `IoError` variant also carries `raw_os_error: Option<i32>` so the friendly error mapper can match on platform-specific errno codes.
191
+
192
+
**Decision**: Friendly error mapping is two layers: errno mapping, then provider enrichment
193
+
**Why**: Not every provider+errno combination needs custom copy. The base errno message is always useful. Provider enrichment is additive, making the suggestion more specific when we recognize who manages the mount. Keeping them separate avoids a combinatorial explosion of messages.
194
+
195
+
**Decision**: Friendly error mapping lives in Rust, not the frontend
196
+
**Why**: The mapping needs access to the full path (for provider detection) and platform-specific errno codes. Doing it in Rust keeps the frontend thin (principle: smart backend, thin frontend) and avoids duplicating errno knowledge in TypeScript. The frontend receives a ready-to-render `FriendlyError` struct with markdown strings.
97
197
98
198
**Decision**: `LocalPosixVolume` uses `symlink_metadata` for `exists()` instead of `Path::exists()`
99
199
**Why**: `Path::exists()` follows symlinks — a dangling symlink returns `false`, which would make the volume claim a file doesn't exist when it visibly does in a directory listing. `symlink_metadata` detects the symlink itself, matching what the user sees.
@@ -150,6 +250,7 @@ provides a manual upgrade path.
150
250
151
251
## Testing
152
252
253
+
-**E2E error injection**: The `Volume` trait has an `inject_error(&self, errno: i32)` method behind the `playwright-e2e` feature flag. `LocalPosixVolume` and `InMemoryVolume` implement it — the next `list_directory` call returns the injected errno, then clears it (single-shot, so retry tests work). Default is no-op.
153
254
-`in_memory_test.rs` — unit tests for `InMemoryVolume` (CRUD, sorting, concurrency, stress 50k entries)
0 commit comments