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>
This commit is contained in:
nesquena-hermes
2026-04-13 22:11:45 -07:00
committed by GitHub
parent 2beebaa6a2
commit 04ed0ff43d
15 changed files with 589 additions and 81 deletions

View File

@@ -8,6 +8,7 @@ the browser with no server-side component.
import re
import urllib.request
import json
import pathlib
BASE = "http://127.0.0.1:8788"
@@ -315,15 +316,31 @@ def test_boot_js_iife_guard():
assert '(function(){' in js or '(function () {' in js
def test_boot_js_browser_unsupported_return():
"""boot.js must bail out (return) early when SpeechRecognition is unavailable."""
def test_boot_js_browser_unsupported_guard_uses_fallback_capabilities():
"""boot.js must keep the mic available when either speech recognition OR recorder capture exists."""
js, _ = get_text("/static/boot.js")
# The IIFE should have an early return when SpeechRecognition is falsy
assert 'if(!SpeechRecognition)' in js or 'if (!SpeechRecognition)' in js
assert 'navigator.mediaDevices' in js
assert 'getUserMedia' in js
assert 'MediaRecorder' in js
assert '_canRecordAudio' in js or 'canRecordAudio' in js, \
"boot.js should compute a recorder fallback instead of bailing only on SpeechRecognition"
def test_boot_js_shows_mic_button_when_supported():
"""boot.js must set display='' on btnMic when SpeechRecognition is available."""
def test_boot_js_media_recorder_fallback_posts_to_transcribe_api():
"""Desktop fallback must send recorded audio to /api/transcribe for transcription."""
js, _ = get_text("/static/boot.js")
assert '/api/transcribe' in js
assert 'fetch(' in js
def test_routes_define_transcribe_endpoint():
"""Server routes must expose /api/transcribe for MediaRecorder fallback uploads."""
routes = pathlib.Path(__file__).parent.parent.joinpath("api/routes.py").read_text(encoding="utf-8")
assert '"/api/transcribe"' in routes
def test_boot_js_shows_mic_button_when_any_voice_path_is_supported():
"""boot.js must reveal btnMic when speech recognition or recorder fallback is available."""
js, _ = get_text("/static/boot.js")
assert "btn.style.display=''" in js or 'btn.style.display = ""' in js