Commit Graph

2142 Commits

Author SHA1 Message Date
nesquena-hermes 5eff106e14 Swap brand to new cyan caduceus: favicon family, titlebar + empty-state logo (theme-safe navy badge), remove per-skin titlebar fill overrides
New brand caduceus (vector mark, cyan #08EBF1->#3889FD) on a radial-depth navy
badge. Favicon family regenerated (svg/ico/png/apple-touch). Titlebar and
empty-state logos now use the self-contained navy badge so they render correctly
on every theme (light + dark) without washing out. Removed 5 per-skin CSS rules
that force-filled the titlebar icon to a flat accent color (they assumed the old
monochrome caduceus).
2026-06-28 07:44:16 +00:00
Paladin173 aceb5411ee Clear archived saved session on boot restore 2026-06-28 06:51:36 +00:00
Paladin173 a51e58efc0 fix: skip archived saved session on boot 2026-06-28 06:51:36 +00:00
Rod Boev 861c29b35b fix(#4731): enlarge mobile message action tap targets 2026-06-28 06:41:16 +00:00
nesquena-hermes 031b5b342a fix(extensions): define TTS registry at module scope + route auto-read through registered engines
Codex gate found 2 functional bugs:
1. registerHermesTtsEngine was inside the voice-mode IIFE, which returns early
   on browsers without SpeechRecognition/speechSynthesis — so the documented API
   didn't exist there. Moved the registry (registerHermesTtsEngine / _hermesTtsSynth
   / option helpers) to module scope so it runs unconditionally.
2. autoReadLastAssistant() ignored a selected registered extension engine (only
   handled elevenlabs/edge, fell through to browser TTS). Added the registered-engine
   branch mirroring speakMessage(), with graceful fallback when the saved engine
   is no longer registered.
2026-06-28 06:03:08 +00:00
nesquena-hermes cd71403be9 feat(extensions): TTS-engine registration capability (registerHermesTtsEngine)
Lets a trusted local extension contribute a TTS engine that appears in the
Settings -> TTS Engine dropdown and is used by BOTH playback paths (voice-mode
auto-read in boot.js _speakResponse + the per-message Listen button in ui.js
speakMessage). Mirrors window.registerHermesSkin.

  window.registerHermesTtsEngine({ id, label, synthesize(text, opts) })
    -> Promise<ArrayBuffer|Blob> of audio; core coerces + plays through the same
       <audio> lifecycle as the Edge engine (stop/rearm in voice mode).

Core owns selection, the dropdown <option> (label via textContent, never HTML),
and playback. Built-in ids (browser/edge/elevenlabs) are reserved; invalid
descriptors are rejected; empty/failed synth degrades gracefully. The settings
panel re-adds registered options on render (the markup hardcodes only built-ins).

Unblocks TTS-engine extensions (e.g. a local VOICEVOX engine). Docs in
docs/EXTENSIONS.md; CHANGELOG [Unreleased]. New regression test drives the real
registry via a Node harness (register/validation/reserved-guard/synth coercion).

Full suite: 10893 passed, 0 failed. Verified end-to-end in a live browser with a
companion VOICEVOX extension: engine registers, appears in the dropdown
(browser/edge/elevenlabs/voicevox), and the Listen button routes through the
registered synthesize (both VOICEVOX steps called).
2026-06-28 05:43:02 +00:00
nesquena-hermes e4152c0398 fix(extensions): render skin picker label via textContent, not innerHTML (XSS hardening)
Codex flagged that _buildSkinPicker injected the extension-controlled skin
label/name via btn.innerHTML, so a malicious registerHermesSkin descriptor
(label='<img src=x onerror=...>') could execute when Settings -> Appearance
builds the picker. Rebuild the button with DOM nodes + textContent for the
label and set swatch backgrounds via element.style.background. Adds regression
tests asserting the innerHTML sink is gone.
2026-06-28 05:22:26 +00:00
nesquena-hermes dfe64313dc theme-registration: preserve pending ext skin across reload + expand accent token allowlist
Found via live browser testing of the first consumer (e-ink skin):
- An extension-registered skin selected by the user was lost on reload: both
  the pre-paint inline script in index.html AND the boot-time appearance sync
  normalized the persisted skin against the (extension-less) known-skins list
  and clobbered localStorage back to 'default' before the extension's
  registerHermesSkin() could run. Both sites now PRESERVE an unknown non-default
  persisted skin (a likely pending extension skin), so it survives the reload
  and the extension re-applies it on load. The server push is skipped for a
  still-pending ext skin.
- Expanded the token allowlist to the full accent family
  (--accent-hover/-text/-bg/-bg-strong/-rgb + --surface-subtle) so a skin can
  fully recolor accent-driven chrome (active rail, chips, profile labels), not
  just --accent. Value regex now also accepts a bare RGB triple for --accent-rgb.

Regression tests added for both preservation sites. Full suite: 10876 passed.
2026-06-28 05:22:26 +00:00
nesquena-hermes 6d8135df3a feat(extensions): theme-registration capability — registerHermesSkin()
Lets a trusted local extension contribute a custom skin to the NATIVE
Settings -> Appearance picker (instead of a parallel theme switcher).
window.registerHermesSkin(descriptor) validates + sanitizes the skin in
core, once, so every theme extension inherits the guard:
- token names restricted to a documented allowlist
- values sanitized against a strict color/dimension regex (rejects url(),
  expression(), semicolons, braces, and other CSS-injection vectors)
- reserved built-in skin keys cannot be overwritten
- descriptor with no valid tokens is refused
- idempotent re-register (updates in place) for a live theme editor

Injects a managed <style> for the :root[data-skin] tokens, appends to
_SKINS + _VALID_SKINS, refreshes the picker, and re-applies a persisted
selection that was waiting on a late-registered skin.

Unblocks theme-pack and theme-creator extensions. Documented in
docs/EXTENSIONS.md. Regression test drives the real registration +
sanitization logic via a Node harness (adversarial input).

Full suite: 10874 passed, 0 failed.
2026-06-28 05:22:26 +00:00
Frank Song ddb8ddd811 Merge current master into extension gallery source permissions
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-28 13:05:12 +08:00
Frank Song 06465bbd91 Improve extension gallery source and permissions display 2026-06-28 12:44:57 +08:00
nesquena-hermes dbfa33a999 feat(moa): route /moa through WebUI via server-resolved MoA config (#5070, #5057)
Adds a dedicated GET /api/commands/moa/resolve endpoint + resolve_moa_config()
that reads the MoA preset/usage server-side. The /moa send-path passes only a
moa_config:true boolean to /api/chat/start; the server re-resolves the real
config itself (never trusts a browser-supplied dict) and threads it per-turn into
the live agent run. Fails closed (409) on gateway-backed sessions.

Replaces the originally-proposed broad 43-command subprocess approach (bounced)
with this narrow, security-reviewed /moa route. Maintainer fixes on top of the
contributor's converged design: moa_config is added to run_conversation only
when not None (older-agent TypeError safety) at the main call AND both auth
self-heal retry sites, and forwarded through the legacy-journal runtime adapter.

Gate: Codex SAFE (after the maintainer fixes) + full suite 10784 pass + the
activity-stream regression gate clean.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-28 00:23:51 +00:00
nesquena-hermes 3c3c9047bb skins: add GitHub, Codex, and Terracotta opt-in appearance skins (#4634, #4636, #4635)
GitHub (#4634): graphite + Primer-blue. Codex (#4636): minimal editor, sage accent.
Terracotta (#4635): warm clay accent, renamed from the originally-proposed name to a
descriptive material name. All CSS-only, namespaced under [data-skin], opt-in via
Settings > Appearance. Gate: Codex SAFE + full suite 10778 pass + 3 accents verified live.

Co-authored-by: gottipx <gottipx@users.noreply.github.com>
2026-06-27 23:57:37 +00:00
nesquena-hermes e2ade27bfe fix(anchor): preserve settled mixed content order (#4958)
When a settled assistant turn carries _anchor_activity_scene and has mixed
content[] (text interleaved with tool_use), promote that ordering into anchor
scene rows (prose / tool / thinking rows in sequence) instead of only the raw
content[] fallback, and mirror the same scene model in the backend hydration
path so a cold reload reconstructs an identical transcript.

Also fixes: post-tool_use text/thinking in a NON-FINAL assistant message was
silently dropped — only the turn-final assistant's post-last-tool text is the
"final answer" tail; earlier assistant messages keep post-tool content as
activity rows. Tail thinking is excluded from the final-answer text but still
emitted as a thinking row (verified on both JS and Python paths).

Tool-row de-duplication is conservative — ID-equal always merges; name/
invocation matches are cardinality-gated and same-message; body-prefix is only
a secondary confirmation inside the id-flexible branch (also requires matching
started_at + compatible name + invocation). It biases toward an extra visible
tool card over ever silently merging two distinct calls.

Gate: Codex SAFE (tail-thinking edge verified already-handled both sides),
Opus SHIP (dedup fails toward duplicate-not-loss; non-final preservation, XSS,
non-mixed regression all clean), full suite 10778 passed (only the 4 known
pre-existing flakes). One non-blocking follow-up noted: bring the prose/thinking
peer-dedup into JS↔Python parity (cosmetic cold-reload extra-narration row).

Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
2026-06-27 22:54:29 +00:00
nesquena-hermes 519c394dfe fix(boot): preserve typed composer input during saved-session boot restore (#5069)
On a hard refresh, boot-restore auto-opens the last session from localStorage,
but that load can finish after the user has started typing into the fresh
composer — and the saved-session draft restore clobbered the typed input.
_restoreComposerDraft already had a preserveActiveInput guard, but the boot
path never set it.

boot.js now calls loadSession(saved, {preserveActiveInput:true}); loadSession
threads it into the draft restore as
preserveActiveInput:!!opts.preserveActiveInput || (currentSid===sid&&forceReload),
preserving the prior same-session-force-reload behavior. A blank composer still
restores the saved draft (the guard only skips when active input is non-empty
and differs). opts is read via arguments[1] so the call is signature-safe.

The 4 one-line test changes refresh exact-string boot snapshots to the new call
signature; test_issue5060_boot_composer_preserve.py adds behavioral coverage
(2 assertions proven RED on master).

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-27 20:51:46 +00:00
nesquena-hermes 5bb03a6322 feat(settings): sync a Hermes Dashboard chip with Appearance dropdown (#4936)
Adds a "Hermes Dashboard" chip to the Settings > Appearance "Sidebar tabs"
grid, bidirectionally synced with the existing dashboard-mode dropdown
(never/always/auto). Chip off -> dropdown "never"; chip on -> restore last
non-never mode; dropdown change -> chip re-render.

The chip is a proxy for the dashboard config via saveDashboardSettings ->
POST /api/dashboard/config; it NEVER mutates hidden_tabs/tab_order (separate
toggle fn from _toggleTabVisibilityChip). loadDashboardSettings guards a stale
config GET from clobbering a newer save (_dashboardSettingsLoadSeq /
_dashboardSettingsWriteSeq), and a failed chip save rolls the dropdown/chip
back. Chip label via textContent (XSS-safe).

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-27 20:33:51 +00:00
nesquena-hermes 20d0313d0c feat(settings): expose max_tokens override in Settings (#5010)
The streaming path already honored a max_tokens cap from the active profile's
config.yaml, but there was no UI to manage it. Settings > Preferences now has a
"Max output tokens" field that writes a root-level max_tokens override through
the profile config (not settings.json, which the streaming worker never reads
for this), preserving unrelated YAML keys under _cfg_lock. Blank clears the
override so agent.max_tokens fallback resumes; get_max_tokens_status() surfaces
root / effective / fallback so the field shows what a new turn would use.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-27 20:27:18 +00:00
nesquena-hermes 4f64443f63 fix(sessions): reload active transcript only on message-count change (#5061)
The active-session external-refresh check (refreshActiveSessionIfExternallyUpdated)
force-reloaded the whole transcript whenever last_message_at/updated_at advanced.
The post-turn background skill/memory review bumps those timestamps without adding
any chat messages, so the destructive loadSession(force) (clears S.messages, awaits
a round-trip) made the open conversation visibly disappear and reappear with no new
content.

The refresh now reloads only when the message count actually changed, and treats a
timestamp-only bump as metadata (advance the local last-seen marker + refresh the
lightweight sidebar list, no transcript teardown).

Maintainer fix on top of the contributor's PR (Codex gate): the count check fires
in BOTH directions — remoteCount !== localCount, not just >. A LOWER remote count
means another tab/client truncated/undid/retried/regenerated the transcript
(/api/session/truncate, /retry, /undo all shrink s.messages and persist a lower
message_count); the original >-only gate would have treated that as metadata-only
and silently kept the stale longer transcript forever. Added a regression test for
the count-decrease case. The pending-user-message count-inflation path is excluded
by the existing busy/active-stream guards.

Co-authored-by: allenliang2022 <allenliang2022@users.noreply.github.com>
2026-06-27 18:49:08 +00:00
nesquena-hermes 3c493a9e37 fix(cancel): preserve partial turn payload via embedded cancel snapshot (#4647)
Cancelling a running turn briefly collapsed the already-streamed reasoning,
tool cards, and text to a bare "Task cancelled" marker before a follow-up
GET /api/session repopulated them — a visible race between the terminal
cancel SSE event and the refetch (#4076, #1361).

The terminal cancel event now carries a canonical, redacted session snapshot
captured under the per-session agent lock right after the cancelled turn is
persisted (including the recovered partial-assistant message). The frontend
applies that embedded snapshot immediately via _applyCancelSessionPayload and
skips the GET round-trip. The snapshot is a fully detached deep copy (redaction
rebuilds every container) so it cannot alias or be staled by concurrent writes;
it is only applied when its session_id matches the active view; and a
missing/failed snapshot degrades cleanly to the existing GET fallback.

The seven other cancel call sites are standardized via _cancel_event_payload()
(message + type + status), backward-compatible (no session key when none).

Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
2026-06-27 16:43:37 +00:00
Rod Boev f65543f88e fix(#5045): add memory-section hover path titles 2026-06-27 06:05:28 -04:00
nesquena-hermes 11260c8df5 feat(extensions): show post-install next steps in the gallery (#4964)
Renders generic registry post_install guidance + lifecycle requirements in the
Settings extensions gallery card (manifest-driven, no vendor special-casing).
Fixes #4959. Rebased onto current master (squash-applied #4964's net diff so it
coexists with the #4979 sidecar-runtime-status feature that landed in v0.51.697).

Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
2026-06-27 05:44:10 +00:00
nesquena-hermes 64e877b432 fix(extensions): don't preserve loading/error placeholder as existing diagnostics
Codex gate finding on #4979: loadExtensionsPanel({preserveExisting:true}) on the
diagnostics tab treated the 'Loading extension diagnostics…' placeholder as real
content, so a failed refresh returned early (preserveExisting && innerHTML.trim())
and left the panel stuck on 'Loading…' instead of rendering the error. Exclude the
.extensions-loading/.extensions-error placeholders from the preserve check + add a
regression assertion.

Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
2026-06-27 05:21:45 +00:00
Frank Song 33e95277ca Honor preserved extension diagnostics refresh failures
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-27 05:13:10 +00:00
Frank Song 71dd4a6ab8 Polish extension sidecar diagnostics refresh
Keep the Diagnostics tab content visible while status refreshes on repeat visits, and reject malformed or far-future sidecar runtime timestamps before rendering relative age text.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-27 05:12:50 +00:00
Frank Song d2eedd85f8 Show extension sidecar runtime status 2026-06-27 05:12:50 +00:00
b3nw 121ac7f253 fix: coerce msgContent to string in completion notification preview
Greptile P1 on #5036: block-array content can make msgContent return a
non-string; normalize before .replace() in preview helper.
2026-06-27 04:50:08 +00:00
b3nw a84fc8e5c2 Fix completion notification preview to use settled final answer
The SSE done handler previously used assistantText.slice(0, 100), which
captures the start of the live-stream accumulator (early tokens and interim
updates) rather than the final answer shown after tools settle.

Add _completionNotificationPreviewText() using anchor final-answer projection
and settled message content, with a cleaned live-stream fallback. Hoist
lastAsst so background completions on non-active panes still get preview text.

Update PWA notification regression tests.

Closes #5035
2026-06-27 04:50:08 +00:00
nesquena-hermes 0a32fb899f fix(onboarding): rehydrate plain-select fallback so saved model isn't reset to option[0]
Codex gate finding on #5031: when _mountSearchableModelSelect is absent the
fallback returns the old plain <select>, but the post-render rehydration
(modelSel.value = ONBOARDING.form.model) was removed — so a saved/default model
that isn't the first option would be silently replaced by option[0] on render.
Restore the rehydration in an else branch + add a regression test.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-27 04:33:02 +00:00
Rod Boev 88911448ff fix(#4706): keep onboarding picker state aligned after custom clear 2026-06-27 04:25:45 +00:00
Rod Boev d0eb9b57df fix(#4706): reuse the searchable model picker in onboarding 2026-06-27 04:25:45 +00:00
nesquena-hermes 1463aedd69 fix(i18n): rebase French translation onto master — union-merge new keys, restore theme_usage nbsp, bump open_in_vscode locale count 13->14 (fr gap closed)
Co-authored-by: Pichatu <4161398+Pichatu@users.noreply.github.com>
2026-06-27 04:09:36 +00:00
Pichatu 688fd1f6f1 Fixing grammatical errors 2026-06-27 04:04:12 +00:00
Pichatu e83f820995 Fix French 2026-06-27 04:04:12 +00:00
Pichatu 7f887a7839 Fix French translation 2026-06-27 04:04:12 +00:00
nesquena-hermes 1ff3f2f3ac Merge PR #4997 into tier-1 batch 2026-06-27 03:39:10 +00:00
nesquena-hermes 298f7af562 Merge PR #5025 into tier-1 batch 2026-06-27 03:39:10 +00:00
nesquena-hermes 59f14f05cd Merge PR #4888 into tier-1 batch 2026-06-27 03:39:10 +00:00
Rod Boev 1bff768c74 fix(#5021): preserve extra-model label contract 2026-06-27 02:52:33 +00:00
Rod Boev 288cbb55bc fix(#5021): keep boot model budget across restore paths 2026-06-27 02:52:33 +00:00
Rod Boev a18ddae930 fix(#5021): retry generic model auth after boot 401 2026-06-27 02:52:33 +00:00
Rod Boev 576834a195 fix(#5021): keep restored-session boot inside the shared 401 budget 2026-06-27 02:52:33 +00:00
Rod Boev df1cb13cd5 fix(#5021): keep boot 401 budget out of later dropdown refreshes 2026-06-27 02:52:33 +00:00
Rod Boev 79a92f0e3b fix(#5021): bound boot model 401 recovery 2026-06-27 02:52:33 +00:00
Rod Boev 0aa4f6f8cd fix(#4877): bind ephemeral blank boot workspace 2026-06-27 01:15:53 +00:00
Rod Boev 4ebe2980ca fix(#4877): bind default workspace on fresh blank boot 2026-06-27 01:15:53 +00:00
Rod Boev 19b3ad0893 test(#4999): cover every memory path branch 2026-06-26 19:42:03 -04:00
Rod Boev e2e2182bc0 fix(#4999): show on-disk memory file paths 2026-06-26 19:14:36 -04:00
Rod Boev 13879cebe9 docs(workspace): clarify redirect401 401 contract (#5001) 2026-06-26 22:09:09 +00:00
Rod Boev 3722c37eb6 fix(boot): preserve api timeouts while deferring 401 redirect (#5001) 2026-06-26 22:09:09 +00:00
Rod Boev 4d727c2e24 fix(boot): keep active-profile recovery on one redirect path (#5001) 2026-06-26 22:09:09 +00:00