HERMES-GIELIE/static/sw.js

210 lines
8.3 KiB
JavaScript
Raw Normal View History

feat: add PWA support (manifest, service worker, install prompt) (#920) * feat: add PWA support (manifest, service worker, install prompt) (v0.50.178, #911) Co-authored-by: bsgdigital Closes #685 * fix(sw): await caches.match() before `|| fallback` so offline HTML actually shows The offline-navigation fallback was dead code: return caches.match('./') || new Response('<html>...</html>', ...); `caches.match()` returns a Promise, and Promise objects are always truthy in a `||` check — so the `new Response(...)` branch was never taken. On actual offline, `caches.match('./')` resolves to undefined (no cache hit for the root), the SW returns undefined, and the browser falls back to its own default offline page. The custom "Hermes requires a server connection" HTML was unreachable. Fix by threading the match through `.then()` so the resolved value (not the Promise object) feeds the `||`: return caches.match('./').then((cached) => cached || new Response(...)); Added 13 regression tests in tests/test_pwa_manifest_sw.py covering: - manifest.json validity + required PWA fields + icon existence - sw.js cache-version placeholder + API/stream bypass + correct offline pattern (explicitly rejects the broken `|| new Response` shape so it can't regress) - /manifest.json + /sw.js routes serve correct Content-Type, Cache-Control, Service-Worker-Allowed headers and inject WEBUI_VERSION - index.html links manifest, registers SW, has iOS PWA meta tags Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 22:14:21 +00:00
/**
* Hermes WebUI Service Worker
* Minimal PWA service worker — enables "Add to Home Screen".
* No offline caching of API responses (the UI requires a live backend).
* Caches only static shell assets so the app shell loads fast on repeat visits.
*/
// Cache version is injected by the server at request time (routes.py /sw.js handler).
// Bumps automatically whenever the git commit changes — no manual edits needed.
const CACHE_NAME = 'hermes-shell-__WEBUI_VERSION__';
feat: add PWA support (manifest, service worker, install prompt) (#920) * feat: add PWA support (manifest, service worker, install prompt) (v0.50.178, #911) Co-authored-by: bsgdigital Closes #685 * fix(sw): await caches.match() before `|| fallback` so offline HTML actually shows The offline-navigation fallback was dead code: return caches.match('./') || new Response('<html>...</html>', ...); `caches.match()` returns a Promise, and Promise objects are always truthy in a `||` check — so the `new Response(...)` branch was never taken. On actual offline, `caches.match('./')` resolves to undefined (no cache hit for the root), the SW returns undefined, and the browser falls back to its own default offline page. The custom "Hermes requires a server connection" HTML was unreachable. Fix by threading the match through `.then()` so the resolved value (not the Promise object) feeds the `||`: return caches.match('./').then((cached) => cached || new Response(...)); Added 13 regression tests in tests/test_pwa_manifest_sw.py covering: - manifest.json validity + required PWA fields + icon existence - sw.js cache-version placeholder + API/stream bypass + correct offline pattern (explicitly rejects the broken `|| new Response` shape so it can't regress) - /manifest.json + /sw.js routes serve correct Content-Type, Cache-Control, Service-Worker-Allowed headers and inject WEBUI_VERSION - index.html links manifest, registers SW, has iOS PWA meta tags Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 22:14:21 +00:00
// Static assets that form the app shell.
//
// Versioned assets (CSS + JS) include `?v=__WEBUI_VERSION__` to match the
// query string the page sends — see index.html. Without the version query
// here, every cache lookup against `?v=...` URLs would miss and fall through
// to network, defeating the pre-cache.
//
// Do not pre-cache './' or login assets here: under password auth they can be
// either the authenticated app shell or login code, and stale cached responses
// can make valid password submits fail until the user clears browser cache.
// Navigations populate './' only after a successful non-redirect network load.
const VQ = '?v=__WEBUI_VERSION__';
feat: add PWA support (manifest, service worker, install prompt) (#920) * feat: add PWA support (manifest, service worker, install prompt) (v0.50.178, #911) Co-authored-by: bsgdigital Closes #685 * fix(sw): await caches.match() before `|| fallback` so offline HTML actually shows The offline-navigation fallback was dead code: return caches.match('./') || new Response('<html>...</html>', ...); `caches.match()` returns a Promise, and Promise objects are always truthy in a `||` check — so the `new Response(...)` branch was never taken. On actual offline, `caches.match('./')` resolves to undefined (no cache hit for the root), the SW returns undefined, and the browser falls back to its own default offline page. The custom "Hermes requires a server connection" HTML was unreachable. Fix by threading the match through `.then()` so the resolved value (not the Promise object) feeds the `||`: return caches.match('./').then((cached) => cached || new Response(...)); Added 13 regression tests in tests/test_pwa_manifest_sw.py covering: - manifest.json validity + required PWA fields + icon existence - sw.js cache-version placeholder + API/stream bypass + correct offline pattern (explicitly rejects the broken `|| new Response` shape so it can't regress) - /manifest.json + /sw.js routes serve correct Content-Type, Cache-Control, Service-Worker-Allowed headers and inject WEBUI_VERSION - index.html links manifest, registers SW, has iOS PWA meta tags Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 22:14:21 +00:00
const SHELL_ASSETS = [
'./static/style.css' + VQ,
'./static/pwa-startup.js' + VQ,
'./static/boot.js' + VQ,
'./static/assistant_turn_anchors.js' + VQ,
'./static/ui.js' + VQ,
'./static/messages.js' + VQ,
'./static/sessions.js' + VQ,
'./static/panels.js' + VQ,
'./static/commands.js' + VQ,
'./static/icons.js' + VQ,
'./static/i18n.js' + VQ,
'./static/workspace.js' + VQ,
'./static/terminal.js' + VQ,
'./static/onboarding.js' + VQ,
2026-05-28 17:38:50 +00:00
'./static/vendor/smd.min.js' + VQ,
'./static/vendor/katex/0.16.22/katex.min.css' + VQ,
'./static/vendor/katex/0.16.22/katex.min.js' + VQ,
feat: add PWA support (manifest, service worker, install prompt) (#920) * feat: add PWA support (manifest, service worker, install prompt) (v0.50.178, #911) Co-authored-by: bsgdigital Closes #685 * fix(sw): await caches.match() before `|| fallback` so offline HTML actually shows The offline-navigation fallback was dead code: return caches.match('./') || new Response('<html>...</html>', ...); `caches.match()` returns a Promise, and Promise objects are always truthy in a `||` check — so the `new Response(...)` branch was never taken. On actual offline, `caches.match('./')` resolves to undefined (no cache hit for the root), the SW returns undefined, and the browser falls back to its own default offline page. The custom "Hermes requires a server connection" HTML was unreachable. Fix by threading the match through `.then()` so the resolved value (not the Promise object) feeds the `||`: return caches.match('./').then((cached) => cached || new Response(...)); Added 13 regression tests in tests/test_pwa_manifest_sw.py covering: - manifest.json validity + required PWA fields + icon existence - sw.js cache-version placeholder + API/stream bypass + correct offline pattern (explicitly rejects the broken `|| new Response` shape so it can't regress) - /manifest.json + /sw.js routes serve correct Content-Type, Cache-Control, Service-Worker-Allowed headers and inject WEBUI_VERSION - index.html links manifest, registers SW, has iOS PWA meta tags Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 22:14:21 +00:00
'./static/favicon.svg',
'./static/favicon-32.png',
'./manifest.json',
];
2026-05-16 15:44:04 +00:00
function deleteOldShellCaches() {
return caches.keys().then((keys) =>
Promise.all(
keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k))
)
);
}
// Install: prune old shell caches first, then pre-cache the app shell. Doing
// this before caches.open(CACHE_NAME) avoids a temporary double-cache window on
// quota-sensitive browsers during frequent version bumps.
feat: add PWA support (manifest, service worker, install prompt) (#920) * feat: add PWA support (manifest, service worker, install prompt) (v0.50.178, #911) Co-authored-by: bsgdigital Closes #685 * fix(sw): await caches.match() before `|| fallback` so offline HTML actually shows The offline-navigation fallback was dead code: return caches.match('./') || new Response('<html>...</html>', ...); `caches.match()` returns a Promise, and Promise objects are always truthy in a `||` check — so the `new Response(...)` branch was never taken. On actual offline, `caches.match('./')` resolves to undefined (no cache hit for the root), the SW returns undefined, and the browser falls back to its own default offline page. The custom "Hermes requires a server connection" HTML was unreachable. Fix by threading the match through `.then()` so the resolved value (not the Promise object) feeds the `||`: return caches.match('./').then((cached) => cached || new Response(...)); Added 13 regression tests in tests/test_pwa_manifest_sw.py covering: - manifest.json validity + required PWA fields + icon existence - sw.js cache-version placeholder + API/stream bypass + correct offline pattern (explicitly rejects the broken `|| new Response` shape so it can't regress) - /manifest.json + /sw.js routes serve correct Content-Type, Cache-Control, Service-Worker-Allowed headers and inject WEBUI_VERSION - index.html links manifest, registers SW, has iOS PWA meta tags Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 22:14:21 +00:00
self.addEventListener('install', (event) => {
event.waitUntil(
2026-05-16 15:44:04 +00:00
deleteOldShellCaches().then(() =>
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(SHELL_ASSETS).catch((err) => {
// Non-fatal: if any asset fails, still activate
console.warn('[sw] Shell pre-cache partial failure:', err);
});
})
)
feat: add PWA support (manifest, service worker, install prompt) (#920) * feat: add PWA support (manifest, service worker, install prompt) (v0.50.178, #911) Co-authored-by: bsgdigital Closes #685 * fix(sw): await caches.match() before `|| fallback` so offline HTML actually shows The offline-navigation fallback was dead code: return caches.match('./') || new Response('<html>...</html>', ...); `caches.match()` returns a Promise, and Promise objects are always truthy in a `||` check — so the `new Response(...)` branch was never taken. On actual offline, `caches.match('./')` resolves to undefined (no cache hit for the root), the SW returns undefined, and the browser falls back to its own default offline page. The custom "Hermes requires a server connection" HTML was unreachable. Fix by threading the match through `.then()` so the resolved value (not the Promise object) feeds the `||`: return caches.match('./').then((cached) => cached || new Response(...)); Added 13 regression tests in tests/test_pwa_manifest_sw.py covering: - manifest.json validity + required PWA fields + icon existence - sw.js cache-version placeholder + API/stream bypass + correct offline pattern (explicitly rejects the broken `|| new Response` shape so it can't regress) - /manifest.json + /sw.js routes serve correct Content-Type, Cache-Control, Service-Worker-Allowed headers and inject WEBUI_VERSION - index.html links manifest, registers SW, has iOS PWA meta tags Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 22:14:21 +00:00
);
self.skipWaiting();
});
2026-05-16 15:44:04 +00:00
// Activate: keep the old-cache cleanup as a safety net in case install was
// interrupted or an older worker was already waiting.
feat: add PWA support (manifest, service worker, install prompt) (#920) * feat: add PWA support (manifest, service worker, install prompt) (v0.50.178, #911) Co-authored-by: bsgdigital Closes #685 * fix(sw): await caches.match() before `|| fallback` so offline HTML actually shows The offline-navigation fallback was dead code: return caches.match('./') || new Response('<html>...</html>', ...); `caches.match()` returns a Promise, and Promise objects are always truthy in a `||` check — so the `new Response(...)` branch was never taken. On actual offline, `caches.match('./')` resolves to undefined (no cache hit for the root), the SW returns undefined, and the browser falls back to its own default offline page. The custom "Hermes requires a server connection" HTML was unreachable. Fix by threading the match through `.then()` so the resolved value (not the Promise object) feeds the `||`: return caches.match('./').then((cached) => cached || new Response(...)); Added 13 regression tests in tests/test_pwa_manifest_sw.py covering: - manifest.json validity + required PWA fields + icon existence - sw.js cache-version placeholder + API/stream bypass + correct offline pattern (explicitly rejects the broken `|| new Response` shape so it can't regress) - /manifest.json + /sw.js routes serve correct Content-Type, Cache-Control, Service-Worker-Allowed headers and inject WEBUI_VERSION - index.html links manifest, registers SW, has iOS PWA meta tags Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 22:14:21 +00:00
self.addEventListener('activate', (event) => {
2026-05-16 15:44:04 +00:00
event.waitUntil(deleteOldShellCaches());
feat: add PWA support (manifest, service worker, install prompt) (#920) * feat: add PWA support (manifest, service worker, install prompt) (v0.50.178, #911) Co-authored-by: bsgdigital Closes #685 * fix(sw): await caches.match() before `|| fallback` so offline HTML actually shows The offline-navigation fallback was dead code: return caches.match('./') || new Response('<html>...</html>', ...); `caches.match()` returns a Promise, and Promise objects are always truthy in a `||` check — so the `new Response(...)` branch was never taken. On actual offline, `caches.match('./')` resolves to undefined (no cache hit for the root), the SW returns undefined, and the browser falls back to its own default offline page. The custom "Hermes requires a server connection" HTML was unreachable. Fix by threading the match through `.then()` so the resolved value (not the Promise object) feeds the `||`: return caches.match('./').then((cached) => cached || new Response(...)); Added 13 regression tests in tests/test_pwa_manifest_sw.py covering: - manifest.json validity + required PWA fields + icon existence - sw.js cache-version placeholder + API/stream bypass + correct offline pattern (explicitly rejects the broken `|| new Response` shape so it can't regress) - /manifest.json + /sw.js routes serve correct Content-Type, Cache-Control, Service-Worker-Allowed headers and inject WEBUI_VERSION - index.html links manifest, registers SW, has iOS PWA meta tags Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 22:14:21 +00:00
self.clients.claim();
});
// Fetch strategy:
// - API calls (/api/*, /stream) → always network (never cache)
// - Login assets → always network (never cache stale auth code)
// - Page navigations → network-first so auth redirects/cookies are honored
// - Shell assets → network-first with cache fallback
// - Everything else → network-only
feat: add PWA support (manifest, service worker, install prompt) (#920) * feat: add PWA support (manifest, service worker, install prompt) (v0.50.178, #911) Co-authored-by: bsgdigital Closes #685 * fix(sw): await caches.match() before `|| fallback` so offline HTML actually shows The offline-navigation fallback was dead code: return caches.match('./') || new Response('<html>...</html>', ...); `caches.match()` returns a Promise, and Promise objects are always truthy in a `||` check — so the `new Response(...)` branch was never taken. On actual offline, `caches.match('./')` resolves to undefined (no cache hit for the root), the SW returns undefined, and the browser falls back to its own default offline page. The custom "Hermes requires a server connection" HTML was unreachable. Fix by threading the match through `.then()` so the resolved value (not the Promise object) feeds the `||`: return caches.match('./').then((cached) => cached || new Response(...)); Added 13 regression tests in tests/test_pwa_manifest_sw.py covering: - manifest.json validity + required PWA fields + icon existence - sw.js cache-version placeholder + API/stream bypass + correct offline pattern (explicitly rejects the broken `|| new Response` shape so it can't regress) - /manifest.json + /sw.js routes serve correct Content-Type, Cache-Control, Service-Worker-Allowed headers and inject WEBUI_VERSION - index.html links manifest, registers SW, has iOS PWA meta tags Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 22:14:21 +00:00
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
// Never intercept cross-origin requests
if (url.origin !== self.location.origin) return;
// Never intercept the service worker script itself. Returning a cached sw.js
// prevents the browser from seeing a new cache version after local patches.
if (url.pathname.endsWith('/sw.js')) return;
// Login assets must always hit the network. Older login.js builds have had
// subpath-sensitive auth POST paths; if the service worker caches one, the
// password can keep failing until the user manually clears browser cache.
if (
url.pathname.endsWith('/login') ||
url.pathname.endsWith('/static/login.js')
) {
return;
}
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
// API and streaming endpoints — always go to network.
// The WebUI may be mounted under a subpath such as /hermes/, so API
// requests can look like /hermes/api/sessions rather than /api/sessions.
feat: add PWA support (manifest, service worker, install prompt) (#920) * feat: add PWA support (manifest, service worker, install prompt) (v0.50.178, #911) Co-authored-by: bsgdigital Closes #685 * fix(sw): await caches.match() before `|| fallback` so offline HTML actually shows The offline-navigation fallback was dead code: return caches.match('./') || new Response('<html>...</html>', ...); `caches.match()` returns a Promise, and Promise objects are always truthy in a `||` check — so the `new Response(...)` branch was never taken. On actual offline, `caches.match('./')` resolves to undefined (no cache hit for the root), the SW returns undefined, and the browser falls back to its own default offline page. The custom "Hermes requires a server connection" HTML was unreachable. Fix by threading the match through `.then()` so the resolved value (not the Promise object) feeds the `||`: return caches.match('./').then((cached) => cached || new Response(...)); Added 13 regression tests in tests/test_pwa_manifest_sw.py covering: - manifest.json validity + required PWA fields + icon existence - sw.js cache-version placeholder + API/stream bypass + correct offline pattern (explicitly rejects the broken `|| new Response` shape so it can't regress) - /manifest.json + /sw.js routes serve correct Content-Type, Cache-Control, Service-Worker-Allowed headers and inject WEBUI_VERSION - index.html links manifest, registers SW, has iOS PWA meta tags Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 22:14:21 +00:00
if (
url.pathname.startsWith('/api/') ||
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
url.pathname.includes('/api/') ||
feat: add PWA support (manifest, service worker, install prompt) (#920) * feat: add PWA support (manifest, service worker, install prompt) (v0.50.178, #911) Co-authored-by: bsgdigital Closes #685 * fix(sw): await caches.match() before `|| fallback` so offline HTML actually shows The offline-navigation fallback was dead code: return caches.match('./') || new Response('<html>...</html>', ...); `caches.match()` returns a Promise, and Promise objects are always truthy in a `||` check — so the `new Response(...)` branch was never taken. On actual offline, `caches.match('./')` resolves to undefined (no cache hit for the root), the SW returns undefined, and the browser falls back to its own default offline page. The custom "Hermes requires a server connection" HTML was unreachable. Fix by threading the match through `.then()` so the resolved value (not the Promise object) feeds the `||`: return caches.match('./').then((cached) => cached || new Response(...)); Added 13 regression tests in tests/test_pwa_manifest_sw.py covering: - manifest.json validity + required PWA fields + icon existence - sw.js cache-version placeholder + API/stream bypass + correct offline pattern (explicitly rejects the broken `|| new Response` shape so it can't regress) - /manifest.json + /sw.js routes serve correct Content-Type, Cache-Control, Service-Worker-Allowed headers and inject WEBUI_VERSION - index.html links manifest, registers SW, has iOS PWA meta tags Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 22:14:21 +00:00
url.pathname.includes('/stream') ||
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
url.pathname.startsWith('/health') ||
url.pathname.includes('/health')
feat: add PWA support (manifest, service worker, install prompt) (#920) * feat: add PWA support (manifest, service worker, install prompt) (v0.50.178, #911) Co-authored-by: bsgdigital Closes #685 * fix(sw): await caches.match() before `|| fallback` so offline HTML actually shows The offline-navigation fallback was dead code: return caches.match('./') || new Response('<html>...</html>', ...); `caches.match()` returns a Promise, and Promise objects are always truthy in a `||` check — so the `new Response(...)` branch was never taken. On actual offline, `caches.match('./')` resolves to undefined (no cache hit for the root), the SW returns undefined, and the browser falls back to its own default offline page. The custom "Hermes requires a server connection" HTML was unreachable. Fix by threading the match through `.then()` so the resolved value (not the Promise object) feeds the `||`: return caches.match('./').then((cached) => cached || new Response(...)); Added 13 regression tests in tests/test_pwa_manifest_sw.py covering: - manifest.json validity + required PWA fields + icon existence - sw.js cache-version placeholder + API/stream bypass + correct offline pattern (explicitly rejects the broken `|| new Response` shape so it can't regress) - /manifest.json + /sw.js routes serve correct Content-Type, Cache-Control, Service-Worker-Allowed headers and inject WEBUI_VERSION - index.html links manifest, registers SW, has iOS PWA meta tags Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 22:14:21 +00:00
) {
return; // let browser handle normally
}
// Page navigations must be network-first. A stale cached './' response can
// otherwise hide the server's 302-to-login after auth expiry, or ignore a
// freshly set login cookie until the user manually refreshes.
if (event.request.mode === 'navigate') {
event.respondWith(
fetch(new Request(event.request, { cache: 'no-store' })).then((response) => {
if (
event.request.method === 'GET' &&
response.status === 200 &&
!response.redirected
) {
const clone = response.clone();
caches.open(CACHE_NAME).then((cache) => cache.put('./', clone));
}
return response;
}).catch(() => {
return caches.match('./').then((cached) => cached || new Response(
'<html><body style="font-family:sans-serif;padding:2rem;background:#1a1a1a;color:#ccc">' +
'<h2>You are offline</h2>' +
'<p>Hermes requires a server connection. Please check your network and try again.</p>' +
'</body></html>',
{ headers: { 'Content-Type': 'text/html' } }
));
})
);
return;
}
// Only explicit shell assets are cached. Everything else should hit the
// network so stale one-off files (especially auth/login scripts) do not get
// trapped in CacheStorage until a manual cache clear.
const scopePath = new URL(self.registration.scope).pathname;
const relPath = url.pathname.startsWith(scopePath)
? url.pathname.slice(scopePath.length)
: url.pathname.replace(/^\/+/, '');
const shellPath = './' + relPath.replace(/^\/+/, '') + url.search;
if (!SHELL_ASSETS.includes(shellPath)) return;
// Shell assets: network-first with cache fallback. This keeps offline support
// but avoids executing stale JS/CSS after a local hotfix when WEBUI_VERSION
// has not changed yet (e.g. before a guarded restart updates the ?v token).
feat: add PWA support (manifest, service worker, install prompt) (#920) * feat: add PWA support (manifest, service worker, install prompt) (v0.50.178, #911) Co-authored-by: bsgdigital Closes #685 * fix(sw): await caches.match() before `|| fallback` so offline HTML actually shows The offline-navigation fallback was dead code: return caches.match('./') || new Response('<html>...</html>', ...); `caches.match()` returns a Promise, and Promise objects are always truthy in a `||` check — so the `new Response(...)` branch was never taken. On actual offline, `caches.match('./')` resolves to undefined (no cache hit for the root), the SW returns undefined, and the browser falls back to its own default offline page. The custom "Hermes requires a server connection" HTML was unreachable. Fix by threading the match through `.then()` so the resolved value (not the Promise object) feeds the `||`: return caches.match('./').then((cached) => cached || new Response(...)); Added 13 regression tests in tests/test_pwa_manifest_sw.py covering: - manifest.json validity + required PWA fields + icon existence - sw.js cache-version placeholder + API/stream bypass + correct offline pattern (explicitly rejects the broken `|| new Response` shape so it can't regress) - /manifest.json + /sw.js routes serve correct Content-Type, Cache-Control, Service-Worker-Allowed headers and inject WEBUI_VERSION - index.html links manifest, registers SW, has iOS PWA meta tags Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 22:14:21 +00:00
event.respondWith(
fetch(new Request(event.request, { cache: 'no-store' })).then((response) => {
if (
event.request.method === 'GET' &&
response.status === 200
) {
const clone = response.clone();
caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone));
}
return response;
}).catch(() => caches.match(event.request).then((cached) => cached || new Response('Offline', {
status: 503,
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
})))
feat: add PWA support (manifest, service worker, install prompt) (#920) * feat: add PWA support (manifest, service worker, install prompt) (v0.50.178, #911) Co-authored-by: bsgdigital Closes #685 * fix(sw): await caches.match() before `|| fallback` so offline HTML actually shows The offline-navigation fallback was dead code: return caches.match('./') || new Response('<html>...</html>', ...); `caches.match()` returns a Promise, and Promise objects are always truthy in a `||` check — so the `new Response(...)` branch was never taken. On actual offline, `caches.match('./')` resolves to undefined (no cache hit for the root), the SW returns undefined, and the browser falls back to its own default offline page. The custom "Hermes requires a server connection" HTML was unreachable. Fix by threading the match through `.then()` so the resolved value (not the Promise object) feeds the `||`: return caches.match('./').then((cached) => cached || new Response(...)); Added 13 regression tests in tests/test_pwa_manifest_sw.py covering: - manifest.json validity + required PWA fields + icon existence - sw.js cache-version placeholder + API/stream bypass + correct offline pattern (explicitly rejects the broken `|| new Response` shape so it can't regress) - /manifest.json + /sw.js routes serve correct Content-Type, Cache-Control, Service-Worker-Allowed headers and inject WEBUI_VERSION - index.html links manifest, registers SW, has iOS PWA meta tags Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 22:14:21 +00:00
);
});
Release v0.51.346 — Release LJ (PWA notification controls #3229) (#3897) * Add PWA notification controls (#3229, #3196) Rebased onto fresh master (#3229 was 281 commits behind). Routes browser notifications through the service worker (reg.showNotification) with a direct Notification fallback, threads the originating session deep link/tag into the payload, and adds Enable/Send-test/permission-status controls in Settings. notificationclick prefers an already-open target-session tab before opening a new window. Added the 6 new i18n keys to the Polish (pl) locale block, which the original PR missed. Co-authored-by: AJV20 <AJV20@users.noreply.github.com> * Apply Opus review fixes to #3229 PWA notifications - MUST-FIX: _showPwaNotification used navigator.serviceWorker.ready, which never settles when no SW registration ever activates (reverse-proxy MIME mishap, SW disabled) — every notification was silently dropped, a regression vs master. Switch to getRegistration() raced against a 2s timeout, falling back to new Notification(). - SHOULD-FIX: notificationclick matched client.url === targetUrl exactly, but _sessionUrlForSid copies the current query/hash into the deep link, so an already-open session tab missed the match and spawned a duplicate window. Compare pathnames instead. - SHOULD-FIX: "Send test" with permission denied was a silent no-op — now surfaces the notifications_denied toast on the force path. - SHOULD-FIX: permission-status span went stale after granting via the button — refresh it in requestNotificationPermission().then. - Consistency: the request-permission grant branch now has the same new Notification fallback as the granted branch. - Update test_pwa_notification_controls.py to assert the pathname match. * Stamp v0.51.346 — Release LJ (PWA notification controls #3229/#3196) --------- Co-authored-by: nesquena-hermes <[email protected]> Co-authored-by: AJV20 <AJV20@users.noreply.github.com>
2026-06-09 22:54:21 +00:00
self.addEventListener('notificationclick', (event) => {
event.notification.close();
const rawUrl = (event.notification.data && event.notification.data.url) || './';
const targetUrl = new URL(rawUrl, self.registration.scope || './').href;
const targetPath = new URL(targetUrl).pathname;
const samePath = (clientUrl) => {
try { return new URL(clientUrl).pathname === targetPath; } catch (_e) { return false; }
};
const sameOrigin = (clientUrl) => {
try { return new URL(clientUrl).origin === self.location.origin; } catch (_e) { return false; }
};
Release v0.51.346 — Release LJ (PWA notification controls #3229) (#3897) * Add PWA notification controls (#3229, #3196) Rebased onto fresh master (#3229 was 281 commits behind). Routes browser notifications through the service worker (reg.showNotification) with a direct Notification fallback, threads the originating session deep link/tag into the payload, and adds Enable/Send-test/permission-status controls in Settings. notificationclick prefers an already-open target-session tab before opening a new window. Added the 6 new i18n keys to the Polish (pl) locale block, which the original PR missed. Co-authored-by: AJV20 <AJV20@users.noreply.github.com> * Apply Opus review fixes to #3229 PWA notifications - MUST-FIX: _showPwaNotification used navigator.serviceWorker.ready, which never settles when no SW registration ever activates (reverse-proxy MIME mishap, SW disabled) — every notification was silently dropped, a regression vs master. Switch to getRegistration() raced against a 2s timeout, falling back to new Notification(). - SHOULD-FIX: notificationclick matched client.url === targetUrl exactly, but _sessionUrlForSid copies the current query/hash into the deep link, so an already-open session tab missed the match and spawned a duplicate window. Compare pathnames instead. - SHOULD-FIX: "Send test" with permission denied was a silent no-op — now surfaces the notifications_denied toast on the force path. - SHOULD-FIX: permission-status span went stale after granting via the button — refresh it in requestNotificationPermission().then. - Consistency: the request-permission grant branch now has the same new Notification fallback as the granted branch. - Update test_pwa_notification_controls.py to assert the pathname match. * Stamp v0.51.346 — Release LJ (PWA notification controls #3229/#3196) --------- Co-authored-by: nesquena-hermes <[email protected]> Co-authored-by: AJV20 <AJV20@users.noreply.github.com>
2026-06-09 22:54:21 +00:00
event.waitUntil(
self.clients.matchAll({type: 'window', includeUncontrolled: true}).then((clientList) => {
// Match on pathname, not the full href: _sessionUrlForSid copies the
// current page's query string + hash into the deep link, so an open tab
// already on /session/<sid> would fail an exact-href match and spawn a
// duplicate window.
const targetClient = clientList.find((client) => samePath(client.url) && 'focus' in client);
if (targetClient) return targetClient.focus();
const openNotificationWindow = () => (
self.clients.openWindow ? self.clients.openWindow(targetUrl) : undefined
);
const focusableClient = clientList.find((client) => sameOrigin(client.url) && 'focus' in client && 'navigate' in client);
Release v0.51.346 — Release LJ (PWA notification controls #3229) (#3897) * Add PWA notification controls (#3229, #3196) Rebased onto fresh master (#3229 was 281 commits behind). Routes browser notifications through the service worker (reg.showNotification) with a direct Notification fallback, threads the originating session deep link/tag into the payload, and adds Enable/Send-test/permission-status controls in Settings. notificationclick prefers an already-open target-session tab before opening a new window. Added the 6 new i18n keys to the Polish (pl) locale block, which the original PR missed. Co-authored-by: AJV20 <AJV20@users.noreply.github.com> * Apply Opus review fixes to #3229 PWA notifications - MUST-FIX: _showPwaNotification used navigator.serviceWorker.ready, which never settles when no SW registration ever activates (reverse-proxy MIME mishap, SW disabled) — every notification was silently dropped, a regression vs master. Switch to getRegistration() raced against a 2s timeout, falling back to new Notification(). - SHOULD-FIX: notificationclick matched client.url === targetUrl exactly, but _sessionUrlForSid copies the current query/hash into the deep link, so an already-open session tab missed the match and spawned a duplicate window. Compare pathnames instead. - SHOULD-FIX: "Send test" with permission denied was a silent no-op — now surfaces the notifications_denied toast on the force path. - SHOULD-FIX: permission-status span went stale after granting via the button — refresh it in requestNotificationPermission().then. - Consistency: the request-permission grant branch now has the same new Notification fallback as the granted branch. - Update test_pwa_notification_controls.py to assert the pathname match. * Stamp v0.51.346 — Release LJ (PWA notification controls #3229/#3196) --------- Co-authored-by: nesquena-hermes <[email protected]> Co-authored-by: AJV20 <AJV20@users.noreply.github.com>
2026-06-09 22:54:21 +00:00
if (focusableClient && 'navigate' in focusableClient) {
return focusableClient.navigate(targetUrl)
.then((client) => (client && 'focus' in client ? client.focus() : focusableClient.focus()))
.catch(() => focusableClient.focus());
}
return openNotificationWindow();
Release v0.51.346 — Release LJ (PWA notification controls #3229) (#3897) * Add PWA notification controls (#3229, #3196) Rebased onto fresh master (#3229 was 281 commits behind). Routes browser notifications through the service worker (reg.showNotification) with a direct Notification fallback, threads the originating session deep link/tag into the payload, and adds Enable/Send-test/permission-status controls in Settings. notificationclick prefers an already-open target-session tab before opening a new window. Added the 6 new i18n keys to the Polish (pl) locale block, which the original PR missed. Co-authored-by: AJV20 <AJV20@users.noreply.github.com> * Apply Opus review fixes to #3229 PWA notifications - MUST-FIX: _showPwaNotification used navigator.serviceWorker.ready, which never settles when no SW registration ever activates (reverse-proxy MIME mishap, SW disabled) — every notification was silently dropped, a regression vs master. Switch to getRegistration() raced against a 2s timeout, falling back to new Notification(). - SHOULD-FIX: notificationclick matched client.url === targetUrl exactly, but _sessionUrlForSid copies the current query/hash into the deep link, so an already-open session tab missed the match and spawned a duplicate window. Compare pathnames instead. - SHOULD-FIX: "Send test" with permission denied was a silent no-op — now surfaces the notifications_denied toast on the force path. - SHOULD-FIX: permission-status span went stale after granting via the button — refresh it in requestNotificationPermission().then. - Consistency: the request-permission grant branch now has the same new Notification fallback as the granted branch. - Update test_pwa_notification_controls.py to assert the pathname match. * Stamp v0.51.346 — Release LJ (PWA notification controls #3229/#3196) --------- Co-authored-by: nesquena-hermes <[email protected]> Co-authored-by: AJV20 <AJV20@users.noreply.github.com>
2026-06-09 22:54:21 +00:00
})
);
});