Skip to content

Per-session TPS indicator in sidebar#1272

Closed
JKJameson wants to merge 18 commits intonesquena:masterfrom
JKJameson:tps-per-session-indicator
Closed

Per-session TPS indicator in sidebar#1272
JKJameson wants to merge 18 commits intonesquena:masterfrom
JKJameson:tps-per-session-indicator

Conversation

@JKJameson
Copy link
Copy Markdown
Contributor

This adds the tokens-per-second display from into the sidebar next
to each streaming session, and adds two UX improvements
for the chat menu button.

Changes

Backend

  • api/metering.py and api/streaming.py updated so sessions receive
    per-conversation metering events, with first_token_ts set before the
    metering stat is computed

Frontend

  • sessions.js adds _tpsWindow, a per-session rolling 5-second buffer
    of raw TPS readings; _refreshTpsLabel(sid) computes the average and
    updates the DOM at most once per second
  • Per-session session-attention-indicator in the sidebar now shows the
    live TPS value (e.g. 42 or 1.2K) during streaming, replacing the
    old global tpsStat chip in the header
  • Titlebar layout changes from justify-content:center to
    justify-content:space-between to accommodate the new TPS display slot

UX fixes

  • CSS hides the spinner and TPS value when the session row is hovered or
    the chat menu is open, making the menu button easier to click
  • All four sidebar refresh paths (streaming poll, time-refresh poll,
    gateway fallback poll, SSE sessions_changed) skip the DOM rebuild while
    a chat menu is open, preventing the menu from being accidentally closed

Tests

  • Updated test_sprint9, test_sprint37,
    test_issue856_pinned_indicator_layout, and
    test_workspace_panel_session_list to account for the new sidebar
    layout, versioned file URLs, and the 28px TPS spinner size

@nesquena-hermes
Copy link
Copy Markdown
Collaborator

Thanks for this PR!

What this does: Adds a live per-session tokens-per-second (TPS) indicator in the sidebar, replacing the old global tpsStat chip in the header. Each streaming session now shows its own TPS value next to the session attention indicator.

The approach looks correct:

  • _tpsWindow as a per-session rolling 5-second buffer of raw readings
  • _refreshTpsLabel(sid) computing the average and throttling DOM updates to once per second — avoids thrashing during fast streams
  • Hiding the spinner/TPS value on hover and when chat menu is open (so the menu button is accessible) is a thoughtful UX fix
  • Skipping DOM rebuild while chat menu is open prevents the menu from closing unexpectedly during a streaming poll

The diff is large (692 additions, 922 deletions) and touches metering.py, streaming.py, sessions.js, style.css, ui.js, and index.html — plus 7 test files. This is a significant refactor. A few things for the reviewer to focus on:

  1. The ui.js changes are large (-742 / +187). What was removed? The summary doesn't describe ui.js changes beyond the TPS slot.
  2. The added plan file (.hermes/plans/2026-06-18_hermes-webui-chat-history-wal.md) appears unrelated to the TPS feature — is that intentional?
  3. The first_token_ts change in metering.py — was it previously set after the metering stat was computed? Worth confirming the fix direction is correct.

Overall the core TPS display feature looks well-designed. The large diff warrants careful review before merge.

@JKJameson JKJameson force-pushed the tps-per-session-indicator branch 3 times, most recently from 41f0358 to acfac3d Compare April 29, 2026 22:17
@JKJameson
Copy link
Copy Markdown
Contributor Author

JKJameson commented Apr 29, 2026

Sorry for the mess. I want to be upfront about what happened rather than just push a clean commit and move on.

The short version: this is my first real contribution to a repo that moves this fast, and I got sloppy with mixing concerns across multiple sessions without rebasing or splitting things out properly. The TPS feature itself is solid, the rest is a learning curve.

What went wrong:

  • The .hermes/plans/2026-06-18_hermes-webui-chat-history-wal.md file was accidentally included, it belongs to a different branch (fix/chat-history-wal-recovery) and has no business in this PR. I've force-pushed to remove it from the history entirely.
  • The diff is large because I let this branch accumulate fixes over multiple days rather than rebasing or splitting them into focused PRs. The TPS display (7b59d6a onward) is the core feature, but it's buried under right-click guards, draft preservation, queue drain fixes, and UI test updates that should arguably be separate commits or even separate PRs.
  • The ui.js changes are bigger than they need to be. I removed the old global TPS chip and its update loop as part of the TPS feature, but some of the surrounding changes (the setBusy rework, busy-input send actions) are from parallel work that got merged in during the rebase rather than being kept separate.

What I'd do differently:

  • Keep the TPS feature in a clean branch and rebase onto master before opening the PR, rather than accumulating fixes on top.
  • Split the non-TPS fixes into separate PRs or at least separate commits with clear commit messages.
  • Be more careful about what lands in a given branch, the plan file and the WAL work should never have touched this branch at all.

On the first_token_ts question: yes, the fix moves first_token_ts to _mark_start() so it's set immediately when the stream begins, before any reading is recorded. Previously it was set inside _record_reading() which runs after the stat update, so a session that had started but not yet recorded a reading would incorrectly show first_token_ts = 0. This caused get_stats() to exclude it from the active session count. The fix direction is correct.

Recommended path forward: Drop the plan file, then I can either (a) split this into focused PRs, or (b) if you're comfortable with it, merge as-is with a note that subsequent clean-up PRs will follow. The TPS feature itself (7b59d6a through 8c2eb88) is the part that matters for this review.

@nesquena-hermes
Copy link
Copy Markdown
Collaborator

Thanks for the honest write-up, @JKJameson — this kind of transparency about what happened is genuinely useful for reviewers.

A few thoughts based on what you described:

On the accidental plan file: Good that you force-pushed to remove it from history. For future reference, a quick git log --oneline origin/master..HEAD before opening the PR is a useful check for files that shouldn't be there.

On the diff size: The maintainer will ultimately decide whether to merge as-is or ask for a split. Given that the TPS feature commits are identifiable (7b59d6a onward as you note), it's tractable to review even without splitting — as long as the non-TPS changes are correct and don't introduce regressions. If the maintainer wants a cleaner history, the path forward is probably: close this, open a focused TPS PR from 7b59d6a forward, and file separate PRs for the other fixes.

On the first_token_ts fix: The description makes sense — moving the assignment to _mark_start() so it's set immediately when the stream begins, before any reading is recorded, is the right behavior. A session that has started but not yet recorded a reading should still count as active.

Suggested next step for maintainers: Review the TPS core commits (7b59d6a8c2eb88) first. If those look good, the question is whether the surrounding changes (setBusy rework, busy-input send actions, right-click guards, draft preservation, queue drain) are clean enough to merge together or warrant their own PRs.

@nesquena-hermes
Copy link
Copy Markdown
Collaborator

Hold — skipped XSS sanitizer tests must pass before merge

Thanks for the TPS-per-session feature, @JKJameson! The core metering work looks well-structured. A few things need resolution before this can merge:

  1. Skipped XSS tests — the PR commits include:

    skip: XSS sanitizer tests not yet on this branch

    TestRendererSanitization (7 tests) and TestRawPreCodePreservation (1 test) are marked @pytest.mark.skip. We cannot merge a PR that skips existing tests. Please rebase onto current master (commit 9f269a4) to pick up the XSS sanitizer that's already there, then confirm all 8 tests pass.

  2. _HOUR_SECS / rolling HIGH/LOW history removal — the metering PR removes the 60-minute rolling history for high/low TPS tracking. That's fine if the frontend no longer uses those fields — please confirm the UI doesn't consume high/low from the metering event, or that it gracefully handles them being absent.

  3. Draft commit — one commit is titled cleanup: remove filter-branch backup artifacts which suggests stale git history artifacts. Please double-check the branch history is clean.

Once the rebase is done and all tests pass (full pytest tests/ run), we'll pull this right in — the per-session TPS indicator is a great feature!

@nesquena nesquena added the ux User experience / visual polish label Apr 30, 2026
nesquena-hermes added a commit that referenced this pull request Apr 30, 2026
## 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)
@JKJameson JKJameson force-pushed the tps-per-session-indicator branch from acfac3d to e91bdfe Compare April 30, 2026 21:08
@JKJameson
Copy link
Copy Markdown
Contributor Author

JKJameson commented Apr 30, 2026

XSS tests: Rebased onto origin/master

high/low rolling history: Confirmed no frontend consumption. Updated the stale docstring in api/metering.py which still described the removed 60-minute rolling history.

cleanup: removed filter-branch backup artifacts.

This was referenced Apr 30, 2026
Josh added 15 commits May 1, 2026 09:47
…menu open

- CSS: hide .session-state-indicator.is-streaming on hover (via :has) and
  when .menu-open, so the menu button is easier to click without the live
  indicator covering it
- sessions.js: guard all four sidebar refresh paths (streaming poll,
  time-refresh poll, gateway fallback poll, SSE sessions_changed) with
  if(_sessionActionMenu) return to prevent closeSessionActionMenu() from
  destroying an open menu mid-interaction
…extarea draft on chat finish

- index.html: add role=img aria-label=Hermes to .app-titlebar-icon (was aria-hidden=true)
- style.css: add .app-titlebar-rename-input CSS class matching .session-title-input styling
- messages.js: save raw textarea draft to S._drafts[sid] before clearing on send()
- ui.js: in setBusy(false), restore draft for current session when no queued message fires
…witch

The S._drafts path in messages.js was dead code:
1. Saved to S._drafts[sid] then immediately deleted in the same function,
   so nothing could ever be read from it.
2. sessions.js reads from S.composerDrafts[sid] anyway — completely disjoint keys.

Draft persistence works correctly via S.composerDrafts in sessions.js:
switch-away saves it, loadSession() restores it.
Bug 1 — composerDrafts key mismatch:
sessions.js writes drafts to S.composerDrafts (correct), but ui.js
setBusy(false) restore path read from S._drafts (never written).
Fix: switch ui.js to S.composerDrafts on both read and delete.

Bug 2 — stream_end missing queue drain:
The stream_end handler closed the SSE source but did not call
_queueDrainSid=activeSid;setBusy(false), so queued messages were never
drained when a stream ended without a preceding done event.
Fix: add the same drain call that done{} performs.
cancel_stream() uses Session.load() (imported as _Session locally) not
get_session(), so the test patches must target api.models.Session.load.
The original get_session patches were a leftover from before the WAL
crash-recovery changes (852708d) switched cancel_stream to use Session.load.
@JKJameson JKJameson force-pushed the tps-per-session-indicator branch from e91bdfe to e6d2a81 Compare May 1, 2026 08:47
@nesquena-hermes nesquena-hermes added the ready-for-review Held PR feedback addressed; awaiting maintainer to remove hold label May 2, 2026
@nesquena-hermes
Copy link
Copy Markdown
Collaborator

Re-reviewed after the 2026-04-30 push. The skipped XSS sanitizer tests are now rebased and passing, the stale 60-min rolling-history docstring in api/metering.py is updated, and filter-branch backup artifacts are cleaned up. CI is green across 3.11 / 3.12 / 3.13.

Adding ready-for-review label — flagging as a candidate for hold removal in the next maintainer pass. Per house policy on UI-surface PRs of this size (449 LOC, 13 files), this will get an independent review pass once the hold is lifted, before merge.

Thanks for the persistence and the transparent write-up about the workflow earlier.

@nesquena nesquena closed this May 3, 2026
JKJameson pushed a commit to JKJameson/hermes-webui that referenced this pull request May 3, 2026
JKJameson pushed a commit to JKJameson/hermes-webui that referenced this pull request May 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

hold ready-for-review Held PR feedback addressed; awaiting maintainer to remove hold ux User experience / visual polish

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants