Problem
In src/renderer/src/providers/clips/storage.ts, a failure to load stored history is handled carefully: the catch at line 95 sets loadError, Clips.tsx:87 renders <LoadFailedBanner>, and saving stays disabled for the session so blank state is never written over the real file. The doc comment on the hook spells this contract out.
A failure to save gets none of that. Both save paths swallow the error into the console and return:
storage.ts:154 — catch (error) { console.error('Failed to save clips to storage:', error); }, the debounced storageSaveClips that persists the clip history and lock state.
storage.ts:174 — the same shape for storageSaveSettings({ maxClips }).
Nothing in the window changes. For an app whose whole premise is that it quietly remembers everything you copy, that is the worst failure to hide: if the encrypted store is unwritable (disk full, permissions, a keystore hiccup, the main process wedged), the user keeps copying all day, sees a full, healthy-looking clip list, and discovers on next launch that none of it survived. Locked clips — the ones deliberately marked as worth keeping — are lost the same way. The maxClips case is quieter but equally confusing: change the limit in Settings, it appears to take, and it is back to the old value after a restart.
The pieces to fix it are already in place. useToast is imported and called in this provider's own parent, src/renderer/src/providers/clips/index.tsx:152, and LoadFailedBanner already exists for the persistent case.
Proposal
Surface save failures in the UI, in one reviewable change:
- Have
useClipsStorage track a save failure alongside loadError and return it — e.g. saveError, set when storageSaveClips or storageSaveSettings rejects, and cleared on the next save that succeeds. Use the existing errorText helper for the message.
- Show it. A toast on the first failure ("Clips could not be saved — ") plus a persistent indicator while the condition lasts reads best: a toast alone can be missed, and the debounced save retries on every change, so the state can persist for a long time. Reusing/parameterising
LoadFailedBanner for a "not saving" variant keeps this to one banner component.
- Do not disable anything or drop clips. Unlike the load path, a failed save is safe to keep retrying — the in-memory list is still the truth. Just make sure repeated failures do not spam a toast per debounce tick (only notify on transitions into the failing state).
Out of scope: retry/backoff policy in the main process, changes to the storage format or encryption, and anything about the load path, which already behaves correctly.
Value
Anyone whose clip store becomes unwritable finds out while they can still act on it — copy the important thing elsewhere, free disk space, restart the app — rather than after a restart has taken the history with it. You would know it worked by making storageSaveClips reject (a mocked IPC failure in a unit test, or a read-only store directory by hand) and seeing the app say so instead of only the devtools console.
Problem
In
src/renderer/src/providers/clips/storage.ts, a failure to load stored history is handled carefully: the catch at line 95 setsloadError,Clips.tsx:87renders<LoadFailedBanner>, and saving stays disabled for the session so blank state is never written over the real file. The doc comment on the hook spells this contract out.A failure to save gets none of that. Both save paths swallow the error into the console and return:
storage.ts:154—catch (error) { console.error('Failed to save clips to storage:', error); }, the debouncedstorageSaveClipsthat persists the clip history and lock state.storage.ts:174— the same shape forstorageSaveSettings({ maxClips }).Nothing in the window changes. For an app whose whole premise is that it quietly remembers everything you copy, that is the worst failure to hide: if the encrypted store is unwritable (disk full, permissions, a keystore hiccup, the main process wedged), the user keeps copying all day, sees a full, healthy-looking clip list, and discovers on next launch that none of it survived. Locked clips — the ones deliberately marked as worth keeping — are lost the same way. The
maxClipscase is quieter but equally confusing: change the limit in Settings, it appears to take, and it is back to the old value after a restart.The pieces to fix it are already in place.
useToastis imported and called in this provider's own parent,src/renderer/src/providers/clips/index.tsx:152, andLoadFailedBanneralready exists for the persistent case.Proposal
Surface save failures in the UI, in one reviewable change:
useClipsStoragetrack a save failure alongsideloadErrorand return it — e.g.saveError, set whenstorageSaveClipsorstorageSaveSettingsrejects, and cleared on the next save that succeeds. Use the existingerrorTexthelper for the message.LoadFailedBannerfor a "not saving" variant keeps this to one banner component.Out of scope: retry/backoff policy in the main process, changes to the storage format or encryption, and anything about the load path, which already behaves correctly.
Value
Anyone whose clip store becomes unwritable finds out while they can still act on it — copy the important thing elsewhere, free disk space, restart the app — rather than after a restart has taken the history with it. You would know it worked by making
storageSaveClipsreject (a mocked IPC failure in a unit test, or a read-only store directory by hand) and seeing the app say so instead of only the devtools console.