Skip to content

Fix batch session actions and in-flight reload recovery#1473

Merged
2 commits merged intonesquena:masterfrom
Thanatos-Z:fix-batch-select-and-inflight-reload
May 2, 2026
Merged

Fix batch session actions and in-flight reload recovery#1473
2 commits merged intonesquena:masterfrom
Thanatos-Z:fix-batch-select-and-inflight-reload

Conversation

@Thanatos-Z
Copy link
Copy Markdown
Contributor

@Thanatos-Z Thanatos-Z commented May 2, 2026

Summary

Fixes two WebUI session sidebar regressions:

  • Batch select actions could render as an empty/global bottom bar, show literal {0} placeholders in confirmation text, or report 0 selected after clicking checkboxes because checkbox events could bubble into the row-level select toggle.
  • Reloading /session/<id> during an in-flight turn could show the empty chat home even though the sidebar still showed the running zero-message session.

Changes

  • Render the batch action bar inline inside the session list instead of mounting it as a fixed global footer.
  • Re-render batch action buttons when selection changes instead of only revealing an empty bar.
  • Add string-valued {0} interpolation support to the shared t() i18n helper.
  • Anchor the batch project picker inside the batch action bar and override the shared absolute picker styling.
  • Prevent checkbox clicks from double-toggling through the row pointer handler.
  • Keep zero-message sessions visible and loaded when they have active_stream_id or pending_user_message during reload recovery.

Tests

  • uv run pytest tests/test_session_batch_select.py
  • Result: 18 passed in 1.02s

@nesquena-hermes
Copy link
Copy Markdown
Collaborator

Thanks for the surgical PR — both fixes target real regressions and the diagnosis on each is on point. The change is small enough I can review it inline.

(1) Batch action bar — moving from fixed footer → inline sidebar

Solid call. The previous position:fixed; bottom:0 overlay was already conflicting with the composer on narrower viewports and made batch-mode discoverability worse on mobile (the bar floated over the input). Inline-in-the-sidebar is the right home.

A few things I want to confirm:

  • Bar visibility on first selection. _updateBatchActionBar now calls _renderBatchActionBar instead of just toggling display. The first time a user clicks a checkbox, the bar gets rendered fresh — good. But on subsequent toggles it re-runs bar.innerHTML='' + full rebuild, which is fine for ~6 buttons but means any inline picker (.batch-project-picker) is destroyed mid-interaction if the user clicks a row checkbox while the picker is open. Probably acceptable since clicking outside should close the picker anyway, but worth confirming the project picker actually closes cleanly in that race.
  • bar.style.display='flex' literal. The CSS uses display:none as the hidden state and the inline display:flex to show. That works but means the CSS .batch-action-bar never gets a non-none display declaration of its own — anyone reading the stylesheet won't see the layout intent. Consider adding &.open{display:flex;} and toggling a class instead, but this is a style nit not a blocker.

(2) t() i18n string-placeholder interpolation

Nice — this was a real footgun. The existing function-valued translations ((n) => ...) work, but the simple {0} string form silently fell through. Two notes:

  • The new regex /\{(\d+)\}/g correctly handles {0}/{1}/{N} but doesn't handle escaping (\{0\} literal). Probably out of scope — none of the current locale strings need to render literal {0}. Worth a one-line comment explaining the limitation though, so a future contributor doesn't add {0} to a locale string and wonder why it disappears.
  • The Object.prototype.hasOwnProperty.call(args, idx) guard is the right shape (preserves unknown placeholders instead of injecting undefined).

(3) In-flight reload recovery — keeping zero-message streaming sessions visible

This is the more interesting fix and the one I want to think about more carefully. Two paths converge:

// boot.js
const _restoredInFlight = S.session && (
  S.session.active_stream_id ||
  S.session.pending_user_message
);
if(S.session && (S.session.message_count||0) === 0 && !_restoredInFlight){
  S.session=null; S.messages=[];
  ...
}
// sessions.js — renderSessionListFromCache filter
const withMessages=allMatched.filter(s=>
  (s.message_count||0)>0 ||
  _isSessionEffectivelyStreaming(s) ||
  !!s.active_stream_id ||
  !!s.pending_user_message ||
  ...
);

Both are guarding the right invariant: a zero-message session that is actually streaming (server-tracked active_stream_id) or has a pending_user_message (user submitted but agent hasn't replied yet) must stay visible across reload. The previous logic dropped them as "ephemeral new chat" sessions, which matched #1171 — but that filter was too aggressive for the streaming case.

One concern I want to verify: how does the server expose active_stream_id and pending_user_message on the session list payload (GET /api/sessions)? If the list endpoint returns message_count=0 but doesn't include active_stream_id, the filter on the client doesn't help — we'd still drop them. Looking at api/session_ops.py and api/routes.py should confirm whether the list endpoint already includes these fields. If not, this PR needs a server-side companion change.

The unit tests assert that the JS references active_stream_id and pending_user_message, but they don't assert that the server actually populates those fields on /api/sessions. A targeted Python test that hits the list endpoint with a synthetic streaming session and asserts those fields are present would close the loop.

Tests

Eighteen passing tests is good coverage for the static-file-string assertions. But these are structural tests — they assert the source code contains certain strings, not that the runtime behavior is correct. For the in-flight reload path specifically, an end-to-end browser test (or at minimum a JSDOM test that mocks S.session with active_stream_id set and verifies the boot path doesn't null it) would be more valuable than the string-match assertions.

Not a blocker for merge — the string assertions catch regressions and are cheap to maintain. Just calling out that they're a different category of test than what the PR description ("18 passed") implies.

Verdict

The shape is right and both fixes are real. Three things I want addressed before merge:

  1. Confirm the server already populates active_stream_id / pending_user_message on the session-list payload — paste a curl -s http://127.0.0.1:8787/api/sessions | jq '.[0]' showing those fields. If not, this needs a backend companion.
  2. Add a one-line comment in the t() regex explaining the {0} placeholder format (so future locale contributors know it's now supported).
  3. Confirm the project picker closes cleanly when the user clicks a row checkbox while the picker is open (the _renderBatchActionBar rebuild may strand the close-on-outside-click handler).

If those check out, I'd merge this. cc @nesquena — this touches both #1171 (zero-message session filter) and the streaming-resume work.

Labelling bug + needs-info pending the three items above.

@nesquena-hermes nesquena-hermes closed this pull request by merging all changes into nesquena:master in 5650d11 May 2, 2026
Thanatos-Z pushed a commit to Thanatos-Z/hermes-webui that referenced this pull request May 2, 2026
Thanatos-Z pushed a commit to Thanatos-Z/hermes-webui that referenced this pull request May 2, 2026
…w-up

- CHANGELOG.md: v0.50.267 entry detailing nesquena#1454/nesquena#1474/nesquena#1461/nesquena#1465/nesquena#1467/nesquena#1460/nesquena#1473
  + Opus advisor SHOULD-FIX trailing-empty guard for _norm_model_id
- ROADMAP.md: bump to v0.50.267, 3776 tests collected
- TESTING.md: bump header + total to 3776
- api/config.py: trailing-empty fallback in _norm_model_id (parts[-1] or s)
- static/ui.js: mirror trailing-empty fallback in _normalizeConfiguredModelKey
- tests/test_norm_model_id_trailing_empty_guard.py: 5 regression tests
@Thanatos-Z Thanatos-Z deleted the fix-batch-select-and-inflight-reload branch May 2, 2026 17:32
nesquena-hermes added a commit that referenced this pull request May 2, 2026
v0.50.269 — Bootstrap supervisor fix (#1478) + #1473 follow-ups (#1479, #1480)
nesquena-hermes pushed a commit to ccqqlo/hermes-webui that referenced this pull request May 2, 2026
…267 follow-ups

- CHANGELOG.md: v0.50.269 entry detailing nesquena#1478 nesquena#1479 nesquena#1480
- ROADMAP.md: bump to v0.50.269, 3847 tests collected
- TESTING.md: bump header + total to 3847

nesquena#1478: nesquena APPROVED self-built bootstrap.py --foreground mode
       (closes nesquena#1458 Bug nesquena#1, +Opus follow-ups: XPC noise filter, executability guard)
nesquena#1479: surgical follow-up to nesquena#1473 — Session.compact() now includes pending_user_message
nesquena#1480: bfcache pageshow restores active session via loadSession + checkInflightOnBoot

3847 tests pass (+47 net). Opus advisor on stage diff: no blockers.
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.

2 participants