Skip to content

feat(ui): offer the backend picker when secret storage is unreachable - #5

Merged
starvy merged 1 commit into
mainfrom
feat/secret-backend-recovery
Jul 21, 2026
Merged

feat(ui): offer the backend picker when secret storage is unreachable#5
starvy merged 1 commit into
mainfrom
feat/secret-backend-recovery

Conversation

@starvy

@starvy starvy commented Jul 21, 2026

Copy link
Copy Markdown
Owner

Closes the dead end left by the switchable-backends work: a machine with no Secret Service hit a
failed send, got a correct "OS keychain secret storage is unavailable" error, and had no indication
that a setting existed to change it. Switchable backends only help if you can find the switch.

What changed

An unreachable backend — as distinct from a missing entry — now offers a Secret storage…
button that opens App Settings:

  • the send-failure card (the surface where the error is actually read)
  • the response footer, beside Retry (covers a failure while on a non-Body tab)
  • the toasts from the Secrets panel and the Auth tab's save

Only that case gets it. A secret that simply isn't set still just says so, because there the fix is
to set a value, not to switch backend. That distinction is the NotFound vs Unavailable split
added earlier.

The stringly-typed bit, and why

posel_template::SecretResolver returns Result<String, String>, so by the time a send-time
failure reaches the UI its type is gone. Threading a typed error through a headless trait used by
every protocol is a bigger change than this warrants, so secret_backend_unavailable(&str) matches
the message — with a test that asserts a real Unavailable error is detected both through
to_string() and through the resolver's own String conversion. Reword the #[error] attribute and
that test fails, rather than the recovery route silently disappearing.

Two things found while building it

The footer clipped mid-word. It pasted the entire error into a 38px status bar, so it collided
with whatever sat to its right. Pre-existing — the new button just made it obvious.
format::status_summary now takes the first line and caps it on a word break (char-boundary safe,
tested with multi-byte input); the full text stays in the failure card and the hover tooltip. The
right-hand group also gains a margin: overflow clips at the padding box, so padding on the left
group couldn't produce that gap.

before Failed · secret 'demo_token' could nSecret storage…
after Failed · secret 'demo_token' couldSecret storage… Retry

Focusing the picker was tried and reverted. My original pitch said it would "jump straight to
the picker". It doesn't — it opens the page. Focusing the control without scrolling to it is worse
than not focusing: the settings page has no scroll anchor, so focus lands off-screen and a keypress
opens a dropdown the user can't see. Verified that empirically, then backed it out. Doing it
properly means giving the settings page a ScrollHandle and splitting its single scroll child into
addressable sections — a real change, not a drive-by, and not something to land unreviewed right
before a publish. Left as a follow-up with a note in the code.

Verification

  • cargo fmt --all --check, cargo clippy --workspace --all-targets -- -D warnings,
    cargo test --workspace404 passed, 0 failed (+3)
  • Windows cross-compile (x86_64-pc-windows-gnu, clippy -D warnings) clean
  • Driven live under Xvfb on a box with no Secret Service: reproduced the first-run failure, saw the
    button on both surfaces, clicked it and confirmed App Settings opens
  • False-positive guard: a send that succeeds via the env backend shows no picker

A machine with no Secret Service (headless Linux, container, CI) hit a dead
end: the send failed, the error correctly said the keychain was unavailable,
and nothing told the user that a setting exists to change it. Switchable
backends only help if you can find the switch.

An unreachable backend — as opposed to a missing entry — now carries a
"Secret storage…" button to App Settings, on the send-failure card, the
response footer, and the toasts raised by the Secrets panel and the Auth
tab's save. Only that case gets it: a secret that simply isn't set still
just says so, because there the fix is to set a value.

`SecretResolver` returns Result<String, String>, so a send-time failure
reaches the UI with its type long gone. Rather than thread a typed error
through a headless trait used by every protocol, `secret_backend_unavailable`
matches the message, and a test asserts a real Unavailable error is detected
through both `to_string()` and the resolver's own String conversion — so
rewording the Display can't silently remove the recovery route.

Two things surfaced while building it:

- The footer pasted the whole error into a 38px bar, so it clipped mid-word
  against whatever sat beside it (pre-existing; the new button made it
  obvious). `format::status_summary` takes the first line and caps it on a
  word break, char-boundary safe; the full text stays in the card and the
  hover tooltip. The right-hand group also gains a margin — overflow clips at
  the padding box, so padding on the left group couldn't create that gap.

- Focusing the picker on arrival was tried and reverted: the settings page
  doesn't scroll-anchor, so focus landed on an off-screen control where a
  keypress would open an invisible dropdown. The button opens the page;
  scrolling to the section needs a ScrollHandle and addressable sections,
  which is a real change rather than a drive-by.
@starvy
starvy merged commit 21b3afe into main Jul 21, 2026
0 of 3 checks passed
@starvy
starvy deleted the feat/secret-backend-recovery branch July 21, 2026 18:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant