Commit Graph

166 Commits

Author SHA1 Message Date
nesquena-hermes
6c5911a79f fix: light theme dialogs, workspace panel snap, model cache staleness, docker-compose docs — v0.50.68
Fixes four bugs + locks in one existing fix with regression tests.

Closes #594 (light theme dialogs), #576 (workspace panel snap), #585 (stale model list after CLI change), #567 (docker-compose macOS UID docs). Confirms and tests #590 (transcribing spinner already present).

Reviewed and approved by @nesquena. 1340 tests passing.
2026-04-16 11:55:18 -07:00
nesquena-hermes
54e83fb8b6 feat: support subpath mount via reverse proxy — v0.50.67 (PR #588 by @vcavichini)
Squash-merges feature from PR #588 by @vcavichini. Dynamic <base href> injection + api() helper slash-stripping enables deploying hermes-webui behind a reverse proxy at any subpath without configuration. Also fixes pre-existing bug: api/upload was using location.origin instead of location.href (closes #596). Co-authored-by: vcavichini <vcavichini@users.noreply.github.com>
2026-04-16 11:20:08 -07:00
nesquena-hermes
8a1bc134fa chore: CHANGELOG + v0.50.66 version badge (post-merge for PR #582)
Docs/version-only follow-up for PR #582. Nathan authorized full merge pipeline in session.
2026-04-16 10:21:39 -07:00
nesquena-hermes
db1240dde5 fix: HERMES_WEBUI_SKIP_ONBOARDING unconditional + guard against config writes + 10 skipped tests fixed — v0.50.65
Fixes two SKIP_ONBOARDING bugs and eliminates 10 permanently-skipped integration tests.

- SKIP_ONBOARDING=1 now honoured unconditionally (no longer gated on chat_ready)
- apply_onboarding_setup refuses to write config/env files when SKIP_ONBOARDING is set
- TestMediaEndpointIntegration (6) and TestOnboardingGateIntegration (4): collection-time
  skip guards removed; server reachability checked at runtime with fail() not skip()

Tests: 1327 passed, 0 skipped.

Admin merge — self-built PR, Nathan authorized full merge process in session.
2026-04-16 10:19:10 -07:00
nesquena-hermes
2efc1fb8e8 chore: CHANGELOG + v0.50.64 badge + Today-bucket test (post-merge follow-up for #584)
Admin merge — docs-only follow-up: CHANGELOG entry, version badge v0.50.64, one new test. No code logic. Nathan authorized end-to-end merge in session.
2026-04-16 10:09:51 -07:00
nesquena-hermes
a512f2020e feat: MCP toolsets in WebUI + onboarding fix for non-standard providers — v0.50.63
Squash-merges PR #578 (rebased from #574 by @renheqiang + #575 by @nesquena-hermes). MCP server toolsets now included in WebUI sessions; onboarding wizard no longer fires for non-standard providers. 1331 tests pass. Nathan override applied for self-built #575.
2026-04-15 23:39:07 -07:00
nesquena-hermes
45426bdcd1 fix: make hermes-agent source optional in Docker startup — v0.50.62
Squash-merges PR #577 (rebased from #573 by @nesquena). Docker hard-exit on missing hermes-agent → graceful warning. 1319 tests pass. Fixes #570.
2026-04-15 23:22:26 -07:00
nesquena-hermes
360379136b feat(upload): support Excel and Word file attachments — v0.50.61
Squash-merges PR #571 (rebased from contributor PR #566 by @renheqiang). Adds .xls/.xlsx/.doc/.docx to the file picker and MIME map. 1319 tests pass.
2026-04-15 22:43:31 -07:00
nesquena-hermes
e4fec9e4e0 test: skip onboarding config tests when PyYAML unavailable, remove duplicate definition — v0.50.60
Merges #564. Adds PyYAML skip guards to two onboarding tests. Removes duplicate _HAS_YAML/_needs_yaml block. No production code changed. 1319 tests pass.
2026-04-15 20:45:42 -07:00
nesquena-hermes
8cf10b152b fix: false Connection lost message after settled stream disconnect — v0.50.59
* fix: 避免流结束后误插入 Connection lost 错误

* chore: bump version to v0.50.59, update CHANGELOG

---------

Co-authored-by: fiver <fiver@example.com>
Co-authored-by: Hermes Agent <agent@hermes>
2026-04-15 20:25:31 -07:00
Hermes Agent
4f79b3f941 chore: bump version to v0.50.58, update CHANGELOG 2026-04-16 01:32:15 +00:00
Hermes Agent
8864001941 chore: bump version to v0.50.57, update CHANGELOG 2026-04-16 00:07:08 +00:00
Hermes Agent
ec44a437a2 fix: version badge v0.50.54→v0.50.55, add CHANGELOG entry for v0.50.55
The v0.50.55 release (Docker honcho fix) missed the index.html version
badge bump and CHANGELOG entry. Caught during post-session QA sweep.
2026-04-15 23:40:11 +00:00
Hermes Agent
27d3f9543e chore: bump version to v0.50.54, update CHANGELOG 2026-04-15 23:04:06 +00:00
Hermes Agent
7bfba201da chore: bump version to v0.50.53, update CHANGELOG 2026-04-15 22:13:14 +00:00
Hermes Agent
25fae902d3 chore: bump version to v0.50.52, update CHANGELOG 2026-04-15 19:42:11 +00:00
Hermes Agent
d6b58b9ce0 fix: queue simultaneous approval requests per session (fixes #527)
Changes _pending from a single overwriting dict value to a list,
so parallel tool calls each get their own approval slot.

api/routes.py:
- Wraps submit_pending() to append to a list and assign a stable
  approval_id (uuid4) to each entry.
- _handle_approval_pending() returns the first queued entry plus
  pending_count so the UI can show '1 of N'.
- _handle_approval_respond() pops by approval_id (falls back to
  oldest entry for backward-compat with old clients).
- Backward-compat: legacy single-dict values in _pending are
  handled without crashing.

static/messages.js:
- respondApproval() sends approval_id in the POST body.
- showApprovalCard() accepts pendingCount, shows '1 of N pending'
  counter when multiple approvals are queued.
- _approvalCurrentId tracks the approval_id of the displayed card.
- Poll loop passes pending_count to showApprovalCard.

static/index.html:
- Adds approvalCounter element for the '1 of N' display.

tests/test_approval_queue.py:
- 14 tests: static-analysis checks (Python + JS + HTML),
  functional tests that inject two simultaneous approvals and
  verify both are surfaced and independently resolvable.
2026-04-15 19:16:14 +00:00
Hermes Agent
ce4e01ea92 chore: bump version to v0.50.51, update CHANGELOG 2026-04-15 19:06:54 +00:00
Hermes Agent
03df7132d0 chore: bump version to v0.50.50, update CHANGELOG 2026-04-15 19:02:43 +00:00
armorbreak001
50d7d1cf88 fix(themes): swap Prism syntax-highlighting theme on light/dark switch
The Prism CSS was hardcoded to prism-tomorrow (dark-only), so code
blocks stayed dark even when switching to Light or other non-dark themes.

- Add id='prism-theme' to the <link> element for runtime lookup
- In _applyTheme(), swap href between prism-tomorrow (dark) and
  prism (light) based on resolved theme
- Skips DOM write when the target href is already active

Fixes #505
2026-04-15 19:01:52 +00:00
Hermes Agent
e077d110c3 chore: bump version to v0.50.49, update CHANGELOG 2026-04-15 16:46:53 +00:00
Hermes Agent
d3251fdbfd chore: bump version to v0.50.48, update CHANGELOG 2026-04-15 08:04:24 +00:00
Hermes Agent
178251a5c0 chore: bump version to v0.50.47, update CHANGELOG 2026-04-15 07:52:23 +00:00
Hermes Agent
44a544362f feat: add System (auto) theme following OS prefers-color-scheme (#504)
Synthesized from PRs #506, #509, #514 (all by armorbreak001 and cloudyun888).

Implementation:
- static/index.html: flicker-prevention head script resolves 'system' to
  'dark'/'light' via matchMedia before first paint. Adds 'System (auto)'
  as first option in theme picker. onchange calls _applyTheme().
- static/boot.js: new _applyTheme(name) helper — resolves 'system' via
  matchMedia, sets data-theme, registers a MQ change listener so the UI
  tracks OS switches live. loadSettings() now calls _applyTheme() instead
  of direct data-theme assignment.
- static/commands.js: adds 'system' to valid /theme command names,
  delegates apply to _applyTheme().
- static/panels.js: _settingsThemeOnOpen reads from localStorage (preserves
  'system' string, not the resolved 'dark'/'light'). _revertSettingsPreview
  calls _applyTheme() so reverting to 'system' correctly re-enables OS tracking.
- static/i18n.js: cmd_theme description now lists 'system' first in all 5
  locales (en, es, de, zh-Hans, zh-Hant).

Design choices vs submitted PRs:
- No separate system-theme.js file (unnecessary indirection).
- matchMedia listener does NOT POST to /api/settings (OS can change rapidly;
  persisting on every OS switch would hammer the server).

Co-authored-by: armorbreak001 <armorbreak001@users.noreply.github.com>
Co-authored-by: cloudyun888 <cloudyun888@users.noreply.github.com>
2026-04-15 07:45:20 +00:00
Hermes Agent
0b96f08b3e chore: bump version to v0.50.46, update CHANGELOG 2026-04-15 07:35:25 +00:00
Frank Song
ccba2f5c01 feat: harden clarify dialog flow and refresh recovery 2026-04-15 13:10:50 +08:00
Hermes Agent
7b9f08c774 fix: suppress N/A source_tag in session list sidebar (#429)
- sessions.js _formatSourceTag(): return null for unrecognised tags
  instead of raw string — prevents legacy 'N/A' values from surfacing
- sessions.js metaBits push: guarded with _stLabel null check so only
  known platform labels appear in the session metadata line
- sessions.js [SYSTEM:] title fallback: drop raw s.source_tag middle
  term, fall back directly to 'Gateway' for unknown sources

7 new tests in test_issue429.py.
1 updated test in test_sprint40_ui_polish.py (new guarded push pattern).

Closes #429
2026-04-14 22:14:31 +00:00
Hermes Agent
887893ecd1 fix: code-in-table CSS sizing + markdown image rendering (#486, #487)
- static/style.css: add td code / th code rules (font-size 0.85em,
  padding 1px 4px, vertical-align baseline) for both .msg-body and
  .preview-md to fix cramped inline code in table cells (#486)

- static/ui.js inlineMd(): add image pass (![alt](url) → <img
  class=msg-media-img>) running while _code_stash is active (protects
  image syntax inside backticks), add _img_stash (\x00G) to shield
  rendered <img> src= from autolink, add img to SAFE_INLINE (#487)

- static/ui.js renderMd() outer: add image pass before outer link pass
  for images in plain paragraphs, add img to SAFE_TAGS allowlist (#487)

- tests/test_issue486_487.py: 45 new tests covering CSS source checks,
  JS source structure, rendering behaviour, and combination edge cases
  (code + image + link in same table cell, image inside code span, etc.)

Closes #486, closes #487
2026-04-14 21:52:34 +00:00
Hermes Agent
2343dc1d85 docs: v0.50.43 CHANGELOG + version bump (test count TBD) 2026-04-14 21:15:02 +00:00
Hermes Agent
bb445ffe9a docs: v0.50.42 CHANGELOG, version bump (test count TBD) 2026-04-14 20:58:30 +00:00
Hermes Agent
c9a0e02301 docs: v0.50.41 CHANGELOG, version bump, test count (1117) 2026-04-14 19:36:14 +00:00
Hermes Agent
8199fa333e docs: v0.50.40 CHANGELOG and version bump (test count TBD) 2026-04-14 19:07:10 +00:00
Nathan Esquenazi
5fd7dc0c17 docs: v0.50.39 CHANGELOG, version bump, test count (1078) 2026-04-14 17:54:54 +00:00
Nathan Esquenazi
61ecdaded3 docs: v0.50.38 CHANGELOG, version bump, test count (1073) 2026-04-14 17:14:40 +00:00
Aron Prins
db392bd532 feat(ui): remove mobile bottom nav on phones
Closes #425:
2026-04-14 17:13:03 +00:00
Nathan Esquenazi
57a50591ee fix(onboarding): skip wizard if Hermes already configured
Closes #420:
2026-04-14 16:45:12 +00:00
Nathan Esquenazi
16c58e60f4 docs: v0.50.37 CHANGELOG, version bump, test count 2026-04-14 16:44:58 +00:00
nesquena-hermes
37850a4dfd fix: workspace list cleaner — all 1055 tests pass (#418)
* fix: workspace list cleaner — allow own-profile paths, remove brittle string filter

Two bugs in _clean_workspace_list() caused workspace adds to silently vanish
on the next load, making the duplicate-check test and workspace rename test fail:

1. Brittle string filter: 'if test-workspace in path or webui-mvp-test in path:
   continue' — removed. The test server's workspace IS under these paths, so any
   workspace added during testing got silently dropped on the next load_workspaces()
   call. The p.is_dir() check already handles non-existent paths.

2. Cross-profile filter too broad: 'if p is under ~/.hermes/profiles/: skip' —
   this correctly blocked cross-profile leakage but also blocked the current
   profile's own paths (e.g. ~/.hermes/profiles/webui/webui-mvp-test/...).
   Fixed: only skip if the path is under profiles/ AND under a DIFFERENT profile's
   directory. Paths under the current profile's own home are kept.

* docs: v0.50.36 release — version badge and CHANGELOG

---------

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-14 00:14:25 -07:00
nesquena-hermes
415270ff03 fix: cross-platform multi-workspace trust boundary (#417)
* fix: relax workspace trust boundary to user home directory

The previous restriction required workspaces to be under DEFAULT_WORKSPACE
(/home/hermes/workspace), which blocked all profile-specific workspaces
(~/CodePath, ~/General, ~/WebUI, ~/Camanji, etc.) since each profile uses
a different directory under home.

New boundary: any directory under Path.home() is trusted.
This still blocks /etc, /tmp, /var, /root, /usr and all paths outside the
user's home, while allowing any legitimate workspace under ~/

Also updates test assertions from 'trusted workspace root' to 'outside'
since the new error message says 'outside the user home directory'.

* fix: workspace trust uses home-dir + saved-list, not single ancestor

Three-layer trust model that works cross-platform and multi-workspace:

1. BLOCKLIST: /etc, /usr, /var, /bin, /sbin, /boot, /proc, /sys, /dev, /root,
   /lib, /lib64, /opt/homebrew — always rejected, even if somehow saved
2. HOME CHECK: any path under Path.home() is trusted — covers ~/CodePath,
   ~/hermes-webui-public, ~/WebUI, ~/General, ~/Camanji simultaneously;
   Path.home() is cross-platform (Linux ~/..., macOS ~/..., Windows C:\Users\...\...)
3. SAVED LIST ESCAPE HATCH: if a path is already in the saved workspace list,
   it's trusted regardless of location — covers self-hosted deployments where
   workspaces live outside home (/data/projects, /opt/workspace, etc.)

None/empty → DEFAULT_WORKSPACE (always trusted, validated at startup)

* docs: v0.50.35 release — version badge and CHANGELOG

---------

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 23:57:51 -07:00
nesquena-hermes
2a7a5ddfaf [security] fix(workspace): restrict session workspaces to trusted roots (#416)
* fix(workspace): restrict session workspaces to trusted roots

* fix: use boot-time DEFAULT_WORKSPACE instead of profile default for trusted workspace root

_profile_default_workspace() reads the agent's terminal.cwd which may differ
from the WebUI's configured workspace root. Use _BOOT_DEFAULT_WORKSPACE (which
respects HERMES_WEBUI_DEFAULT_WORKSPACE for test isolation) to stay consistent
with how new_session() seeds the initial workspace.

* docs: v0.50.34 release — version badge and CHANGELOG

---------

Co-authored-by: hinotoi-agent <paperlantern.agent@gmail.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 23:44:03 -07:00
nesquena-hermes
a5abe51cc5 fix: workspace panel close button — no duplicate X on desktop, mobile X respects file preview (#414)
* fix: workspace panel close button — no duplicate X on desktop, mobile X respects file preview

Two bugs fixed in the workspace right panel:

1. Duplicate X on desktop (bug): #btnClearPreview (the X icon) was always
   visible alongside #btnCollapseWorkspacePanel (the chevron), producing two
   close controls at once. Fixed in syncWorkspacePanelUI() — on desktop, the X
   is now hidden when no file preview is open (display:none), and only shown
   when the user is viewing a file. The chevron remains as the sole close
   control in browse mode.

2. Mobile X collapses panel instead of dismissing file (bug): .mobile-close-btn
   was calling closeWorkspacePanel() directly, which collapsed the whole panel
   even when a file was open. Changed to handleWorkspaceClose(), which already
   has the correct two-step logic: clear preview first, close panel only if
   no preview is visible.

Files changed:
- static/boot.js: syncWorkspacePanelUI() hides btnClearPreview on desktop
  when hasPreview is false, guarded by !isCompact so mobile is unaffected
- static/index.html: mobile-close-btn onclick changed from
  closeWorkspacePanel() to handleWorkspaceClose()
- tests/test_sprint44.py: 10 new regression tests
- tests/test_mobile_layout.py: updated test_workspace_close_button_present()
  to accept handleWorkspaceClose() as the valid onclick target

* fix: widen test_server_delete_invalidates_index window to 1200 chars

The test extracted a 600-char window starting from the session/delete
handler to check for SESSION_INDEX_FILE. Commit 3cc5839 added session_id
character validation and path traversal guards before the unlink call,
pushing SESSION_INDEX_FILE to ~764 chars from the match — beyond the
600-char limit, causing the test to fail on CI.

Widened the window to 1200 chars, which accommodates any reasonable
amount of guard code before the SESSION_INDEX_FILE.unlink() call.

* docs: v0.50.33 release — version badge and CHANGELOG

---------

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 23:25:26 -07:00
nesquena-hermes
3cc5839bf3 [security] fix(sessions): validate session_id before deleting session files (#412)
* fix(sessions): validate session_id before deleting files

* fix: remove premature session index invalidation before validation check

* docs: v0.50.32 release — version badge and CHANGELOG

---------

Co-authored-by: hinotoi-agent <paperlantern.agent@gmail.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 23:10:46 -07:00
nesquena-hermes
539501ed2b fix: delegate all live model fetching to agent provider_model_ids() (#411)
* fix: delegate all live model fetching to agent's provider_model_ids()

Previously _handle_live_models() maintained its own per-provider logic:
- anthropic, google, gemini returned 'not_supported' (hardcoded exclusions)
- openai-codex had a custom branch (added in v0.50.30)
- openai/copilot had hardcoded base URLs
- other providers fell through to a generic /v1/models fetch

Now the handler delegates entirely to hermes_cli.models.provider_model_ids(),
which is the agent's authoritative resolver:
- anthropic:    live fetch via /v1/models with correct API-key or OAuth headers
- copilot:      live fetch from api.githubcopilot.com/models with Copilot headers
- openai-codex: Codex OAuth endpoint + ~/.codex/ cache fallback
- nous:         live fetch from Nous inference portal
- deepseek, kimi-coding: generic OpenAI-compat /v1/models
- opencode-zen/go: OpenCode live catalog
- openrouter:   curated static list (live returns 300+ which is overwhelming)
- google/gemini, zai, minimax: static list (non-standard or Anthropic-compat endpoints)
- any others:   graceful static fallback

Also removed the client-side skip guard in _fetchLiveModels() (ui.js) that
blocked live fetching for anthropic, google, and gemini.

The hardcoded model lists in _PROVIDER_MODELS remain as the fallback when
credentials are missing or network is unavailable — they are never shown
when live data is available.

* docs: v0.50.31 release — version badge and CHANGELOG

---------

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 22:57:58 -07:00
nesquena-hermes
c91eaaf05f fix: route openai-codex live model fetch through agent get_codex_model_ids() (#410)
* fix: route openai-codex live model fetch through agent's get_codex_model_ids()

Previously _handle_live_models() grouped openai-codex with openai and sent a
request to https://api.openai.com/v1/models, which returns 403 because Codex
auth is OAuth-based via chatgpt.com, not a standard API key. The live fetch
silently failed and the UI showed only the hardcoded static list.

Now: openai-codex has a dedicated early-exit branch that calls
hermes_cli.codex_models.get_codex_model_ids() — the same path the agent CLI
uses. It resolves models in order: live Codex API (if OAuth token available) >
~/.codex/ local cache > DEFAULT_CODEX_MODELS. This means:

- If the user has a valid Codex OAuth session, the UI gets the exact model list
  their subscription provides (e.g. gpt-5.2, gpt-5.3-codex-spark that aren't
  in the hardcoded list)
- If the OAuth session is expired, falls back to local ~/.codex/ cache
- Always has DEFAULT_CODEX_MODELS as final fallback

Also: improved label generation for Codex model IDs (GPT-5.4 Mini vs GPT 5 4 Mini).
Added 1 structural regression test.

* docs: v0.50.30 release — version badge and CHANGELOG

---------

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 22:49:04 -07:00
nesquena-hermes
d3fea34c41 fix: correct tool call card rendering on session load after context compaction (#408)
* fix: correct tool call card rendering on session load

Two bugs caused duplicate/incorrect tool call cards when loading
sessions (especially after context compaction):

1. loadSession() sanitized messages (B9 filter) but did NOT update
   the session-level tool_calls array's assistant_msg_idx references.
   Since compact() returns only sanitized messages and recomputes
   tool_calls with indices into the compacted array, the original
   assistant_msg_idx values became stale/misaligned.

2. loadSession() then assigned the broken session-level tool_calls
   directly to S.toolCalls. This prevented renderMessages()'s fallback
   path (which derives tool_calls from per-message tool_calls using
   correct sanitized-array indices) from ever running.

Fix:
- Keep full sanitization loop with index remapping for session-level
  tool_calls (in case they're needed by other code paths).
- Instead of assigning broken session-level tool_calls to S.toolCalls,
  set S.toolCalls=[] so renderMessages() uses the fallback derivation
  from per-message tool_calls, which already have correct indices.

* test: add 8 regression tests for issue #401 tool call index remapping

* docs: v0.50.29 release — version badge and CHANGELOG

---------

Co-authored-by: Frank Song <franksong2702@gmail.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 22:41:31 -07:00
nesquena-hermes
a2258139f2 fix: expand openai-codex model catalog to match DEFAULT_CODEX_MODELS (#407)
* fix: expand openai-codex model catalog to match agent DEFAULT_CODEX_MODELS

The _PROVIDER_MODELS["openai-codex"] catalog only listed codex-mini-latest,
so the model dropdown for profiles using openai-codex provider (e.g. CodePath)
showed only that one entry — even when the profile's saved default_model was
gpt-5.4 or another standard Codex model.

Updated to match DEFAULT_CODEX_MODELS from hermes_cli/codex_models.py:
- gpt-5.4
- gpt-5.4-mini
- gpt-5.3-codex
- gpt-5.2-codex
- gpt-5.1-codex-max
- gpt-5.1-codex-mini
- codex-mini-latest (kept, relabeled as 'Codex Mini (latest)')

Also adds 2 regression tests: catalog includes gpt-5.4, display name correct.

* docs: v0.50.28 release — version badge and CHANGELOG

---------

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 22:35:27 -07:00
nesquena-hermes
1345ccccee feat: relative time labels in session sidebar (#406)
* feat: add relative time to session sidebar

(cherry picked from commit 272be9787fdff75d3da2dbc73175820477a3390e)

* fix: address session sidebar relative-time review feedback

* docs: v0.50.27 release — version badge and CHANGELOG

---------

Co-authored-by: Jordan SkyLF <jordan@skylinkfiber.net>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 22:26:05 -07:00
nesquena-hermes
4de4ed9a15 fix(sessions): redact sensitive titles in session list and search responses (#405)
* fix(sessions): redact titles in list and search responses

* docs: v0.50.26 release — version badge and CHANGELOG

---------

Co-authored-by: hinotoi-agent <paperlantern.agent@gmail.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 22:20:21 -07:00
nesquena-hermes
04ed0ff43d v0.50.25: mobile scroll, import timestamps, profile security, mic fallback (#404)
* fix: restore mobile chat scrolling and drawer close (#397)

- static/style.css: add min-height:0 to .layout and .main (flex shrink chain fix for mobile scroll)
- static/style.css: add -webkit-overflow-scrolling:touch, touch-action:pan-y, overscroll-behavior-y:contain to .messages
- static/boot.js: call closeMobileSidebar() on new-conversation button onclick and Ctrl+K shortcut
- tests/test_mobile_layout.py: 41 new lines covering all three CSS fixes and both JS call sites

Original PR by @Jordan-SkyLF

* fix: preserve imported session timestamps (#395)

- api/models.py: add touch_updated_at: bool = True param to Session.save(); import_cli_session() accepts created_at/updated_at kwargs and saves with touch_updated_at=False
- api/routes.py: extract created_at/updated_at from get_cli_sessions() metadata and forward to import_cli_session(); use touch_updated_at=False on post-import save
- tests/test_gateway_sync.py: +53 lines — integration test verifying imported session keeps original timestamp and sorts correctly vs newer sessions; also fix: add WebUI session file cleanup in finally block

Original PR by @Jordan-SkyLF

* fix(profiles): block path traversal in profile switch and delete flows (#399)

Master was vulnerable: switch_profile and delete_profile_api joined user-supplied profile
names directly into filesystem paths with no validation. An attacker could send
'../../etc/passwd' as a profile name to traverse outside the profiles directory.

- api/profiles.py: add _resolve_named_profile_home(name) — validates name with
  ^[a-z0-9][a-z0-9_-]{0,63}$ regex then enforces path containment via
  candidate.resolve().relative_to(profiles_root); use in switch_profile()
- api/profiles.py: add _validate_profile_name() call to delete_profile_api() entry
- api/routes.py: add _validate_profile_name() call at HTTP handler level for
  both /api/profile/switch and /api/profile/delete (fail-fast at API boundary)
- tests/test_profile_path_security.py: 3 tests — traversal rejected, valid name passes

Cherry-picked commit aae7a30 from @Hinotoi-agent (PR was 62 commits behind master)

* feat: add desktop microphone transcription fallback (#396)

Mic button now works in browsers that support getUserMedia/MediaRecorder but
lack SpeechRecognition (e.g. Firefox desktop, some Chromium builds).

- static/boot.js: detect _canRecordAudio (navigator.mediaDevices + getUserMedia + MediaRecorder);
  keep mic button enabled when either SpeechRecognition or MediaRecorder is available;
  MediaRecorder fallback records audio, sends blob to /api/transcribe, inserts transcript
  into the composer; _stopMic() handles all three states (recognition, mediaRecorder, neither)
- api/upload.py: add transcribe_audio() helper — saves uploaded blob to temp file, calls
  transcription_tools.transcribe_audio(), always cleans up temp file
- api/routes.py: add /api/transcribe POST handler — CSRF protected, auth-gated, 20MB limit,
  returns {text:...} or {error:...}
- api/helpers.py: change Permissions-Policy microphone=() to microphone=(self) (required to
  allow getUserMedia in the same origin)
- tests/test_voice_transcribe_endpoint.py: 87 new lines — 3 tests with mocked transcription
- tests/test_sprint19.py: +1 regression guard (microphone=(self) in Permissions-Policy)
- tests/test_sprint20.py: 3 updated tests for new fallback-capability checks

Original PR by @Jordan-SkyLF

* docs: v0.50.25 release — version badge and CHANGELOG

---------

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 22:11:45 -07:00
nesquena-hermes
2beebaa6a2 feat: opt-in chat bubble layout (closes #336) (#403)
* feat(ui): opt-in chat bubble layout

Closes #336.

Adds a settings toggle that right-aligns user messages and left-aligns
assistant replies. Off by default - the current full-width layout is
friendlier to code blocks and tool output, so bubbles are strictly
opt-in per the maintainer note on the issue.

Wiring follows the existing token-usage / cli-sessions pattern:

- api/config.py: new bubble_layout bool in _SETTINGS_DEFAULTS and
  _SETTINGS_BOOL_KEYS, validated + persisted like the rest.
- static/style.css: .bubble-layout gated selectors using :has() to
  tag msg-rows by .msg-role.user / .msg-role.assistant without any JS
  changes to message creation. User rows get align-self: flex-end,
  max-width: 75%, and a row-reverse header; assistant rows flex-start.
  A 700px media query widens the max to 92% on narrow screens.
- static/index.html: new checkbox with i18n keys next to the existing
  token-usage toggle.
- static/panels.js: loads the setting into the checkbox, saves it
  back, and toggles body.bubble-layout immediately on save.
- static/boot.js: applies the class on initial load so refreshed
  tabs honor the persisted setting without a flash.
- static/i18n.js: English label + description.

Test suite errors are environmental (test server fails to start on
port 8788 on main as well).

* i18n(es): add Spanish translations for bubble_layout setting

* fix+test: boot.js bubble-layout reset on failure; add 22 tests for issue #336

* docs: v0.50.24 release — version badge and CHANGELOG

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 21:42:01 -07:00