v0.47.0: dialogs, session menu, /skills, mobile fixes, mobile QA suite
* fix: custom provider with slash model name no longer rerouted to OpenRouter (#255)
When base_url is configured in config.yaml, resolve_model_provider() now
trusts the configured provider/base_url entirely and skips the slash-based
OpenRouter heuristic. Fixes google/gemma-4-26b-a4b with provider:custom
being silently routed to OpenRouter, resulting in 401 errors.
Fixes #230
* test: mobile layout regression suite — 14 tests for every QA run (#254)
Adds tests/test_mobile_layout.py with 14 static regression tests that run
on every QA pass to catch mobile layout breakage before it reaches prod.
Covers: breakpoints at 900px/640px, right panel slide-over CSS, mobile
overlay, bottom nav, files button, profile dropdown z-index, chip overflow,
workspace close, 100dvh, 44px touch targets, 16px font-size on textarea.
* feat: /skills slash command lists and filters available Hermes skills (#257)
Adds /skills [query] command to commands.js. Fetches from /api/skills,
groups by category (alphabetically sorted), displays as a formatted
assistant message. Optional query filters by name, description, or category.
i18n keys added for en, de, zh, zh-Hant. 1 regression test added.
Fixes #248
* feat: shared app dialogs replace native confirm()/prompt() calls (#251)
Adds showConfirmDialog() and showPromptDialog() helpers to ui.js, backed
by a themed #appDialogOverlay. Replaces all 11 native browser confirm/prompt
call sites across panels.js, sessions.js, ui.js, workspace.js.
Supports: danger mode, keyboard focus trap (Tab/Escape/Enter), focus restore,
ARIA roles, mobile-responsive stacked buttons at 640px. i18n for en/de/zh/zh-Hant.
5 new tests in test_sprint33.py verify markup, CSS, helpers, and absence of
native dialog calls.
Extracted from PR #242.
* fix: Android Chrome mobile — workspace panel close + profile dropdown (#256)
Fix #247: toggleMobileFiles() now shows/hides the mobile overlay when
toggling the right workspace panel. New closeMobileFiles() helper closes
the panel with correct overlay state tracking. Overlay onclick calls both
closeMobileSidebar() and closeMobileFiles(). Mobile-only close button (x)
added to workspace panel header.
Fix #246: profile dropdown uses position:fixed;top:56px;right:8px at
max-width:900px, escaping the overflow-x:auto stacking context that was
clipping it on Android Chrome.
Fix applied during review: closeMobileSidebar() now checks if the right
panel is still open before hiding the overlay, preventing the overlay from
disappearing when only the sidebar is closed.
Fixes #247 Fixes #246
* feat: session ⋯ action dropdown replaces per-row buttons (#252)
Replaces the 5 per-row hover action buttons (pin/move/archive/duplicate/trash)
with a single ⋯ trigger that opens a positioned dropdown menu. Menu has full
keyboard (Escape), click-outside, scroll, and resize-reposition handling.
Position:fixed prevents sidebar clipping.
5 actions: Pin/Unpin, Move to project, Archive/Unarchive, Duplicate, Delete
(danger style). Each with icon and descriptive subtitle.
Updated test_sprint16.py: test_sessions_js_uses_action_menu_not_per_row_buttons
asserts the new trigger and menu functions exist, old per-row classes are gone.
Extracted from PR #242.
* docs: v0.47.0 release notes, bump version, update test counts (645)
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-11 19:19:12 +00:00
|
|
|
|
"""
|
|
|
|
|
|
Mobile layout regression tests — run on every QA pass.
|
|
|
|
|
|
|
|
|
|
|
|
These tests check that the CSS and HTML structure required for correct
|
|
|
|
|
|
mobile rendering (375px–640px viewport widths) is intact after every change.
|
|
|
|
|
|
They are static checks (no server needed) that catch common regressions:
|
|
|
|
|
|
|
|
|
|
|
|
- Mobile breakpoints present for key layout elements
|
|
|
|
|
|
- Right panel slide-over markup and CSS intact
|
|
|
|
|
|
- Profile dropdown not clipped by overflow on mobile
|
|
|
|
|
|
- Composer footer chips scroll correctly on narrow viewports
|
2026-04-14 17:13:03 +00:00
|
|
|
|
- Mobile sidebar navigation stays available on phones
|
v0.47.0: dialogs, session menu, /skills, mobile fixes, mobile QA suite
* fix: custom provider with slash model name no longer rerouted to OpenRouter (#255)
When base_url is configured in config.yaml, resolve_model_provider() now
trusts the configured provider/base_url entirely and skips the slash-based
OpenRouter heuristic. Fixes google/gemma-4-26b-a4b with provider:custom
being silently routed to OpenRouter, resulting in 401 errors.
Fixes #230
* test: mobile layout regression suite — 14 tests for every QA run (#254)
Adds tests/test_mobile_layout.py with 14 static regression tests that run
on every QA pass to catch mobile layout breakage before it reaches prod.
Covers: breakpoints at 900px/640px, right panel slide-over CSS, mobile
overlay, bottom nav, files button, profile dropdown z-index, chip overflow,
workspace close, 100dvh, 44px touch targets, 16px font-size on textarea.
* feat: /skills slash command lists and filters available Hermes skills (#257)
Adds /skills [query] command to commands.js. Fetches from /api/skills,
groups by category (alphabetically sorted), displays as a formatted
assistant message. Optional query filters by name, description, or category.
i18n keys added for en, de, zh, zh-Hant. 1 regression test added.
Fixes #248
* feat: shared app dialogs replace native confirm()/prompt() calls (#251)
Adds showConfirmDialog() and showPromptDialog() helpers to ui.js, backed
by a themed #appDialogOverlay. Replaces all 11 native browser confirm/prompt
call sites across panels.js, sessions.js, ui.js, workspace.js.
Supports: danger mode, keyboard focus trap (Tab/Escape/Enter), focus restore,
ARIA roles, mobile-responsive stacked buttons at 640px. i18n for en/de/zh/zh-Hant.
5 new tests in test_sprint33.py verify markup, CSS, helpers, and absence of
native dialog calls.
Extracted from PR #242.
* fix: Android Chrome mobile — workspace panel close + profile dropdown (#256)
Fix #247: toggleMobileFiles() now shows/hides the mobile overlay when
toggling the right workspace panel. New closeMobileFiles() helper closes
the panel with correct overlay state tracking. Overlay onclick calls both
closeMobileSidebar() and closeMobileFiles(). Mobile-only close button (x)
added to workspace panel header.
Fix #246: profile dropdown uses position:fixed;top:56px;right:8px at
max-width:900px, escaping the overflow-x:auto stacking context that was
clipping it on Android Chrome.
Fix applied during review: closeMobileSidebar() now checks if the right
panel is still open before hiding the overlay, preventing the overlay from
disappearing when only the sidebar is closed.
Fixes #247 Fixes #246
* feat: session ⋯ action dropdown replaces per-row buttons (#252)
Replaces the 5 per-row hover action buttons (pin/move/archive/duplicate/trash)
with a single ⋯ trigger that opens a positioned dropdown menu. Menu has full
keyboard (Escape), click-outside, scroll, and resize-reposition handling.
Position:fixed prevents sidebar clipping.
5 actions: Pin/Unpin, Move to project, Archive/Unarchive, Duplicate, Delete
(danger style). Each with icon and descriptive subtitle.
Updated test_sprint16.py: test_sessions_js_uses_action_menu_not_per_row_buttons
asserts the new trigger and menu functions exist, old per-row classes are gone.
Extracted from PR #242.
* docs: v0.47.0 release notes, bump version, update test counts (645)
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-11 19:19:12 +00:00
|
|
|
|
- No full-viewport overflow that would break scroll
|
|
|
|
|
|
|
|
|
|
|
|
Run as part of the standard test suite:
|
|
|
|
|
|
pytest tests/test_mobile_layout.py -v
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
import pathlib
|
|
|
|
|
|
import re
|
|
|
|
|
|
|
|
|
|
|
|
REPO = pathlib.Path(__file__).parent.parent
|
|
|
|
|
|
HTML = (REPO / "static" / "index.html").read_text(encoding="utf-8")
|
|
|
|
|
|
CSS = (REPO / "static" / "style.css").read_text(encoding="utf-8")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── Mobile breakpoint rules ───────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
def test_mobile_breakpoint_900px_present():
|
|
|
|
|
|
"""@media(max-width:900px) must hide the right panel and show mobile-files-btn."""
|
|
|
|
|
|
assert "@media(max-width:900px)" in CSS or "@media (max-width: 900px)" in CSS, \
|
|
|
|
|
|
"Missing @media(max-width:900px) breakpoint in style.css"
|
|
|
|
|
|
# Right panel should be hidden at 900px, replaced by slide-over
|
|
|
|
|
|
assert ".rightpanel{display:none" in CSS or ".rightpanel {display:none" in CSS or \
|
|
|
|
|
|
re.search(r'max-width:900px\).*?\.rightpanel\{display:none', CSS, re.DOTALL), \
|
|
|
|
|
|
".rightpanel must be display:none at max-width:900px (slide-over replaces it)"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_mobile_breakpoint_640px_present():
|
|
|
|
|
|
"""@media(max-width:640px) must exist for narrow phone layouts."""
|
|
|
|
|
|
assert "@media(max-width:640px)" in CSS or "@media (max-width: 640px)" in CSS, \
|
|
|
|
|
|
"Missing @media(max-width:640px) breakpoint in style.css"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_rightpanel_mobile_slide_over_css():
|
|
|
|
|
|
"""Right panel must have position:fixed slide-over CSS for mobile."""
|
|
|
|
|
|
# At max-width:900px the rightpanel should be position:fixed, off-screen right
|
|
|
|
|
|
assert "position:fixed" in CSS, \
|
|
|
|
|
|
"style.css must have position:fixed for rightpanel mobile slide-over"
|
|
|
|
|
|
assert ".rightpanel.mobile-open{right:0" in CSS or ".rightpanel.mobile-open {right:0" in CSS, \
|
|
|
|
|
|
".rightpanel.mobile-open must set right:0 to slide panel in from right"
|
|
|
|
|
|
assert "right:-320px" in CSS or "right: -320px" in CSS, \
|
|
|
|
|
|
"rightpanel must start off-screen (right:-320px) on mobile"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_mobile_overlay_present():
|
|
|
|
|
|
"""Mobile overlay element must exist for tap-to-close sidebar behavior."""
|
|
|
|
|
|
assert 'id="mobileOverlay"' in HTML, \
|
|
|
|
|
|
"#mobileOverlay element missing from index.html"
|
|
|
|
|
|
assert "mobile-overlay" in CSS, \
|
|
|
|
|
|
".mobile-overlay CSS rule missing from style.css"
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-04-14 17:13:03 +00:00
|
|
|
|
def test_sidebar_nav_present():
|
|
|
|
|
|
"""Sidebar top navigation tabs must be present."""
|
|
|
|
|
|
assert 'class="sidebar-nav"' in HTML, \
|
|
|
|
|
|
".sidebar-nav missing from index.html"
|
|
|
|
|
|
assert ".sidebar-nav{" in CSS or ".sidebar-nav {" in CSS, \
|
|
|
|
|
|
".sidebar-nav CSS rule missing from style.css"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_mobile_does_not_hide_sidebar_nav():
|
|
|
|
|
|
"""Phone breakpoint must keep the sidebar top navigation visible."""
|
|
|
|
|
|
mobile_block = re.search(r'@media\(max-width:640px\)\{(.*)\n\s*\}', CSS, re.DOTALL)
|
|
|
|
|
|
assert mobile_block, "Missing @media(max-width:640px) block in style.css"
|
|
|
|
|
|
assert ".sidebar-nav{display:none" not in mobile_block.group(1).replace(" ", ""), \
|
|
|
|
|
|
".sidebar-nav must stay visible on mobile"
|
v0.47.0: dialogs, session menu, /skills, mobile fixes, mobile QA suite
* fix: custom provider with slash model name no longer rerouted to OpenRouter (#255)
When base_url is configured in config.yaml, resolve_model_provider() now
trusts the configured provider/base_url entirely and skips the slash-based
OpenRouter heuristic. Fixes google/gemma-4-26b-a4b with provider:custom
being silently routed to OpenRouter, resulting in 401 errors.
Fixes #230
* test: mobile layout regression suite — 14 tests for every QA run (#254)
Adds tests/test_mobile_layout.py with 14 static regression tests that run
on every QA pass to catch mobile layout breakage before it reaches prod.
Covers: breakpoints at 900px/640px, right panel slide-over CSS, mobile
overlay, bottom nav, files button, profile dropdown z-index, chip overflow,
workspace close, 100dvh, 44px touch targets, 16px font-size on textarea.
* feat: /skills slash command lists and filters available Hermes skills (#257)
Adds /skills [query] command to commands.js. Fetches from /api/skills,
groups by category (alphabetically sorted), displays as a formatted
assistant message. Optional query filters by name, description, or category.
i18n keys added for en, de, zh, zh-Hant. 1 regression test added.
Fixes #248
* feat: shared app dialogs replace native confirm()/prompt() calls (#251)
Adds showConfirmDialog() and showPromptDialog() helpers to ui.js, backed
by a themed #appDialogOverlay. Replaces all 11 native browser confirm/prompt
call sites across panels.js, sessions.js, ui.js, workspace.js.
Supports: danger mode, keyboard focus trap (Tab/Escape/Enter), focus restore,
ARIA roles, mobile-responsive stacked buttons at 640px. i18n for en/de/zh/zh-Hant.
5 new tests in test_sprint33.py verify markup, CSS, helpers, and absence of
native dialog calls.
Extracted from PR #242.
* fix: Android Chrome mobile — workspace panel close + profile dropdown (#256)
Fix #247: toggleMobileFiles() now shows/hides the mobile overlay when
toggling the right workspace panel. New closeMobileFiles() helper closes
the panel with correct overlay state tracking. Overlay onclick calls both
closeMobileSidebar() and closeMobileFiles(). Mobile-only close button (x)
added to workspace panel header.
Fix #246: profile dropdown uses position:fixed;top:56px;right:8px at
max-width:900px, escaping the overflow-x:auto stacking context that was
clipping it on Android Chrome.
Fix applied during review: closeMobileSidebar() now checks if the right
panel is still open before hiding the overlay, preventing the overlay from
disappearing when only the sidebar is closed.
Fixes #247 Fixes #246
* feat: session ⋯ action dropdown replaces per-row buttons (#252)
Replaces the 5 per-row hover action buttons (pin/move/archive/duplicate/trash)
with a single ⋯ trigger that opens a positioned dropdown menu. Menu has full
keyboard (Escape), click-outside, scroll, and resize-reposition handling.
Position:fixed prevents sidebar clipping.
5 actions: Pin/Unpin, Move to project, Archive/Unarchive, Duplicate, Delete
(danger style). Each with icon and descriptive subtitle.
Updated test_sprint16.py: test_sessions_js_uses_action_menu_not_per_row_buttons
asserts the new trigger and menu functions exist, old per-row classes are gone.
Extracted from PR #242.
* docs: v0.47.0 release notes, bump version, update test counts (645)
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-11 19:19:12 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_mobile_files_button_present():
|
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242)
* Polish workspace panel behavior and app dialogs
* Replace remaining emoji UI glyphs with Lucide icons
* Redesign composer footer around model and context controls
Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill.
Design credit and inspiration: Theo / T3 Code.
Reference implementation: https://github.com/pingdotgg/t3code/
* Remove obsolete activity bar
Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts.
This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status.
* Move workspace and model switching into composer footer
* Move profile switching into composer footer
* Refactor Hermes control center UI
* Redesign control center settings modal layout
Widen the modal to 860px, simplify the tab list to icon+label rows,
stretch the tab column's divider to full height, lock the panel to a
fixed height so switching tabs no longer resizes the outer shell, and
always open on the Conversation tab.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Put session item actions in a dropdown
* Use Hermes mark in sidebar control button
* Reset control center section on close
* Drop session-item left border indicator
Remove the left-border accent used for active, CLI, and project rows —
each state already has a dedicated cue (gold fill, cli badge, project
dot), so the border was redundant. Fully round the row, add 2px
bottom spacing between rows, and strip the matching JS/CSS overrides.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Increase session search input vertical padding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Normalise odd pixel values across UI
Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid
across composer chips, sidebar panels, cron list, settings, approval
buttons, dropdowns, and inline message edit — eliminating the 7/9/11px
drift that was making sibling elements feel subtly misaligned.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite)
The mobile layout regression suite (test_mobile_layout.py) requires:
- #btnMobileFiles onclick=toggleMobileFiles() in topbar chips
- .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints
Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports.
* Improve composer footer mobile responsiveness and UX
- Collapse composer chips to icon-only at <=400px viewports
- Add model chip icon (CPU) so it remains tappable when labels are hidden
- Show send button always (disabled state when empty, hidden during streaming)
- Show context usage indicator on session load, not just after streaming
- Add cancel status fallback timeout to prevent stale "Cancelling..." text
- Update tests to match new send button and busy state behavior
* Fix duplicate files button and broken workspace close on mobile
Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle
in the mobile topbar. Fix workspace panel close button calling undefined
closeMobileFiles() — now calls closeWorkspacePanel().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix model chip icon vertical alignment in composer footer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix workspace toggle button hidden on desktop by conflicting CSS class
Remove mobile-files-btn class from #btnWorkspacePanelToggle — its
display:none!important rule was overriding workspace-toggle-btn visibility
on non-mobile viewports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix session actions dots button inaccessible on mobile sidebar
Always show the session actions trigger on mobile (no hover state on
touch devices) and restore right padding so text truncates with
ellipsis before the dots icon.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer footer manage links not opening sidebar panel
The "Manage profiles" and "Manage workspaces" links in the composer
footer dropdowns called switchPanel() which only changes the active
panel content but doesn't open the sidebar. Replaced with
mobileSwitchPanel() which also opens the sidebar so the panel is
actually visible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Widen icon-only composer chips breakpoint from 400px to 768px
Move the icon-only chip styling up into the existing max-width:768px
media query so chips collapse to icon-only on tablets too, preventing
composer footer overflow on mid-size screens.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer-left vertical scrollbar by setting overflow-y:hidden
When overflow-x is set to auto, the CSS spec implicitly changes
overflow-y from visible to auto, allowing a vertical scrollbar to
appear from slight chip padding/border overflow. Explicitly set
overflow-y:hidden to prevent this.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve rebase conflicts and fix control center test assertions
- Resolved 4 conflicts during rebase onto master (workspace.js,
boot.js, index.html, test_sprint34.py)
- Fixed test_sprint34.py: _controlSection -> _settingsSection,
cc-tab -> settings-tabs (matching actual implementation)
- Fixed quoting syntax error in test assertion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update version badge in System tab to v0.49.4
* docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge
---------
Co-authored-by: Aron Prins <pwf.aron@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
|
|
|
|
"""Mobile files toggle button (#btnWorkspacePanelToggle.workspace-toggle-btn) must be in HTML and CSS."""
|
|
|
|
|
|
assert 'id="btnWorkspacePanelToggle"' in HTML, \
|
|
|
|
|
|
"#btnWorkspacePanelToggle missing from index.html"
|
|
|
|
|
|
assert "workspace-toggle-btn" in CSS, \
|
|
|
|
|
|
".workspace-toggle-btn CSS missing from style.css"
|
v0.47.0: dialogs, session menu, /skills, mobile fixes, mobile QA suite
* fix: custom provider with slash model name no longer rerouted to OpenRouter (#255)
When base_url is configured in config.yaml, resolve_model_provider() now
trusts the configured provider/base_url entirely and skips the slash-based
OpenRouter heuristic. Fixes google/gemma-4-26b-a4b with provider:custom
being silently routed to OpenRouter, resulting in 401 errors.
Fixes #230
* test: mobile layout regression suite — 14 tests for every QA run (#254)
Adds tests/test_mobile_layout.py with 14 static regression tests that run
on every QA pass to catch mobile layout breakage before it reaches prod.
Covers: breakpoints at 900px/640px, right panel slide-over CSS, mobile
overlay, bottom nav, files button, profile dropdown z-index, chip overflow,
workspace close, 100dvh, 44px touch targets, 16px font-size on textarea.
* feat: /skills slash command lists and filters available Hermes skills (#257)
Adds /skills [query] command to commands.js. Fetches from /api/skills,
groups by category (alphabetically sorted), displays as a formatted
assistant message. Optional query filters by name, description, or category.
i18n keys added for en, de, zh, zh-Hant. 1 regression test added.
Fixes #248
* feat: shared app dialogs replace native confirm()/prompt() calls (#251)
Adds showConfirmDialog() and showPromptDialog() helpers to ui.js, backed
by a themed #appDialogOverlay. Replaces all 11 native browser confirm/prompt
call sites across panels.js, sessions.js, ui.js, workspace.js.
Supports: danger mode, keyboard focus trap (Tab/Escape/Enter), focus restore,
ARIA roles, mobile-responsive stacked buttons at 640px. i18n for en/de/zh/zh-Hant.
5 new tests in test_sprint33.py verify markup, CSS, helpers, and absence of
native dialog calls.
Extracted from PR #242.
* fix: Android Chrome mobile — workspace panel close + profile dropdown (#256)
Fix #247: toggleMobileFiles() now shows/hides the mobile overlay when
toggling the right workspace panel. New closeMobileFiles() helper closes
the panel with correct overlay state tracking. Overlay onclick calls both
closeMobileSidebar() and closeMobileFiles(). Mobile-only close button (x)
added to workspace panel header.
Fix #246: profile dropdown uses position:fixed;top:56px;right:8px at
max-width:900px, escaping the overflow-x:auto stacking context that was
clipping it on Android Chrome.
Fix applied during review: closeMobileSidebar() now checks if the right
panel is still open before hiding the overlay, preventing the overlay from
disappearing when only the sidebar is closed.
Fixes #247 Fixes #246
* feat: session ⋯ action dropdown replaces per-row buttons (#252)
Replaces the 5 per-row hover action buttons (pin/move/archive/duplicate/trash)
with a single ⋯ trigger that opens a positioned dropdown menu. Menu has full
keyboard (Escape), click-outside, scroll, and resize-reposition handling.
Position:fixed prevents sidebar clipping.
5 actions: Pin/Unpin, Move to project, Archive/Unarchive, Duplicate, Delete
(danger style). Each with icon and descriptive subtitle.
Updated test_sprint16.py: test_sessions_js_uses_action_menu_not_per_row_buttons
asserts the new trigger and menu functions exist, old per-row classes are gone.
Extracted from PR #242.
* docs: v0.47.0 release notes, bump version, update test counts (645)
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-11 19:19:12 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── Profile dropdown overflow ─────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
def test_profile_dropdown_not_clipped_by_overflow():
|
|
|
|
|
|
"""Profile dropdown must not be inside an overflow:hidden or overflow-x:auto ancestor
|
|
|
|
|
|
without a higher z-index escape hatch.
|
|
|
|
|
|
|
|
|
|
|
|
The topbar-chips container uses overflow-x:auto on mobile, which creates a
|
|
|
|
|
|
stacking context that clips absolutely-positioned children. The profile dropdown
|
|
|
|
|
|
must use position:fixed on mobile OR the topbar-chips must not clip it.
|
|
|
|
|
|
"""
|
|
|
|
|
|
# The profile-chip wrapper must have position:relative so the dropdown can escape
|
|
|
|
|
|
assert 'id="profileChipWrap"' in HTML, \
|
|
|
|
|
|
"#profileChipWrap missing from index.html"
|
|
|
|
|
|
# Profile dropdown must have a z-index high enough to clear the topbar
|
|
|
|
|
|
assert ".profile-dropdown{" in CSS or ".profile-dropdown {" in CSS, \
|
|
|
|
|
|
".profile-dropdown CSS rule missing"
|
|
|
|
|
|
# z-index must be at least 200 (topbar is z-index:10)
|
|
|
|
|
|
m = re.search(r'\.profile-dropdown\{[^}]*z-index:(\d+)', CSS)
|
|
|
|
|
|
if m:
|
|
|
|
|
|
assert int(m.group(1)) >= 100, \
|
|
|
|
|
|
f".profile-dropdown z-index {m.group(1)} is too low — must be >= 100 to clear topbar"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_topbar_chips_mobile_overflow():
|
|
|
|
|
|
"""topbar-chips must use overflow-x:auto on mobile for chip scrolling.
|
|
|
|
|
|
|
|
|
|
|
|
Chips (profile, workspace, model, files) must scroll horizontally on narrow
|
|
|
|
|
|
viewports rather than wrapping onto a second line which would break the topbar layout.
|
|
|
|
|
|
"""
|
|
|
|
|
|
# At narrow viewport, topbar-chips should scroll
|
|
|
|
|
|
assert "overflow-x:auto" in CSS or "overflow-x: auto" in CSS, \
|
|
|
|
|
|
"topbar-chips must have overflow-x:auto for mobile chip scrolling"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── Workspace panel close ─────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
def test_workspace_close_button_present():
|
|
|
|
|
|
"""Workspace panel must have a close/hide button accessible on mobile."""
|
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-14 06:25:26 +00:00
|
|
|
|
# Accept handleWorkspaceClose() (two-step close: file→browse→closed), or the
|
|
|
|
|
|
# lower-level functions directly. handleWorkspaceClose is preferred because
|
|
|
|
|
|
# it dismisses a file preview first before closing the panel.
|
v0.47.0: dialogs, session menu, /skills, mobile fixes, mobile QA suite
* fix: custom provider with slash model name no longer rerouted to OpenRouter (#255)
When base_url is configured in config.yaml, resolve_model_provider() now
trusts the configured provider/base_url entirely and skips the slash-based
OpenRouter heuristic. Fixes google/gemma-4-26b-a4b with provider:custom
being silently routed to OpenRouter, resulting in 401 errors.
Fixes #230
* test: mobile layout regression suite — 14 tests for every QA run (#254)
Adds tests/test_mobile_layout.py with 14 static regression tests that run
on every QA pass to catch mobile layout breakage before it reaches prod.
Covers: breakpoints at 900px/640px, right panel slide-over CSS, mobile
overlay, bottom nav, files button, profile dropdown z-index, chip overflow,
workspace close, 100dvh, 44px touch targets, 16px font-size on textarea.
* feat: /skills slash command lists and filters available Hermes skills (#257)
Adds /skills [query] command to commands.js. Fetches from /api/skills,
groups by category (alphabetically sorted), displays as a formatted
assistant message. Optional query filters by name, description, or category.
i18n keys added for en, de, zh, zh-Hant. 1 regression test added.
Fixes #248
* feat: shared app dialogs replace native confirm()/prompt() calls (#251)
Adds showConfirmDialog() and showPromptDialog() helpers to ui.js, backed
by a themed #appDialogOverlay. Replaces all 11 native browser confirm/prompt
call sites across panels.js, sessions.js, ui.js, workspace.js.
Supports: danger mode, keyboard focus trap (Tab/Escape/Enter), focus restore,
ARIA roles, mobile-responsive stacked buttons at 640px. i18n for en/de/zh/zh-Hant.
5 new tests in test_sprint33.py verify markup, CSS, helpers, and absence of
native dialog calls.
Extracted from PR #242.
* fix: Android Chrome mobile — workspace panel close + profile dropdown (#256)
Fix #247: toggleMobileFiles() now shows/hides the mobile overlay when
toggling the right workspace panel. New closeMobileFiles() helper closes
the panel with correct overlay state tracking. Overlay onclick calls both
closeMobileSidebar() and closeMobileFiles(). Mobile-only close button (x)
added to workspace panel header.
Fix #246: profile dropdown uses position:fixed;top:56px;right:8px at
max-width:900px, escaping the overflow-x:auto stacking context that was
clipping it on Android Chrome.
Fix applied during review: closeMobileSidebar() now checks if the right
panel is still open before hiding the overlay, preventing the overlay from
disappearing when only the sidebar is closed.
Fixes #247 Fixes #246
* feat: session ⋯ action dropdown replaces per-row buttons (#252)
Replaces the 5 per-row hover action buttons (pin/move/archive/duplicate/trash)
with a single ⋯ trigger that opens a positioned dropdown menu. Menu has full
keyboard (Escape), click-outside, scroll, and resize-reposition handling.
Position:fixed prevents sidebar clipping.
5 actions: Pin/Unpin, Move to project, Archive/Unarchive, Duplicate, Delete
(danger style). Each with icon and descriptive subtitle.
Updated test_sprint16.py: test_sessions_js_uses_action_menu_not_per_row_buttons
asserts the new trigger and menu functions exist, old per-row classes are gone.
Extracted from PR #242.
* docs: v0.47.0 release notes, bump version, update test counts (645)
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-11 19:19:12 +00:00
|
|
|
|
has_close = (
|
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-14 06:25:26 +00:00
|
|
|
|
'onclick="handleWorkspaceClose()"' in HTML or
|
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242)
* Polish workspace panel behavior and app dialogs
* Replace remaining emoji UI glyphs with Lucide icons
* Redesign composer footer around model and context controls
Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill.
Design credit and inspiration: Theo / T3 Code.
Reference implementation: https://github.com/pingdotgg/t3code/
* Remove obsolete activity bar
Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts.
This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status.
* Move workspace and model switching into composer footer
* Move profile switching into composer footer
* Refactor Hermes control center UI
* Redesign control center settings modal layout
Widen the modal to 860px, simplify the tab list to icon+label rows,
stretch the tab column's divider to full height, lock the panel to a
fixed height so switching tabs no longer resizes the outer shell, and
always open on the Conversation tab.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Put session item actions in a dropdown
* Use Hermes mark in sidebar control button
* Reset control center section on close
* Drop session-item left border indicator
Remove the left-border accent used for active, CLI, and project rows —
each state already has a dedicated cue (gold fill, cli badge, project
dot), so the border was redundant. Fully round the row, add 2px
bottom spacing between rows, and strip the matching JS/CSS overrides.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Increase session search input vertical padding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Normalise odd pixel values across UI
Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid
across composer chips, sidebar panels, cron list, settings, approval
buttons, dropdowns, and inline message edit — eliminating the 7/9/11px
drift that was making sibling elements feel subtly misaligned.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite)
The mobile layout regression suite (test_mobile_layout.py) requires:
- #btnMobileFiles onclick=toggleMobileFiles() in topbar chips
- .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints
Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports.
* Improve composer footer mobile responsiveness and UX
- Collapse composer chips to icon-only at <=400px viewports
- Add model chip icon (CPU) so it remains tappable when labels are hidden
- Show send button always (disabled state when empty, hidden during streaming)
- Show context usage indicator on session load, not just after streaming
- Add cancel status fallback timeout to prevent stale "Cancelling..." text
- Update tests to match new send button and busy state behavior
* Fix duplicate files button and broken workspace close on mobile
Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle
in the mobile topbar. Fix workspace panel close button calling undefined
closeMobileFiles() — now calls closeWorkspacePanel().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix model chip icon vertical alignment in composer footer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix workspace toggle button hidden on desktop by conflicting CSS class
Remove mobile-files-btn class from #btnWorkspacePanelToggle — its
display:none!important rule was overriding workspace-toggle-btn visibility
on non-mobile viewports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix session actions dots button inaccessible on mobile sidebar
Always show the session actions trigger on mobile (no hover state on
touch devices) and restore right padding so text truncates with
ellipsis before the dots icon.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer footer manage links not opening sidebar panel
The "Manage profiles" and "Manage workspaces" links in the composer
footer dropdowns called switchPanel() which only changes the active
panel content but doesn't open the sidebar. Replaced with
mobileSwitchPanel() which also opens the sidebar so the panel is
actually visible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Widen icon-only composer chips breakpoint from 400px to 768px
Move the icon-only chip styling up into the existing max-width:768px
media query so chips collapse to icon-only on tablets too, preventing
composer footer overflow on mid-size screens.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer-left vertical scrollbar by setting overflow-y:hidden
When overflow-x is set to auto, the CSS spec implicitly changes
overflow-y from visible to auto, allowing a vertical scrollbar to
appear from slight chip padding/border overflow. Explicitly set
overflow-y:hidden to prevent this.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve rebase conflicts and fix control center test assertions
- Resolved 4 conflicts during rebase onto master (workspace.js,
boot.js, index.html, test_sprint34.py)
- Fixed test_sprint34.py: _controlSection -> _settingsSection,
cc-tab -> settings-tabs (matching actual implementation)
- Fixed quoting syntax error in test assertion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update version badge in System tab to v0.49.4
* docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge
---------
Co-authored-by: Aron Prins <pwf.aron@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
|
|
|
|
'onclick="closeWorkspacePanel()"' in HTML or
|
|
|
|
|
|
'onclick="toggleWorkspacePanel()"' in HTML
|
v0.47.0: dialogs, session menu, /skills, mobile fixes, mobile QA suite
* fix: custom provider with slash model name no longer rerouted to OpenRouter (#255)
When base_url is configured in config.yaml, resolve_model_provider() now
trusts the configured provider/base_url entirely and skips the slash-based
OpenRouter heuristic. Fixes google/gemma-4-26b-a4b with provider:custom
being silently routed to OpenRouter, resulting in 401 errors.
Fixes #230
* test: mobile layout regression suite — 14 tests for every QA run (#254)
Adds tests/test_mobile_layout.py with 14 static regression tests that run
on every QA pass to catch mobile layout breakage before it reaches prod.
Covers: breakpoints at 900px/640px, right panel slide-over CSS, mobile
overlay, bottom nav, files button, profile dropdown z-index, chip overflow,
workspace close, 100dvh, 44px touch targets, 16px font-size on textarea.
* feat: /skills slash command lists and filters available Hermes skills (#257)
Adds /skills [query] command to commands.js. Fetches from /api/skills,
groups by category (alphabetically sorted), displays as a formatted
assistant message. Optional query filters by name, description, or category.
i18n keys added for en, de, zh, zh-Hant. 1 regression test added.
Fixes #248
* feat: shared app dialogs replace native confirm()/prompt() calls (#251)
Adds showConfirmDialog() and showPromptDialog() helpers to ui.js, backed
by a themed #appDialogOverlay. Replaces all 11 native browser confirm/prompt
call sites across panels.js, sessions.js, ui.js, workspace.js.
Supports: danger mode, keyboard focus trap (Tab/Escape/Enter), focus restore,
ARIA roles, mobile-responsive stacked buttons at 640px. i18n for en/de/zh/zh-Hant.
5 new tests in test_sprint33.py verify markup, CSS, helpers, and absence of
native dialog calls.
Extracted from PR #242.
* fix: Android Chrome mobile — workspace panel close + profile dropdown (#256)
Fix #247: toggleMobileFiles() now shows/hides the mobile overlay when
toggling the right workspace panel. New closeMobileFiles() helper closes
the panel with correct overlay state tracking. Overlay onclick calls both
closeMobileSidebar() and closeMobileFiles(). Mobile-only close button (x)
added to workspace panel header.
Fix #246: profile dropdown uses position:fixed;top:56px;right:8px at
max-width:900px, escaping the overflow-x:auto stacking context that was
clipping it on Android Chrome.
Fix applied during review: closeMobileSidebar() now checks if the right
panel is still open before hiding the overlay, preventing the overlay from
disappearing when only the sidebar is closed.
Fixes #247 Fixes #246
* feat: session ⋯ action dropdown replaces per-row buttons (#252)
Replaces the 5 per-row hover action buttons (pin/move/archive/duplicate/trash)
with a single ⋯ trigger that opens a positioned dropdown menu. Menu has full
keyboard (Escape), click-outside, scroll, and resize-reposition handling.
Position:fixed prevents sidebar clipping.
5 actions: Pin/Unpin, Move to project, Archive/Unarchive, Duplicate, Delete
(danger style). Each with icon and descriptive subtitle.
Updated test_sprint16.py: test_sessions_js_uses_action_menu_not_per_row_buttons
asserts the new trigger and menu functions exist, old per-row classes are gone.
Extracted from PR #242.
* docs: v0.47.0 release notes, bump version, update test counts (645)
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-11 19:19:12 +00:00
|
|
|
|
)
|
|
|
|
|
|
assert has_close, \
|
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-14 06:25:26 +00:00
|
|
|
|
"handleWorkspaceClose() or closeWorkspacePanel() must be wired to a button to close the workspace panel on mobile"
|
v0.47.0: dialogs, session menu, /skills, mobile fixes, mobile QA suite
* fix: custom provider with slash model name no longer rerouted to OpenRouter (#255)
When base_url is configured in config.yaml, resolve_model_provider() now
trusts the configured provider/base_url entirely and skips the slash-based
OpenRouter heuristic. Fixes google/gemma-4-26b-a4b with provider:custom
being silently routed to OpenRouter, resulting in 401 errors.
Fixes #230
* test: mobile layout regression suite — 14 tests for every QA run (#254)
Adds tests/test_mobile_layout.py with 14 static regression tests that run
on every QA pass to catch mobile layout breakage before it reaches prod.
Covers: breakpoints at 900px/640px, right panel slide-over CSS, mobile
overlay, bottom nav, files button, profile dropdown z-index, chip overflow,
workspace close, 100dvh, 44px touch targets, 16px font-size on textarea.
* feat: /skills slash command lists and filters available Hermes skills (#257)
Adds /skills [query] command to commands.js. Fetches from /api/skills,
groups by category (alphabetically sorted), displays as a formatted
assistant message. Optional query filters by name, description, or category.
i18n keys added for en, de, zh, zh-Hant. 1 regression test added.
Fixes #248
* feat: shared app dialogs replace native confirm()/prompt() calls (#251)
Adds showConfirmDialog() and showPromptDialog() helpers to ui.js, backed
by a themed #appDialogOverlay. Replaces all 11 native browser confirm/prompt
call sites across panels.js, sessions.js, ui.js, workspace.js.
Supports: danger mode, keyboard focus trap (Tab/Escape/Enter), focus restore,
ARIA roles, mobile-responsive stacked buttons at 640px. i18n for en/de/zh/zh-Hant.
5 new tests in test_sprint33.py verify markup, CSS, helpers, and absence of
native dialog calls.
Extracted from PR #242.
* fix: Android Chrome mobile — workspace panel close + profile dropdown (#256)
Fix #247: toggleMobileFiles() now shows/hides the mobile overlay when
toggling the right workspace panel. New closeMobileFiles() helper closes
the panel with correct overlay state tracking. Overlay onclick calls both
closeMobileSidebar() and closeMobileFiles(). Mobile-only close button (x)
added to workspace panel header.
Fix #246: profile dropdown uses position:fixed;top:56px;right:8px at
max-width:900px, escaping the overflow-x:auto stacking context that was
clipping it on Android Chrome.
Fix applied during review: closeMobileSidebar() now checks if the right
panel is still open before hiding the overlay, preventing the overlay from
disappearing when only the sidebar is closed.
Fixes #247 Fixes #246
* feat: session ⋯ action dropdown replaces per-row buttons (#252)
Replaces the 5 per-row hover action buttons (pin/move/archive/duplicate/trash)
with a single ⋯ trigger that opens a positioned dropdown menu. Menu has full
keyboard (Escape), click-outside, scroll, and resize-reposition handling.
Position:fixed prevents sidebar clipping.
5 actions: Pin/Unpin, Move to project, Archive/Unarchive, Duplicate, Delete
(danger style). Each with icon and descriptive subtitle.
Updated test_sprint16.py: test_sessions_js_uses_action_menu_not_per_row_buttons
asserts the new trigger and menu functions exist, old per-row classes are gone.
Extracted from PR #242.
* docs: v0.47.0 release notes, bump version, update test counts (645)
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-11 19:19:12 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_toggle_mobile_files_js_defined():
|
|
|
|
|
|
"""toggleMobileFiles() must be defined in boot.js."""
|
|
|
|
|
|
boot_js = (REPO / "static" / "boot.js").read_text(encoding="utf-8")
|
|
|
|
|
|
assert "function toggleMobileFiles()" in boot_js, \
|
|
|
|
|
|
"toggleMobileFiles() missing from static/boot.js"
|
|
|
|
|
|
assert "mobile-open" in boot_js, \
|
|
|
|
|
|
"toggleMobileFiles() must toggle mobile-open class on the right panel"
|
|
|
|
|
|
|
|
|
|
|
|
|
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-14 05:11:45 +00:00
|
|
|
|
def test_new_conversation_closes_mobile_sidebar():
|
|
|
|
|
|
"""New conversation must close the mobile drawer so the chat pane is visible immediately."""
|
|
|
|
|
|
boot_js = (REPO / "static" / "boot.js").read_text(encoding="utf-8")
|
|
|
|
|
|
click_line = next((ln for ln in boot_js.splitlines() if "$('btnNewChat').onclick" in ln), "")
|
|
|
|
|
|
assert click_line, "btnNewChat onclick handler missing from static/boot.js"
|
|
|
|
|
|
assert "closeMobileSidebar" in click_line, \
|
|
|
|
|
|
"btnNewChat handler must closeMobileSidebar() after creating the new session"
|
|
|
|
|
|
|
|
|
|
|
|
shortcut_line = next((ln for ln in boot_js.splitlines() if "e.key==='k'" in ln or "e.key === 'k'" in ln), "")
|
|
|
|
|
|
assert shortcut_line, "Cmd/Ctrl+K new chat shortcut missing from static/boot.js"
|
|
|
|
|
|
shortcut_block = "\n".join(boot_js.splitlines()[boot_js.splitlines().index(shortcut_line):boot_js.splitlines().index(shortcut_line)+4])
|
|
|
|
|
|
assert "closeMobileSidebar" in shortcut_block, \
|
|
|
|
|
|
"Cmd/Ctrl+K new chat shortcut must closeMobileSidebar() after creating the new session"
|
|
|
|
|
|
|
|
|
|
|
|
|
v0.47.0: dialogs, session menu, /skills, mobile fixes, mobile QA suite
* fix: custom provider with slash model name no longer rerouted to OpenRouter (#255)
When base_url is configured in config.yaml, resolve_model_provider() now
trusts the configured provider/base_url entirely and skips the slash-based
OpenRouter heuristic. Fixes google/gemma-4-26b-a4b with provider:custom
being silently routed to OpenRouter, resulting in 401 errors.
Fixes #230
* test: mobile layout regression suite — 14 tests for every QA run (#254)
Adds tests/test_mobile_layout.py with 14 static regression tests that run
on every QA pass to catch mobile layout breakage before it reaches prod.
Covers: breakpoints at 900px/640px, right panel slide-over CSS, mobile
overlay, bottom nav, files button, profile dropdown z-index, chip overflow,
workspace close, 100dvh, 44px touch targets, 16px font-size on textarea.
* feat: /skills slash command lists and filters available Hermes skills (#257)
Adds /skills [query] command to commands.js. Fetches from /api/skills,
groups by category (alphabetically sorted), displays as a formatted
assistant message. Optional query filters by name, description, or category.
i18n keys added for en, de, zh, zh-Hant. 1 regression test added.
Fixes #248
* feat: shared app dialogs replace native confirm()/prompt() calls (#251)
Adds showConfirmDialog() and showPromptDialog() helpers to ui.js, backed
by a themed #appDialogOverlay. Replaces all 11 native browser confirm/prompt
call sites across panels.js, sessions.js, ui.js, workspace.js.
Supports: danger mode, keyboard focus trap (Tab/Escape/Enter), focus restore,
ARIA roles, mobile-responsive stacked buttons at 640px. i18n for en/de/zh/zh-Hant.
5 new tests in test_sprint33.py verify markup, CSS, helpers, and absence of
native dialog calls.
Extracted from PR #242.
* fix: Android Chrome mobile — workspace panel close + profile dropdown (#256)
Fix #247: toggleMobileFiles() now shows/hides the mobile overlay when
toggling the right workspace panel. New closeMobileFiles() helper closes
the panel with correct overlay state tracking. Overlay onclick calls both
closeMobileSidebar() and closeMobileFiles(). Mobile-only close button (x)
added to workspace panel header.
Fix #246: profile dropdown uses position:fixed;top:56px;right:8px at
max-width:900px, escaping the overflow-x:auto stacking context that was
clipping it on Android Chrome.
Fix applied during review: closeMobileSidebar() now checks if the right
panel is still open before hiding the overlay, preventing the overlay from
disappearing when only the sidebar is closed.
Fixes #247 Fixes #246
* feat: session ⋯ action dropdown replaces per-row buttons (#252)
Replaces the 5 per-row hover action buttons (pin/move/archive/duplicate/trash)
with a single ⋯ trigger that opens a positioned dropdown menu. Menu has full
keyboard (Escape), click-outside, scroll, and resize-reposition handling.
Position:fixed prevents sidebar clipping.
5 actions: Pin/Unpin, Move to project, Archive/Unarchive, Duplicate, Delete
(danger style). Each with icon and descriptive subtitle.
Updated test_sprint16.py: test_sessions_js_uses_action_menu_not_per_row_buttons
asserts the new trigger and menu functions exist, old per-row classes are gone.
Extracted from PR #242.
* docs: v0.47.0 release notes, bump version, update test counts (645)
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-11 19:19:12 +00:00
|
|
|
|
# ── Viewport and scroll safety ────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
def test_body_overflow_hidden():
|
|
|
|
|
|
"""body must have overflow:hidden to prevent double scrollbars on mobile."""
|
|
|
|
|
|
assert "body{" in CSS or "body {" in CSS, \
|
|
|
|
|
|
"body rule missing from style.css"
|
|
|
|
|
|
assert re.search(r'body\{[^}]*overflow:hidden', CSS), \
|
|
|
|
|
|
"body must have overflow:hidden to prevent double scrollbars"
|
|
|
|
|
|
|
|
|
|
|
|
|
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-14 05:11:45 +00:00
|
|
|
|
def test_flex_parents_allow_message_scroller_to_shrink():
|
|
|
|
|
|
"""The top-level flex containers must opt into min-height:0 so .messages can scroll on mobile.
|
|
|
|
|
|
|
|
|
|
|
|
Mobile Safari/Chrome can trap scroll when a flex child with overflow:auto sits inside
|
|
|
|
|
|
parents whose min-height remains auto. Both .layout and .main need min-height:0.
|
|
|
|
|
|
"""
|
|
|
|
|
|
assert re.search(r'\.layout\{[^}]*min-height:0', CSS), \
|
|
|
|
|
|
".layout must set min-height:0 so the chat column can shrink and scroll"
|
|
|
|
|
|
assert re.search(r'\.main\{[^}]*min-height:0', CSS), \
|
|
|
|
|
|
".main must set min-height:0 so .messages remains scrollable while busy"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_messages_touch_scrolling_hints_present():
|
|
|
|
|
|
"""The messages scroller must advertise touch-friendly scrolling behavior.
|
|
|
|
|
|
|
|
|
|
|
|
On mobile browsers, momentum scrolling and explicit pan-y/overscroll behavior help
|
|
|
|
|
|
prevent the chat area from feeling locked while the app body itself stays overflow:hidden.
|
|
|
|
|
|
"""
|
|
|
|
|
|
assert re.search(r'\.messages\{[^}]*-webkit-overflow-scrolling:\s*touch', CSS), \
|
|
|
|
|
|
".messages must enable -webkit-overflow-scrolling:touch for mobile momentum scroll"
|
|
|
|
|
|
assert re.search(r'\.messages\{[^}]*touch-action:\s*pan-y', CSS), \
|
|
|
|
|
|
".messages must set touch-action:pan-y so vertical swipe gestures scroll the transcript"
|
|
|
|
|
|
assert re.search(r'\.messages\{[^}]*overscroll-behavior-y:\s*contain', CSS), \
|
|
|
|
|
|
".messages must contain vertical overscroll so the transcript keeps the gesture"
|
|
|
|
|
|
|
|
|
|
|
|
|
v0.47.0: dialogs, session menu, /skills, mobile fixes, mobile QA suite
* fix: custom provider with slash model name no longer rerouted to OpenRouter (#255)
When base_url is configured in config.yaml, resolve_model_provider() now
trusts the configured provider/base_url entirely and skips the slash-based
OpenRouter heuristic. Fixes google/gemma-4-26b-a4b with provider:custom
being silently routed to OpenRouter, resulting in 401 errors.
Fixes #230
* test: mobile layout regression suite — 14 tests for every QA run (#254)
Adds tests/test_mobile_layout.py with 14 static regression tests that run
on every QA pass to catch mobile layout breakage before it reaches prod.
Covers: breakpoints at 900px/640px, right panel slide-over CSS, mobile
overlay, bottom nav, files button, profile dropdown z-index, chip overflow,
workspace close, 100dvh, 44px touch targets, 16px font-size on textarea.
* feat: /skills slash command lists and filters available Hermes skills (#257)
Adds /skills [query] command to commands.js. Fetches from /api/skills,
groups by category (alphabetically sorted), displays as a formatted
assistant message. Optional query filters by name, description, or category.
i18n keys added for en, de, zh, zh-Hant. 1 regression test added.
Fixes #248
* feat: shared app dialogs replace native confirm()/prompt() calls (#251)
Adds showConfirmDialog() and showPromptDialog() helpers to ui.js, backed
by a themed #appDialogOverlay. Replaces all 11 native browser confirm/prompt
call sites across panels.js, sessions.js, ui.js, workspace.js.
Supports: danger mode, keyboard focus trap (Tab/Escape/Enter), focus restore,
ARIA roles, mobile-responsive stacked buttons at 640px. i18n for en/de/zh/zh-Hant.
5 new tests in test_sprint33.py verify markup, CSS, helpers, and absence of
native dialog calls.
Extracted from PR #242.
* fix: Android Chrome mobile — workspace panel close + profile dropdown (#256)
Fix #247: toggleMobileFiles() now shows/hides the mobile overlay when
toggling the right workspace panel. New closeMobileFiles() helper closes
the panel with correct overlay state tracking. Overlay onclick calls both
closeMobileSidebar() and closeMobileFiles(). Mobile-only close button (x)
added to workspace panel header.
Fix #246: profile dropdown uses position:fixed;top:56px;right:8px at
max-width:900px, escaping the overflow-x:auto stacking context that was
clipping it on Android Chrome.
Fix applied during review: closeMobileSidebar() now checks if the right
panel is still open before hiding the overlay, preventing the overlay from
disappearing when only the sidebar is closed.
Fixes #247 Fixes #246
* feat: session ⋯ action dropdown replaces per-row buttons (#252)
Replaces the 5 per-row hover action buttons (pin/move/archive/duplicate/trash)
with a single ⋯ trigger that opens a positioned dropdown menu. Menu has full
keyboard (Escape), click-outside, scroll, and resize-reposition handling.
Position:fixed prevents sidebar clipping.
5 actions: Pin/Unpin, Move to project, Archive/Unarchive, Duplicate, Delete
(danger style). Each with icon and descriptive subtitle.
Updated test_sprint16.py: test_sessions_js_uses_action_menu_not_per_row_buttons
asserts the new trigger and menu functions exist, old per-row classes are gone.
Extracted from PR #242.
* docs: v0.47.0 release notes, bump version, update test counts (645)
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-11 19:19:12 +00:00
|
|
|
|
def test_100dvh_viewport_height():
|
|
|
|
|
|
"""Layout must use 100dvh (dynamic viewport height) for correct mobile sizing.
|
|
|
|
|
|
|
|
|
|
|
|
On mobile Safari and Chrome, 100vh includes the browser chrome (address bar),
|
|
|
|
|
|
causing content to be hidden. 100dvh accounts for the actual available height.
|
|
|
|
|
|
"""
|
|
|
|
|
|
assert "100dvh" in CSS, \
|
|
|
|
|
|
"style.css must use 100dvh for correct mobile viewport height (100vh hides content under address bar)"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_composer_touch_target_size():
|
|
|
|
|
|
"""Send button and composer inputs must have minimum 44px touch targets on mobile.
|
|
|
|
|
|
|
|
|
|
|
|
Apple HIG and Google Material guidelines both require 44px minimum touch targets.
|
|
|
|
|
|
"""
|
|
|
|
|
|
# Check that mobile CSS doesn't make the send button smaller than 44×44
|
|
|
|
|
|
# We check that there's at least a min-height definition for touch targets
|
|
|
|
|
|
assert re.search(r'(min-height|height).*44px', CSS), \
|
|
|
|
|
|
"style.css must define 44px minimum touch targets for mobile (send button, nav buttons)"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── Input zoom prevention ─────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
def test_composer_textarea_font_size_mobile():
|
|
|
|
|
|
"""Composer textarea must have font-size >= 16px on mobile.
|
|
|
|
|
|
|
|
|
|
|
|
iOS Safari zooms the viewport when an input with font-size < 16px is focused,
|
|
|
|
|
|
which breaks the layout. The composer textarea must be >= 16px at mobile widths.
|
|
|
|
|
|
"""
|
|
|
|
|
|
# Check for 16px font-size on the textarea in a mobile breakpoint
|
|
|
|
|
|
assert re.search(r'font-size:16px', CSS), \
|
|
|
|
|
|
"Composer textarea must have font-size:16px at mobile widths to prevent iOS zoom-on-focus"
|
2026-04-12 07:41:52 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-04-14 17:13:03 +00:00
|
|
|
|
# ── Sidebar tabs on mobile ───────────────────────────────────────────────────
|
2026-04-12 07:41:52 +00:00
|
|
|
|
|
2026-04-14 17:13:03 +00:00
|
|
|
|
def test_profiles_sidebar_tab_present():
|
|
|
|
|
|
"""Sidebar tab strip must include Profiles."""
|
|
|
|
|
|
assert 'class="nav-tab" data-panel="profiles"' in HTML, \
|
|
|
|
|
|
"Sidebar nav must have a Profiles tab"
|
2026-04-12 07:41:52 +00:00
|
|
|
|
|
|
|
|
|
|
|
2026-04-14 17:13:03 +00:00
|
|
|
|
def test_mobile_bottom_nav_removed():
|
|
|
|
|
|
"""The old fixed mobile bottom nav should not be present anymore."""
|
|
|
|
|
|
assert "mobile-bottom-nav" not in HTML, \
|
|
|
|
|
|
"mobile-bottom-nav markup should be removed from index.html"
|
|
|
|
|
|
assert "mobile-bottom-nav" not in CSS, \
|
|
|
|
|
|
"mobile-bottom-nav CSS should be removed from style.css"
|
2026-04-12 19:41:12 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── Mobile Enter key inserts newline (PR #315, fixes #269) ───────────────────
|
|
|
|
|
|
|
|
|
|
|
|
def test_mobile_enter_newline_condition_present():
|
|
|
|
|
|
"""boot.js keydown handler must detect touch-primary devices via pointer:coarse."""
|
|
|
|
|
|
boot_js = (REPO / "static" / "boot.js").read_text(encoding="utf-8")
|
|
|
|
|
|
assert "pointer:coarse" in boot_js, \
|
|
|
|
|
|
"boot.js must use pointer:coarse media query for mobile Enter detection"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_mobile_enter_newline_uses_match_media():
|
|
|
|
|
|
"""boot.js must call matchMedia for pointer detection, not a hardcoded flag."""
|
|
|
|
|
|
boot_js = (REPO / "static" / "boot.js").read_text(encoding="utf-8")
|
|
|
|
|
|
assert "matchMedia('(pointer:coarse)')" in boot_js or 'matchMedia("(pointer:coarse)")' in boot_js, \
|
|
|
|
|
|
"boot.js must use matchMedia('(pointer:coarse)') for mobile detection"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_mobile_enter_newline_only_overrides_enter_default():
|
|
|
|
|
|
"""Mobile newline override must only apply when _sendKey is the default 'enter'."""
|
|
|
|
|
|
boot_js = (REPO / "static" / "boot.js").read_text(encoding="utf-8")
|
|
|
|
|
|
# The _mobileDefault check must gate on _sendKey==='enter' so ctrl+enter users aren't affected
|
|
|
|
|
|
assert "_sendKey===" in boot_js and "'enter'" in boot_js, \
|
|
|
|
|
|
"Mobile newline fallback must check window._sendKey==='enter' to avoid overriding user preference"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_mobile_enter_does_not_affect_desktop_logic():
|
|
|
|
|
|
"""The mobile Enter override must not alter the existing else branch for desktop users."""
|
|
|
|
|
|
boot_js = (REPO / "static" / "boot.js").read_text(encoding="utf-8")
|
|
|
|
|
|
# The else branch (desktop, sends on Enter without Shift) must still be present
|
|
|
|
|
|
assert "if(!e.shiftKey){e.preventDefault();send();" in boot_js, \
|
|
|
|
|
|
"Desktop Enter-to-send logic (else branch) must still be present in boot.js"
|