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>
This commit is contained in:
nesquena-hermes
2026-04-12 11:55:40 -07:00
committed by GitHub
parent ed2d55f020
commit ede1a5fc50
23 changed files with 1333 additions and 610 deletions

View File

@@ -172,7 +172,7 @@ def pytest_collection_modifyitems(config, items):
skipped += 1
if skipped:
print(f"\n⚠️ hermes-agent not found {skipped} agent-dependent tests will be skipped\n")
print(f"\nWARNING: hermes-agent not found; {skipped} agent-dependent tests will be skipped\n")
# ── Helpers ──────────────────────────────────────────────────────────────────

View File

@@ -70,11 +70,11 @@ def test_mobile_bottom_nav_present():
def test_mobile_files_button_present():
"""Mobile files toggle button (#btnMobileFiles) must be in HTML and CSS."""
assert 'id="btnMobileFiles"' in HTML, \
"#btnMobileFiles missing from index.html"
assert "mobile-files-btn" in CSS, \
".mobile-files-btn CSS missing from style.css"
"""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"
# ── Profile dropdown overflow ─────────────────────────────────────────────────
@@ -115,13 +115,13 @@ def test_topbar_chips_mobile_overflow():
def test_workspace_close_button_present():
"""Workspace panel must have a close/hide button accessible on mobile."""
# Either a dedicated mobile close button or the X button that closes the panel
# Either a dedicated mobile close button or the toggle button that closes the panel
has_close = (
'onclick="toggleMobileFiles()"' in HTML or
'toggleMobileFiles' in HTML
'onclick="closeWorkspacePanel()"' in HTML or
'onclick="toggleWorkspacePanel()"' in HTML
)
assert has_close, \
"toggleMobileFiles() must be wired to a button to close the workspace panel on mobile"
"closeWorkspacePanel() or toggleWorkspacePanel() must be wired to a button to close the workspace panel on mobile"
def test_toggle_mobile_files_js_defined():

View File

@@ -226,8 +226,8 @@ def test_loadSession_resets_busy_state_for_idle_session(cleanup_test_sessions):
src = (REPO_ROOT / "static/sessions.js").read_text()
# The fix adds explicit S.busy=false in the non-inflight else branch
assert "S.busy=false;" in src, "sessions.js loadSession must set S.busy=false when loading a non-inflight session"
# btnSend must be explicitly re-enabled
assert "$('btnSend').disabled=false;" in src, "sessions.js loadSession must enable btnSend for non-inflight sessions"
# btnSend state must be refreshed via updateSendBtn
assert "updateSendBtn()" in src, "sessions.js loadSession must call updateSendBtn for non-inflight sessions"
def test_done_handler_guards_setbusy_with_inflight_check(cleanup_test_sessions):
@@ -352,12 +352,12 @@ def test_respond_approval_uses_approval_session_id(cleanup_test_sessions):
assert "_approvalSessionId" in fn_body, "respondApproval must read _approvalSessionId, not S.session.session_id"
# ── R11: Activity bar shows cross-session tool status ─────────────────────
# ── R11: Tool progress must not use shared status chrome ──────────────────
def test_tool_status_only_shown_for_current_session(cleanup_test_sessions):
"""R11: The activity bar setStatus() call in the tool SSE handler must only
fire when the user is viewing the session that triggered the tool.
When missing, session A's tool names would appear in session B's activity bar.
"""R11: Tool progress should not drive the global status bar or composer
status. Live tool cards in the current conversation are the authoritative
progress UI, which avoids cross-session status leakage entirely.
"""
src = (REPO_ROOT / "static/messages.js").read_text()
# Sprint 12: handler moved into _wireSSE(source)
@@ -366,14 +366,10 @@ def test_tool_status_only_shown_for_current_session(cleanup_test_sessions):
tool_idx = src.find("es.addEventListener('tool'")
assert tool_idx >= 0
tool_block = src[tool_idx:tool_idx+400]
# setStatus must be inside the activeSid guard, not before it
status_pos = tool_block.find("setStatus(")
guard_pos = tool_block.find("S.session.session_id===activeSid")
assert guard_pos >= 0, "tool handler must guard with activeSid check"
# The guard must appear BEFORE or AROUND the setStatus call
# (status only fires for the current session)
assert status_pos > tool_block.find("activeSid"), \
"setStatus in tool handler must be inside the activeSid guard"
assert "setStatus(" not in tool_block, \
"tool handler should not use the global activity/status bar"
assert "setComposerStatus(" not in tool_block, \
"tool handler should not use composer status for tool progress"
# ── R12: Live tool cards lost on switch-away and switch-back ──────────────

View File

@@ -1,6 +1,6 @@
"""
Sprint 16 Tests: safe HTML rendering in renderMd(), active session styling,
session sidebar polish (SVG icons, overlay actions).
session sidebar polish (SVG icons, dropdown actions).
"""
import html as _html
import pathlib
@@ -676,20 +676,22 @@ def test_sessions_js_has_svg_icons(cleanup_test_sessions):
assert "<svg" in code, "SVG content not found in ICONS"
def test_sessions_js_has_overlay_actions(cleanup_test_sessions):
"""sessions.js must use .session-actions overlay div for action buttons."""
def test_sessions_js_has_dropdown_actions(cleanup_test_sessions):
"""sessions.js must use a single trigger button and dropdown for session actions."""
src = REPO_ROOT / "static" / "sessions.js"
code = src.read_text()
assert "session-actions" in code, ".session-actions overlay not found in sessions.js"
assert "session-actions-trigger" in code, "session action trigger button not found in sessions.js"
assert "session-action-menu" in code, "session action dropdown menu not found in sessions.js"
def test_style_css_has_session_actions_overlay(cleanup_test_sessions):
"""style.css must define .session-actions with position:absolute."""
def test_style_css_has_session_actions_dropdown(cleanup_test_sessions):
"""style.css must define trigger and dropdown styles for session actions."""
src = REPO_ROOT / "static" / "style.css"
code = src.read_text()
assert ".session-actions" in code, ".session-actions not found in style.css"
assert "position:absolute" in code or "position: absolute" in code, \
".session-actions must use position:absolute for overlay"
assert ".session-action-menu" in code, ".session-action-menu not found in style.css"
assert "position:fixed" in code or "position: fixed" in code, \
".session-action-menu must use position:fixed to avoid sidebar clipping"
def test_style_css_active_session_uses_gold(cleanup_test_sessions):

View File

@@ -23,12 +23,12 @@ def test_send_button_present():
assert 'id="btnSend"' in html
def test_send_button_hidden_by_default():
"""btnSend must start hidden (display:none) — only shown when there is content."""
def test_send_button_disabled_by_default():
"""btnSend must start disabled — enabled only when there is content."""
html, _ = get_text("/")
btn_match = re.search(r'id="btnSend"[^>]*>', html)
assert btn_match, "btnSend element not found"
assert 'display:none' in btn_match.group(0)
assert 'disabled' in btn_match.group(0)
def test_send_button_no_text_label():
@@ -264,14 +264,13 @@ def test_update_send_btn_uses_visible_class():
assert 'visible' in fn_body
def test_update_send_btn_uses_display_none():
"""updateSendBtn must hide the button with display:none when no content."""
def test_update_send_btn_uses_disabled():
"""updateSendBtn must disable the button when no content or busy."""
js, _ = get_text("/static/ui.js")
fn_idx = js.find('function updateSendBtn')
fn_end = js.find('\n}', fn_idx) + 2
fn_body = js[fn_idx:fn_end]
assert 'display' in fn_body
assert 'none' in fn_body
assert 'disabled' in fn_body
def test_set_busy_calls_update_send_btn():
@@ -321,14 +320,13 @@ def test_send_button_still_has_send_btn_class():
assert 'class="send-btn"' in html
def test_ui_js_set_busy_still_disables_btn():
"""setBusy must still set btnSend.disabled (not just hide it)."""
def test_ui_js_set_busy_calls_update_send_btn():
"""setBusy must call updateSendBtn to manage button disabled state."""
js, _ = get_text("/static/ui.js")
busy_idx = js.find('function setBusy')
busy_end = js.find('\n}', busy_idx) + 2
busy_body = js[busy_idx:busy_end]
assert "btnSend" in busy_body
assert 'disabled' in busy_body
assert 'updateSendBtn' in busy_body
def test_index_html_attach_button_unchanged():

View File

@@ -226,3 +226,19 @@ class TestOnboardingStatusApiOAuth:
assert data["system"]["setup_state"] in valid, (
f"Unexpected setup_state: {data['system']['setup_state']!r}"
)
# ── Control Center: section reset on close ─────────────────────────────────
def test_control_center_resets_active_section_on_close():
"""Closing the control center must reset _settingsSection to 'conversation'."""
src = open('static/panels.js').read()
assert '_settingsSection' in src, '_settingsSection state variable missing from panels.js'
assert "_settingsSection = 'conversation'" in src or "_settingsSection='conversation'" in src, \
'Control center does not reset section to conversation on close'
def test_control_center_tab_highlight_on_open():
"""Opening the control center must use settings-tabs for section navigation."""
css = open('static/style.css').read()
assert 'settings-tabs' in css, 'settings-tabs CSS class for control center tabs missing from style.css'

View File

@@ -36,6 +36,7 @@ def test_index_html_served():
assert status == 200
assert b"sidebarResize" in raw, "Resize handle not found in HTML"
assert b"cronCreateForm" in raw, "Cron create form not found in HTML"
assert b"btnHermesPanel" in raw, "Hermes control center trigger not found in HTML"
assert b"btnExportJSON" in raw, "Export JSON button not found in HTML"
def test_index_html_file_exists():