mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-25 19:20:16 +00:00
fix: add workspace heading root actions
This commit is contained in:
+1
-1
@@ -1118,7 +1118,7 @@
|
||||
<aside class="rightpanel">
|
||||
<div class="resize-handle" id="rightpanelResize"></div>
|
||||
<div class="panel-header">
|
||||
<span>Workspace</span>
|
||||
<span id="workspacePanelHeading" class="workspace-panel-heading" role="button" tabindex="0" title="Workspace root">Workspace</span>
|
||||
<span class="git-badge" id="gitBadge" style="display:none"></span>
|
||||
<div class="panel-actions">
|
||||
<button class="panel-icon-btn has-tooltip has-tooltip--bottom" id="btnCollapseWorkspacePanel" data-tooltip="Hide workspace panel" onclick="toggleWorkspacePanel(false)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="15 18 9 12 15 6"/></svg></button>
|
||||
|
||||
@@ -1160,6 +1160,8 @@
|
||||
@container queries below cut to display:none at hard breakpoints. */
|
||||
.panel-header{padding:12px 16px;border-bottom:1px solid var(--border);font-size:11px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.1em;display:flex;align-items:center;gap:6px;overflow:visible;}
|
||||
.panel-header > span:first-child{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0;flex-shrink:2;}
|
||||
.workspace-panel-heading{cursor:pointer;border-radius:6px;padding:2px 4px;margin:-2px -4px;}
|
||||
.workspace-panel-heading:hover,.workspace-panel-heading:focus{color:var(--text);background:var(--hover-bg);outline:none;}
|
||||
.git-badge{font-size:9px;font-weight:600;color:var(--muted);background:var(--hover-bg);padding:2px 7px;border-radius:4px;letter-spacing:.02em;white-space:nowrap;font-family:'SF Mono',ui-monospace,monospace;flex-shrink:3;overflow:hidden;min-width:0;}
|
||||
.topbar-source-badge{display:inline-flex;align-items:center;margin-left:6px;padding:2px 7px;border-radius:999px;background:var(--accent-bg);color:var(--accent-text);font-size:10px;font-weight:700;letter-spacing:.02em;vertical-align:middle;}
|
||||
.git-badge.dirty{color:var(--accent-text);background:var(--accent-bg);}
|
||||
|
||||
@@ -6103,6 +6103,95 @@ try{S.showHiddenWorkspaceFiles=localStorage.getItem('hermes-workspace-show-hidde
|
||||
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',_syncWorkspaceHiddenToggle);
|
||||
else _syncWorkspaceHiddenToggle();
|
||||
|
||||
function bindWorkspaceHeadingActions(){
|
||||
const heading=$('workspacePanelHeading');
|
||||
if(!heading||heading.dataset.bound==='1')return;
|
||||
heading.dataset.bound='1';
|
||||
const goRoot=()=>{
|
||||
if(S.session&&S.session.workspace) loadDir('.');
|
||||
};
|
||||
heading.onclick=goRoot;
|
||||
heading.onkeydown=(e)=>{
|
||||
if(e.key==='Enter'||e.key===' '){
|
||||
e.preventDefault();
|
||||
goRoot();
|
||||
}
|
||||
};
|
||||
heading.oncontextmenu=(e)=>{
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if(S.session&&S.session.workspace) _showWorkspaceRootContextMenu(e);
|
||||
};
|
||||
}
|
||||
if(document.readyState==='loading') document.addEventListener('DOMContentLoaded',bindWorkspaceHeadingActions);
|
||||
else bindWorkspaceHeadingActions();
|
||||
|
||||
function _workspaceContextMenuItem(label, onClick, opts={}){
|
||||
const item=document.createElement('div');
|
||||
item.textContent=label;
|
||||
item.style.cssText='padding:7px 14px;cursor:pointer;font-size:13px;color:'+(opts.danger?'var(--error,#e94560)':'var(--text)')+';';
|
||||
item.onmouseenter=()=>item.style.background='var(--hover-bg)';
|
||||
item.onmouseleave=()=>item.style.background='';
|
||||
item.onclick=onClick;
|
||||
return item;
|
||||
}
|
||||
|
||||
function _copyTextWithFallback(text, successMsg, failurePrefix){
|
||||
const done=()=>showToast(successMsg);
|
||||
const fail=(err)=>showToast(failurePrefix+(err&&err.message?err.message:String(err||'')));
|
||||
if(navigator.clipboard&&navigator.clipboard.writeText){
|
||||
return navigator.clipboard.writeText(text).then(done).catch(err=>{
|
||||
const ta=document.createElement('textarea');
|
||||
ta.value=text;
|
||||
ta.style.cssText='position:fixed;left:-9999px;top:-9999px;';
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
let copied=false;
|
||||
try{copied=document.execCommand('copy');}catch(_){}
|
||||
ta.remove();
|
||||
if(copied) done(); else fail(err);
|
||||
});
|
||||
}
|
||||
const ta=document.createElement('textarea');
|
||||
ta.value=text;
|
||||
ta.style.cssText='position:fixed;left:-9999px;top:-9999px;';
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
let copied=false;
|
||||
try{copied=document.execCommand('copy');}catch(err){ta.remove();fail(err);return Promise.resolve();}
|
||||
ta.remove();
|
||||
if(copied) done(); else fail('clipboard unavailable');
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
function _showWorkspaceRootContextMenu(e){
|
||||
document.querySelectorAll('.file-ctx-menu').forEach(el=>el.remove());
|
||||
const menu=document.createElement('div');
|
||||
menu.className='file-ctx-menu workspace-root-ctx-menu';
|
||||
menu.style.cssText='position:fixed;background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:6px 0;z-index:9999;min-width:160px;box-shadow:0 4px 16px rgba(0,0,0,.35);';
|
||||
const vw=window.innerWidth,vh=window.innerHeight;
|
||||
menu.style.left=(e.clientX+160>vw?e.clientX-170:e.clientX)+'px';
|
||||
menu.style.top=(e.clientY+80>vh?e.clientY-80:e.clientY)+'px';
|
||||
|
||||
menu.appendChild(_workspaceContextMenuItem(t('reveal_in_finder'),async()=>{
|
||||
menu.remove();
|
||||
try{await api('/api/file/reveal',{method:'POST',body:JSON.stringify({session_id:S.session.session_id,path:'.'})});}
|
||||
catch(err){showToast(t('reveal_failed')+(err.message||err));}
|
||||
}));
|
||||
|
||||
menu.appendChild(_workspaceContextMenuItem(t('copy_file_path'),async()=>{
|
||||
menu.remove();
|
||||
try{
|
||||
const r=await api('/api/file/path',{method:'POST',body:JSON.stringify({session_id:S.session.session_id,path:'.'})});
|
||||
await _copyTextWithFallback((r&&r.path)||'.',t('path_copied'),t('path_copy_failed'));
|
||||
}catch(err){showToast(t('path_copy_failed')+(err.message||err));}
|
||||
}));
|
||||
|
||||
document.body.appendChild(menu);
|
||||
const dismiss=()=>{menu.remove();document.removeEventListener('click',dismiss);};
|
||||
setTimeout(()=>document.addEventListener('click',dismiss),0);
|
||||
}
|
||||
|
||||
// Track expanded directories for tree view
|
||||
if(!S._expandedDirs) S._expandedDirs=new Set();
|
||||
// Cache of fetched directory contents: path -> entries[]
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
INDEX_HTML = (ROOT / "static" / "index.html").read_text(encoding="utf-8")
|
||||
UI_JS = (ROOT / "static" / "ui.js").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_workspace_heading_is_interactive_root_control():
|
||||
"""The WORKSPACE panel heading should behave like the breadcrumb root."""
|
||||
assert 'id="workspacePanelHeading"' in INDEX_HTML
|
||||
assert "bindWorkspaceHeadingActions" in UI_JS
|
||||
assert "loadDir('.')" in UI_JS
|
||||
|
||||
|
||||
def test_workspace_heading_context_menu_exposes_root_reveal_and_copy_path():
|
||||
"""Right-clicking the heading should expose root-scoped Reveal and Copy path actions."""
|
||||
assert "_showWorkspaceRootContextMenu" in UI_JS
|
||||
assert "'/api/file/reveal'" in UI_JS
|
||||
assert "'/api/file/path'" in UI_JS
|
||||
assert "path:'.'" in UI_JS.replace(" ", "")
|
||||
assert "copy_file_path" in UI_JS
|
||||
assert "reveal_in_finder" in UI_JS
|
||||
Reference in New Issue
Block a user