fix: add workspace heading root actions

This commit is contained in:
ai-ag2026
2026-05-07 18:54:22 +02:00
committed by ai-ag2026
parent ef3d34527a
commit 72982db94b
4 changed files with 115 additions and 1 deletions
+1 -1
View File
@@ -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>
+2
View File
@@ -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);}
+89
View File
@@ -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