Skip to content

Surface save failures in the UI - #170

Open
dantheuber wants to merge 7 commits into
mainfrom
freight/feature-surface-save-failures
Open

dantheuber wants to merge 7 commits into
mainfrom
freight/feature-surface-save-failures

Conversation

@dantheuber

@dantheuber dantheuber commented Sep 21, 2026 •

Copy link
Copy Markdown
Owner

This branch assembles the listed issues for review.

Issues in this consist

Closes #169

What & why

Debounced clip and settings saves that failed were only ever logged to the devtools console, so an unwritable store looked healthy until the app was relaunched. This surfaces those failures in the window itself: a toast on the transition into a failing state, plus a persistent banner for as long as the failure lasts.

Changes

  • useClipsStorage now reports saveError alongside the existing loadError, tracking clip and settings save failures independently so one succeeding can't mask the other failing.
  • The clips provider toasts "Clips could not be saved" (with the reason) on the transition into a failing state, gated by a ref so repeated failures from the debounced retry don't spam duplicate toasts; the gate resets once a save lands so a later failure is announced again.
  • Clips.tsx renders the failure via a new StorageBanner component that makes the precedence explicit: an unreadable history banner wins over a refused-save banner, since the load-failure path also pauses saving. LoadFailedBanner and SaveFailedBanner share one StorageFailedBanner shell (title, lines, reason).
  • README updated to document the new save-failure banner and toast.
  • Version bumped to 2.4.0 (minor) — new user-facing capability, no breaking changes.

Review notes

  • Nothing is disabled and no clips are dropped on a save failure; the in-memory list remains the source of truth and the next successful save clears the reported error.
  • Worth double-checking the toast de-dupe ref logic in the provider and the banner precedence rule in Clips.tsx.
  • Tests added for the storage hook, provider, and list component; full suite is green on this branch.

@github-actions

github-actions Bot commented Sep 21, 2026 •

Copy link
Copy Markdown

📊 Coverage Report

Metric Coverage
Statements 94.01%
Branches 93.56%
Functions 94.45%
Lines 94.28%

@github-actions

Copy link
Copy Markdown

🧪 E2E Test Results

✅ 38 passed, 0 failed, 0 skipped

Test Suite Result
Analytics consent ✅
App Launch ✅
Clipboard ✅
Context Menu ✅
Image Clipboard ✅
Quick Clips ✅
Quick look ✅
Quick look — clipboard writes ✅
Quick look — rendered html ✅
Settings window ✅
Theme ✅
Settings — Tools tab ✅
Quick look — pattern scanning ✅
Quick look — clip templates ✅

Issue #169. The debounced clip and settings saves swallowed their errors
into the console, so an unwritable store looked healthy until the next
launch. The hook now returns saveError alongside loadError: the two save
paths report independently and the list shows the first still failing, so
a settings write that lands cannot mask a refused clip write. Saving is
not disabled and no clips are dropped - the in-memory list is still the
truth, and the next successful save clears the report.

Files: providers/clips/storage.ts, storage.test.tsx. UI surfacing (toast
plus banner) follows in the next commit.
Issue #169. A save that never lands now shows in the window instead of
only the devtools console: the provider toasts "Clips could not be saved"
with the reason on the transition into the failing state, and the list
carries a persistent banner for as long as it lasts, since the debounced
save retries on every change and a toast is easy to miss.

Key decisions: LoadFailedBanner and the new SaveFailedBanner share one
StorageFailedBanner shell (title, lines, reason) rather than a second
component; the load banner wins when both are set, because that path
pauses saving and its copy says so. A ref gates the toast so repeated
failures cannot spam one per debounce tick, and it resets once a save
lands so a later failure is announced again. Nothing is disabled and no
clips are dropped.

Files: components/clips/Clips.tsx (+test), providers/clips/index.tsx
(+test), providers/clips/types.ts. Note for a resumed run: the provider
test drives repeat saves through the settings-update listener, and
several providers register one, so it fans the update out to all of them.
Replace the ternary-and-&& expression in the list render with a small
StorageBanner component that spells out the precedence between an
unreadable history and a refused save. Build the load banner's lines
with a plain conditional instead of a spread, and note the new save
failure banner and toast in the README.
@cb-jeeves
cb-jeeves marked this pull request as ready for review September 21, 2026 01:34
@cb-jeeves cb-jeeves added the Jeeves-Review Jeeves: request a skeptical review of this issue label Sep 21, 2026

@cb-jeeves cb-jeeves left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adds a save-failure path to useClipsStorage and a SaveFailedBanner plus toast in the clips window, with the banner choice pulled into its own StorageBanner component. The UI work is sound, but the feature does not fire in the shipped app: the main-process save wrappers swallow their errors and resolve false, so the renderer's catch never runs. That is the one blocking item. The other two are worth a look while you are in these files.

All four lenses ran this round.

The submit pass ran on claude-opus-5 after its primary model failed.

The correctness pass ran on claude-opus-5 after its primary model failed; maintainability pass ran on claude-opus-5 after its primary model failed; ux-dx pass ran on claude-opus-5 after its primary model failed.

Comment thread src/renderer/src/providers/clips/storage.ts
Comment thread src/renderer/src/components/clips/Clips.tsx
Comment thread src/renderer/src/providers/clips/storage.ts Outdated
saveClips and saveSettings in the main process caught every error and
resolved false, so the renderer's catch never ran in the shipped app and
the save-failed banner could not appear. They now log and rethrow like
exportData and importData, so the handler rejects and the storage
error's own message reaches the window.

Electron wraps a rejected handler as "Error invoking remote method
'channel': Error: reason"; errorText strips that wrapper so banners and
toasts show only the reason.
A failed settings save used to render the clip-loss copy even though
every clip was on disk. saveError now carries its source, the list shows
settings-specific lines for a refused clip limit, and the toast title
follows suit. The hook keeps the two outcomes in one state value with a
functional update instead of a ref mirrored into state.
@cb-jeeves

Copy link
Copy Markdown
Collaborator

Responded to review round 1, pushed 5eca56d:

  • Addressed main-process save wrappers swallowing errors: saveClips and saveSettings in storage-integration.ts now log and rethrow, so the IPC handlers reject and the renderer catch runs with the real reason. errorText strips the "Error invoking remote method" wrapper Electron adds. Tests cover the wrapper rejecting and the hook receiving the Electron-shaped rejection.
  • Addressed settings failure showing clip-loss copy: saveError carries its source, the list shows settings-specific lines for a refused clip limit, and the toast title follows the source.
  • Addressed ref mirrored into state: one useState value with a functional update, saveError derived with useMemo.

@cb-jeeves cb-jeeves left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The refused-save path now rejects through to the renderer and the banner names the file, which closes the blocking item from round 1. No further findings.

The submit pass ran on claude-opus-5 after its primary model failed.

The correctness pass ran on claude-opus-5 after its primary model failed; maintainability pass ran on claude-opus-5 after its primary model failed; ux-dx pass ran on claude-opus-5 after its primary model failed.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Jeeves-Review Jeeves: request a skeptical review of this issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Surface clip and settings save failures in the UI

2 participants