feat(ui): add PWA standalone reload button and pull-to-refresh gesture#2548
Conversation
Adds a reload button to the app titlebar visible only in PWA standalone
or fullscreen mode, and a pull-to-refresh gesture on the messages container
that smooth-scrolls to the top before activating.
The reload button sits next to the message count label and provides a
one-tap refresh for users who installed the WebUI as a home-screen app
where browser navigation controls are unavailable.
The pull-to-refresh gesture detects downward drag at the top of the
message list, shows a visual indicator ('Pull to refresh' / 'Release to refresh'),
and reloads on release past the 80px threshold. When triggered mid-conversation,
it smooth-scrolls to the top first.
Phase 0 fit + UX gate — agent-review queued, applying
|
4-Viewport Gate Evidence ✅Screenshots uploaded to Reload Button390px (mobile): light · light:hover · dark · dark:hover Pull-to-Refresh Active390px: light · dark Full page idle (dark, 390px)Notes
|
UX gate review on viewport evidence — clears the 4-viewport bar; one concrete code-level concernRead the PR head ( What the screenshots confirmThe reload button at all 4 widths × 2 themes × hover/idle renders cleanly. The One behavioral concern that screenshots can't captureReading the touchmove handler at el.addEventListener('touchmove',function(e){
if(_ptrState!==1) return;
_ptrCurrentY=e.touches[0].clientY;
const pull=_ptrCurrentY-_ptrStartY;
if(pull<0){ _ptrReset(); return; }
/* If not at the top, smooth-scroll to top first.
Next pull gesture will trigger the refresh. */
if(el.scrollTop>0){
el.scrollTo({top:0,behavior:'smooth'});
_ptrReset();
return;
}
const progress=Math.min(pull/THRESHOLD,1);
_ptrUpdate(progress);
_ptrState=progress>=1?2:1;
if(progress>0.3) e.preventDefault();
},{passive:false});The Two ways to make this feel intentional rather than accidental:
Both are 5-line tweaks. Neither blocks the PR if the PWA pool is mostly people who restart from a fresh open, but it's worth a paragraph in the PR body explaining the chosen tradeoff for the next reviewer. Other things that look fine
Lifecycle questionWhat happens to the PTR listeners if a user installs the PWA mid-session and Otherwise UX gate clears. I'm not seeing reasons to block. The velocity/distance behavior is the only thing worth a small follow-up commit before this routes into a batch. |
71c7035
… 0.51.92) (#560) This PR contains the following updates: | Package | Update | Change | |---|---|---| | [ghcr.io/nesquena/hermes-webui](https://github.com/nesquena/hermes-webui) | patch | `0.51.90` → `0.51.92` | --- ### Release Notes <details> <summary>nesquena/hermes-webui (ghcr.io/nesquena/hermes-webui)</summary> ### [`v0.51.92`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v05192--2026-05-19--Release-BP-stage-385--7-PR-full-sweep-batch--RFC-Slice-3c-clarification--workspace-tree-icon-alignment--project-move-cache-refresh--auto-compression-handoff-metadata--Grok-OAuth-provider-catalog--anonymous-custom-endpoint-picker-fallback--PWA-standalone-reload--pull-to-refresh) [Compare Source](nesquena/hermes-webui@v0.51.91...v0.51.92) ##### Fixed - **PR [#​2563](nesquena/hermes-webui#2563 by [@​Michaelyklam](https://github.com/Michaelyklam) (closes [#​2554](nesquena/hermes-webui#2554)) — Align workspace-tree file rows with sibling directory rows by reserving the same expand/collapse toggle slot for files via a new `.file-tree-toggle-placeholder` element. Expanded directories now show child files stepped in at the same icon column as child folders. Directory toggles and file interactions are unchanged; source-level regression coverage and before/after PNGs included. - **PR [#​2561](nesquena/hermes-webui#2561 by [@​nanookclaw](https://github.com/nanookclaw) (closes [#​2551](nesquena/hermes-webui#2551)) — Refresh the authoritative `_allSessions` cache when the project picker moves a session to/from a project. Previous code mutated only the shallow sidebar row copy, so `renderSessionListFromCache()` re-read the unchanged cache and repainted a stale project dot until the next `/api/sessions` poll healed the UI. Both the "Removed from project" and "Moved to <project>" branches now write the new `project_id` into `_allSessions[idx]` before re-rendering. - **PR [#​2567](nesquena/hermes-webui#2567 by [@​dso2ng](https://github.com/dso2ng) (refs [#​2477](nesquena/hermes-webui#2477)) — Surface automatic-compression handoff metadata through the `compressed` SSE event so the active browser stream keeps its completion card even after the backend rotates the session id from the origin to a compressed continuation. The event now carries both `old_session_id` and `new_session_id`/`continuation_session_id`; the frontend `compressed` listener accepts either, and the automatic-compression detail line names the compressed continuation session so the done state isn't silently dropped. - **PR [#​2568](nesquena/hermes-webui#2568 by [@​Michaelyklam](https://github.com/Michaelyklam) (closes [#​2545](nesquena/hermes-webui#2545)) — Add the Hermes Agent `xai-oauth` provider to the WebUI's OAuth provider catalog so Grok OAuth accounts authenticated via the Hermes CLI appear in Settings → Providers and the `/api/models` picker. The provider is treated as CLI-managed OAuth (no WebUI API-key form) and uses the live Hermes CLI model catalog when available with a Grok 4.20 static fallback. - **PR [#​2550](nesquena/hermes-webui#2550 by [@​espokaos-ops](https://github.com/espokaos-ops) (refs [#​2542](nesquena/hermes-webui#2542)) — Keep anonymous custom OpenAI-compatible endpoints in the model picker even when the configured `/v1/models` probe fails. Lightweight relays and llama-server-style deployments that authenticate `/v1/chat/completions` but not `/v1/models` no longer have their provider group silently dropped from the picker. Users can type a model id manually in the free-form input when no live catalog is available. ##### Added - **PR [#​2548](nesquena/hermes-webui#2548 by [@​espokaos-ops](https://github.com/espokaos-ops) — Add a PWA-standalone reload affordance. A small refresh button appears in the app titlebar (visible only under `@media (display-mode: standalone), (display-mode: fullscreen)`) so users running the WebUI as an installed home-screen PWA can reload without re-launching the app. Adds a complementary pull-to-refresh gesture on the messages container with an 80px threshold and a smooth-scroll-to-top guard so accidental triggers while reading history feel intentional. 4-viewport screenshots (390/1280/1440/1920, light/dark, hover/idle) included under `docs/pr-media/2548/`. ##### Documentation - **PR [#​2560](nesquena/hermes-webui#2560 by [@​Michaelyklam](https://github.com/Michaelyklam) (refs [#​1925](nesquena/hermes-webui#1925)) — Clarify the RuntimeAdapter Slice 3c state after [#​2544](nesquena/hermes-webui#2544) shipped. The RFC now distinguishes shipped `/api/goal` routing through `RuntimeAdapter.update_goal(...)` from the still-staged `queue_message(...)` protocol method, and explicitly warns not to add a new server-side queue endpoint or queue scheduler merely for adapter symmetry while `/queue` remains browser-side queue/drain behavior. ### [`v0.51.91`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v05191--2026-05-18--Release-BO-stage-384--5-PR-full-sweep-batch--reasoning-replay-history-fix--archive-extract-per-session-inbox--fallback-streaming-warnings--sanitized-custom-provider-env-hints--Slice-3c-queuegoal-adapter-routing) [Compare Source](nesquena/hermes-webui@v0.51.90...v0.51.91) ##### Fixed - **PR [#​2536](nesquena/hermes-webui#2536 by [@​Michaelyklam](https://github.com/Michaelyklam) (closes [#​2514](nesquena/hermes-webui#2514), refs [#​2535](nesquena/hermes-webui#2535)) — Stop reasoning-only Thinking entries from being replayed into provider-facing history as blank assistant turns. Long WebUI sessions were accumulating duplicated stale Thinking blocks and inflated Activity/tool metadata on later turns when reasoning-only display entries (from interrupted/canceled turns) got reinserted into the restored conversation history. The fix keeps visible Thinking cards in the transcript while filtering them out of provider-facing replay. Settled compact Activity rerenders now also clear previously inserted Thinking rows before rebuilding the visible transcript. - **PR [#​2520](nesquena/hermes-webui#2520 by [@​OneFat3](https://github.com/OneFat3) (refs [#​2247](nesquena/hermes-webui#2247)) — Route archive extraction (`/api/upload/extract`) through the per-session attachment inbox (`_session_attachment_dir`) instead of hardcoded `Path(s.workspace)`, matching the single-file upload path. Extracted archives now land at `<attachment_root>/<session_id>/<archive_stem>/` so session deletion cleanup covers them and per-session isolation is preserved when `HERMES_WEBUI_ATTACHMENT_DIR` is configured. - **PR [#​2505](nesquena/hermes-webui#2505 by [@​cyberdyne187](https://github.com/cyberdyne187) — Surface provider fallback and rate-limit lifecycle notices as auto-clearing fallback warnings in the streaming composer status. The new bridge in `_agent_status_callback` matches agent lifecycle messages containing `rate limited` / `switching to fallback` / `falling back` / `fallback activated` / `trying fallback` and emits them as `warning` events with `type=fallback`, so the existing `static/messages.js` warning channel surfaces them with the correct auto-clear contract instead of letting them drop silently. - **PR [#​2556](nesquena/hermes-webui#2556 by [@​Michaelyklam](https://github.com/Michaelyklam) (closes [#​2541](nesquena/hermes-webui#2541)) — Sanitize auto-generated custom-provider API-key environment variable names so endpoint-derived provider ids such as `custom:gpu.local-8000` use POSIX-safe names like `CUSTOM_GPU_LOCAL_8000_API_KEY`. Runtime custom-provider key resolution now checks the sanitized env var first and falls back to the legacy punctuation-preserving name with a one-shot deprecation warning. Configured literal `api_key` values and explicit `key_env` config are unchanged. ##### Documentation - **PR [#​2544](nesquena/hermes-webui#2544 by [@​Michaelyklam](https://github.com/Michaelyklam) (refs [#​1925](nesquena/hermes-webui#1925)) — Implement the first Slice 3c RuntimeAdapter control routing. `RuntimeAdapter` / `LegacyJournalRuntimeAdapter` now expose `queue_message(...)` and `update_goal(...)` as protocol-translator delegates, and the `/api/goal` route uses `update_goal(...)` only when `HERMES_WEBUI_RUNTIME_ADAPTER=legacy-journal` is enabled while preserving the legacy-direct response shape. The change keeps `/queue`'s existing browser-side drain semantics and goal post-turn evaluation in the current agent loop; no runner/sidecar, WebUI-owned queue, goal scheduler, cached-agent table, or execution-survives-restart claim is introduced. </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about these updates again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMDEuMSIsInVwZGF0ZWRJblZlciI6IjQzLjEwMS4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJyZW5vdmF0ZS9jb250YWluZXIiLCJ0eXBlL3BhdGNoIl19--> Reviewed-on: https://git.erwanleboucher.dev/eleboucher/homelab/pulls/560
…ena#2550/nesquena#2551/nesquena#2554/nesquena#2560/nesquena#2567/nesquena#2568) into process-complete-event PR # Conflicts: # static/sessions.js
When the WebUI is installed as a PWA (home-screen shortcut) on Android, there is no browser address bar or reload button. The only way to refresh the page is to close and reopen the app.
This PR adds two ways to reload from a PWA standalone context:
Reload button — A small refresh icon in the app titlebar, next to the message count. Only visible in display-mode: standalone or fullscreen. Tapping it triggers window.location.reload().
Pull-to-refresh gesture — Dragging down from the top of the message list shows a visual indicator (Pull to refresh / Release to refresh) and reloads on release past an 80px threshold. When triggered mid-conversation (not at the top), the message list smooth-scrolls to the top first so the user does not have to manually scroll up in long sessions.
Both features are gated behind a matchMedia check and have no effect in normal browser mode.
Changes