Merge pull request #1853 from nesquena/fix/1793-workspace-prefs-kebab

fix(workspace): move 'Show hidden files' toggle into kebab + accent-dot state indicator (#1793)
This commit is contained in:
nesquena-hermes 2026-05-07 14:19:34 -07:00 committed by GitHub
commit 5005f1c8ba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 319 additions and 13 deletions

View file

@ -312,6 +312,10 @@ const LOCALES = {
workspace_empty_no_path: 'No workspace selected. Set a workspace in Settings \u2192 Workspace to browse files.',
workspace_empty_dir: 'This workspace is empty.',
workspace_show_hidden_files: 'Show hidden files',
workspace_show_hidden_files_desc: 'Include .DS_Store, .git, node_modules, and other hidden / system files in the file tree.',
workspace_hidden_files_visible: 'hidden visible',
workspace_hidden_files_visible_title: 'Hidden files are visible — click for options',
workspace_options: 'Workspace options',
dialog_confirm_title: 'Confirm action',
dialog_prompt_title: 'Enter a value',
dialog_confirm_btn: 'Confirm',
@ -1326,6 +1330,10 @@ const LOCALES = {
workspace_empty_no_path: 'ワークスペースが選択されていません。設定 → ワークスペースで選択してください。',
workspace_empty_dir: 'このワークスペースは空です。',
workspace_show_hidden_files: 'Show hidden files',
workspace_show_hidden_files_desc: 'Include .DS_Store, .git, node_modules, and other hidden / system files in the file tree.',
workspace_hidden_files_visible: 'hidden visible',
workspace_hidden_files_visible_title: 'Hidden files are visible — click for options',
workspace_options: 'Workspace options',
dialog_confirm_title: '操作の確認',
dialog_prompt_title: '値を入力',
dialog_confirm_btn: '確認',
@ -2239,6 +2247,10 @@ const LOCALES = {
settings_autosave_retry: 'Повторить',
workspace_empty_dir: 'Это рабочее пространство пусто.',
workspace_show_hidden_files: 'Show hidden files',
workspace_show_hidden_files_desc: 'Include .DS_Store, .git, node_modules, and other hidden / system files in the file tree.',
workspace_hidden_files_visible: 'hidden visible',
workspace_hidden_files_visible_title: 'Hidden files are visible — click for options',
workspace_options: 'Workspace options',
workspace_empty_no_path: 'Рабочее пространство не выбрано. Настройте его в Настройки → Рабочее пространство.',
available_personalities: 'Доступные личности:',
personality_switch_hint: '\n\nИспользуйте `/personality <name>` для переключения или `/personality none` для сброса.',
@ -3196,6 +3208,10 @@ const LOCALES = {
workspace_empty_no_path: 'No hay espacio de trabajo seleccionado. Configure un espacio de trabajo en Ajustes \u2192 Workspace para explorar archivos.',
workspace_empty_dir: 'Este espacio de trabajo está vacío.',
workspace_show_hidden_files: 'Show hidden files',
workspace_show_hidden_files_desc: 'Include .DS_Store, .git, node_modules, and other hidden / system files in the file tree.',
workspace_hidden_files_visible: 'hidden visible',
workspace_hidden_files_visible_title: 'Hidden files are visible — click for options',
workspace_options: 'Workspace options',
// workspace.js
unsaved_confirm: 'Tienes cambios sin guardar en la vista previa. ¿Descartar y navegar?',
save: 'Guardar',
@ -4125,6 +4141,10 @@ const LOCALES = {
workspace_empty_no_path: 'Kein Workspace ausgewählt. Wähle einen Workspace unter Einstellungen \u2192 Workspace, um Dateien zu durchsuchen.',
workspace_empty_dir: 'Dieser Workspace ist leer.',
workspace_show_hidden_files: 'Versteckte Dateien anzeigen',
workspace_show_hidden_files_desc: '.DS_Store, .git, node_modules und weitere versteckte / Systemdateien im Dateibaum anzeigen.',
workspace_hidden_files_visible: 'versteckt sichtbar',
workspace_hidden_files_visible_title: 'Versteckte Dateien sind sichtbar — Klicken für Optionen',
workspace_options: 'Arbeitsbereich-Optionen',
dialog_confirm_title: 'Aktion bestätigen',
dialog_prompt_title: 'Wert eingeben',
dialog_confirm_btn: 'Bestätigen',
@ -5069,6 +5089,10 @@ const LOCALES = {
workspace_empty_no_path: '未选择工作区。请在 设置 → 工作区 中设置工作区以浏览文件。',
workspace_empty_dir: '此工作区为空。',
workspace_show_hidden_files: 'Show hidden files',
workspace_show_hidden_files_desc: 'Include .DS_Store, .git, node_modules, and other hidden / system files in the file tree.',
workspace_hidden_files_visible: 'hidden visible',
workspace_hidden_files_visible_title: 'Hidden files are visible — click for options',
workspace_options: 'Workspace options',
no_personalities: '\u6ca1\u6709\u627e\u5230\u4eba\u8bbe\uff08\u53ef\u6dfb\u52a0\u5230 ~/.hermes/personalities/\uff09',
available_personalities: '\u53ef\u7528\u4eba\u8bbe\uff1a',
personality_switch_hint: '\n\n\u4f7f\u7528 `/personality <name>` \u5207\u6362\uff0c\u6216\u7528 `/personality none` \u6e05\u7a7a\u3002',
@ -5972,6 +5996,10 @@ const LOCALES = {
workspace_empty_no_path: '未選擇工作區。請在 設定 → 工作區 中設定工作區以瀏覽檔案。',
workspace_empty_dir: '此工作區為空。',
workspace_show_hidden_files: 'Show hidden files',
workspace_show_hidden_files_desc: 'Include .DS_Store, .git, node_modules, and other hidden / system files in the file tree.',
workspace_hidden_files_visible: 'hidden visible',
workspace_hidden_files_visible_title: 'Hidden files are visible — click for options',
workspace_options: 'Workspace options',
no_personalities: '\u6c92\u6709\u627e\u5230\u4eba\u8a2d\uff08\u53ef\u6dfb\u52a0\u5230 ~/.hermes/personalities/\uff09',
available_personalities: '\u53ef\u7528\u4eba\u8a2d\uff1a',
personality_switch_hint: '\n\n\u4f7f\u7528 `/personality <name>` \u5207\u63db\uff0c\u6216\u7528 `/personality none` \u6e05\u7a7a\u3002',
@ -7025,6 +7053,10 @@ const LOCALES = {
workspace_empty_no_path: 'Nenhum workspace selecionado. Configure em Configurações → Workspace.',
workspace_empty_dir: 'Este workspace está vazio.',
workspace_show_hidden_files: 'Show hidden files',
workspace_show_hidden_files_desc: 'Include .DS_Store, .git, node_modules, and other hidden / system files in the file tree.',
workspace_hidden_files_visible: 'hidden visible',
workspace_hidden_files_visible_title: 'Hidden files are visible — click for options',
workspace_options: 'Workspace options',
dialog_confirm_title: 'Confirmar ação',
dialog_prompt_title: 'Digite um valor',
dialog_confirm_btn: 'Confirmar',
@ -7922,6 +7954,10 @@ const LOCALES = {
workspace_empty_no_path: 'No workspace selected. Set a workspace in Settings \u2192 Workspace to browse files.',
workspace_empty_dir: 'This workspace is empty.',
workspace_show_hidden_files: 'Show hidden files',
workspace_show_hidden_files_desc: 'Include .DS_Store, .git, node_modules, and other hidden / system files in the file tree.',
workspace_hidden_files_visible: 'hidden visible',
workspace_hidden_files_visible_title: 'Hidden files are visible — click for options',
workspace_options: 'Workspace options',
dialog_confirm_title: 'Confirm action',
dialog_prompt_title: 'Enter a value',
dialog_confirm_btn: 'Confirm',

View file

@ -1118,7 +1118,7 @@
<aside class="rightpanel">
<div class="resize-handle" id="rightpanelResize"></div>
<div class="panel-header">
<span id="workspacePanelHeading" class="workspace-panel-heading" role="button" tabindex="0" title="Workspace root">Workspace</span>
<span id="workspacePanelHeading" class="workspace-panel-heading" role="button" tabindex="0" title="Workspace root">Workspace</span><span id="workspaceHiddenIndicator" class="workspace-hidden-indicator" data-i18n-title="workspace_hidden_files_visible_title" title="Hidden files are visible — click for options" hidden onclick="toggleWorkspacePrefsMenu(event)"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg><span data-i18n="workspace_hidden_files_visible">hidden visible</span></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>
@ -1126,15 +1126,12 @@
<button class="panel-icon-btn has-tooltip has-tooltip--bottom" id="btnNewFile" data-tooltip="New file" onclick="promptNewFile()"><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"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg></button>
<button class="panel-icon-btn has-tooltip has-tooltip--bottom" id="btnNewFolder" data-tooltip="New folder" onclick="promptNewFolder()"><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"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg></button>
<button class="panel-icon-btn has-tooltip has-tooltip--bottom" id="btnRefreshPanel" data-tooltip="Refresh" onclick="if(S.session)loadDir(S.currentDir)"><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="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg></button>
<button class="panel-icon-btn has-tooltip has-tooltip--bottom" id="btnWorkspacePrefs" data-tooltip="Workspace options" data-i18n-title="workspace_options" aria-haspopup="true" aria-expanded="false" onclick="toggleWorkspacePrefsMenu(event)"><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"><circle cx="12" cy="5" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="12" cy="19" r="1.5"/></svg><span class="workspace-prefs-dot" id="workspacePrefsDot" hidden></span></button>
<button class="panel-icon-btn close-preview has-tooltip has-tooltip--bottom" id="btnClearPreview" data-tooltip="Close preview"><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"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>
<button class="panel-icon-btn mobile-close-btn" onclick="handleWorkspaceClose()" title="Close" aria-label="Close workspace panel">×</button>
</div>
</div>
<div class="breadcrumb-bar" id="breadcrumbBar" style="display:none"></div>
<label class="workspace-hidden-toggle" title="Show hidden and system files">
<input type="checkbox" id="workspaceShowHiddenFiles" onchange="toggleWorkspaceHiddenFiles(this.checked)">
<span data-i18n="workspace_show_hidden_files">Show hidden files</span>
</label>
<div class="file-tree" id="fileTree"></div>
<div id="wsEmptyState" style="display:none;flex:1;align-items:center;justify-content:center;padding:24px 16px;text-align:center;color:var(--muted);font-size:12px;line-height:1.6"></div>
<div class="preview-area" id="previewArea">

View file

@ -1187,9 +1187,39 @@
.close-preview{cursor:pointer;opacity:.6;}.close-preview:hover{opacity:1;}
/* Breadcrumb navigation */
.breadcrumb-bar{display:flex;align-items:center;gap:2px;padding:6px 12px;font-size:12px;border-bottom:1px solid var(--border);flex-shrink:0;overflow:hidden;white-space:nowrap;}
.workspace-hidden-toggle{display:flex;align-items:center;gap:7px;padding:6px 12px;border-bottom:1px solid var(--border);color:var(--muted);font-size:11px;line-height:1.2;user-select:none;cursor:pointer;}
.workspace-hidden-toggle:hover{color:var(--text);background:var(--hover-bg);}
.workspace-hidden-toggle input{width:13px;height:13px;margin:0;accent-color:var(--accent-text);}
/* ── Workspace prefs kebab menu (#1793) ─────────────────────────── */
/* Replaces the always-visible "Show hidden files" inline row that used
to sit below the breadcrumb. The toggle now lives behind the kebab
button in the panel-actions row; the small indicator next to the
panel heading flags the non-default "hidden files visible" state. */
.workspace-hidden-indicator{display:inline-flex;align-items:center;gap:4px;margin-left:6px;padding:2px 7px 2px 6px;background:var(--hover-bg);color:var(--muted);font-size:10px;line-height:1.3;border-radius:8px;cursor:pointer;user-select:none;font-weight:600;letter-spacing:.02em;text-transform:none;flex-shrink:0;transition:background .12s,color .12s;border:1px solid var(--border);}
.workspace-hidden-indicator:hover{background:var(--accent-bg);color:var(--text);border-color:var(--accent-bg-strong,var(--border2));}
.workspace-hidden-indicator[hidden]{display:none;}
.workspace-hidden-indicator svg{flex-shrink:0;opacity:.85;}
/* On narrow workspace panels (the default 300px width), hide the chip
entirely — the kebab dot (rendered absolute over the kebab icon) is
enough of a "non-default state" signal and costs zero horizontal
space. The tooltip on the kebab itself still labels what's going on.
On wider panels (resized by the user, or some future layout that
gives the panel more room), the full chip with text appears alongside
the heading. Uses the existing `rightpanel` container declared on
.rightpanel. */
@container rightpanel (max-width: 420px){
.workspace-hidden-indicator{display:none!important;}
}
/* Small accent dot on the kebab button when a non-default pref is on */
#btnWorkspacePrefs{position:relative;}
#btnWorkspacePrefs .workspace-prefs-dot{position:absolute;top:3px;right:3px;width:6px;height:6px;border-radius:50%;background:var(--accent-text);box-shadow:0 0 0 1.5px var(--surface);pointer-events:none;}
#btnWorkspacePrefs .workspace-prefs-dot[hidden]{display:none;}
#btnWorkspacePrefs.active{background:var(--accent-bg);color:var(--text);}
/* Floating menu — pattern shared with .session-action-menu but scoped */
.workspace-prefs-menu{display:block;position:fixed;left:0;top:0;min-width:240px;max-width:min(280px,calc(100vw - 16px));background:var(--surface);border:1px solid var(--border2);border-radius:10px;box-shadow:0 8px 28px rgba(0,0,0,.32);z-index:999;overflow:hidden;}
.workspace-prefs-item{display:flex;align-items:flex-start;gap:10px;padding:10px 14px;color:var(--text);cursor:pointer;font-size:13px;line-height:1.35;user-select:none;transition:background .12s;}
.workspace-prefs-item:hover{background:var(--hover-bg);}
.workspace-prefs-item input{flex-shrink:0;width:14px;height:14px;margin:2px 0 0;accent-color:var(--accent-text);cursor:pointer;}
.workspace-prefs-item .workspace-prefs-copy{display:flex;flex-direction:column;gap:2px;min-width:0;}
.workspace-prefs-item .workspace-prefs-name{font-weight:500;}
.workspace-prefs-item .workspace-prefs-meta{font-size:11px;color:var(--muted);line-height:1.35;opacity:.85;}
.breadcrumb-seg{padding:1px 3px;border-radius:3px;}
.breadcrumb-link{color:var(--muted);cursor:pointer;transition:color .12s;}
.breadcrumb-link:hover{color:var(--text);background:var(--hover-bg);}

View file

@ -6112,6 +6112,19 @@ function _visibleWorkspaceEntries(entries){
function _syncWorkspaceHiddenToggle(){
const el=$('workspaceShowHiddenFiles');
if(el)el.checked=!!S.showHiddenWorkspaceFiles;
// Reflect "hidden files are visible" state on the panel heading + kebab dot,
// so users can see they've flipped a non-default workspace pref without
// having to open the menu. The menu itself stays out of the way otherwise.
const ind=$('workspaceHiddenIndicator');
if(ind){
if(S.showHiddenWorkspaceFiles){ ind.hidden=false; ind.removeAttribute('hidden'); }
else { ind.hidden=true; ind.setAttribute('hidden',''); }
}
const dot=$('workspacePrefsDot');
if(dot){
if(S.showHiddenWorkspaceFiles){ dot.hidden=false; dot.removeAttribute('hidden'); }
else { dot.hidden=true; dot.setAttribute('hidden',''); }
}
}
function toggleWorkspaceHiddenFiles(value){
S.showHiddenWorkspaceFiles=!!value;
@ -6120,6 +6133,95 @@ function toggleWorkspaceHiddenFiles(value){
renderFileTree();
}
try{S.showHiddenWorkspaceFiles=localStorage.getItem('hermes-workspace-show-hidden-files')==='1';}catch(_){}
// ── Workspace preferences kebab menu (#1793 UX refinement) ───────────────
// The "Show hidden files" toggle used to live as a permanent inline row
// below the breadcrumb bar. That ate ~32px of vertical space on every
// panel view (root, subdir, file preview), even though the toggle is a
// set-once preference — most users flip it once or never. Moving the
// control into a kebab dropdown reclaims the space; the small "(hidden
// files visible)" indicator on the heading reflects the non-default state
// so the affordance isn't lost.
let _workspacePrefsMenu = null;
let _workspacePrefsAnchor = null;
function _closeWorkspacePrefsMenu(){
if(_workspacePrefsMenu){ _workspacePrefsMenu.remove(); _workspacePrefsMenu=null; }
if(_workspacePrefsAnchor){
_workspacePrefsAnchor.classList.remove('active');
_workspacePrefsAnchor.setAttribute('aria-expanded','false');
_workspacePrefsAnchor=null;
}
}
function _positionWorkspacePrefsMenu(anchorEl){
if(!_workspacePrefsMenu||!anchorEl) return;
const rect=anchorEl.getBoundingClientRect();
const menuW=Math.min(260, Math.max(220, _workspacePrefsMenu.scrollWidth||220));
let left=rect.right-menuW;
if(left<8) left=8;
if(left+menuW>window.innerWidth-8) left=window.innerWidth-menuW-8;
let top=rect.bottom+6;
const menuH=_workspacePrefsMenu.offsetHeight||0;
if(top+menuH>window.innerHeight-8 && rect.top>menuH+12) top=rect.top-menuH-6;
if(top<8) top=8;
_workspacePrefsMenu.style.left=left+'px';
_workspacePrefsMenu.style.top=top+'px';
}
function _buildWorkspacePrefsMenu(){
const menu=document.createElement('div');
menu.className='workspace-prefs-menu open';
menu.setAttribute('role','menu');
// The checkbox keeps id="workspaceShowHiddenFiles" so existing call
// sites (and the existing test_issue1793_file_tree_cruft_filter test)
// can find it the same way as before. Only the parent container moves.
const labelTxt = (typeof t==='function' ? t('workspace_show_hidden_files') : 'Show hidden files');
const descTxt = (typeof t==='function' ? t('workspace_show_hidden_files_desc') : 'Include .DS_Store, .git, node_modules, and other hidden / system files in the file tree.');
const row=document.createElement('label');
row.className='workspace-prefs-item';
row.setAttribute('role','menuitemcheckbox');
row.innerHTML=
'<input type="checkbox" id="workspaceShowHiddenFiles" '+
'onchange="toggleWorkspaceHiddenFiles(this.checked)">'+
'<span class="workspace-prefs-copy">'+
'<span class="workspace-prefs-name">'+esc(labelTxt)+'</span>'+
'<span class="workspace-prefs-meta">'+esc(descTxt)+'</span>'+
'</span>';
const cb=row.querySelector('input');
if(cb) cb.checked=!!S.showHiddenWorkspaceFiles;
menu.appendChild(row);
return menu;
}
function toggleWorkspacePrefsMenu(e){
if(e&&e.preventDefault) e.preventDefault();
if(e&&e.stopPropagation) e.stopPropagation();
// Anchor preference: the kebab button. The indicator chip can also open
// the same menu (click on "(hidden visible)"), but anchor positioning
// always references the kebab so the menu lands in the same place.
const anchor=$('btnWorkspacePrefs')||(e&&e.currentTarget)||null;
if(_workspacePrefsMenu&&_workspacePrefsAnchor===anchor){ _closeWorkspacePrefsMenu(); return; }
_closeWorkspacePrefsMenu();
const menu=_buildWorkspacePrefsMenu();
document.body.appendChild(menu);
_workspacePrefsMenu=menu;
_workspacePrefsAnchor=anchor;
if(anchor){ anchor.classList.add('active'); anchor.setAttribute('aria-expanded','true'); }
_positionWorkspacePrefsMenu(anchor);
}
document.addEventListener('click',e=>{
if(!_workspacePrefsMenu) return;
if(_workspacePrefsMenu.contains(e.target)) return;
if(_workspacePrefsAnchor&&_workspacePrefsAnchor.contains(e.target)) return;
// Indicator chip is also an opener — clicking it should toggle, not close.
const ind=$('workspaceHiddenIndicator');
if(ind&&ind.contains(e.target)) return;
_closeWorkspacePrefsMenu();
});
document.addEventListener('keydown',e=>{
if(e.key==='Escape'&&_workspacePrefsMenu) _closeWorkspacePrefsMenu();
});
window.addEventListener('resize',()=>{
if(_workspacePrefsMenu&&_workspacePrefsAnchor) _positionWorkspacePrefsMenu(_workspacePrefsAnchor);
});
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',_syncWorkspaceHiddenToggle);
else _syncWorkspaceHiddenToggle();

View file

@ -1,23 +1,47 @@
"""Regression coverage for #1793 — workspace file-tree cruft filter.
Original v0.51.21 work added an inline "Show hidden files" toggle that sat
permanently between the breadcrumb and the file tree, eating ~32px of
vertical space on every panel view (root, subdir, file preview).
Follow-up UX refinement (this commit) moves the toggle behind a kebab
dropdown in the panel-actions row and surfaces the non-default
"hidden-files-visible" state via a small indicator next to the panel
heading. The original filtering behavior is unchanged; only the affordance
shape moved.
"""
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")
STYLE_CSS = (ROOT / "static" / "style.css").read_text(encoding="utf-8")
I18N_JS = (ROOT / "static" / "i18n.js").read_text(encoding="utf-8")
# ── Original filtering behavior (must stay green) ────────────────────────
def test_workspace_panel_has_show_hidden_files_toggle():
"""File-tree cruft must be recoverable via an explicit user toggle."""
assert 'id="workspaceShowHiddenFiles"' in INDEX_HTML
"""File-tree cruft must be recoverable via an explicit user toggle.
The toggle now lives behind the kebab; the checkbox itself is built by
`_buildWorkspacePrefsMenu` in ui.js (so it's literally referenced there
by id), but the existing call site in i18n still resolves the localized
label.
"""
assert "toggleWorkspaceHiddenFiles" in UI_JS
assert 'id="workspaceShowHiddenFiles"' in UI_JS # built dynamically; id preserved
assert "workspace_show_hidden_files" in I18N_JS
def test_file_tree_filters_common_cruft_by_default():
"""macOS/Windows/VCS/cache noise should not render by default."""
assert "WORKSPACE_HIDDEN_FILE_NAMES" in UI_JS
for name in [".DS_Store", "Thumbs.db", "Desktop.ini", ".git", "__pycache__", "node_modules"]:
for name in [".DS_Store", "Thumbs.db", "Desktop.ini", ".git",
"__pycache__", "node_modules"]:
assert name in UI_JS
assert "_visibleWorkspaceEntries" in UI_JS
assert "S.showHiddenWorkspaceFiles" in UI_JS
@ -27,5 +51,122 @@ def test_file_tree_filters_common_cruft_by_default():
def test_hidden_file_toggle_invalidates_tree_render_without_refetch():
"""The toggle should re-render cached entries instead of changing workspace state."""
assert "function toggleWorkspaceHiddenFiles" in UI_JS
assert "renderFileTree()" in UI_JS[UI_JS.index("function toggleWorkspaceHiddenFiles"):]
assert "localStorage.setItem('hermes-workspace-show-hidden-files'" in UI_JS
body_start = UI_JS.index("function toggleWorkspaceHiddenFiles")
body_end = UI_JS.index("\n}", body_start)
body = UI_JS[body_start:body_end]
assert "renderFileTree()" in body
assert "localStorage.setItem('hermes-workspace-show-hidden-files'" in body
# ── Kebab-affordance UX refinement ───────────────────────────────────────
def test_no_inline_workspace_hidden_toggle_row():
"""The always-on inline `<label class="workspace-hidden-toggle">` row
must be gone — it ate vertical space below the breadcrumb on every
panel view. Toggle now lives behind the kebab.
"""
assert "workspace-hidden-toggle" not in INDEX_HTML, (
"inline hidden-files row should have been removed in favor of the "
"kebab menu (#1793 follow-up)"
)
# CSS for the inline row should also be gone — leaving stale rules
# invites future drift where someone re-adds the row and it picks up
# accidental styling.
assert ".workspace-hidden-toggle" not in STYLE_CSS
def test_panel_actions_row_has_workspace_prefs_kebab():
"""A kebab button (`btnWorkspacePrefs`) must exist in the workspace
panel actions row to expose the menu.
"""
assert 'id="btnWorkspacePrefs"' in INDEX_HTML
assert 'onclick="toggleWorkspacePrefsMenu(event)"' in INDEX_HTML
# Tooltip is i18n-aware
assert 'data-i18n-title="workspace_options"' in INDEX_HTML
# Kebab carries an accent dot for non-default state
assert 'id="workspacePrefsDot"' in INDEX_HTML
def test_panel_heading_has_hidden_files_indicator():
"""The non-default "hidden files visible" state must surface as a small
indicator next to the WORKSPACE heading so users don't forget they
flipped the pref. Hidden by default via the `hidden` attribute.
"""
assert 'id="workspaceHiddenIndicator"' in INDEX_HTML
# The indicator opens the same menu when clicked (no separate code path)
block = INDEX_HTML[INDEX_HTML.index('id="workspaceHiddenIndicator"'):]
block = block[: block.index("</span>") + 7]
assert "toggleWorkspacePrefsMenu" in block
# Default-hidden so the chip doesn't clutter normal state
assert " hidden " in block or block.rstrip().endswith("hidden")
def test_kebab_menu_javascript_exists():
"""The dropdown must be self-contained: open/close/position handlers
follow the canonical floating-menu pattern from
`_openSessionActionMenu`.
"""
assert "function toggleWorkspacePrefsMenu" in UI_JS
assert "function _buildWorkspacePrefsMenu" in UI_JS
assert "function _closeWorkspacePrefsMenu" in UI_JS
assert "function _positionWorkspacePrefsMenu" in UI_JS
# Built menu still contains the canonical input id so existing call
# sites and the toggle test above keep working.
build_start = UI_JS.index("function _buildWorkspacePrefsMenu")
build_end = UI_JS.index("\n}", build_start)
build_body = UI_JS[build_start:build_end]
assert 'id="workspaceShowHiddenFiles"' in build_body
def test_kebab_menu_closes_on_escape_and_outside_click():
"""Standard keyboard / click-out close behavior."""
# Escape closes
assert "Escape" in UI_JS and "_closeWorkspacePrefsMenu" in UI_JS
# Outside-click close listener
assert "_workspacePrefsMenu" in UI_JS
assert "if(_workspacePrefsMenu.contains(e.target)) return" in UI_JS
def test_indicator_reflects_localStorage_state_on_load():
"""`_syncWorkspaceHiddenToggle` must drive both the dropdown checkbox
AND the indicator/dot so a page reload with the pref ON shows the
"hidden visible" indicator without the user having to open the menu.
"""
sync_start = UI_JS.index("function _syncWorkspaceHiddenToggle")
sync_end = UI_JS.index("\n}", sync_start)
body = UI_JS[sync_start:sync_end]
assert "workspaceHiddenIndicator" in body
assert "workspacePrefsDot" in body
# Drives the existing checkbox if it's mounted
assert "workspaceShowHiddenFiles" in body
def test_kebab_menu_styles_replace_inline_row():
"""CSS must define the kebab dot, indicator, and floating menu — but
not the legacy inline-row styling (the test above pins removal).
"""
assert ".workspace-prefs-menu{" in STYLE_CSS
assert ".workspace-prefs-item{" in STYLE_CSS
assert ".workspace-hidden-indicator{" in STYLE_CSS
assert "#btnWorkspacePrefs" in STYLE_CSS
def test_new_i18n_keys_present_in_all_locales():
"""The new copy must exist in every locale block so the kebab menu
description and indicator chip don't render `undefined` in non-en
sessions.
"""
# Total locale blocks today: 9 (en, ja, ru, es, de, zh, zh-Hant, pt, ko)
n_locales = I18N_JS.count("workspace_show_hidden_files:")
assert n_locales >= 8, f"unexpected locale count: {n_locales}"
for key in (
"workspace_show_hidden_files_desc:",
"workspace_hidden_files_visible:",
"workspace_hidden_files_visible_title:",
"workspace_options:",
):
assert I18N_JS.count(key) == n_locales, (
f"key {key!r} missing in some locales (expected {n_locales}, "
f"got {I18N_JS.count(key)})"
)