Commit Graph

200 Commits

Author SHA1 Message Date
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
Frank Song 7689046305 Polish handoff flyout alignment 2026-05-03 16:35:50 +00:00
Frank Song c7e52084ba Harden messaging channel handoff 2026-05-03 16:35:50 +00:00
Frank Song 20ef643bb8 Add messaging session handoff summary 2026-05-03 16:35:22 +00:00
Hermes Bot 6755b1eab5 Merge PR #1516 by @franksong2702: YAML code blocks render with newlines (Prism token white-space) — closes #1463 2026-05-03 16:05:56 +00:00
Hermes Bot 6a75907802 feat(sidebar): add "Unassigned" project-filter chip for sessions without a project
Spliced from contributor PRs #1497 (Thanatos-Z) and #1513 (AlexeyDsov), which
both added the ability to filter the sidebar to sessions with no project_id
assigned. Lands here as a focused PR with the best of both:

## Synthesis decisions

- **Sentinel constant approach** (from #1497, Thanatos-Z): single state
  variable (`_activeProject` set to `NO_PROJECT_FILTER` sentinel) instead
  of a parallel `_showNoneProject` boolean. No two-state-machine ambiguity,
  no risk of "All" + "Unassigned" both reading active. Clicking "All"
  automatically clears the unassigned filter because there is only one
  variable to reset.

- **Conditional rendering** (from #1497): the chip only appears when
  there are actually unassigned sessions to filter to (`hasUnprojected`).
  Common case where every session is organized → chip stays hidden,
  uncluttered chip bar. The project-bar itself also renders when there
  are unassigned sessions (was previously gated on `_allProjects.length`).

- **Dashed-border visual treatment** (from #1497): `.project-chip.no-project
  {border-style:dashed;}` distinguishes the chip from real project chips
  so it reads as a meta-filter ("things without a project") rather than
  another project. Subtle but present.

- **"Unassigned" label** (new): clearer than #1497s "No project" (which
  reads like a status filter) or #1513s "None" (which is ambiguous —
  none of what?). Matches the conventional file-manager / task-tracker
  mental model: "things not yet assigned to a category." Tooltip elaborates:
  "Show conversations not yet assigned to a project."

- **Branched empty-state copy**: when the Unassigned filter is active
  and the result is empty, show "No unassigned sessions." instead of
  the generic "No sessions in this project yet."

## Tests

7 new tests in tests/test_sidebar_unassigned_filter.py pin every contract:
sentinel constant declared; filter logic uses !s.project_id when sentinel
is active; chip only renders when hasUnprojected; chip label and click
handler; visual treatment (dashed border + .no-project class); empty-state
copy branches on the active filter; All chip handler clears _activeProject
to null (would catch a regression if a parallel _showNoneProject boolean
is ever reintroduced).

Local full suite: 3929 → 3936 passing (+7).

Live verified at port 8789 with seeded data (5 projects + 73 unassigned
sessions in active profile): chip appears between "All" and project chips
when unassigned sessions exist; click cycles correctly; clicking a real
project hides the Unassigned chip from active state; clicking "All"
deactivates everything; dashed border present per getComputedStyle.

Co-authored-by: Thanatos-Z <thanatos-z@users.noreply.github.com>
Co-authored-by: Alexey Denisov <AlexeyDsov@users.noreply.github.com>
2026-05-03 07:08:08 +00:00
Frank Song b57e80f706 fix: YAML code blocks collapse newlines due to Prism token white-space (#1463)
Prism's YAML grammar wraps tokens in <span> elements where white-space
defaults to normal, collapsing \n characters into spaces. The DOM
textContent is correct (confirmed by reporter's probe), so the bug is
purely CSS.

Force white-space:pre on .token elements inside language-yaml code
blocks for both .msg-body and .preview-md contexts.
2026-05-03 14:54:34 +08:00
Hermes Bot 8f4692b8cf fix(onboarding): allow keyless setup for self-hosted providers (#1499 third sub-bug)
Pre-fix, the wizard rejected an empty api_key for every provider in
_SUPPORTED_PROVIDER_SETUPS — including lmstudio, ollama, and custom,
which run keyless on the vast majority of local installs. The agent's
LMSTUDIO_NOAUTH_PLACEHOLDER substitution at chat-time was the workaround
for the no-auth case, but the wizard side rejected the empty input first.
Users had to type random gibberish into the API key field to clear the
form — the third sub-bug from #1420 that the prior commit's PR description
explicitly punted to a follow-up.

Surfaced by Nathan during PR review: "I think it's too weird for users
to have to type a string into the API key field, right?"  Yes — and the
probe (#1499) makes the cleanest fix strictly better: we accept empty
keys, and the probe gives instant feedback ("Connected. 2 model(s)
available." for keyless servers, "401" for auth-required servers).

Backend changes
---------------

* `api/onboarding.py` — `_SUPPORTED_PROVIDER_SETUPS` gains
  `key_optional: True` for `lmstudio`, `ollama`, `custom`. Cloud
  providers (openrouter, anthropic, openai, gemini, deepseek, …)
  remain key_required.

* `apply_onboarding_setup` skips the "{env_var} is required" check
  when `key_optional` is set AND no key is supplied. No write to .env
  for the empty-key case (no `LM_API_KEY=*** placeholder lying in the
  user's .env`).

* `_status_from_runtime` reports `provider_ready=True` for key_optional
  providers based on `requires_base_url` alone, so the wizard doesn't
  refire on the next page load just because there's no api_key. Cloud
  providers still need a key for provider_ready=True.

* `_build_setup_catalog` exposes the `key_optional` flag to the frontend.

Frontend changes
----------------

* `static/onboarding.js` — new `_renderOnboardingApiKeyField()` helper.
  For key_optional providers:
    - Label: "API key (optional)"
    - Placeholder: "Leave blank for keyless servers"
    - Inline italic muted help: "Most LM Studio / Ollama / vLLM installs
      run keyless — leave this blank if your server doesn't require
      authentication. Use the Test connection button to verify."
  For cloud providers: unchanged (label "API key", standard placeholder,
  no help block).

* The api-key input also now triggers `_scheduleOnboardingProbe()` on
  oninput, so changing the key re-runs the probe — handles "the server
  rejected my empty key with 401, let me add one and retry."

* `static/i18n.js` — 3 new keys × 9 locales (canonical English in `en`,
  English fallback with `// TODO: translate` markers in the other 8).

* `static/style.css` — `.onboarding-api-key-help` rule for the muted
  italic helper paragraph.

Verified end-to-end on port 8789
--------------------------------

Spun up an isolated test server + a mock LM Studio at
`127.0.0.1:11234/v1/models`. Stepped through the wizard:

* Picked LM Studio → field label flipped to "API key (optional)",
  placeholder showed "Leave blank for keyless servers", help text
  rendered in italic muted gray below.
* Switched to Anthropic → label reverted to "API key", help text
  disappeared. Visual hierarchy correct.
* Left api_key blank, set base_url to the mock, clicked Test connection
  → green "Connected. 2 model(s) available." banner. Probe-discovered
  models populated the workspace-step dropdown.
* Continued through to the finish step. config.yaml written with
  provider/model/base_url. **`.env` does NOT exist** — no placeholder
  string written. `chat_ready: true`, `state: ready`.
* Vision tool confirmed the visual hierarchy: subtle italic help
  reads as documentation, prominent green banner pops as status.

Tests
-----

`tests/test_issue1499_keyless_onboarding.py` — 16 tests in 3 classes:

  TestKeyOptionalProviderSchema (5)
    - lmstudio / ollama / custom declare key_optional=True
    - openrouter / anthropic / openai do NOT (regression defense)
    - setup catalog exposes the flag

  TestKeylessOnboarding (6)
    - lmstudio / ollama / custom: empty api_key accepted, no .env write
    - openrouter / anthropic: empty api_key still rejected
    - lmstudio with explicit key still writes .env (regression defense)

  TestKeylessChatReady (5)
    - lmstudio / ollama: provider_ready=True with no key
    - custom: provider_ready=True with key+base_url, False without base_url
    - openrouter: provider_ready=False with no key (regression defense)
    - End-to-end get_onboarding_status reports chat_ready=True

Full suite: 3901 → 3917 passing (+16 from this commit; +22 cumulative
from the PR's earlier commit). 0 failures.

Closes #1499 (all three sub-bugs from #1420 now addressed)
2026-05-03 03:07:07 +00:00
Hermes Bot 8616033605 fix(onboarding,providers): probe LM Studio /models + align env var with agent CLI (#1499 #1500)
Addresses both #1499 (onboarding wizard never probes the configured base URL)
and #1500 (cross-tool env-var name divergence between webui and agent CLI).
Surfaced together because they're both LM-Studio onboarding bugs that pile
on top of each other — fixing only one leaves the broken UX.

#1499 — Onboarding wizard probes <base_url>/models before persisting

Pre-fix, `apply_onboarding_setup` accepted whatever `base_url` the user typed
without ever fetching `<base_url>/models`. @chwps's log timeline in #1420
showed the wizard finishing in 239ms with zero outbound HTTP — onboarding
silently persisted unreachable URLs and left users with empty model
dropdowns they had to populate by hand-editing config.yaml.

Backend:
* New `probe_provider_endpoint(provider, base_url, api_key, timeout=5.0)`
  in `api/onboarding.py`. Stdlib-only (urllib + socket — no httpx dep).
  Returns `{ok, models}` on success; `{ok: False, error: <code>, detail}`
  on failure with stable error codes the frontend can switch on:
  invalid_url, dns, connect_refused, timeout, http_4xx, http_5xx, parse,
  unreachable. 256 KB response cap and 5s timeout keep a hostile or mis-
  pointed endpoint from blocking the wizard.
* New `POST /api/onboarding/probe` route — thin JSON wrapper around the
  function above. Same local-network gate as `/api/onboarding/setup`
  because the body carries an `api_key` the user typed.
* The probe response is NEVER persisted. Only the user's typed selection
  ends up in config.yaml; the probed model list just populates the
  wizard's dropdown.
* SSRF: deliberately does NOT block private-IP ranges. The wizard is
  gated behind WebUI auth and the legitimate target IS a local LM Studio
  / Ollama / vLLM server. A "block private IPs" SSRF defense would make
  the feature useless for its primary use case.

Frontend:
* `static/onboarding.js`:
  - New `ONBOARDING.probe` state ({status, error, detail, models, probedKey}).
  - `_runOnboardingProbe()` — POSTs to /api/onboarding/probe, idempotent
    & cached on (provider, baseUrl, apiKey).
  - Debounced (400ms) on `oninput` of the base URL field.
  - Explicit "Test connection" button.
  - `nextOnboardingStep` blocks Continue at the setup step for any
    provider with `requires_base_url=True` until the probe succeeds.
    Same localized error renders inline.
* `static/i18n.js`: 13 new keys × 9 locales (canonical English in `en`,
  English fallback with `// TODO: translate` markers in the other 8 —
  same convention as v0.50.271 #1488 voice-buttons).
* `static/style.css`: probe banner + Test button styling (red-tinted
  error variant, green-tinted success variant, neutral probing state).

Verified via manual repro on port 8789:
* connect_refused → red banner, helpful "from Docker, try the host IP"
  hint, blocks Continue.
* DNS failure → red banner, "could not resolve host '...'", blocks Continue.
* Success against a mock /v1/models server → green banner, model dropdown
  populates from the probed list, Continue advances normally.

#1500 — webui env var aligned with agent CLI (LM_API_KEY)

The webui has long used `LMSTUDIO_API_KEY` for LM Studio's API key in
both onboarding and Settings detection. The agent CLI runtime
(hermes_cli/auth.py:177-183) reads `LM_API_KEY`. So a user who configured
auth on their LM Studio instance got Settings → Providers reporting
has_key=True (because webui saw its own LMSTUDIO_API_KEY) but the agent
runtime ignored the key and fell back to LMSTUDIO_NOAUTH_PLACEHOLDER →
401 against the auth-enabled LM Studio server. Masked in practice for
the no-auth majority.

Picked Option B from the issue (defer to the agent — single source of
truth) but mitigated the migration cliff by reading the legacy name as
a fallback:

* `api/onboarding.py:_SUPPORTED_PROVIDER_SETUPS["lmstudio"]`:
  - `env_var: "LM_API_KEY"` (canonical, what onboarding writes going forward).
  - `env_var_aliases: ["LMSTUDIO_API_KEY"]` (read-only fallback for
    pre-#1500 users so detection keeps working without forcing an
    .env rewrite).
* `api/onboarding.py:_provider_api_key_present` reads aliases too.
* `api/providers.py:_PROVIDER_ENV_VAR["lmstudio"] = "LM_API_KEY"`.
* `api/providers.py:_PROVIDER_ENV_VAR_ALIASES["lmstudio"] = ("LMSTUDIO_API_KEY",)`
  — new dict, used by `_provider_has_key` and `get_providers`'s
  key_source resolution. Drops in cleanly when other providers later
  rename their env vars too.

Verified:

```
before fix:  webui writes LMSTUDIO_API_KEY → agent ignores it → 401 on chat
 after fix:  webui writes LM_API_KEY → agent picks it up → chat works
             pre-#1500 .env with LMSTUDIO_API_KEY → still has_key=True in Settings
                                                  → key_source='env_file'
```

Tests

* `tests/test_issue1499_onboarding_probe.py` — 17 tests:
  3 invalid_url variants, dns, connect_refused, success (OpenAI shape),
  success (bare-list shape), http_4xx, http_5xx, parse non-JSON, parse
  wrong-shape, api_key authorization header passthrough, "probe must
  not write to config.yaml or .env", PROBE_ERROR_CODES contract pin,
  3 end-to-end route-level smoke tests against the live server fixture.
* `tests/test_issue1500_lmstudio_env_var_alignment.py` — 5 tests:
  onboarding declares LM_API_KEY canonical with LMSTUDIO_API_KEY alias,
  onboarding writes ONLY the canonical name, legacy env var still
  detected post-migration, canonical takes precedence when both are
  set, _provider_api_key_present reads aliases.
* `tests/test_issue1420_lmstudio_provider_env_var.py` — updated:
  the original 5-test #1420 suite now pins LM_API_KEY as canonical
  and LMSTUDIO_API_KEY as alias.

Full suite: 3879 → 3901 passing (+22), 0 failures.

Out of scope (explicitly NOT addressed here)

The third LM Studio onboarding sub-bug from #1420's thread — that
`apply_onboarding_setup` requires a non-empty api_key for lmstudio
even though most LM Studio installs run keyless — remains. The agent's
`LMSTUDIO_NOAUTH_PLACEHOLDER` substitution kicks in at runtime, but
the onboarding wizard rejects the empty-key case at submit. Fixing
this requires a UX decision (auto-write a sentinel? loosen the
required-key check for self-hosted providers?) and is left as a
separate follow-up.

Closes #1499
Closes #1500

Co-authored-by: chwps <106549456+chwps@users.noreply.github.com>
Co-authored-by: AdoneyGalvan <25235323+AdoneyGalvan@users.noreply.github.com>
2026-05-03 02:46:24 +00:00
bergeouss 24a5457471 fix: P0 bugfixes — tool-card args, sw.js path, CLI rename, scroll pinning
- #1481: Use absolute path for service worker registration to avoid
  <base> tag resolution on session pages causing JSON 404
- #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
- #1486: Prefer WebUI JSON title over state.db title for CLI sessions,
  fixing rename-not-persisting after compression chain extension
- #1469/#1360: Add _programmaticScroll guard to distinguish
  programmatic scrolls from user scrolls, preventing the race
  condition where scrollIfPinned() re-pins after user scrolls up
2026-05-02 23:39:52 +00:00
Hermes Bot 273888df48 fix(sidebar): nest child sessions under lineage roots (#1450) 2026-05-02 17:41:05 +00:00
youzhi 40d2563d51 Fix batch session actions and inflight reload 2026-05-02 22:45:49 +08:00
nesquena-hermes c73f2ff387 v0.50.264 polish followups: i18n parity + assistant-output readability
Closes #1442 (server-side _LOGIN_LOCALE missing ja/pt/ko)
Closes #1443 (promote _isImeEnter helper to 6 other Safari Enter guards)
Closes #1446 (glued-bold-heading lift for LLM thinking-block output)
Closes #1447 (markdown heading visual hierarchy in chat messages)

All four issues were filed by the Opus pre-release advisor on the v0.50.264 batch
or by Cygnus via Discord (relayed by @AvidFuturist, May 1 2026). They share a
common shape — narrow, well-scoped, independent of each other, all adding
regression tests.

== #1442: _LOGIN_LOCALE parity (api/routes.py + static/i18n.js) ==

Added entries for ja/pt/ko to the server-side _LOGIN_LOCALE dict that renders
the localized login page BEFORE the JS i18n bundle loads. With v0.50.264
shipping Japanese as the 8th built-in locale, ja/pt/ko users were seeing the
English login page even with their language preference set.

While auditing static/i18n.js for English leakage, also fixed:
  - ko: 10 user-facing login/sign-out/password keys still in English
  - es: 3 sign-out/auth-disabled keys still in English

Tests: tests/test_login_locale_parity.py (20 tests) — pins both invariants:
  (a) every locale in i18n.js LOCALES has a matching _LOGIN_LOCALE entry
  (b) every locale's login-flow keys (13 of them) are translated, not English

== #1443: window._isImeEnter promotion ==

PR #1441 fixed the Safari IME-composition Enter race in the chat composer
(`#msg`) by widening the guard from `e.isComposing` to a `_isImeEnter(e)`
helper that combines three signals (isComposing || keyCode===229 ||
_imeComposing flag). Six other Enter-input handlers were left on the original
narrow guard and would still drop IME composition Enters on Safari for
Japanese/Chinese/Korean users.

Promoted the helper to `window._isImeEnter` (defined in static/boot.js) and
replaced the `e.isComposing` guards at all six sites:

  - static/sessions.js: session rename, project create, project rename
  - static/ui.js: app dialog (confirm/prompt), message edit, workspace rename

The state-free part of the helper (`isComposing || keyCode===229`) handles
Safari's race for any focused input without needing per-input composition
listeners — only `#msg` keeps the local `_imeComposing` flag.

Tests:
  - tests/test_issue1443_ime_helper_promotion.py (9 tests) — pins each site
    + verifies no raw `e.isComposing` Enter-guards remain in sessions.js/ui.js
  - tests/test_ime_composition.py — alternation regex extended to accept
    the windowed helper form (loosen-test-on-shape-change pattern from
    v0.50.264 reflection notes)

== #1446: glued-bold-heading lift (static/ui.js renderMd + Python mirror) ==

LLMs in thinking/reasoning mode emit "section headers" glued to the end of the
previous paragraph with no whitespace:

    Para 1 text.**Heading to Para 2**

    Para 2 text.**Heading to Para 3**

The renderer correctly produces inline `<strong>` per CommonMark, but it looks
like trailing emphasis on the body text rather than a section break. Cygnus
reported this as "Markdown feedback 2 of 3."

Added a single regex pre-pass in renderMd():

    s.replace(/([.!?])\*\*([^*\n]{1,80})\*\*\n\n/g, '$1\n\n**$2**\n\n')

Constraints chosen to avoid false positives:
  - Trigger only on `[.!?]` IMMEDIATELY before `**` (no space) — almost always
    an LLM-glued heading, not intentional emphasis
  - Inner text ≤80 chars, no `*` or newline (single-line only)
  - Trailing `\n\n` required — preserves "this is **important** to know."
    mid-paragraph emphasis untouched
  - Position: after rawPreStash restore, before fence_stash restore — fenced
    code blocks stay protected (their content is `\x00P` / `\x00F` tokens
    when the lift runs)

Mirrored in tests/test_sprint16.py render_md() so both stay in sync.

Tests: tests/test_issue1446_glued_heading_lift.py (17 tests, 5 of which drive
the actual ui.js renderMd via node) — covers all 3 trigger forms (.!?), all 4
preserve-emphasis cases the issue spec'd, fenced/inline code protection,
chained glued headings, source-level position pin, regex shape pin.

== #1447: markdown heading visual hierarchy (static/style.css) ==

Pre-fix sizes in `.msg-body`:
  h1 18px, h2 16px, h3 14px (= body), h4 13px, h5 12px, h6 11px

So h3 was indistinguishable from body and h4/h5/h6 were SMALLER than body.
Cygnus's report: "Markdown feedback 3 of 3 — Headings seem to be missing
across the board in Hermes. They're there, but all plaintext."

New sizes:
  h1 24px (border-bottom)  h2 20px (border-bottom)  h3 17px  h4 15px
  h5 14px (uppercase, tracked)  h6 13px (uppercase, tracked, muted)

All headings now `font-weight:700` + `color:var(--strong)` for stronger ink.
h5/h6 use uppercase + letter-spacing for "label-style" affordance instead
of being smaller-than-body.

Synced .preview-md (file preview pane) to match exactly so a markdown file
preview and a chat message render identically. Added missing h4/h5/h6 rules
to .preview-md (it only had h1-h3 before).

Updated data-font-size="small"/"large" h1-h6 overrides to scale
proportionally with the new defaults. Hierarchy preserved at all three
font-size settings.

Tests: tests/test_issue1447_heading_hierarchy.py (9 tests) — pins the size
hierarchy, the bottom borders on h1/h2, the uppercase affordance on h5/h6,
the .preview-md sync, and the small/large override scaling.

== Verification ==

  pytest tests/ -q                                  → 3748 passed (+56 new)
  bash ~/WebUI/scripts/run-browser-tests.sh         → 20 + 11 PASS
  bash ~/WebUI/scripts/webui_qa_agent.sh 8789       → 23/23 PASS

Visual confirmation in browser at port 8789:
  - Heading hierarchy clearly visible at all 6 levels
  - Glued-bold lift produces separate paragraphs as designed
  - window._isImeEnter accessible from any module after boot.js
  - Login page renders ja/pt/ko strings correctly (curl -s /login)
2026-05-02 04:19:28 +00:00
Dennis Soong 082f3d45b7 fix: nest child sessions under lineage roots 2026-05-02 12:09:36 +08:00
nesquena-hermes 8ceeef3716 Apply Opus pre-release fixes: dropdown resize guard + display:block
Three fixes from Opus advisor review of stage-261:

1. CRITICAL: dropdown-survives-resize bug. The composerToolsetsDropdown is a
   DOM sibling of composerToolsetsWrap, not a child, so CSS hiding the wrap
   does not cascade-hide an open dropdown. If a user opens the dropdown at
   composer-footer >= 1100px and then opens the workspace panel (or resizes
   the window), the dropdown would stay open without a visible anchor.

   Fixed in three places (defense-in-depth):
   - resize listener: closes dropdown when chip.offsetParent === null
   - _positionToolsetsDropdown: closes if chip hidden (defense-in-depth)
   - toggleToolsetsDropdown: early-returns if chip hidden (defense against
     future #1431 redesign code that might invoke from elsewhere)

2. MEDIUM: display:flex changed to display:block to match sibling wraps
   (.composer-profile-wrap, .composer-model-wrap, .composer-reasoning-wrap
   all use the natural block display).

3. Added 3 new regression tests to pin all three guards.

Refs #1431, #1433.
2026-05-02 00:21:15 +00:00
nesquena-hermes a6884ca40f Make composer-footer toolsets chip responsive instead of always-hidden
Replaces PR #1433 unconditional JS display:none with a CSS @container query
that shows the chip only at composer-footer widths >= 1100px. JS now clears
inline style instead of setting display:none, so the CSS responsive cascade
is the single source of truth. Also removed inline style=\"display:none\" from
index.html so the CSS base rule provides the default-hidden state.

10 regression tests pin the base hide, wide-container show, narrow-container
hide (520px container query), mobile viewport hide (640px @media), JS does
not force display:none, JS clears inline style, /api/session/toolsets and
the dropdown machinery (toggleToolsetsDropdown, _populateToolsetsDropdown)
are preserved.

Refs #1431, #1433.
2026-05-02 00:04:12 +00:00
nesquena-hermes bc17229a7d Merge PR #1402 from bergeouss: P2 improvements — cron history, toolsets per session, Codex OAuth
# Conflicts:
#	static/i18n.js
2026-05-01 18:20:05 +00:00