feat: workspace panel state persists across refreshes (#321)
localStorage key hermes-webui-workspace-panel saves open/closed on every state change; restored on boot before syncWorkspacePanelState(). 7 new tests, 753 total.
This commit is contained in:
@@ -6,6 +6,12 @@
|
|||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## [v0.50.2] Workspace panel state persists across refreshes
|
||||||
|
|
||||||
|
- **Workspace panel open/closed persists** (localStorage key `hermes-webui-workspace-panel`): Once you open the workspace/files pane, it stays open after a page refresh. Closing it explicitly saves the closed state, which also survives a refresh. The restore happens in the boot sequence before the first render, so there is no flash of the wrong state. Works for both desktop and mobile.
|
||||||
|
- State is stored as `'open'` or `'closed'` — `'open'` restores as `'browse'` mode; any preview state is re-evaluated normally.
|
||||||
|
- 7 new tests in `tests/test_sprint37.py`; 753 tests total (up from 746)
|
||||||
|
|
||||||
## [v0.50.1] Mobile Enter key inserts newline (PR #315, fixes #269)
|
## [v0.50.1] Mobile Enter key inserts newline (PR #315, fixes #269)
|
||||||
|
|
||||||
- **Enter inserts newline on mobile** (closes #269): On touch-primary devices (detected via `matchMedia('(pointer:coarse)')`), the Enter key now inserts a newline instead of sending. Users send via the Send button, which is always visible on mobile. Desktop behavior is unchanged — Enter sends, Shift+Enter inserts a newline.
|
- **Enter inserts newline on mobile** (closes #269): On touch-primary devices (detected via `matchMedia('(pointer:coarse)')`), the Enter key now inserts a newline instead of sending. Users send via the Send button, which is always visible on mobile. Desktop behavior is unchanged — Enter sends, Shift+Enter inserts a newline.
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ function _setWorkspacePanelMode(mode){
|
|||||||
if(!layout||!panel)return;
|
if(!layout||!panel)return;
|
||||||
_workspacePanelMode=(mode==='browse'||mode==='preview')?mode:'closed';
|
_workspacePanelMode=(mode==='browse'||mode==='preview')?mode:'closed';
|
||||||
const open=_workspacePanelMode!=='closed';
|
const open=_workspacePanelMode!=='closed';
|
||||||
|
// Persist open/closed across refreshes (browse/preview → open; closed → closed)
|
||||||
|
localStorage.setItem('hermes-webui-workspace-panel', open ? 'open' : 'closed');
|
||||||
layout.classList.toggle('workspace-panel-collapsed',!open);
|
layout.classList.toggle('workspace-panel-collapsed',!open);
|
||||||
if(_isCompactWorkspaceViewport()){
|
if(_isCompactWorkspaceViewport()){
|
||||||
panel.classList.toggle('mobile-open',open);
|
panel.classList.toggle('mobile-open',open);
|
||||||
@@ -503,6 +505,10 @@ function applyBotName(){
|
|||||||
await loadWorkspaceList();
|
await loadWorkspaceList();
|
||||||
await loadOnboardingWizard();
|
await loadOnboardingWizard();
|
||||||
_initResizePanels();
|
_initResizePanels();
|
||||||
|
// Restore workspace panel open/closed state from last visit
|
||||||
|
if(localStorage.getItem('hermes-webui-workspace-panel')==='open'){
|
||||||
|
_workspacePanelMode='browse';
|
||||||
|
}
|
||||||
const saved=localStorage.getItem('hermes-webui-session');
|
const saved=localStorage.getItem('hermes-webui-session');
|
||||||
if(saved){
|
if(saved){
|
||||||
try{await loadSession(saved);syncWorkspacePanelState();await renderSessionList();if(typeof startGatewaySSE==='function')startGatewaySSE();await checkInflightOnBoot(saved);return;}
|
try{await loadSession(saved);syncWorkspacePanelState();await renderSessionList();if(typeof startGatewaySSE==='function')startGatewaySSE();await checkInflightOnBoot(saved);return;}
|
||||||
|
|||||||
@@ -526,7 +526,7 @@
|
|||||||
<div class="settings-section-title">System</div>
|
<div class="settings-section-title">System</div>
|
||||||
<div class="settings-section-meta">Instance version and access controls.</div>
|
<div class="settings-section-meta">Instance version and access controls.</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="settings-version-badge">v0.50.1</span>
|
<span class="settings-version-badge">v0.50.2</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-field" style="border-top:1px solid var(--border);padding-top:12px;margin-top:8px">
|
<div class="settings-field" style="border-top:1px solid var(--border);padding-top:12px;margin-top:8px">
|
||||||
<label for="settingsPassword" data-i18n="settings_label_password">Access Password</label>
|
<label for="settingsPassword" data-i18n="settings_label_password">Access Password</label>
|
||||||
|
|||||||
82
tests/test_sprint37.py
Normal file
82
tests/test_sprint37.py
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
"""
|
||||||
|
Sprint 37 Tests: Workspace panel open/closed state persists across refreshes via localStorage.
|
||||||
|
"""
|
||||||
|
import pathlib
|
||||||
|
import re
|
||||||
|
|
||||||
|
REPO_ROOT = pathlib.Path(__file__).parent.parent
|
||||||
|
BOOT_JS = (REPO_ROOT / "static" / "boot.js").read_text()
|
||||||
|
HTML = (REPO_ROOT / "static" / "index.html").read_text()
|
||||||
|
|
||||||
|
|
||||||
|
# ── Persistence: save on change ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_workspace_panel_saves_to_localstorage():
|
||||||
|
"""_setWorkspacePanelMode must call localStorage.setItem with hermes-webui-workspace-panel."""
|
||||||
|
assert "hermes-webui-workspace-panel" in BOOT_JS, \
|
||||||
|
"boot.js must use localStorage key 'hermes-webui-workspace-panel' to persist panel state"
|
||||||
|
|
||||||
|
|
||||||
|
def test_workspace_panel_save_inside_set_mode():
|
||||||
|
"""localStorage.setItem for panel state must live inside _setWorkspacePanelMode."""
|
||||||
|
fn_idx = BOOT_JS.find("function _setWorkspacePanelMode(")
|
||||||
|
fn_end = BOOT_JS.find("\n}", fn_idx) + 2
|
||||||
|
fn_body = BOOT_JS[fn_idx:fn_end]
|
||||||
|
assert "hermes-webui-workspace-panel" in fn_body, \
|
||||||
|
"localStorage save must be inside _setWorkspacePanelMode so every state change is captured"
|
||||||
|
|
||||||
|
|
||||||
|
def test_workspace_panel_saves_open_value():
|
||||||
|
"""When the panel is open, localStorage must be set to 'open'."""
|
||||||
|
fn_idx = BOOT_JS.find("function _setWorkspacePanelMode(")
|
||||||
|
fn_end = BOOT_JS.find("\n}", fn_idx) + 2
|
||||||
|
fn_body = BOOT_JS[fn_idx:fn_end]
|
||||||
|
assert "'open'" in fn_body or '"open"' in fn_body, \
|
||||||
|
"_setWorkspacePanelMode must store 'open' for an open panel state"
|
||||||
|
|
||||||
|
|
||||||
|
def test_workspace_panel_saves_closed_value():
|
||||||
|
"""When the panel is closed, localStorage must be set to 'closed'."""
|
||||||
|
fn_idx = BOOT_JS.find("function _setWorkspacePanelMode(")
|
||||||
|
fn_end = BOOT_JS.find("\n}", fn_idx) + 2
|
||||||
|
fn_body = BOOT_JS[fn_idx:fn_end]
|
||||||
|
assert "'closed'" in fn_body or '"closed"' in fn_body, \
|
||||||
|
"_setWorkspacePanelMode must store 'closed' for a closed panel state"
|
||||||
|
|
||||||
|
|
||||||
|
# ── Persistence: restore on boot ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_workspace_panel_restored_on_boot():
|
||||||
|
"""Boot IIFE must read hermes-webui-workspace-panel from localStorage and restore the mode."""
|
||||||
|
# Find the boot IIFE (the async IIFE at the bottom of boot.js)
|
||||||
|
iife_idx = BOOT_JS.rfind("(async function")
|
||||||
|
if iife_idx < 0:
|
||||||
|
iife_idx = BOOT_JS.rfind("(async()=>{")
|
||||||
|
iife_body = BOOT_JS[iife_idx:]
|
||||||
|
assert "hermes-webui-workspace-panel" in iife_body, \
|
||||||
|
"Boot IIFE must read 'hermes-webui-workspace-panel' from localStorage to restore panel state on load"
|
||||||
|
|
||||||
|
|
||||||
|
def test_workspace_panel_restore_sets_browse_mode():
|
||||||
|
"""When localStorage says 'open', boot must set _workspacePanelMode to 'browse' before syncing."""
|
||||||
|
iife_idx = BOOT_JS.rfind("(async function")
|
||||||
|
if iife_idx < 0:
|
||||||
|
iife_idx = BOOT_JS.rfind("(async()=>{")
|
||||||
|
iife_body = BOOT_JS[iife_idx:]
|
||||||
|
# The restore block must assign _workspacePanelMode = 'browse'
|
||||||
|
assert "_workspacePanelMode='browse'" in iife_body or "_workspacePanelMode = 'browse'" in iife_body, \
|
||||||
|
"Boot must set _workspacePanelMode='browse' when restoring an open panel"
|
||||||
|
|
||||||
|
|
||||||
|
def test_workspace_panel_restore_before_sync():
|
||||||
|
"""Restore must happen before syncWorkspacePanelState() so the state drives the initial render."""
|
||||||
|
iife_idx = BOOT_JS.rfind("(async function")
|
||||||
|
if iife_idx < 0:
|
||||||
|
iife_idx = BOOT_JS.rfind("(async()=>{")
|
||||||
|
iife_body = BOOT_JS[iife_idx:]
|
||||||
|
restore_pos = iife_body.find("hermes-webui-workspace-panel")
|
||||||
|
sync_pos = iife_body.find("syncWorkspacePanelState()")
|
||||||
|
assert restore_pos >= 0, "restore read must be present in boot IIFE"
|
||||||
|
assert sync_pos >= 0, "syncWorkspacePanelState call must be present in boot IIFE"
|
||||||
|
assert restore_pos < sync_pos, \
|
||||||
|
"Workspace panel restore must happen BEFORE syncWorkspacePanelState() so the correct mode is applied"
|
||||||
Reference in New Issue
Block a user