* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
- Read X-Forwarded-For and X-Real-IP before falling back to raw socket IP
- Add HERMES_WEBUI_ONBOARDING_OPEN=1 env var escape hatch for remote servers
- Error message now includes the env var hint
- 18 new tests (TestOnboardingIPLogic + TestOnboardingSetupEndpoint)
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
* fix: persist durable inflight reload snapshots
* fix: remove duplicate loadInflightState stub, update CHANGELOG test count
The stub added in the previous review branch is superseded by the author's
real localStorage-backed implementation in the cherry-picked commit 36051c0.
Remove the duplicate. Update CHANGELOG to 961 tests and document the durable
inflight state feature.
---------
Co-authored-by: Jordan SkyLF <jordan@skylinkfiber.net>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
* fix: preserve live session output across chat switches
(cherry picked from commit 401e3b643d25e8dad8c06883b478b3c3073f07a5)
* fix: preserve todo state after session reload
(cherry picked from commit 7ee093ba19978af23b79148df2f2347e2f1e5bde)
* fix: preserve live assistant anchor across rerenders
* fix: stream live reasoning and tool progress
* fix: recover inflight session state after reload
* fix: add loadInflightState stub + CHANGELOG v0.50.21
- static/ui.js: add loadInflightState() function (currently returns null —
the typeof guard in sessions.js means reload recovery works via the
else-path attachLiveStream call; this stub satisfies the guard cleanly
and documents the extension point for future localStorage-backed state)
- CHANGELOG.md: v0.50.21 entry; 960 tests (up from 949)
---------
Co-authored-by: Jordan SkyLF <jordan@skylinkfiber.net>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
* fix: silent errors, stale models, live model fetching (#373, #374, #375)
- api/streaming.py: detect empty agent response (_assistant_added check),
emit apperror(type='no_response' or 'auth_mismatch') instead of silent done
- api/streaming.py: add _token_sent flag so guard works for streaming agents
- static/messages.js: done handler belt-and-suspenders guard for zero replies
- static/messages.js: apperror handler labels 'no_response' type distinctly
- api/config.py: remove gpt-4o and o3 from _FALLBACK_MODELS and
_PROVIDER_MODELS['openai'] (superseded by gpt-5.4-mini and o4-mini)
- api/routes.py: new /api/models/live?provider= endpoint, fetches /v1/models
from provider API with B310 scheme check + SSRF guard
- static/ui.js: _fetchLiveModels() background fetch after static list loads,
appends new models to dropdown, caches per session, skips unsupported providers
Other:
- tests/test_issues_373_374_375.py: 25 new structural tests
- tests/test_regressions.py: extend done-handler window 1500->2500 chars
- CHANGELOG.md: v0.50.19 entry; 947 tests (up from 922)
* fix: SSRF hostname bypass + auth detection operator precedence
1. routes.py: SSRF guard used substring matching (any(k in hostname))
which allows bypass via hostnames like evil-ollama.attacker.com.
Changed to exact hostname matching against a fixed set of known
local hostnames (localhost, 127.0.0.1, 0.0.0.0, ::1).
2. streaming.py: _is_auth detection had a Python operator precedence
bug on the ternary expression. The line:
'AuthenticationError' in type(...).__name__ if _last_err else False
parsed as the ternary absorbing the rest of the or-chain when
_last_err was falsy. Fixed to: (_last_err and 'AuthenticationError' in ...)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: fix v0.50.20 CHANGELOG version number and test count (949 tests)
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: KaTeX math rendering for $..$ and $$..$$ in chat and previews (fixes#347)
- Stash math delimiters before markdown pipeline, restore as .katex-block/.katex-inline elements
- KaTeX JS lazy-loaded from CDN on first math block (mirrors mermaid pattern)
- KaTeX CSS loaded eagerly in <head> to prevent layout shift
- SRI hashes on both CDN tags
- throwOnError:false — bad LaTeX degrades to code span
- Supports $$, $, \\(...\\), \\[...\\] delimiters
- 18 new tests, 831/831 passing
* fix: remove invalid \' escape sequences in math stash lines
Lines 311, 314, 316, 317 had \' (backslash-quote) instead of plain '
in the arrow function bodies. This is a JS syntax error — node --check
fails with 'Invalid or unexpected token'. Likely caused by a
serialization artifact during code generation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: swap stash order (fence before math) to protect code spans; add renderKatexBlocks to workspace preview
- static/ui.js: fence_stash now runs BEFORE math_stash so dollar signs
inside backtick code spans are not extracted as math. Previously
`$x$` would render as KaTeX inside a <code> tag instead of
showing the literal string $x$.
- static/workspace.js: add requestAnimationFrame(renderKatexBlocks)
after markdown preview renders so math works in workspace file
previews, not only in chat messages.
* feat: KaTeX math rendering + stash order fix + workspace wiring (#352)
- tests/test_issue347.py: 11 new tests (29 total) covering fence-before-math
ordering, workspace.js renderKatexBlocks call, stash token distinctness,
false-positive prevention, safe-tags boundary check
- CHANGELOG.md: v0.50.15 entry; 870 tests total (up from 841)
* fix: use literal null byte (\x00M) in math stash token — matches restore regex
The original PR's second commit (fix: remove invalid \' escapes) accidentally
doubled the backslash in the math stash tokens: '\\x00M' is a 5-char string
(backslash + x + 0 + 0 + M) but the restore regex /\x00M/ expects a null byte.
Result: $...$ in messages produced visible \x00M0\x00 tokens instead of
KaTeX spans.
Changed all 4 math stash return statements to use '\x00M' (single backslash =
null byte, same convention as fence_stash's '\x00F').
Also updates test_stash_tokens_distinct to check for the correct pattern.
* fix: add null-byte token test; update CHANGELOG to v0.50.15 with fixes
- tests/test_issue347.py: add test_math_stash_token_uses_single_backslash_null_byte
to catch the \\x00M double-backslash regression; 30 tests total (up from 29)
- CHANGELOG.md: v0.50.15 entry documents all fixes including the token bug
and workspace preview wiring; 871 tests total
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The zh (Simplified Chinese) locale had ~40 strings from a "missing keys"
section that were actually Traditional Chinese or garbled text. This
replaces them with correct Simplified Chinese, removes duplicate keys,
and fixes a garbled zh-Hant string (姊妹允許 → 始終允許).
Fixes#337
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Expands the onboarding setup IP check from 127.0.0.1-only to any loopback or RFC-1918 private address. Docker containers connect via 172.17.x.x — previously blocked with a 403. Public IPs still blocked unless auth enabled. 791 tests pass.
Normalizes hyphens to dots in backend model-ID comparison so claude-sonnet-4-6 (hermes-agent format) matches claude-sonnet-4.6 (WebUI list) and no duplicate entry is injected. README line counts and test count corrected. 791 tests, all pass.
Remove ^ anchor from think/Gemma regexes in ui.js; trimStart() before startsWith checks in messages.js streaming path. Fixes MiniMax M2.7 and any model emitting leading newlines before <think>. 10 new tests, 768 total.
localStorage key hermes-webui-workspace-panel saves open/closed on every state change; restored on boot before syncWorkspacePanelState(). 7 new tests, 753 total.
Cherry-pick of mobile Enter newline fix from #315. On touch-primary devices (pointer:coarse), Enter inserts a newline. Desktop unchanged. 4 new tests, 746 total.
* 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>