From b1d687ba229d0adf60137dea66db16cf39f6cbd6 Mon Sep 17 00:00:00 2001 From: Nathan Esquenazi Date: Sat, 4 Apr 2026 01:59:49 +0000 Subject: [PATCH] feat: persist workspace tree expanded state across refreshes Store expanded directory paths in localStorage keyed by workspace path (key: 'hermes-webui-expanded:{workspacePath}'). On root load (loadDir('.')), restore the saved set for the current workspace and pre-fetch dir contents for any restored expanded directories so the tree renders fully on first paint without requiring a second click to expand. Saves on every expand/collapse toggle. Switching workspaces automatically picks up that workspace's own saved state. Per-workspace (not per-session) so the same tree state is shared across sessions using the same workspace, which is the natural expectation. --- static/ui.js | 2 ++ static/workspace.js | 35 ++++++++++++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/static/ui.js b/static/ui.js index 91bc78f..7c7c8fd 100644 --- a/static/ui.js +++ b/static/ui.js @@ -919,9 +919,11 @@ function _renderTreeItems(container, entries, depth){ e.stopPropagation(); if(S._expandedDirs.has(item.path)){ S._expandedDirs.delete(item.path); + if(typeof _saveExpandedDirs==='function')_saveExpandedDirs(); renderFileTree(); }else{ S._expandedDirs.add(item.path); + if(typeof _saveExpandedDirs==='function')_saveExpandedDirs(); // Fetch children if not cached if(!S._dirCache[item.path]){ try{ diff --git a/static/workspace.js b/static/workspace.js index 13eb439..9b432cd 100644 --- a/static/workspace.js +++ b/static/workspace.js @@ -12,13 +12,46 @@ async function api(path,opts={}){ return ct.includes('application/json')?res.json():res.text(); } +// Persist/restore expanded directory state per workspace in localStorage +function _wsExpandKey(){ + const ws=S.session&&S.session.workspace; + return ws?'hermes-webui-expanded:'+ws:null; +} +function _saveExpandedDirs(){ + const key=_wsExpandKey();if(!key)return; + try{localStorage.setItem(key,JSON.stringify([...(S._expandedDirs||new Set())]));}catch(e){} +} +function _restoreExpandedDirs(){ + const key=_wsExpandKey(); + if(!key){S._expandedDirs=new Set();return;} + try{ + const raw=localStorage.getItem(key); + S._expandedDirs=raw?new Set(JSON.parse(raw)):new Set(); + }catch(e){S._expandedDirs=new Set();} +} + async function loadDir(path){ if(!S.session)return; try{ - if(!path||path==='.'){ S._dirCache={}; if(S._expandedDirs)S._expandedDirs=new Set(); } + if(!path||path==='.'){ + S._dirCache={}; + _restoreExpandedDirs(); // restore per-workspace expanded state on root load + } S.currentDir=path||'.'; const data=await api(`/api/list?session_id=${encodeURIComponent(S.session.session_id)}&path=${encodeURIComponent(path)}`); S.entries=data.entries||[];renderBreadcrumb();renderFileTree(); + // Pre-fetch contents of restored expanded dirs so they render without a second click + if(!path||path==='.'){ + for(const dirPath of (S._expandedDirs||[])){ + if(!S._dirCache[dirPath]){ + try{ + const dc=await api(`/api/list?session_id=${encodeURIComponent(S.session.session_id)}&path=${encodeURIComponent(dirPath)}`); + S._dirCache[dirPath]=dc.entries||[]; + }catch(e2){S._dirCache[dirPath]=[];} + } + } + if(S._expandedDirs&&S._expandedDirs.size>0)renderFileTree(); + } if(typeof clearPreview==='function'){ if(typeof _previewDirty!=='undefined'&&_previewDirty){ if(confirm('You have unsaved changes in the preview. Discard and navigate?'))clearPreview();