Codex gate findings: (1) the opt-in flag was only set when Settings opened, so an
enabled setting didn't take effect on a fresh load — now hydrated from /api/settings
at boot (mirrors _largeTextPasteAsAttachment, default-false); (2) toggling the
checkbox now rebuilds the sidebar so the + buttons appear/disappear immediately.
- Neon Soft: lighter dark-mode surfaces with the same neon purple/cyan palette
as the existing Neon skin.
- Neon Paint: cyberpunk-themed skin with hot pink + electric cyan + warm amber
on a deep purple-gray background.
Both skins include full light and dark mode variable sets plus component-level
overrides (selection, titlebar, sidebar, inputs, buttons, scrollbars, tool
cards, tree nodes, session badges, diff blocks, and links). Skin registration
is updated in boot.js and the inline skin whitelist in index.html
Add a user setting 'large_text_paste_as_attachment' (default: on) that lets
users paste large text inline instead of having it auto-converted to .md file
attachments. The toggle lives in Control Center → Chat tab and persists via
settings.json, following the established upstream pattern for boolean settings.
Apply the warm-review polish: parseInt radix 10, fall back to 1800 for
missing/NaN/non-positive values, and floor at 200ms so a mistyped tiny/negative
hermes-voice-silence-ms can't make the recognizer auto-send instantly. De-brittle
the source-string test to assert behavior (reads key / 1800 fallback / guards
non-positive) instead of one literal expression. Default behavior unchanged.
The voice-mode loop hardcoded SILENCE_MS=1800 and
_recognition.continuous=false, giving users no way to tune the
auto-send timing or keep the mic open across natural pauses.
Changes:
- SILENCE_MS now reads from localStorage key 'hermes-voice-silence-ms'
with 1800ms fallback — users set it from the dev console or a future
settings toggle
- _recognition.continuous reads from 'hermes-voice-continuous' (default
false) — set to 'true' to keep the mic open across pauses within an
utterance, with the silence timer remaining the sole send decision
New test file (tests/test_issue4761_voice_mode_config.py, 8 tests)
pins both localStorage reads and verifies the surrounding voice-mode
infrastructure is intact.
Refs #4761
Per maintainer decision: #4657 bundled a scope-creep change flipping
show_quota_chip default off->on plus a migration force-resetting stored false->true
(stomping explicit opt-outs). Master deliberately ships quota-chip default-off.
Reverted config.py + the quota-chip wiring in panels.js/boot.js + the quota test
to master, keeping ONLY the footer fit-by-overflow engine. The mobile quota-action
selector addition (hide_composer_quota_chip selectors) is kept (fit-related, not a
default change).
Co-authored-by: Paladin173 <Paladin173@users.noreply.github.com>
Expose a profile-scoped default_workspace on GET /api/profile/active (resolved
by the profile-aware get_last_workspace(), fail-open) and hydrate boot.js from
it, overriding the global GET /api/settings value. GET /api/settings stays
global-only so the settings UI can't clobber the shared default. Combines
claw-io's #5173 (primary) with the fail-open backend hardening + regression test
from #5171.
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Rebased Paladin173's #4657 onto current master. The per-control visibility
toggles half already shipped via his own #4598, so this rebase carries only the
net-new JS fit-by-overflow engine (_fitComposerFooter: ResizeObserver +
MutationObserver measure .composer-left overflow, step full -> cf-icons ->
cf-burger). Co-authored-by: Paladin173 <Paladin173@users.noreply.github.com>
Salvages PR #3976 by ksj5131513-ops, ported fresh onto current master and
re-applied on top of #4195's external-refresh gate (the original PR predated
#3916/#4195's rework of refreshActiveSessionIfExternallyUpdated).
Two parts:
1. Avoid an unconditional same-session force reload the moment streaming
settles. _scheduleActiveSessionIdleReload previously called
loadSession(force:true) on every idle reconcile, which on mobile PWA caused
a visible end-of-turn flash and could briefly restore the pane with stale
layout geometry. It now reconciles against server metadata first via
refreshActiveSessionIfExternallyUpdated('idle-reconcile', {ignoreStreamJustFinished:true})
— reloading ONLY when the message count actually changed — and falls back to
the original forced reload only if the metadata probe itself fails.
This COEXISTS with #4195: the poll-only external-session early-return
(if((reason||'poll')==='poll' && !_isExternalSession(S.session)) return) is
left untouched. The idle path uses a non-'poll' reason so it never trips that
gate, and opts (read via arguments[1], same pattern as loadSession) only
bypasses the post-stream cooldown, not the external gate. The function keeps
its (reason) signature and now returns a status string
('skipped'/'unchanged'/'reloaded'/'failed') for callers that want it.
2. Mobile viewport reflow guard for PWA visualViewport changes. When the
on-screen keyboard / browser chrome shows or hides, the phone layout can stay
painted against stale geometry. _forceMobileViewportReflow() toggles a
one-frame html.viewport-reflow class (cheap GPU-promotion transform) to force
a repaint, then resyncs the workspace panel + sidebar aria. Wired to
visualViewport resize/scroll (debounced) where available, falling back to the
window resize handler otherwise. Adds safe-area-inset padding to topbar,
messages-inner, and composer-wrap on phones. Uses overflow-x:clip (NOT hidden)
on .layout per the #4856 lesson to avoid coercing overflow-y into a scroller.
Co-authored-by: ksj5131513-ops <ksj5131513-ops@users.noreply.github.com>
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.
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).
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.
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.
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.
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>
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>