Commit Graph

127 Commits

Author SHA1 Message Date
Michael Lam 4d613e723f feat: add workspace panel edge reopen toggle 2026-05-15 18:33:27 -07:00
Hermes Agent 62413067e6 Merge pull request #2317 into stage-361
fix: preserve explicit light/dark theme fallback (Michaelyklam, refs #2312)
2026-05-15 19:17:03 +00:00
Michael Lam 957bffc49e fix: preserve explicit light dark theme fallback 2026-05-15 10:10:34 -07:00
dobby-d-elf 0f86030f5f fix: single close button on workspace panel, tooltip → 'Close'
- Remove duplicate mobile-close-btn from HTML
- Remove dead .mobile-close-btn CSS rules; unhide .close-preview at all viewports
- Change btnClearPreview tooltip from 'Hide workspace panel' to 'Close'
- Update tests across test_sprint41.py, test_sprint44.py, test_issue781.py,
  and test_mobile_layout.py to match new single-button model
2026-05-15 09:43:18 -06:00
dobby-d-elf 0e9017a665 refine workspace panel header layout 2026-05-15 09:43:18 -06:00
Hermes Agent 90fd16e273 Merge pull request #2306 into stage-359
Fix iPhone PWA mobile shell and workspace header layout (dobby-d-elf, regression fix for #2238)
2026-05-15 14:55:17 +00:00
Hermes Agent ad76db8651 Merge pull request #2291 into stage-359
feat: add Nous Research skin (linuxid10t)
2026-05-15 14:55:10 +00:00
dobby-d-elf 256b9d6294 fix: restore iPhone PWA mobile workspace layout 2026-05-15 08:14:53 -06:00
linuxid10t b2d4f13c5b feat: add Nous Research skin
Adds a cold steel-blue/monospace skin inspired by nousresearch.com:
- Steel-blue accent (#4682B4) replacing warm gold
- Monospace typography (SF Mono, Roboto Mono, Courier New)
- Sharp corners, technical dashed borders
- Dark navy palette (#0A0E14) for dark mode

Files changed:
- static/style.css — Nous skin CSS variables and component overrides
- static/boot.js — Nous skin entry in _SKINS array
- static/index.html — nous in inline skin validation list
- api/config.py — nous + sienna in server-side _SETTINGS_SKIN_VALUES
2026-05-15 00:28:34 -05:00
linuxid10t 45fe6294f9 fix: prevent theme reset on refresh when autosave failed
The boot IIFE unconditionally overwrote localStorage with whatever
settings.json had on the server.  If the appearance autosave POST
ever failed (network glitch, transient error) the next page load
would revert the user's chosen theme/skin to the server's stale
defaults.

Fix: reconcile localStorage against the server on boot.  When
localStorage carries a non-default skin or system theme (the user
explicitly chose something), localStorage wins and the fix pushes
those values back to the server.  When localStorage is at defaults
(new browser / first visit), the server still wins.

Tested scenarios:
- User chose non-default skin, autosave failed → preserved + reconciled
- New browser, server has non-default skin → server value applied
- Normal use (autosave works) → unchanged behavior
2026-05-14 23:52:57 -05:00
Hermes Agent ec689e32be Merge pull request #2099 into stage-358
feat: add opt-in streaming text fade (dobby-d-elf, off-by-default)
2026-05-14 21:27:52 +00:00
Frank Song 6beb59d61f Improve mobile sidebar panel navigation 2026-05-14 15:16:33 +08:00
Jordan SkyLF 1ad5fe9c14 fix: keep boot settings regression close 2026-05-13 16:00:46 -07:00
Jordan SkyLF bec21eafa0 Add What's New summary toggle 2026-05-13 15:53:01 -07:00
dobby-d-elf 67e29fa991 feat: add opt-in streaming text fade 2026-05-11 13:13:26 -06:00
nesquena-hermes 2dbee503c2 feat(ux): collapse sidebar by clicking the active rail icon (fuses #1884 + #1924)
Lets desktop users collapse the session-list sidebar to maximise the chat
area, without adding any visible UI affordance. Default appearance is
identical to master — only users who actively try to toggle (or know the
keyboard shortcut) ever see a difference.

## Behaviour (desktop only, ≥641px)

| State                              | Action                | Result                                  |
|------------------------------------|-----------------------|-----------------------------------------|
| Sidebar open, click active rail    | Toggle                | Sidebar collapses to width:0            |
| Sidebar open, click different rail | Normal switch         | **Sidebar stays open** (no surprise)    |
| Sidebar collapsed, click any rail  | Expand + switch       | Sidebar expands, then panel switches    |
| Anywhere, Cmd/Ctrl+B               | Toggle                | Same as same-active-rail click          |
| Mobile (<641px), any of the above  | No-op                 | Mobile overlay behaviour unchanged       |

Two discoverability paths, both opt-in. **No new visible buttons.** Users
who never click the active rail icon see zero UI change vs. master.

## Surface-minimal design

The behaviour is contained behind one extra arg on the rail/sidebar-nav
onclick: `switchPanel('chat',{fromRailClick:true})`. Without that flag the
function preserves master's behaviour exactly — every programmatic
`switchPanel(name)` callsite (commands, deeplinks, internal state changes)
is unaffected. The guard chain inside `switchPanel`:

  opts.fromRailClick && _isDesktopWidth() && (
      _isSidebarCollapsed() ? expandSidebar() :
      prevPanel === nextPanel ? (toggleSidebar(true); return false))

is the ONLY new code path that can cause a collapse. Cross-panel clicks
fall through to the existing switch logic untouched.

## Polish from both source PRs

- **Click-active gesture** as the primary toggle (#1884 @jasonjcwu — the
  genuine UX innovation; no extra button needed)
- **Cmd/Ctrl+B keyboard shortcut** (#1924 @spektro33; VS Code convention).
  Guarded against firing when typing in INPUT / TEXTAREA / contenteditable
  so the shortcut never steals from in-progress text editing.
- **Inline flash-prevention `<script>`** in `<head>` (#1924) sets
  `data-sidebar-collapsed='1'` on `<html>` BEFORE the stylesheet loads,
  so cold loads with a persisted-collapsed state paint correctly from
  frame 0 with no flicker. Cleared by JS once the class system takes over.
- **Smooth slide animation** via `.24s cubic-bezier(.22,1,.36,1)`
  (#1924, mirrors the existing workspace-panel collapse on the right)
- **`aria-expanded` mirrored** on the active rail button (#1884) so
  screen readers announce open/collapsed transitions.
- **`body.resizing` transition-suppression** (#1884) keeps the drag-resize
  cursor instant — no animation during a width-resize gesture.
- **bfcache `pageshow` re-sync** (#1884) — if another tab toggled the
  sidebar while this page was frozen, bring it in line on restore.

## Drops vs. #1924

- No persistent rail "toggle sidebar" button (Nathan: keep the UI stealth)
- No close-X button in chat panel head (same reason)
- No i18n keys for the dropped buttons

## What did NOT change

- 22 rail/sidebar-nav `onclick` handlers gained the `{fromRailClick:true}`
  arg — function-call shape, invisible to users
- 1 inline `<script>` in `<head>` (flash prevention) — invisible
- 5 lines of CSS — invisible unless someone collapses

That's the entire visible-UI delta. **23 ins / 22 del on `index.html`,
all string-replace.**

## Verification

- 5,151 pytest passing including a new 34-test structural suite covering
  every contract (CSS rules, JS functions, fromRailClick guard, legacy
  proxy forwarding, flash-prevention `<script>` ordering, mobile
  exclusion via :not(.mobile-open) selector, aria-expanded sync).

- Live browser walkthrough at 1280px verified:
  - Default boot state identical to master (sidebar open, width 300px)
  - Click active rail → collapse (width 1, opacity 0, translateX -14px,
    localStorage='1', aria-expanded=false). Panel unchanged.
  - Click active rail again → expand back to width 300, aria=true
  - Click DIFFERENT rail → normal switch, sidebar stays open (legacy-
    preserving case, verified explicitly)
  - Click rail while collapsed → expand + switch in one gesture
  - Cmd+B toggles correctly
  - Cmd+B inside `<textarea>` → suppressed (defaultPrevented=false)
  - Reload with collapsed state persisted → restores without flash
  - Mobile simulation (matchMedia returns false for min-width:641px):
    same-active-rail click is no-op, Cmd+B is no-op, sidebar stays at 300px

Co-authored-by: jasonjcwu <jasonjcwu@users.noreply.github.com>
Co-authored-by: spektro33 <spektro33@users.noreply.github.com>
Closes #1884
Closes #1924
2026-05-11 04:49:18 +00:00
Minimax 08c4ef8d88 feat: persistent composer draft — server-side, cross-client, survives refresh
- Session.composer_draft field: {text, files} stored in session JSON
- POST+GET /api/session/draft endpoint for save/load
- loadSession: save draft before switch, restore from S.session.composer_draft
- textarea input: debounced 400ms auto-save to server
- send(): clear draft after message is sent
- lockComposerForClarify(): save draft before card locks composer
- _restoreComposerDraft: clears textarea when target has no draft, guards
  against stale responses racing new session loads, exact text comparison
- Session.compact(): includes composer_draft in response
- Fix: use handler.command instead of parsed.method (ParseResult has no .method)

Co-authored-by: Minimax <noreply@minimax.io>
2026-05-09 13:47:57 +01:00
nesquena-hermes bec4433c2a Stage 325: PR #1929 — feat: add opt-in session endless scroll by @ai-ag2026
Conflict resolution: both #1928 (session jump buttons) and #1929 (endless
scroll) add their own settings/UI/i18n keys. Resolved by keeping both —
the features are independent opt-in toggles.
2026-05-08 21:23:34 +00:00
nesquena-hermes fba860da48 Stage 325: PR #1928 — feat: add opt-in session jump buttons by @ai-ag2026 2026-05-08 21:16:33 +00:00
ai-ag2026 ea8aca2818 feat: add opt-in session endless scroll 2026-05-08 21:16:21 +00:00
ai-ag2026 df1ba9fde8 feat: add opt-in session jump buttons 2026-05-08 21:16:19 +00:00
ai-ag2026 8f58a8c94e feat: add browser offline recovery and PWA cache hardening 2026-05-08 21:16:17 +00:00
Frank Song 8bc2677691 fix: repair file picker and html preview interactions 2026-05-07 16:59:00 +00:00
Michael Lam ee5600e46c fix: keep workspace open from preview breadcrumb 2026-05-07 06:25:17 +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
nesquena-hermes e9aac079e1 feat(theme): expose active --bg via <meta name="theme-color"> for native chrome bridges
The Mac Swift app (hermes-webui/hermes-swift-mac) and any other native
WKWebView wrapper need the active theme background to keep AppKit
chrome (tab bar, title bar, traffic-light area) in sync with the page.

The current Mac approach pixel-samples the page via
elementsFromPoint, which is fragile against modals/lightboxes/file-tree
overlays — any opaque overlay over a sample point can poison the
chrome colour for the entire app. (See swift-mac issue #70.)

Surface the active theme's background as the canonical, overlay-resistant
source of truth via <meta name="theme-color">:

- Two static prefers-color-scheme variants in <head> for browsers that
  read theme-color before any JS runs (mobile Safari, PWAs).
- One id="hermes-theme-color" runtime tag with an inline pre-paint
  seed script that reads localStorage hermes-theme so the meta tag
  is correct on first paint, before boot.js loads.
- New _syncThemeColorMeta() helper in static/boot.js that reads
  getComputedStyle(html).getPropertyValue('--bg') and writes it into
  the runtime meta tag. Called from _setResolvedTheme (both branches —
  prism-loaded and prism-absent) and from _applySkin so every theme
  toggle and skin switch updates the meta tag.

Reading --bg via getComputedStyle means each skin (Default, Sienna,
Sisyphus, Charizard, etc.) reaches the meta tag with its distinct
background — no per-skin lookup table to drift.

Browser-verified end to end on port 8789:
  - light + default      → meta=#FEFCF7 (matches --bg)
  - light + Sienna       → meta=#FAF9F5 (skin's distinct bg)
  - dark + Sienna        → meta=#1F1E1C (skin's dark variant)

10 regression tests added in tests/test_theme_color_meta_bridge.py
covering: static media variants present, runtime id stable, pre-paint
seed reads localStorage, helper defined and reads computed --bg,
helper targets known id, both _setResolvedTheme branches call sync,
_applySkin calls sync, root --bg defaults still match.

Companion PR coming on hermes-webui/hermes-swift-mac to switch the
theme bridge from elementsFromPoint pixel-sampling to reading
document.querySelector('meta[name="theme-color"][id="hermes-theme-color"]').content.

Refs hermes-webui/hermes-swift-mac#70.
2026-05-06 17:24:23 +00:00
Dennis Soong 8138ca8479 fix: keep saved running sessions sidebar-only on root boot
Root page loads should not automatically project a localStorage-saved running session into the active pane. Keep explicit /session/<sid> behavior unchanged while leaving the saved session discoverable from the sidebar.

(cherry picked from commit bb60cf21d911a84e285363bcecf46fb441181fb9)
2026-05-06 14:53:40 +00:00
Basit Mustafa 9a0a6214cf fix: guard localStorage.setItem('hermes-webui-model') against QuotaExceededError
On some setups the localStorage quota is exhausted; the bare setItem
call throws an unhandled DOMException that breaks model selection and
prevents the chat UI from loading.

Wrap both call-sites (boot.js model-select onChange, onboarding.js
_saveOnboardingDefaults) in try/catch so the error is logged to the
console as a warning instead of surfacing as a fatal exception.

Fixes: 'Failed to execute setItem on Storage: Setting the value of
hermes-webui-model exceeded the quota.'
2026-05-05 17:29:47 +00:00
Michael Lam 8c8e2d3573 fix: keep multi-image paste attachments 2026-05-05 08:45:14 -07:00
Michael Lam 89099928db fix: make TPS header display optional 2026-05-04 21:26:43 +00:00
test 8a10532d29 Stage 292: PR #1601 — keep frontend routes under subpath mounts by @Michaelyklam 2026-05-04 15:34:08 +00:00
Jash Lee 1ad0ab42e5 Fix #1620: don't attach image on paste when clipboard also has text
When the clipboard carries both text and an image (rich-text sources like
Notes, Word, Slack, browser selection attach a rendered preview alongside
the plain text), the paste handler in static/boot.js unconditionally
called e.preventDefault() and routed the image into addFiles(), silently
discarding the text payload.

Fix:
  - Detect text in the clipboard via items[].kind === 'string' &&
    (type === 'text/plain' || type === 'text/html'). When present, return
    early so the browser's default text-paste runs.
  - Tighten the image filter to kind === 'file' && type.startsWith('image/')
    so string items advertising an image MIME (e.g. text/html with an
    embedded data URI) are not misclassified as a true screenshot paste.

Pure-screenshot paste (image-only clipboard, e.g. Cmd+Shift+Ctrl+4 on macOS)
is unchanged.

Adds tests/test_1620_paste_text_with_image.py with 6 static-analysis checks
on the handler shape, matching the pattern of test_issue1095_pasted_images.py.
2026-05-04 10:48:36 -04:00
Michael Lam e9d7d5e427 fix: keep frontend routes under subpath mounts 2026-05-04 00:06:58 -07:00
Frank Song f32989d5bb fix: voice-mode pref toggle-off now stops the recognizer (#1491)
When a user disables 'Hands-free voice mode' in Settings while voice
mode is active, the button hides but the SpeechRecognition keeps
running — the user can't stop it because the button is invisible.

Fix: _applyVoiceModePref() now checks if voice mode is active and
calls _deactivate() when the pref is toggled off. Move
_voiceModeActive declaration above the function to avoid TDZ.

Also removes a duplicate window._applyVoiceModePref assignment.
2026-05-03 15:03:17 +08:00
Dennis Soong cbb251b823 fix: add sidebar cancel for running sessions 2026-05-03 08:46:36 +08:00
Hermes Bot 341b1ee6b6 fix(composer): distinct voice-mode icon, descriptive labels, opt-in pref (#1488)
Composer footer rendered two near-identical mic icons whose tooltips both
said "Voice input" — push-to-talk dictation and hands-free voice mode were
visually indistinguishable. Researched how ChatGPT/Claude/Gemini solve the
same problem and adopt the industry convention.

Changes:
- btnVoiceMode now uses Lucide audio-lines (6 vertical bars), the
  universal voice-conversation glyph. Also registered in LI_PATHS.
- Distinct localized tooltips: voice_dictate ("Dictate") and
  voice_mode_toggle ("Voice mode"), with active-state flips
  (voice_dictate_active "Stop dictation", voice_mode_toggle_active
  "Exit voice mode"). Legacy voice_toggle key removed (it resolved to
  "Voice input" in every locale and caused the duplicate-tooltip bug).
- Voice mode is opt-in via Settings -> Preferences ->
  "Hands-free voice mode button" (default off). Dictation mic stays
  visible by default, unchanged. localStorage-backed; panels.js onchange
  calls window._applyVoiceModePref() so the button appears/disappears
  immediately without reload.
- 17 regression tests pin: distinct titles, audio-lines glyph, all 4
  new keys in all 9 locales, removal of stale voice_toggle, English
  labels match convention, pref gating (no unconditional display=''
  left in boot.js), Settings checkbox + i18n, panels.js wiring,
  active-state tooltip flips.

Browser-verified on port 8789: default state shows 1 mic; enabling
the pref makes the audio-waveform button appear live; tooltips read
"Dictate" and "Voice mode" distinctly.

Closes #1488
2026-05-02 22:16:23 +00:00
Dennis Soong 5e806f6fd8 fix: restore inflight session on bfcache pageshow 2026-05-03 01:53:01 +08: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
nesquena-hermes e6e9868625 Opus pre-release follow-up: blur resets _imeComposing flag
Opus advisor caught a recoverable footgun in PR #1441's manual flag: if
focus is lost mid-composition (window blur or older Safari WebKit IME
quirk), compositionend may never fire and _imeComposing stays true
until the next full composition cycle. Result: Enter-to-send is
silently broken until page reload — an unrecoverable stuck state for
something that's supposed to be transient.

Add a blur listener that also resets the flag. Cheap belt-and-suspenders
against the stuck state. Adds 1 regression test pinning the listener.

(other Opus findings logged in /tmp/stage-264-brief.md as follow-up
issues: _LOGIN_LOCALE parity for ja/pt/ko, promote _isImeEnter to the
6 other Safari-affected Enter guards in sessions.js + ui.js)
2026-05-02 02:56:48 +00:00
nesquena-hermes 641da8b9cc Merge PR #1441: Fix IME composition Enter (East Asian input) 2026-05-02 02:42:49 +00:00
RZ 39c99b015a Fix IME composition Enter sending message prematurely
East Asian IMEs (Japanese/Chinese/Korean) use Enter to commit composition.
The existing isComposing guard misses Safari, where the committing keydown
fires after compositionend with isComposing=false. Also track composition
manually and check keyCode===229 for broader coverage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 11:12:14 +09:00
nesquena-hermes 26d0f45791 fix: new-chat guard ignores in-flight streams (#1432) + profile form auto-capitalizes typed values (#1423)
Two unrelated UX bugs, both small surgical fixes with regression tests.

Issue #1432 — "+" button doesn't open new chat during streaming
================================================================
Reported by @Olyno: clicking "+" after sending a first message keeps
redirecting to the same chat instead of opening a new blank conversation,
making parallel chats impossible until the first response finishes.

Root cause:
  static/boot.js:691 (and the Cmd/Ctrl+K branch at :844) had an empty-session
  guard from #1171 that skipped newSession() when message_count===0:

    if(S.session && (S.session.message_count||0)===0){
      $('msg').focus(); closeMobileSidebar(); return;
    }

  But during the first user turn of a brand-new session, message_count is
  still 0 server-side because the user message hasn't been merged into
  s.messages yet. The guard treated that as "empty" and silently dropped
  the click, blocking parallel chats for the entire stream duration.

Fix:
  Tighten the predicate to also exclude in-flight state:

    if(S.session
       && (S.session.message_count||0)===0
       && !S.busy
       && !S.session.active_stream_id
       && !S.session.pending_user_message){
      $('msg').focus(); closeMobileSidebar(); return;
    }

  Same predicate applied to the Cmd/Ctrl+K handler at :844. The in-flight
  signal (active_stream_id || pending_user_message) is the same one
  _restoreSettledSession() in messages.js:1081 already uses to decide
  whether a session is "settled" — keeping both call sites aligned.

  Verified end-to-end: with S.busy=true and pending_user_message set, the
  old guard returned `block=true` (= the bug), the new guard returns
  `block=false` (= fixed). With a truly empty session (no busy, no pending),
  both old and new guards still block — preserving #1171 behavior.

Issue #1423 — Profile name field auto-capitalizes typed values
==============================================================
Self-reported (Mac app, May 1 2026): typing `hello` into the New Profile
"Name" field shows `Hello` after blur/autofill, contradicting the
"Lowercase letters, numbers, hyphens, underscores only" hint right next
to it. The form lowercases on submit so stored data is correct, but the
displayed value during typing is misleading.

Root cause:
  static/panels.js:2532 had only autocomplete="off":

    <input type="text" id="profileFormName"
           placeholder="..." autocomplete="off" required>

  Missing three attributes that actually prevent the misbehavior:
  - autocapitalize="none" — mobile keyboards (iOS Safari, Android Chrome,
    WKWebView in the Mac app) auto-capitalize the first letter without it
  - autocorrect="off" — Safari runs autocorrect on blur, can rewrite hello→Hello
  - spellcheck="false" — desktop browsers may run spellcheck on blur

Fix:
  Add the three attributes to profileFormName. Also added to
  profileFormBaseUrl since URLs are similarly bad targets for
  autocapitalize/autocorrect. profileFormApiKey is type="password" and
  already has correct browser behavior.

  Verified end-to-end against the live DOM: openProfileCreate() →
  getElementById('profileFormName').getAttribute(...) returns the new
  attributes correctly, with required preserved.

Tests
-----
3648 passed, 2 skipped, 3 xpassed (was 3640 — added 8 new regression tests
in test_1432_newchat_and_1423_profile_input.py).

One pre-existing test had to be widened: tests/test_mobile_layout.py
test_new_conversation_closes_mobile_sidebar grabbed only the first 500
chars of the btnNewChat handler block to scan for closeMobileSidebar.
The new comment block pushed closeMobileSidebar past that window even
though both calls are still present. Bumped the window to 1500 chars
and the shortcut-block lines from 12 to 24 to match the multi-line guard.

Closes #1432
Closes #1423

Reported by @Olyno (#1432, GitHub)
2026-05-02 00:52:41 +00:00
nesquena-hermes 5ce516ed38 v0.50.255: Opus follow-ups (4 fixes) + CHANGELOG
Opus pre-release advisor caught 4 issues in stage-255 (#1390 + #1405):

1. MUST-FIX: api/rollback.py path-traversal — _checkpoint_root() / ws_hash /
   checkpoint did NOT normalize Path() / "../escape", so an authenticated
   caller could read or restore from another allowlisted workspace via
   ../<other-ws-hash>/<sha>. New _validate_checkpoint_id() regex-guards
   with ^[A-Za-z0-9_-][A-Za-z0-9_.-]{0,63}$ and rejects . and .. literals.
   Both get_checkpoint_diff and restore_checkpoint validate.

2. SHOULD-FIX: redact_session_data perf cliff — the new api_redact_enabled
   toggle in #1405 called uncached load_settings() per string, recursed
   across messages[] and tool_calls[]. For a 50-message session: hundreds
   of disk reads per /api/session response. Now read once at the top and
   thread _enabled through via private kwarg.

3. SHOULD-FIX: voice-mode wrong-session TTS — the patched autoReadLastAssistant
   fires globally; if the user navigated to a different session between
   sending and stream completion, TTS would speak the wrong session\\s reply.
   New _voiceModeThinkingSid closure captures S.session.session_id at
   thinking-time; _speakResponse bails to _startListening() on mismatch.

4. NIT: rollback._inspect_checkpoint had bare Exception in the except tuple
   alongside specific catches, swallowing everything. Now (TimeoutExpired,
   OSError) only.

6 regression tests in test_v050255_opus_followups.py. Full suite: 3587 passed,
2 skipped, 3 xpassed.
2026-05-01 17:19:53 +00:00
nesquena-hermes 6ad7a4cc83 Merge PR #1405 from bergeouss: P3 features (insights, rollback, voice mode, subagent tree, redact toggle) 2026-05-01 16:58:49 +00:00
nesquena-hermes 6f55b973e5 Merge PR #1390 from starship-s: preserve session provider context 2026-05-01 16:58:48 +00:00
bergeouss d9f3a69d29 fix: address PR #1405 review feedback — security, voice loop, locale coverage, test fixes
- Point 4 (security): _resolve_workspace now validates against known workspaces
  from workspaces.json to prevent arbitrary path write via restore endpoint
- Point 5 (voice mode): bail out of voice mode on not-allowed, service-not-allowed,
  and audio-capture errors instead of infinite retry loop
- Point 1 (locale coverage): added ~40 new English keys as placeholders with
  TODO:translate comments in zh, zh-Hant, ko, ru, es, de, pt locales
- Point 2 (test fix): tightened test regex to anchor on branch-indicator class
  to avoid collision with _sessionLineageKey helper
- Point 3 (test fix): accept both inline and parentEl variable forms for
  body.appendChild pattern in pinned indicator test

All 6 previously failing tests now pass.
2026-05-01 15:54:27 +00:00
bergeouss ae40af03d7 feat: P3 improvements — insights panel, rollback UI, voice mode, subagent tree, api redact toggle
- #464 Insights panel: usage analytics dashboard with session/message/token stats,
  model breakdown, activity by day/hour charts, token breakdown (GET /api/insights)
- #466 Rollback UI: checkpoint list, diff viewer, restore confirmation
  (api/rollback.py, GET /api/rollback/{list,diff}, POST /api/rollback/restore)
- #1333 Voice mode: turn-based STT→send→TTS loop using Web Speech API,
  progressive enhancement with pulsing indicator and auto-resume
- #494 Subagent session tree: parent→children grouping in sidebar with
  expand/collapse chevrons, child count badges, localStorage persistence
- #1396 API redact toggle: Settings checkbox to disable forced redaction for
  self-hosted users (lazy check at call-time, default ON)
- #1385 Closed: compact tool activity toggle already exists in Settings
- #497 Commented: proposed shared-file bridge for cross-process gateway approvals
- i18n: tab_insights added to all 8 locales, voice/checkpoint keys to EN+RU
2026-05-01 13:43:10 +00:00
Dennis Soong 0ec4aad949 fix: anchor active sessions per browser tab 2026-05-01 19:52:05 +08:00
starship-s bdc328d034 fix: preserve webui model provider context
Persist session model_provider separately from model IDs so active/default provider selections like gpt-5.5 remain bare while routing through OpenAI Codex. Keep @provider:model for picker disambiguation and runtime bridging, and preserve explicit OpenRouter plus custom/proxy base_url routing.
2026-04-30 23:23:47 -06:00