Commit Graph

216 Commits

Author SHA1 Message Date
nesquena-hermes 56d88723cf fix(ux): add has-tooltip--left variant for right-edge buttons + fix tests
(1) Send-button tooltip clipping fix:
    The send button (btnSend) sits at the right edge of the composer area.
    Its side-positioned tooltip extended 'Send message' (~95px wide) past
    the viewport edge, leaving only 'Se' visible in some viewports —
    confirmed by maintainer screenshot review.

    Added a new `.has-tooltip--left` variant that flips the tooltip to
    the LEFT side of the trigger via `right: calc(100% + 8px)` instead
    of `left: calc(100% + 8px)`. Applied to btnSend in index.html.
    Browser-verified: full 'Send message' text now readable to the left
    of the gold Send button, no clipping.

(2) Test compatibility for the tooltip coverage expansion:
    5 pre-existing tests hardcoded specific class strings or 'title='
    attributes that no longer apply after we added has-tooltip + replaced
    title= with data-tooltip= on 11 high-traffic icon buttons.

    - tests/test_issue1488_composer_voice_buttons.py:
      - test_dictation_button_has_dictate_i18n_key: accept either
        title='Dictate' or data-tooltip='Dictate' as the static fallback.
      - test_buttons_have_distinct_static_titles: extracted helper
        _static_tooltip() that prefers data-tooltip over title.
    - tests/test_sprint20.py::test_mic_button_has_mic_btn_class:
      regex tolerant to additional utility classes between icon-btn and
      mic-btn (now 'icon-btn mic-btn has-tooltip').
    - tests/test_sprint20b.py::test_send_button_has_title_attribute:
      accept title= OR data-tooltip= per #1775.
    - tests/test_sprint20b.py::test_send_button_still_has_send_btn_class:
      regex tolerant to additional utility classes.
    - tests/test_workspace_panel_session_list.py::TestWorkspacePanelCollapsePriority::test_panel_header_no_longer_uses_space_between:
      panel-header was changed from overflow:hidden to overflow:visible
      so its tooltips can escape the header bar. The title-text ellipsis
      moved to the inner span (.panel-header > span:first-child) which
      already had its own overflow:hidden + text-overflow:ellipsis.
      Test now accepts either parent-level or inner-span overflow handling.

All 192 of the previously-failing or impacted tests now pass.
2026-05-07 04:30:02 +00:00
nesquena-hermes 53ad5eccba fix(ux): allow tooltips to escape panel-header overflow + polish shadow
Browser-verified two issues with stage-311 tooltip rendering:

(1) Workspace panel header tooltips (NewFile, NewFolder, Refresh, etc.)
    were being clipped because .panel-header had overflow:hidden. The
    title span at `.panel-header > span:first-child` already has its own
    overflow:hidden + text-overflow:ellipsis for the workspace name
    truncation, so the parent doesn't need it. Changed .panel-header to
    overflow:visible — verified tooltip now floats correctly below the
    icon row, ellipsis on the title still works because the inner span
    handles it locally.

(2) Strengthened tooltip body styling per browser screenshot review:
    - Border: var(--border) (#2A2A45 dark slate) → var(--accent-bg-strong)
      (gold-tinted at 15% alpha). Subtle brand-tied edge that's slightly
      more visible against the very dark page background.
    - Shadow: 6px/20px / 0.55 alpha + 1px ring at 0.25 → 8px/24px / 0.65
      alpha + 1px ring at 0.35 + 1px inset highlight at 0.04 alpha. Gives
      the tooltip more elevation against the dark theme so it reads as a
      floating element rather than painted onto the background.

All 19 tooltip pytest checks still pass. Browser-verified on rail
(Tasks, Settings), composer (Attach files, Send message), and workspace
panel header (New folder) — screenshots delivered to maintainer for
visual sign-off.
2026-05-07 04:24:31 +00:00
nesquena-hermes 6dd133b1f7 fix(ux): drop tooltip arrow/caret, use spatial proximity instead
Browser verification of the rail tooltip showed the 5px arrow ::before
pseudo-element was rendering as a tiny rectangle slice (not a triangle)
because the global `*, ::before, ::after { box-sizing: border-box }`
reset makes the colored border eat inward from a 10×10 box rather than
projecting outward from a 0×0 box. Adding `box-sizing: content-box`
inline to the pseudo fixes the geometry but at 11px text size and 5px
border-width the resulting triangle reads as visual noise rather than
a clear connector — multiple AI vision passes consistently couldn't
identify the arrow even when it was rendering correctly.

VS Code, Slack, and Linear's rail/icon-button tooltips all skip the
arrow for the same reason: spatial proximity at small sizes (an 8px gap
between trigger and tooltip body) is sufficient association without
the visual clutter of a tiny triangle.

Removes both ::before pseudo-rules. Tooltip body unchanged. Side
tooltip moved 12px → 8px gap (closer to trigger now that the arrow is
gone), bottom tooltip 10px → 8px for the same reason.

Browser-verified: rail Tasks tooltip rendering at 8/10 polish per
vision-AI assessment of the standalone tooltip body (solid surface bg,
solid border, warm-white text, 6px shadow + 1px ring, z-index 1500).

Co-authored-by: Jason Wu <jasonjcwu@users.noreply.github.com>
2026-05-07 04:11:40 +00:00
nesquena-hermes d41555cec6 fix(ux): polish CSS tooltips + clear native title + extend coverage
Stage 311 maintainer-side enhancements on top of @jasonjcwu's PR #1782,
addressing browser-verified issues + extending coverage to high-traffic
icon buttons:

(1) Clear native title when custom data-tooltip is present (the core bug fix):
    - static/i18n.js: when data-i18n-title runs against an element that has
      data-tooltip, sync data-tooltip AND removeAttribute('title'). Without
      this, the slow ~1.5s native browser tooltip co-fires alongside the
      fast custom CSS tooltip — exactly the bug #1775 reports.
    - static/ui.js _applyDashboardStatus: same treatment for the dashboard
      rail/mobile buttons (was setting btn.title=warning unconditionally).
    - static/boot.js: added _setButtonTooltip() helper, replaced 6 direct
      .title assignments (workspace toggle/collapse/clear, voice dictate,
      voice mode active/inactive) with calls through the helper.

(2) Extend coverage to high-traffic icon buttons in static/index.html:
    - Composer area (side tooltip): btnAttach, btnMic, btnVoiceMode,
      btnWorkspacePanelToggle, btnSend.
    - Workspace panel header (bottom tooltip): btnCollapseWorkspacePanel,
      btnUpDir, btnNewFile, btnNewFolder, btnRefreshPanel, btnClearPreview.
    - All 11 buttons gain has-tooltip[--bottom] class and data-tooltip,
      lose their native title=. Total covered surfaces: rail (12), sidebar
      nav-tabs (12), panel-head (31), composer/workspace icons (11) = 66.

(3) CSS polish (browser-verified visible improvement):
    - z-index 60 → 1500/1501 so the tooltip clears all sidebar/panel
      stacking contexts. Earlier verification showed the tooltip overlapping
      the Filter conversations search input.
    - background: var(--bg-strong, ...) → var(--surface) (solid #1A1A2E
      instead of falling back via undefined cascade).
    - color: var(--text, var(--accent-text)) → var(--text) (solid warm white
      #FFF8DC instead of gold which clashed at body-text size).
    - border: var(--accent-bg-strong) → var(--border) (#2A2A45 solid
      instead of gold at 0.15 alpha — the old border was barely visible
      and the arrow ::before triangle was invisible).
    - shadow: 4px/0.45 alpha → 6px/0.55 alpha + 0 0 0 1px ring fallback.
    - Added 150ms hover-onset delay (matches Cygnus's spec in #1775); 0s
      dismissal-delay so quick mouse-aways don't leave the tooltip behind.
    - Fixed has-tooltip--bottom arrow direction: was pointing down (wrong),
      now points up at the trigger (border-color order corrected).
    - Bumped offsets: side tooltip 10px → 12px (clearance from icon edge),
      bottom tooltip 8px → 10px.

(4) Test fixes (the 2 CI failures):
    - tests/test_cron_refresh_button_835.py: assertion accepts either
      title= or data-tooltip= per #1775 (was hardcoded title=).
    - tests/test_mobile_layout.py::test_profiles_sidebar_tab_present:
      regex tolerant to additional utility classes (has-tooltip).

(5) Regression tests added to tests/test_css_tooltips.py:
    - test_native_title_cleared_when_custom_tooltip_present: pins the
      removeAttribute('title') call so we don't regress to dual tooltips.
    - test_native_title_path_preserved_for_non_tooltip_elements: pins the
      el.title fallback for elements without data-tooltip.

Browser-verified: all 72 has-tooltip elements have zero native title at
runtime (was 94 with native, 2 stuck via dashboard JS path).

Co-authored-by: Jason Wu <jasonjcwu@users.noreply.github.com>
2026-05-07 04:00:40 +00:00
fxd-jason b86bdf9dc8 fix(ux): replace native title tooltips with custom CSS tooltips (#1775)
- Add .has-tooltip CSS utility class with 300ms delay (vs ~1500ms native)
  - Position-aware: right side for rail buttons, bottom for nav/panel buttons
  - Arrow indicator pointing back at trigger element
  - :focus-visible support for keyboard accessibility
  - prefers-reduced-motion: no animation for users who opt out
- Replace native title="" with data-tooltip="" on all rail-btn, sidebar
  nav-tab, and panel-head-btn elements in index.html
- Sync data-tooltip via data-i18n-title handler for locale switching
- 17 tests covering HTML coverage, CSS class definitions, and i18n sync

Closes #1775
2026-05-07 03:58:16 +00:00
Michael Lam 2d20842450 fix: surface Codex usage exhaustion errors 2026-05-07 01:39:52 +00:00
nesquena-hermes 52e1689083 chore(release): stamp v0.51.13 — single-PR composer UX (#1758)
Constituent PR:
- #1758 (@nesquena-hermes) — feat(composer): click pasted/attached image
  thumbnails to lightbox-zoom them. Refs #1733. Companion Mac PR
  hermes-webui/hermes-swift-mac#74 for sequential-paste filename uniqueness.

Independent review: @nesquena APPROVED with exhaustive headless-Chrome
behavioural harness verifying all 4 click paths (thumb-image, ×-on-image,
×-on-audio, audio-element). Pre-fix verification confirmed 4/5 of the new
tests catch regressions to the previous state.

Opus advisor: SHIP, all 6 verification questions clean. One non-blocking
nit absorbed in-release: wrap .attach-thumb:hover in @media (hover: hover)
for iPad sticky-hover hygiene (3-LOC defensive cleanup).

Tests: 4637 → 4642 collected (+5). 4630 passed, 9 skipped, 3 xpassed,
0 failed.

Pre-release verification:
- pytest 4630 passed, 0 failed
- node -c clean on static/ui.js
- 11/11 browser API endpoints PASS
- Pre-stamp re-fetch: PR head still matches local rebase
- Opus advisor: SHIP, 0 MUST-FIX

Refs #1733.
2026-05-06 20:14:10 +00:00
nesquena-hermes 759c25655d feat(composer): click pasted/attached image thumbnails to lightbox-zoom them
When pasting screenshots into the composer (especially multiple in
sequence, now possible end-to-end with hermes-webui/hermes-swift-mac
PR #74) the user has no way to verify the right image attached. The
56x56 thumbnail in the chip is fine as a UI affordance but offers no
detail at all. Quote from the request:

  When I hit Cmd+C and save an image to the clipboard and then paste
  the clipboard out, I want to be able to click on any one of those
  uploaded images that's inside the composer bar and have it zoom up
  like a lightbox so I can see the image in full once it's been
  pasted in to the composer input.

The lightbox infrastructure already exists for message-attached
images (static/ui.js:269 _openImgLightbox + the doc-level click
delegate at :298 for .msg-media-img). This PR extends the same
delegate to also fire on .attach-thumb composer chips:

  - Clicking the thumbnail opens the existing image lightbox with the
    blob URL as src and the file name as alt text.
  - Audio/video chips are excluded (they have their own native
    <audio> / <video> controls and don't render an .attach-thumb
    img).
  - SVG thumbnails (.attach-thumb attach-thumb--svg) qualify — they
    are images visually.
  - The chip's x remove button is a sibling, not an ancestor, of the
    thumb — closest('.attach-thumb') from the button returns null,
    so removing still works without lightbox interference.

Also updates static/style.css:
  - cursor: zoom-in on .attach-thumb (was cursor: default — actively
    misleading).
  - Subtle :hover emphasis (brightness 1.05 + scale 1.04, 120ms ease)
    so users discover the affordance before clicking.

5 regression tests in tests/test_composer_chip_lightbox.py pinning:
  - delegate handles .attach-thumb on IMG elements
  - delegate still handles .msg-media-img (no regression)
  - audio/video chips do NOT render an .attach-thumb img
  - cursor:zoom-in declared on the .attach-thumb selector
  - hover emphasis rule present

Browser-verified live on port 8789:
  - addFiles three distinct screenshot files (mimicking three Mac
    sequential pastes) -> 3 chips, 3 thumbs, all distinct.
  - Click thumb #2 -> lightbox opens with the right image, alt text
    matches filename.
  - Click x on chip #2 -> removes that chip, no lightbox.
  - Escape key closes lightbox.

Companion PR on the Mac side:
hermes-webui/hermes-swift-mac#74 (unique filename per paste so
sequential pastes actually appear as distinct chips).

Refs nesquena/hermes-webui#1733.
2026-05-06 19:54:04 +00:00
Michael Lam a7b6cd2cda fix: simplify compact activity summaries 2026-05-06 06:27:13 +00:00
Michael Lam fe9e4645ac fix: move system health panel into insights 2026-05-05 17:30:56 +00:00
Michael Lam fdeac578da feat: add VPS resource health panel 2026-05-05 17:30:56 +00:00
bergeouss 6173d6d0ea fix(ui): inline provider chip + group model count in model picker (#1425)
- Add .model-opt-provider chip (right-aligned, muted) on every model row
  that belongs to a provider group, making same-name models across
  providers visually distinguishable at a glance.
- Add per-group model count to group headings: 'OpenRouter (47)'.
- Add subtle border-top divider between provider groups for visual
  separation during scroll.

Scope: Shape A from #1425 — smallest change, ~15 LOC, no API churn.
Note: Settings model picker is a native <select> and already has optgroup
labels; this targets the custom dropdown used in the composer.

Closes #1425
2026-05-05 15:41:22 +00:00
test 449f37ebd8 Stage 300: PR #1673 — feat: show LLM Gateway routing metadata by @Michaelyklam 2026-05-05 02:27:24 +00:00
test 32f37d3d78 Stage 300: PR #1676 — Add Hermes agent heartbeat alert by @Michaelyklam 2026-05-05 02:27:24 +00:00
Michael Lam c94ec31dec feat: show LLM Gateway routing metadata 2026-05-05 02:26:55 +00:00
Michael Lam 22df075b8a feat: add active provider quota status 2026-05-05 02:26:52 +00:00
Michael Lam 960e45f77f feat: add agent heartbeat alert 2026-05-05 02:25:06 +00:00
test df8ee6a8ad Stage 299: PR #1662 — feat(logs): add Logs tab MVP by @Michaelyklam 2026-05-05 01:53:56 +00:00
Michael Lam af1c628292 feat: add logs tab MVP 2026-05-05 01:51:05 +00:00
Michael Lam 2684d6fa98 feat: add LLM Wiki status panel 2026-05-05 01:48:32 +00:00
test 3699e83c43 Stage 298: PR #1677 — feat: link official Hermes dashboard by @Michaelyklam 2026-05-05 01:29:49 +00:00
Michael Lam b0953b6a7f feat: link official Hermes dashboard 2026-05-05 01:23:55 +00:00
Michael Lam e0e991126f feat: add searchable MCP tool inventory 2026-05-05 01:20:32 +00:00
test 2ec18b728a Stage 298: PR #1670 — feat: add MCP server visibility panel by @Michaelyklam 2026-05-05 01:18:35 +00:00
test 8c93b995ef Stage 298: PR #1678 — Add Claude Code session imports by @Michaelyklam 2026-05-05 01:18:35 +00:00
test def1507828 Stage 298: PR #1674 — feat(tasks): add scheduled job profile selector by @Michaelyklam 2026-05-05 01:18:35 +00:00
test dfb3798470 Stage 298: PR #1663 — feat: add plugins visibility panel by @Michaelyklam 2026-05-05 01:18:35 +00:00
Michael Lam 399326f923 feat: add MCP server visibility panel 2026-05-05 01:18:34 +00:00
Michael Lam e54a0470f0 Add Claude Code session imports 2026-05-05 01:18:34 +00:00
Michael Lam 3f3092a84e feat: add scheduled job profile selector 2026-05-05 01:18:34 +00:00
Michael Lam 60ed948f42 feat: add plugins visibility panel 2026-05-05 01:18:33 +00:00
test 890f53465c Stage 298: PR #1668 — feat(insights): add daily token trends and model usage costs by @Michaelyklam 2026-05-05 01:12:26 +00:00
test cc36dac64b Stage 298: PR #1667 — feat: add WebUI status command card by @Michaelyklam 2026-05-05 01:12:26 +00:00
Michael Lam 66755b7fb1 feat: add insights token trends 2026-05-05 01:12:08 +00:00
Michael Lam d12b028c81 feat: add WebUI status command card 2026-05-05 01:12:07 +00:00
Michael Lam b2f35a41e1 fix: window long session message rendering 2026-05-05 01:12:07 +00:00
Nathan Esquenazi 397d851bdb feat(kanban): multi-board management + SSE live event stream
Closes the remaining gaps to first-party Hermes Agent dashboard parity:
multi-board CRUD on /api/kanban/boards and a real-time event stream over
Server-Sent Events. Builds on top of #1660 (review-feedback hardening).

== Multi-board ==

Five new endpoints mirror the agent dashboard plugin contract verbatim
(plugins/kanban/dashboard/plugin_api.py) so a single CLI / gateway slash
command / dashboard / WebUI all share the same active-board pointer:

  GET    /api/kanban/boards
  POST   /api/kanban/boards
  PATCH  /api/kanban/boards/<slug>
  DELETE /api/kanban/boards/<slug>
  POST   /api/kanban/boards/<slug>/switch

All existing endpoints accept ?board=<slug> (and writes also accept
'board' in the JSON body) — query takes precedence over body. The slug
travels through the kanban_db library which already had multi-board
support; the bridge is mostly thin wrappers around create_board /
remove_board / list_boards / set_current_board / get_current_board.

The default board is protected from deletion. Slugs are normalised
through kb._normalize_board_slug() with path-traversal rejection.
Archive is the default for DELETE; ?delete=1 hard-deletes.

Frontend gets a 'Default ▾' switcher pill in the panel header. The menu
lists every board (current first), per-status total badges, plus three
actions (New / Rename / Archive). Create + rename use the same modal
with a slug auto-derived from the name. Archive routes through the
existing showConfirmDialog with a clear 'tasks remain on disk and the
board can be restored from kanban/boards/_archived/' message.

Active-board state is persisted to localStorage so a refresh stays put.
The on-disk pointer in kanban/current is the cross-process source of
truth, kept in sync via POST /boards/<slug>/switch.

== SSE event stream ==

GET /api/kanban/events/stream is a long-lived Server-Sent Events feed
that mirrors the agent dashboard's WebSocket /events contract. The
WebUI uses SSE rather than WebSocket because (1) the existing transport
is BaseHTTPServer, not async — WS would require a significant refactor
or a hijack-the-socket hack; (2) SSE is the right tool for unidirectional
server-pushed event streams; (3) browsers auto-reconnect on drop;
(4) the existing /api/approval/stream and /api/clarify/stream patterns
are proven and easy to copy.

The handler polls task_events at 300ms (matching the agent dashboard's
WebSocket poll cadence) so write-to-receive latency is identical.
Heartbeats every 15s prevent proxy/CDN reaping. Hard cap of 200 events
per batch.

Frontend uses EventSource by default and falls back to 30s HTTP polling
after 3 SSE failures. A 250ms debounce coalesces bursts of N events
into a single board re-fetch. Stream is torn down when the user leaves
the Kanban panel.

== Bugs fixed during build ==

(1) read_only=True legacy lie. _board_payload, _events_payload,
    _task_log_payload, and the no-change short-circuit all hardcoded
    read_only=True from the read-only-bridge era of #1645. Bridge has
    been writable since #1649 — flag now matches reality.

(2) Modal + dropdown menu transparent backgrounds. The PR stack used
    var(--panel) which is undefined in the WebUI design system (uses
    --surface, --bg, gradient panels). Replaced with the same gradient
    + accent border pattern used by the .app-dialog overlay.

(3) Archive race. kb.connect(board=<slug>) auto-materialises the
    directory + sqlite on first call, so any in-flight SSE poll on a
    board mid-archive would silently un-archive it by re-creating the
    directory. Two-layer fix: (a) frontend stops the SSE stream BEFORE
    the DELETE call, restarts on failure; (b) bridge's _kanban_sse_fetch_new
    checks kb.board_exists() before connect(), returning empty results
    when the board is gone.

(4) Save vs. Cancel button visual hierarchy. Both rendered as identical
    secondary buttons in the modal. Save now uses the .primary class
    with accent-tinted gold styling.

(5) Mobile viewport gaps. Added 9 rules under @media (max-width: 640px)
    covering the switcher button (smaller padding/font), name truncation
    (max-width:140px), menu sizing (min(280px, 100vw - 24px)), modal
    padding, and inline-row stacking.

== Tests ==

+45 new tests across two files. Bridge tests: 18 covering board CRUD
endpoints, slug validation, default-board protection, dispatcher routing,
board isolation (verified via connect() spy), and 3 SSE tests including
a worker-thread integration test with threading.Event watchdog. UI static
tests: 11 covering switcher markup, modal markup, JS handler presence,
REST verb usage, board-param plumbing, localStorage persistence,
showConfirmDialog usage, EventSource subscription, polling fallback,
panel-switch teardown, and 250ms debouncing.

Bridge tests: 18 → 36 (+18 multi-board, +3 SSE)
UI static tests: 15 → 26 (+11)
Total kanban: 33 → 63

Full repo test suite: 4351 passed, 0 regressions.

== Live verification ==

End-to-end browser walkthrough on port 8789:
- Create Sprint 12 + Backlog via modal: switcher updates ✓
- Switch between boards: count isolation correct ✓
- Add task on Sprint 12 via API: SSE delivers in 400ms ✓
- 5-task burst: 250ms debounce coalesces to single render ✓
- Rename board via modal: switcher label updates ✓
- Archive board: confirm dialog → board moved to _archived/, no zombie
  directory (race fix verified) ✓
- Zero JS errors throughout 11-step flow

Co-authored-by: ai-ag2026 <ai-ag2026@users.noreply.github.com>
2026-05-05 00:18:36 +00:00
Nathan Esquenazi 7e48a2fd85 fix(kanban): polish + ImportError fallback
Four follow-up issues found in the combined-stack live verification:

(1) handle_kanban_get had no exception handler; ImportError (webui-only deploy
    without hermes_cli), ValueError, LookupError, RuntimeError would bubble
    as 500. Wrapped in same exception cascade as POST/PATCH/DELETE.

(2) ImportError on any verb now returns 503 "kanban unavailable: <reason>"
    instead of 500. Frontend's existing try/catch surfaces a clean toast.

(3) The 'Read-only view' banner (legacy of read-only PR #1645) was always
    visible regardless of actual board state. Default-hidden in HTML;
    loadKanban() toggles based on _kanbanBoard.read_only.

(4) .btn / .btn.secondary class names were referenced in 4 places (Bulk
    action / Nudge dispatcher / New task / Back to board) but no matching
    CSS shipped — buttons rendered as browser-default beveled controls
    that clashed with the dark theme. Added scoped CSS rules under the
    kanban-* parent containers.

+4 behavioral + static UI tests covering the contracts.

Co-authored-by: ai-ag2026 <ai-ag2026@users.noreply.github.com>
2026-05-04 23:32:05 +00:00
Manfred 711e33e7db feat: harden Kanban review feedback
- add canonical PATCH and DELETE routing for Kanban writes
- fix task detail log rendering and add close/back affordance
- improve timestamps, event summaries, stats HUD, and mobile layout
- cover route and detail behavior with targeted tests
2026-05-04 22:56:43 +00:00
Manfred d7671f8366 feat: polish Kanban UI parity 2026-05-04 22:56:43 +00:00
Manfred dc3418c209 feat: add Kanban dashboard parity core 2026-05-04 22:56:43 +00:00
Manfred 5093e01640 feat: add Kanban write semantics MVP 2026-05-04 22:56:43 +00:00
Manfred fafc2ab4f1 feat: expand Kanban task detail view 2026-05-04 22:56:43 +00:00
Manfred 88bf62b6e4 feat: add native read-only Kanban panel 2026-05-04 22:56:43 +00:00
Michael Lam 3ad8846a27 fix: show TPS in assistant message headers 2026-05-04 21:26:43 +00:00
Frank Song 3f56ed7283 Polish forked session indicator 2026-05-04 21:50:40 +08:00
Hermes Bot 47d1a29ead Stage 290: PR #1464 — workspace dropdown sort+search+chip-sync by @JKJameson (maintainer-augmented: ternary fix + regression test) 2026-05-04 04:51:43 +00:00
Josh 4174a7a860 fix: immediate syncTopbar on chat switch + sortable searchable workspace dropdown
Co-authored-by: Josh Jameson <josh@jjameson.com>

Maintainer-augmented:
- Flip noResults ternary (visible?'none':'' instead of visible?'':'none') —
  the contributor's first-push bug rendered 'No workspaces found' alongside
  valid filtered results. Verified on contributor's own screenshot in PR.
- Add tests/test_issue1464_workspace_dropdown_filter.py to lock the
  visibility relationship (mirror-image opt/noResults ternaries) so future
  edits cannot silently re-invert.
- Rebased onto master (was 124 commits behind v0.50.275).
2026-05-04 04:51:30 +00:00
Michael Lam f3fa106cd7 feat: show agent turn duration 2026-05-03 20:20:17 -07:00
nesquena-hermes a2b793be4f fix(picker): Nous Portal featured-set cap + endpoint symmetry (closes #1567)
Two related dropdown bugs in one PR — same root shape (model-picker
endpoints disagreeing about which Nous Portal models exist) plus the
preemptive UX guard against the picker becoming unusable on large-tier
Nous accounts.

#1567 — Endpoint disagreement
=============================
Reporter (Deor, Discord, May 03 2026) saw Settings → Providers card
showing "Nous Portal — 396 models · OAuth" while the in-conversation
picker dropdown listed only the four hardcoded curated entries.

Two structural causes:

1. ``api/providers.py:get_providers`` iterates ALL OAuth providers
   regardless of authentication state and unconditionally live-fetches
   the catalog.
2. ``api/config.py:_build_available_models_uncached`` only iterates
   providers in ``detected_providers``, gated on
   ``hermes_cli.models.list_available_providers().authenticated``.
   That flag can disagree with ``get_auth_status(<id>).logged_in`` on
   some hermes_cli versions.

When the disagreement happens for Nous, the picker silently falls
through to the curated 4-entry static list while the providers card
keeps showing the live catalog — exactly the asymmetry users report.

Plus: the Nous live-fetch branch in `_build_available_models_uncached`
fell back to the same curated 4-entry list when `provider_model_ids`
returned an empty list (transient failure / OAuth refresh in flight),
which doubles down on the disagreement instead of healing it.

UX cap (the design concern Nathan flagged on triage)
====================================================
Even with the disagreement fixed, dumping a 397-model catalog into a
flat dropdown is unusable. We trim the visible picker to a curated
~15-entry featured set when the catalog exceeds 25 models, and surface
the rest under a new ``extra_models`` field so:

- ``/model`` slash autocomplete (commands.js) covers the full catalog
- ``_dynamicModelLabels`` (ui.js) hydrates from both lists, so a model
  selected from outside the featured slice still gets a proper label
- The optgroup label gets ``" (15 of 397)"`` appended so the user
  understands the dropdown is intentionally trimmed, not broken
- The providers card surfaces ``models_total`` separately so the
  header still reads "397 models · OAuth"
- A small "+N more" disclosure pill appears at the end of the rendered
  pill list (only fires for non-OAuth providers — OAuth cards never
  render pills) with a tooltip pointing at the slash command

Featured selection rules
------------------------
Deterministic; same algorithm runs in both `/api/models` and
`/api/models/live` so background enrichment doesn't undo the trim:

1. Always include the user's currently-selected model (sticky — no
   orphan IDs in the dropdown after a refresh)
2. Always include every entry from the curated static
   ``_PROVIDER_MODELS["nous"]`` list whose id maps onto a live id
3. Top up to 15 by walking ``_NOUS_VENDOR_PRIORITY`` round-robin
   (one model per vendor each pass) so no vendor monopolises the slots

Changes by file
===============

api/config.py
- New `_format_nous_label` neighbour: `_NOUS_FEATURED_THRESHOLD = 25`,
  `_NOUS_FEATURED_TARGET = 15`, `_NOUS_VENDOR_PRIORITY` tuple,
  `_build_nous_featured_set()` helper (~80 LOC)
- `_build_available_models_uncached` Nous branch:
  - Apply featured-set cap with sticky-selection signal
  - Return `extra_models` alongside `models` for the catalog tail
  - Decorate optgroup label with truncation count
  - Drop stale-4 fallback when authenticated but live-fetch empty
    (omit the group entirely; truth lives in the providers card and
    the next cache rebuild will heal it)
  - Keep stale-4 fallback when hermes_cli is unavailable (test envs,
    package mismatches) — that's a different failure mode
- Detection symmetry: explicit `get_auth_status("nous").logged_in`
  check after the existing `list_available_providers()` loop, so the
  picker matches the providers card on hermes_cli versions where the
  two signals disagree

api/providers.py:get_providers
- Apply same featured-set cap so card body doesn't render 397 pills
- Add `models_total` field reporting full catalog size (used by
  frontend for the "N models · OAuth" header text)

api/routes.py:_handle_live_models
- Apply same featured-set cap for `/api/models/live` so background
  enrichment via `_fetchLiveModels()` doesn't undo the dropdown trim
- Use sticky-selection from `cfg["model"]["model"]` matching the main
  endpoint's logic

static/ui.js:populateModelDropdown
- Hydrate `_dynamicModelLabels` from `g.extra_models` so a selection
  outside the visible dropdown still renders with its proper label

static/commands.js:_loadSlashModelSubArgs
- Iterate `group.extra_models` so `/model` autocomplete covers the
  full catalog (not just the trimmed featured slice)

static/panels.js:_buildProviderCard
- Header count uses `p.models_total` (full catalog size) instead of
  `p.models.length` (trimmed slice)
- Render trailing "+N more" disclosure pill when `models.length <
  models_total` with a tooltip pointing at the slash command

static/style.css
- New `.provider-card-model-tag-more` rule (italic, dashed border,
  cursor:help, no select) — visually distinct from real model pills

Tests
=====

`tests/test_issue1567_nous_picker_capacity_and_symmetry.py` (20 tests):

- TestBuildNousFeaturedSet (8): unit tests on the helper —
  small-catalog no-op, large-catalog cap to target, disjoint+complete
  invariants, priority-vendor round-robin guarantee, sticky selection
  with and without `@nous:` prefix, curated-flagship preservation,
  empty-catalog handling, determinism
- TestApiModelsLargeCatalog (2): /api/models cap behavior end-to-end
  on a synthetic 397-model catalog vs a 20-model catalog
- TestNousDetectionSymmetry (2): picker includes Nous when
  `get_auth_status` agrees but `list_available_providers` disagrees;
  picker omits Nous when both disagree
- TestNousLiveFetchEmpty (2): authenticated + empty-fetch omits group;
  hermes_cli unavailable still falls back to static-4
- TestProvidersCardPickerSymmetry (1): both endpoints agree on
  exactly the same featured-set IDs + total catalog count
- TestFrontendExtrasContract (4): static-source assertions pinning
  the JS contract for `extra_models`, `models_total`, and the "+N more"
  disclosure

Verified live on port 8789 (30-model catalog):
- /api/models Nous group: provider="Nous Portal (15 of 30)", 15 models,
  15 extra_models
- /api/models/live?provider=nous: 15 entries (matches main path)
- /api/providers Nous card: models_total=30, models=15
- Browser dropdown after backfill: 15 options, 30 entries in
  _dynamicModelLabels
- Sticky selection: Claude Opus 4.7 (the active model) in the featured
  slice as expected

4073 pytest passed (was 4053 → 4073, +20 from this PR).
3 CI test runs (3.11/3.12/3.13) green.
QA harness 11/11 passed.

Reporter: Deor (Discord #report-bugs, May 03 2026 14:15 PT)
Relayed by: AvidFuturist
2026-05-03 21:44:22 +00:00