Skip to content

fix: P0 bugfixes — tool-card args, sw.js path, CLI rename, scroll pinning#1492

Merged
2 commits merged intonesquena:masterfrom
bergeouss:fix/p0-bugfixes
May 3, 2026
Merged

fix: P0 bugfixes — tool-card args, sw.js path, CLI rename, scroll pinning#1492
2 commits merged intonesquena:masterfrom
bergeouss:fix/p0-bugfixes

Conversation

@bergeouss
Copy link
Copy Markdown
Contributor

P0 Bugfixes — 4 fixes in one PR

🤖 AI-assisted via Hermes Agent

Fixes

Issue Severity Description
#1481 bug PWA: /sw.js returns JSON 404 on session pages — Service worker registration used relative URL sw.js which resolves against the dynamic <base href>. On session pages (/session/abc), the browser requests /session/sw.js which doesn't exist. Fix: absolute path /sw.js.
#1484 bug Tool-card expanded args unreadable — Arguments displayed ~1.5 wrapped lines with destroyed code formatting. Root cause: word-break:break-all on .tool-arg-val + missing white-space:pre-wrap. Newlines and indentation collapsed. Fix: pre-wrap + break-word + display:block.
#1486 bug Sidebar rename for CLI sessions reverts on hard refresh — Compression chain extension after import caused title to revert to original state.db head title. Fix: get_cli_sessions() now checks for a WebUI JSON file and prefers its title.
#1469 / #1360 bug Auto-scroll overrides user scroll on macOS — Race condition: scrollIfPinned() triggered scroll events that re-set _scrollPinned=true, overriding the user's explicit scroll-up. Fix: _programmaticScroll guard flag distinguishes programmatic from user scrolls.

Testing

Files Changed

  • static/index.html — 1 line (absolute SW path)
  • static/style.css — 2 lines (tool-arg formatting)
  • static/ui.js — 7 lines (scroll guard)
  • api/models.py — 10 lines (CLI session title override)

…ning

- nesquena#1481: Use absolute path for service worker registration to avoid
  <base> tag resolution on session pages causing JSON 404
- nesquena#1484: Fix tool-card expanded args readability — replace
  word-break:break-all with pre-wrap+break-word, add display:block
  so newlines and indentation are preserved
- nesquena#1486: Prefer WebUI JSON title over state.db title for CLI sessions,
  fixing rename-not-persisting after compression chain extension
- nesquena#1469/nesquena#1360: Add _programmaticScroll guard to distinguish
  programmatic scrolls from user scrolls, preventing the race
  condition where scrollIfPinned() re-pins after user scrolls up
Copy link
Copy Markdown
Owner

@nesquena nesquena left a comment

Choose a reason for hiding this comment

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

Review — end-to-end ❌ (request changes — /sw.js change is a subpath-mount regression; other 3 fixes look good)

Thanks @bergeouss for bundling 4 P0 fixes. Three of them are reasonable and should land — the fourth (/sw.js absolute path) is unfortunately a regression for installs behind a reverse proxy at a subpath. Details on each below.

Fix 1 — /sw.js path change ❌ blocking regression

The change (static/index.html:50):

- navigator.serviceWorker.register('sw.js?v=__WEBUI_VERSION__').catch(...)
+ navigator.serviceWorker.register('/sw.js?v=__WEBUI_VERSION__').catch(...)

Why this breaks subpath-mount installs. static/index.html:16-17 explicitly documents the convention:

<!-- base href enables subpath mount support; all static paths must stay relative (no leading slash) -->
<script>(function(){var path=location.pathname,marker='/session/',i=path.indexOf(marker),p;i>=0?p=(path.slice(0,i+1)||'/'):p=(path.endsWith('/')?path:(path.replace(/\/[^\/]*$/,'/')||'/'));document.write('<base href="'+location.origin+p+'">');})()</script>

The dynamic <base href> is in place specifically so installs behind nginx/Apache at e.g. /hermes/ resolve every static asset URL correctly. Verified the logic with a Node harness across the URL shapes hermes-webui actually serves:

/                   -> base /,        sw.js -> /sw.js                 ✓
/index.html         -> base /,        sw.js -> /sw.js                 ✓
/session/abc        -> base /,        sw.js -> /sw.js                 ✓
/session/           -> base /,        sw.js -> /sw.js                 ✓
/hermes/            -> base /hermes/, sw.js -> /hermes/sw.js          ✓ subpath
/hermes/session/x   -> base /hermes/, sw.js -> /hermes/sw.js          ✓ subpath

Relative sw.js resolves correctly in every case, including subpath mounts. The PR description's premise — "On session pages (/session/abc), the browser requests /session/sw.js" — is incorrect. The base-href script computes <base href="<origin>/"> for any path with the /session/ marker (because path.slice(0, i+1) with i=0 returns /), so a relative sw.js resolves to <origin>/sw.js, not <origin>/session/sw.js.

What the change actually breaks. Behind a reverse proxy at /hermes/, the absolute form /sw.js requests <origin>/sw.js — outside the mount root. The upstream proxy 404s before reaching the webui. The relative form correctly requests <origin>/hermes/sw.js, which the proxy forwards.

What I'd recommend. Revert this one line back to relative. The other root cause for issue #1481 ("/sw.js whitelisted but no service worker file exists") is already addressed in master — static/sw.js was added in PR #920 long ago and the route handler at api/routes.py:1173-1194 serves it with a Service-Worker-Allowed: / header. There's nothing left to fix in this code path; the original relative sw.js already works for both root and subpath mounts.

A regression test pinning the relative form would be a nice add (so a future "absolute is cleaner" rewrite can't quietly regress subpath users again).

Fix 2 — tool-card args readability ✅ partial but correct

static/style.css:1700-1701:

-.tool-arg-key{color:var(--blue);font-family:'SF Mono',ui-monospace,monospace;font-size:var(--font-size-xs);}
-.tool-arg-val{color:var(--muted);font-family:'SF Mono',ui-monospace,monospace;font-size:var(--font-size-xs);word-break:break-all;}
+.tool-arg-key{color:var(--blue);font-family:'SF Mono',ui-monospace,monospace;font-size:var(--font-size-xs);display:block;margin-bottom:2px;}
+.tool-arg-val{color:var(--muted);font-family:'SF Mono',ui-monospace,monospace;font-size:var(--font-size-xs);white-space:pre-wrap;word-break:break-word;display:block;overflow-x:auto;}

This correctly addresses the primary issue from #1484: white-space: pre-wrap preserves newlines and indentation, word-break: break-word wraps on word boundaries instead of mid-character. ✅

Two follow-ups left on the table from issue #1484 (non-blocking, can ship in a follow-up):

  • .tool-arg-val has no max-height + overflow-y — for very large args (e.g. a 100-line code arg to execute_code), the args section will just expand the whole card. The issue suggested max-height: 360px + overflow-y: auto so very long args stay scrollable.
  • .tool-card-result pre max-height stays at 240px — the issue noted this was tight inside a 600px parent and suggested 480px. Not addressed by this PR.

Strictly an improvement over master. Worth merging the partial fix.

Fix 3 — CLI session rename revert ✅ pragmatic, with one perf note

api/models.py:1043-1052 adds:

try:
    _webui_meta = Session.load_metadata_only(sid)
    if _webui_meta and getattr(_webui_meta, 'title', None):
        _title = _webui_meta.title
except Exception:
    pass

Traced: after a CLI session is imported (creates <sid>.json) and then renamed via /api/session/rename (api/routes.py:1978-1990), the JSON file's title field is updated. On the next refresh, _project_agent_session_rows (api/agent_sessions.py:175-203) merges the chain head's title into the row but exposes the tip's id. Without this fix, the rename is silently overwritten. The PR's lookup Session.load_metadata_only(sid=tip_id) correctly finds the JSON the rename API wrote to.

The issue's suggested fix (walk parent_session_id upward to find the lineage root) is more thorough — it would handle the case where a rename happens before compression and then compression creates a new tip. The PR doesn't cover that scenario, but the repro in the issue (compression first, then rename) is what users actually hit, and the PR fixes that. ✅

Performance note (non-blocking). This adds one Session.load_metadata_only(sid) call per CLI row in _project_agent_session_rows. For a user with 200+ CLI sessions, that's 200 file existence checks + small reads on every sidebar refresh. load_metadata_only only reads the first ~16KB so it's fast (~5ms total in practice), and the existing function already does cron job-name lookups for cron rows (api/models.py:1026-1042) — comparable cost. Not a problem in practice; flagging for awareness in case sidebar latency ever becomes a concern.

Fix 4 — scroll pinning programmatic-vs-user disambiguation ✅ partial but correct

static/ui.js:1180-1196,1399-1410:

let _programmaticScroll=false;
// ... in scroll listener:
if(_programmaticScroll) return; // ignore scrolls we triggered ourselves
// ... in scrollIfPinned() / scrollToBottom():
_programmaticScroll=true; el.scrollTop=el.scrollHeight;
setTimeout(()=>{_programmaticScroll=false;},0);

This is one of the two suggestions from @berkinduz in #1469 — decouple programmatic from user scrolls. The fix is correct: scroll events fire async (microtask after scrollTop=...), and setTimeout(0) queues the flag clear in the next macrotask, so the listener correctly bails on the programmatic scroll while still firing for genuine user scrolls.

Not addressed (per @berkinduz's other suggestions):

  • Drive unpin from user intent (wheel deltaY < 0, touchmove, keydown ArrowUp/PageUp/Home). The 250px position threshold is still in place, so slow trackpad scrolling within an active stream may still re-snap before the user accumulates enough delta to cross the threshold.
  • Re-pin only when user reaches actual bottom (<8px tolerance). Still uses 250px.

The PR is strictly an improvement (no more programmatic-scroll false-positive re-pinning), but the slow-trackpad case @berkinduz described in the issue may still reproduce. Worth a follow-up that adopts intent-based unpinning. Not blocking for this PR.

Other audit — things that are correct

  • ✅ No XSS surface in any of the 4 fixes; CSS-only and DOM-only changes.
  • Session.load_metadata_only(sid) validates the sid shape (api/models.py:427) so no path traversal even if state.db somehow contained a hostile id.
  • ✅ Cross-tool: webui-only changes; no config.yaml writes, no agent-side surface.
  • ✅ Tests pass: 3813 passed, 55 skipped, 3 xpassed, 0 failures on full suite.
  • ✅ CI green on 3.11/3.12/3.13.

Edge-case trace

Scenario Fix Expected Actual
Page at /session/abc, registers SW #1481 /sw.js requested via base href ✅ via relative — but PR makes it absolute, identical for root mounts
Page at /hermes/session/abc (subpath) #1481 /hermes/sw.js ❌ PR requests /sw.js outside mount, 404
Tool card with multiline code arg #1484 newlines + indent preserved
Tool card with very long single arg (>360px) #1484 scroll ⚠️ no max-height, card grows
CLI session renamed in sidebar after compression #1486 new title persists on refresh
CLI session renamed before compression, then compression occurs #1486 new title persists ❌ PR doesn't handle (JSON for old head id, row points at new tip)
User scrolls up during streaming, programmatic scroll fires #1469 user's scroll-up wins
Slow trackpad scroll within active stream #1469 doesn't re-snap ⚠️ 250px threshold still racy

Recommendation

Request changes. Three of the four fixes (#1484, #1486, #1469) are correct partial improvements and worth merging. The /sw.js change (#1481) is the blocker — it makes the path absolute in violation of the documented "all static paths must stay relative" convention, and breaks reverse-proxy installs at a subpath. Reverting that one line plus adding a regression test pinning the relative form would unblock this PR.

If you'd prefer to drop #1481 entirely and ship the other three: that also works — issue #1481's premise (no sw.js file exists) is already addressed in master by the existing static/sw.js + the route handler at api/routes.py:1173-1194 that serves it. There's nothing left to fix in the path code; the original relative sw.js?v=... works correctly for both root and subpath mounts.

Happy to do a fast follow-up review when the sw.js portion is reverted or removed.

Repository owner deleted a comment from nesquena-hermes May 3, 2026
@nesquena-hermes
Copy link
Copy Markdown
Collaborator

Marking this hold + need-updates per the review feedback above. Three of the four bundled fixes are good and merge-ready; the /sw.js line is the blocker.

To unblock this PR, either of the two paths from the review works:

Option A — keep this PR, revert one line: change static/index.html:50 back to relative form:

- navigator.serviceWorker.register('/sw.js?v=__WEBUI_VERSION__').catch(...)
+ navigator.serviceWorker.register('sw.js?v=__WEBUI_VERSION__').catch(...)

The dynamic <base href> script at static/index.html:16-17 already resolves the relative form to /sw.js for both root mounts and subpath mounts (/hermes/). Adding a regression test pinning the relative form would also be welcome to make sure nobody re-rewrites this.

Option B — drop fix #4 (sw.js) entirely from this PR. Issue #1481's premise ("no sw.js file exists") is already addressed in master via static/sw.js + the route at api/routes.py:1173-1194 — there's nothing left to fix there. The other three fixes (#1484 tool-card args, #1486 CLI rename, #1469 scroll pinning) can ship as-is.

Either way works for us. Once you push the update, comment here or remove need-updates and we'll re-review and pull off hold. Thanks again for bundling these — the other three are real improvements and worth landing.

- Revert '/sw.js' back to relative 'sw.js' in serviceWorker.register()
  (static/index.html:50). The dynamic <base href> script resolves
  relative paths correctly for both root and subpath mounts.
  Absolute path breaks reverse-proxy installs at e.g. /hermes/.

- Add regression test test_index_sw_registration_uses_relative_path
  to prevent future absolute-path rewrites from silently breaking
  subpath-mount installs.

Addresses reviewer feedback on PR nesquena#1492 (review by @nesquena).
@bergeouss
Copy link
Copy Markdown
Contributor Author

Review Feedback Addressed

  • Issue: sw.js registration uses absolute path (/sw.js), breaking subpath-mount installs behind reverse proxies (e.g. /hermes/). Reviewer requested reverting to relative path + adding a regression test.
  • Fix:
    • Reverted static/index.html:50 back to relative sw.js?v=__WEBUI_VERSION__ (removed the leading /). The dynamic <base href> script at lines 16-17 already resolves relative paths correctly for both root and subpath mounts.
    • Added regression test test_index_sw_registration_uses_relative_path in tests/test_pwa_manifest_sw.py that asserts the relative form is present AND the absolute form is absent — so future rewrites can't silently break subpath users again.
  • Files: static/index.html, tests/test_pwa_manifest_sw.py

Full test suite: 3815 passed, 0 failures. Ready for re-review.

🤖 AI-assisted via Hermes Agent

@nesquena-hermes nesquena-hermes closed this pull request by merging all changes into nesquena:master in 6c3ff3f May 3, 2026
pull Bot pushed a commit to soitun/hermes-webui that referenced this pull request May 3, 2026
…e + scroll pinning + sw.js relative-path regression test)
pull Bot pushed a commit to soitun/hermes-webui that referenced this pull request May 3, 2026
…ix + P0 polish bundle (nesquena#1466 nesquena#1494 nesquena#1469 nesquena#1484 nesquena#1486)

3 PRs in this batch (3866 → 3874 tests, +8):

- nesquena#1493 (@dso2ng) — sidebar Stop response cancels row's stream not active pane's (closes nesquena#1466, follow-up to nesquena#1480)
- nesquena#1495 (self-built; reported by @insecurejezza in nesquena#1494) — state.db connection FD leak in sidebar polling (closes nesquena#1494, addresses Bug #2 of nesquena#1458)
- nesquena#1492 (@bergeouss) — P0 bugfixes bundle: tool-card args readability + CLI rename persistence + scroll pinning + sw.js relative-path regression test (closes nesquena#1469 nesquena#1484 nesquena#1486)

This release closes Bug #2 of the umbrella issue nesquena#1458. Bug #1 was closed by v0.50.269 (nesquena#1483) + v0.50.270 (nesquena#1487). Bug #3 (HTTP-unhealthy without FD exhaustion) is the remaining work item.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants