* fix: make PWA install and OpenCode update toasts dismissible
Both 'Install OpenChamber' and 'OpenCode update available' toasts use
duration: Infinity with no close affordance, so they persist on screen
until the user accepts (install/update) or reloads the tab. For users
who do not want to install the PWA or upgrade right now, this is
intrusive and there is no opt-out.
Add a Dismiss button (sonner cancel action) to both toasts. When the
user dismisses:
- PWA: persist a flag in localStorage so the prompt does not reappear
on future sessions. Accepting Install still works as before.
- OpenCode update: persist the dismissed version in localStorage. The
toast will appear again only when a newer version becomes available.
Adds new i18n keys pwa.installPrompt.dismiss and
opencodeUpdate.toast.actions.dismiss across all seven locales (en,
es, ko, pl, pt-BR, uk, zh-CN).
* test: extract toast dedup helpers and cover with 28 unit tests
Lift the dismissal-decision logic out of usePwaInstallPrompt and
OpenCodeUpdateToast into a React-free sibling module so it can be
unit-tested directly. The React surfaces remain sole owners of side
effects (storage writes, toast.info, event listeners); the new module
only answers 'should we show?'.
New module openCodeUpdateDedup.ts exposes four helpers:
- shouldShowPwaInstallToast(input) - three gates: dismissed,
sessionShown, hasActiveToast.
- shouldShowOpenCodeUpdateToast(input) - empty version, seen set,
dismissed===version gates; a different dismissed version lets the
toast resurface for the new release.
- resolveOpenCodeUpdateVersion(detail) - parses CustomEvent payloads
defensively (null/non-object/non-string -> '').
- resolveOpenCodeUpgradeStatusVersion(status) - parses upgrade status
payloads (status falsy / available!==true / latestVersion non-string
-> '').
Consumers now call the helpers and only run the side-effect when the
decision is true. Behaviour is unchanged.
Coverage: 28 tests via bun:test, 33 expects, all pass first try.