diff --git a/CHANGELOG.md b/CHANGELOG.md
index dc78bd1..78bb11f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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)
- **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.
diff --git a/static/boot.js b/static/boot.js
index fed5361..24450aa 100644
--- a/static/boot.js
+++ b/static/boot.js
@@ -40,6 +40,8 @@ function _setWorkspacePanelMode(mode){
if(!layout||!panel)return;
_workspacePanelMode=(mode==='browse'||mode==='preview')?mode:'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);
if(_isCompactWorkspaceViewport()){
panel.classList.toggle('mobile-open',open);
@@ -503,6 +505,10 @@ function applyBotName(){
await loadWorkspaceList();
await loadOnboardingWizard();
_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');
if(saved){
try{await loadSession(saved);syncWorkspacePanelState();await renderSessionList();if(typeof startGatewaySSE==='function')startGatewaySSE();await checkInflightOnBoot(saved);return;}
diff --git a/static/index.html b/static/index.html
index 1db8121..4b1b941 100644
--- a/static/index.html
+++ b/static/index.html
@@ -526,7 +526,7 @@
System
Instance version and access controls.
- v0.50.1
+ v0.50.2
diff --git a/tests/test_sprint37.py b/tests/test_sprint37.py
new file mode 100644
index 0000000..4f60b52
--- /dev/null
+++ b/tests/test_sprint37.py
@@ -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"