refactor(ui): three-column layout with left rail + main-view migration (#899)

refactor(ui): three-column layout with left rail + main-view migration (#899)

Unifies the shell into a three-column layout (rail + sidebar + main) matching the
hermes-desktop reference, and migrates every per-item detail/edit surface into a
shared main-view canvas with consistent headers, empty states, and action buttons.

Changes:
- New desktop-only left rail (48px) with 8 nav tabs (chat/tasks/skills/memory/workspaces/profiles/todos/settings)
- Persistent app titlebar (replaces per-chat topbar), active conversation title shown
- All panel detail/create/edit views migrated to #mainSkills, #mainTasks, #mainSettings, #mainWorkspaces, #mainProfiles, #mainMemory
- Settings moved out of modal into main-view page; ESC closes it
- YAML frontmatter rendered in collapsible <details> block in skill detail
- Toasts repositioned from bottom-center to top-right with theme-aware success/error/warning/info variants
- Composer workspace chip split into two-button group: files-icon toggles file panel, label opens workspace picker
- .settings-menu → .side-menu / .side-menu-item (generalised, shared by memory and settings panels)
- i18n: ~25 new keys across en/ru/es/de/zh/zh-Hant for all new form labels, placeholders, and empty states
- Mobile: hamburger in titlebar, slide-in sidebar; box-shadow removed from sidebar
- New regression test: tests/test_settings_navigation_and_detail_refresh.py (9 tests)

Co-authored-by: Aron Prins <pwf.aron@gmail.com>
This commit is contained in:
Aron Prins
2026-04-24 18:05:25 +02:00
committed by GitHub
parent 061af78cde
commit 970bc1d3fd
15 changed files with 2310 additions and 862 deletions

View File

@@ -405,8 +405,7 @@ $('importFileInput').onchange=async(e)=>{
if(res.ok&&res.session){
await loadSession(res.session.session_id);
await renderSessionList();
const overlay=$('settingsOverlay');
if(overlay) overlay.style.display='none';
if(_currentPanel==='settings') switchPanel('chat');
showToast(t('session_imported'));
}
}catch(err){
@@ -524,9 +523,8 @@ document.addEventListener('keydown',async e=>{
if(typeof skipOnboarding==='function') skipOnboarding();
return;
}
// Close settings overlay if open
const settingsOverlay=$('settingsOverlay');
if(settingsOverlay&&settingsOverlay.style.display!=='none'){_closeSettingsPanel();return;}
// Close settings panel if active
if(_currentPanel==='settings'){_closeSettingsPanel();return;}
// Close workspace dropdown
closeWsDropdown();
// Clear session search
@@ -648,7 +646,10 @@ function _setResolvedTheme(isDark){
const want=isDark
?'https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-tomorrow.min.css'
:'https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism.min.css';
if(link.href!==want){ link.href=want; }
const wantIntegrity=isDark
?'sha384-wFjoQjtV1y5jVHbt0p35Ui8aV8GVpEZkyF99OXWqP/eNJDU93D3Ugxkoyh6Y2I4A'
:'sha384-rCCjoCPCsizaAAYVoz1Q0CmCTvnctK0JkfCSjx7IIxexTBg+uCKtFYycedUjMyA2';
if(link.href!==want){ link.integrity=wantIntegrity; link.href=want; }
}
function _applyTheme(name){

View File

@@ -248,6 +248,7 @@ const LOCALES = {
tab_workspaces: 'Spaces',
tab_profiles: 'Profiles',
tab_todos: 'Todos',
tab_settings: 'Settings',
new_conversation: 'New conversation',
filter_conversations: 'Filter conversations...',
session_time_unknown: 'Unknown',
@@ -455,6 +456,19 @@ const LOCALES = {
skill_name_required: 'Skill name is required',
skill_updated: 'Skill updated',
skill_created: 'Skill created',
skill_deleted: 'Skill deleted',
skill_delete_confirm: 'Delete skill "{0}"?',
skills_empty_title: 'Select a skill',
skills_empty_sub: 'Pick a skill from the sidebar to view its contents, or create a new one.',
skills_edit: 'Edit',
skills_delete: 'Delete',
skills_back_to: 'Back to {0}',
tasks_empty_title: 'Select a scheduled job',
tasks_empty_sub: 'Pick a job from the sidebar to view its details and runs, or create a new one.',
workspaces_empty_title: 'Select a space',
workspaces_empty_sub: 'Pick a space from the sidebar to view its files and settings, or add a new one.',
profiles_empty_title: 'Select a profile',
profiles_empty_sub: 'Pick an agent profile from the sidebar to view and edit its settings, or create a new one.',
memory_notes_label: 'memory (notes)',
memory_saved: 'Memory saved',
my_notes: 'My Notes',
@@ -470,6 +484,7 @@ const LOCALES = {
workspace_add_path_placeholder: 'Add workspace path (e.g. /home/user/my-project)',
workspace_paths_validated_hint: 'Paths are validated as existing directories before saving.',
workspace_added: 'Workspace added',
workspace_renamed: 'Workspace renamed',
workspace_remove_confirm_title: 'Remove workspace',
workspace_remove_confirm_message: (path) => `Remove "${path}"?`,
workspace_removed: 'Workspace removed',
@@ -519,6 +534,36 @@ const LOCALES = {
disable_auth_failed: 'Failed to disable auth: ',
bg_error_single: (title) => `"${title}" has encountered an error`,
bg_error_multi: (count) => `${count} sessions have encountered an error`,
// skill form
skill_name: 'Name',
skill_category: 'Category',
skill_category_placeholder: 'Optional, e.g. devops',
skill_content: 'SKILL.md content',
skill_content_placeholder: 'YAML frontmatter + markdown body',
skill_rename_not_supported: 'Renaming a skill is not supported. Create a new skill and delete the old one to rename.',
skill_metadata: 'Metadata',
// cron form
cron_name_label: 'Name',
cron_name_placeholder: 'Optional',
cron_schedule_label: 'Schedule',
cron_schedule_hint: "Cron expression or shorthand like 'every 1h'.",
cron_prompt_label: 'Prompt',
cron_deliver_label: 'Deliver output to',
cron_deliver_local: 'Local (save output only)',
cron_skills_label: 'Skills',
cron_skills_placeholder: 'Add skills (optional)…',
cron_skills_edit_hint: 'Skill list is not editable after creation.',
// workspace form
workspace_name_label: 'Name',
workspace_name_placeholder: 'Optional friendly name',
workspace_path_label: 'Path',
workspace_path_required: 'Path is required',
workspace_path_readonly: 'Path cannot be changed. Rename only.',
workspace_new_title: 'New space',
// profile form
profile_name_label: 'Name',
profile_base_url_label: 'Base URL',
profile_api_key_label: 'API key',
},
ru: {
@@ -694,6 +739,7 @@ const LOCALES = {
tab_workspaces: 'Рабочие пространства',
tab_profiles: 'Профили',
tab_todos: 'Список дел',
tab_settings: 'Настройки',
new_conversation: 'Новая беседа',
filter_conversations: 'Фильтр бесед...',
session_time_unknown: 'Неизвестно',
@@ -939,6 +985,7 @@ const LOCALES = {
workspace_add_path_placeholder: 'Добавьте путь к рабочему пространству (например, /Users/you/project)',
workspace_paths_validated_hint: 'Перед сохранением пути проверяются на существование.',
workspace_added: 'Рабочее пространство добавлено',
workspace_renamed: 'Рабочее пространство переименовано',
workspace_remove_confirm_title: 'Удалить рабочее пространство',
workspace_remove_confirm_message: (path) => `Удалить «${path}»?`,
workspace_removed: 'Рабочее пространство удалено',
@@ -1006,6 +1053,49 @@ const LOCALES = {
disable_auth_failed: 'Не удалось отключить авторизацию: ',
bg_error_single: (title) => `В "${title}" возникла ошибка`,
bg_error_multi: (count) => `${count} сеансов столкнулись с ошибкой`,
skill_deleted: 'Навык удалён',
skill_delete_confirm: 'Удалить навык "{0}"?',
skills_empty_title: 'Выберите навык',
skills_empty_sub: 'Выберите навык на боковой панели, чтобы просмотреть его содержимое, или создайте новый.',
skills_edit: 'Редактировать',
skills_delete: 'Удалить',
skills_back_to: 'Назад к {0}',
tasks_empty_title: 'Выберите запланированное задание',
tasks_empty_sub: 'Выберите задание на боковой панели, чтобы просмотреть его детали и запуски, или создайте новое.',
workspaces_empty_title: 'Выберите пространство',
workspaces_empty_sub: 'Выберите пространство на боковой панели, чтобы просмотреть его файлы и настройки, или добавьте новое.',
profiles_empty_title: 'Выберите профиль',
profiles_empty_sub: 'Выберите профиль агента на боковой панели, чтобы просмотреть и изменить его настройки, или создайте новый.',
// skill form
skill_name: 'Имя',
skill_category: 'Категория',
skill_category_placeholder: 'Необязательно, например devops',
skill_content: 'Содержимое SKILL.md',
skill_content_placeholder: 'YAML-заголовок + тело markdown',
skill_rename_not_supported: 'Переименование навыка не поддерживается. Создайте новый навык и удалите старый.',
skill_metadata: 'Метаданные',
// cron form
cron_name_label: 'Имя',
cron_name_placeholder: 'Необязательно',
cron_schedule_label: 'Расписание',
cron_schedule_hint: "Cron-выражение или сокращение, например 'every 1h'.",
cron_prompt_label: 'Запрос',
cron_deliver_label: 'Доставлять вывод',
cron_deliver_local: 'Локально (только сохранение)',
cron_skills_label: 'Навыки',
cron_skills_placeholder: 'Добавить навыки (необязательно)…',
cron_skills_edit_hint: 'Список навыков нельзя изменить после создания.',
// workspace form
workspace_name_label: 'Имя',
workspace_name_placeholder: 'Необязательное понятное имя',
workspace_path_label: 'Путь',
workspace_path_required: 'Путь обязателен',
workspace_path_readonly: 'Путь нельзя изменить. Только переименование.',
workspace_new_title: 'Новое пространство',
// profile form
profile_name_label: 'Имя',
profile_base_url_label: 'Базовый URL',
profile_api_key_label: 'API-ключ',
},
es: {
@@ -1188,6 +1278,7 @@ const LOCALES = {
tab_workspaces: 'Espacios',
tab_profiles: 'Perfiles',
tab_todos: 'Todos',
tab_settings: 'Ajustes',
new_conversation: 'Nueva conversación',
filter_conversations: 'Filtrar conversaciones...',
session_time_unknown: 'Desconocido',
@@ -1395,6 +1486,19 @@ const LOCALES = {
skill_name_required: 'Skill name is required',
skill_updated: 'Skill updated',
skill_created: 'Skill created',
skill_deleted: 'Skill deleted',
skill_delete_confirm: 'Delete skill "{0}"?',
skills_empty_title: 'Select a skill',
skills_empty_sub: 'Pick a skill from the sidebar to view its contents, or create a new one.',
skills_edit: 'Edit',
skills_delete: 'Delete',
skills_back_to: 'Back to {0}',
tasks_empty_title: 'Select a scheduled job',
tasks_empty_sub: 'Pick a job from the sidebar to view its details and runs, or create a new one.',
workspaces_empty_title: 'Select a space',
workspaces_empty_sub: 'Pick a space from the sidebar to view its files and settings, or add a new one.',
profiles_empty_title: 'Select a profile',
profiles_empty_sub: 'Pick an agent profile from the sidebar to view and edit its settings, or create a new one.',
memory_notes_label: 'memory (notes)',
memory_saved: 'Memory saved',
my_notes: 'My Notes',
@@ -1410,6 +1514,7 @@ const LOCALES = {
workspace_add_path_placeholder: 'Add workspace path (e.g. /home/user/my-project)',
workspace_paths_validated_hint: 'Paths are validated as existing directories before saving.',
workspace_added: 'Workspace added',
workspace_renamed: 'Espacio renombrado',
workspace_remove_confirm_title: 'Remove workspace',
workspace_remove_confirm_message: (path) => `Remove "${path}"?`,
workspace_removed: 'Workspace removed',
@@ -1459,6 +1564,36 @@ const LOCALES = {
disable_auth_failed: 'Failed to disable auth: ',
bg_error_single: (title) => `"${title}" has encountered an error`,
bg_error_multi: (count) => `${count} sessions have encountered an error`,
// skill form
skill_name: 'Nombre',
skill_category: 'Categoría',
skill_category_placeholder: 'Opcional, p. ej. devops',
skill_content: 'Contenido de SKILL.md',
skill_content_placeholder: 'Frontmatter YAML + cuerpo markdown',
skill_rename_not_supported: 'No se admite renombrar una habilidad. Crea una nueva y elimina la anterior.',
skill_metadata: 'Metadatos',
// cron form
cron_name_label: 'Nombre',
cron_name_placeholder: 'Opcional',
cron_schedule_label: 'Programación',
cron_schedule_hint: "Expresión cron o abreviatura como 'every 1h'.",
cron_prompt_label: 'Prompt',
cron_deliver_label: 'Entregar salida a',
cron_deliver_local: 'Local (solo guardar salida)',
cron_skills_label: 'Habilidades',
cron_skills_placeholder: 'Añadir habilidades (opcional)…',
cron_skills_edit_hint: 'La lista de habilidades no es editable después de crear.',
// workspace form
workspace_name_label: 'Nombre',
workspace_name_placeholder: 'Nombre opcional',
workspace_path_label: 'Ruta',
workspace_path_required: 'La ruta es obligatoria',
workspace_path_readonly: 'La ruta no se puede cambiar. Solo renombrar.',
workspace_new_title: 'Nuevo espacio',
// profile form
profile_name_label: 'Nombre',
profile_base_url_label: 'URL base',
profile_api_key_label: 'Clave API',
},
de: {
@@ -1644,6 +1779,7 @@ const LOCALES = {
tab_workspaces: 'Spaces',
tab_profiles: 'Profile',
tab_todos: 'Todos',
tab_settings: 'Einstellungen',
new_conversation: 'Neuer Chat',
filter_conversations: 'Chats filtern...',
scheduled_jobs: 'Geplante Aufgaben',
@@ -1711,6 +1847,36 @@ const LOCALES = {
onboarding_password_keep_existing: 'Aktuelles Passwort beibehalten',
onboarding_password_remains_disabled: 'Bleibt deaktiviert',
profile_delete_confirm_message: 'Alle Sitzungen, Konfigurationen, Fähigkeiten und Erinnerungen dieses Profils werden dauerhaft gelöscht. Diese Aktion kann nicht rückgängig gemacht werden.',
// skill form
skill_name: 'Name',
skill_category: 'Kategorie',
skill_category_placeholder: 'Optional, z. B. devops',
skill_content: 'SKILL.md-Inhalt',
skill_content_placeholder: 'YAML-Frontmatter + Markdown-Text',
skill_rename_not_supported: 'Umbenennen einer Fähigkeit wird nicht unterstützt. Erstelle eine neue und lösche die alte.',
skill_metadata: 'Metadaten',
// cron form
cron_name_label: 'Name',
cron_name_placeholder: 'Optional',
cron_schedule_label: 'Zeitplan',
cron_schedule_hint: "Cron-Ausdruck oder Kurzform wie 'every 1h'.",
cron_prompt_label: 'Prompt',
cron_deliver_label: 'Ausgabe senden an',
cron_deliver_local: 'Lokal (nur speichern)',
cron_skills_label: 'Fähigkeiten',
cron_skills_placeholder: 'Fähigkeiten hinzufügen (optional)…',
cron_skills_edit_hint: 'Die Fähigkeitenliste kann nach der Erstellung nicht bearbeitet werden.',
// workspace form
workspace_name_label: 'Name',
workspace_name_placeholder: 'Optionaler Anzeigename',
workspace_path_label: 'Pfad',
workspace_path_required: 'Pfad ist erforderlich',
workspace_path_readonly: 'Pfad kann nicht geändert werden. Nur umbenennen.',
workspace_new_title: 'Neuer Bereich',
// profile form
profile_name_label: 'Name',
profile_base_url_label: 'Basis-URL',
profile_api_key_label: 'API-Schlüssel',
},
zh: {
@@ -1893,6 +2059,7 @@ const LOCALES = {
tab_todos: '待办',
tab_workspaces: '工作区',
tab_profiles: '配置',
tab_settings: '设置',
new_conversation: '新建对话',
filter_conversations: '筛选对话…',
session_time_unknown: '未知',
@@ -2113,6 +2280,7 @@ const LOCALES = {
workspace_add_path_placeholder: '添加工作区路径(例如 /home/user/my-project',
workspace_paths_validated_hint: '保存前会校验路径是否为已存在目录。',
workspace_added: '工作区已添加',
workspace_renamed: '工作区已重命名',
workspace_remove_confirm_title: '移除工作区',
workspace_remove_confirm_message: (path) => `要移除"${path}"吗?`,
workspace_removed: '工作区已移除',
@@ -2162,6 +2330,49 @@ const LOCALES = {
disable_auth_failed: '停用认证失败:',
bg_error_single: (title) => `${title}”出现错误`,
bg_error_multi: (count) => `${count} 个会话出现错误`,
skill_deleted: '技能已删除',
skill_delete_confirm: '删除技能 "{0}"',
skills_empty_title: '选择一个技能',
skills_empty_sub: '从侧边栏中选择一个技能以查看其内容,或创建一个新技能。',
skills_edit: '编辑',
skills_delete: '删除',
skills_back_to: '返回 {0}',
tasks_empty_title: '选择一个计划任务',
tasks_empty_sub: '从侧边栏中选择一个任务以查看其详情和运行记录,或创建一个新任务。',
workspaces_empty_title: '选择一个空间',
workspaces_empty_sub: '从侧边栏中选择一个空间以查看其文件和设置,或添加一个新空间。',
profiles_empty_title: '选择一个配置文件',
profiles_empty_sub: '从侧边栏中选择一个代理配置文件以查看和编辑其设置,或创建一个新配置文件。',
// skill form
skill_name: '名称',
skill_category: '分类',
skill_category_placeholder: '可选,例如 devops',
skill_content: 'SKILL.md 内容',
skill_content_placeholder: 'YAML frontmatter + markdown 正文',
skill_rename_not_supported: '不支持重命名技能。请创建新技能并删除旧技能。',
skill_metadata: '元数据',
// cron form
cron_name_label: '名称',
cron_name_placeholder: '可选',
cron_schedule_label: '计划',
cron_schedule_hint: "Cron 表达式或简写,例如 'every 1h'。",
cron_prompt_label: '提示词',
cron_deliver_label: '输出位置',
cron_deliver_local: '本地(仅保存输出)',
cron_skills_label: '技能',
cron_skills_placeholder: '添加技能(可选)…',
cron_skills_edit_hint: '创建后无法再编辑技能列表。',
// workspace form
workspace_name_label: '名称',
workspace_name_placeholder: '可选的友好名称',
workspace_path_label: '路径',
workspace_path_required: '路径是必填项',
workspace_path_readonly: '路径不可更改。只能重命名。',
workspace_new_title: '新空间',
// profile form
profile_name_label: '名称',
profile_base_url_label: '基础 URL',
profile_api_key_label: 'API 密钥',
},
// Traditional Chinese (zh-Hant)
@@ -2423,7 +2634,10 @@ const LOCALES = {
// ui.js
workspace_desc: '\u8acb\u9078\u64c7\u5de5\u4f5c\u5340\uff0c\u6216\u8f09\u5165\u65b0\u540d\u7a31\u5beb\u4e00\u500b',
tab_profiles: '\u914d\u7f6e',
tab_settings: '\u8a2d\u5b9a',
profile_delete_confirm_message: '\u6b64\u914d\u7f6e\u6a94\u7684\u6240\u6709\u6703\u8a71\u3001\u8a2d\u5b9a\u3001\u6280\u80fd\u548c\u8a18\u61b6\u5c07\u88ab\u6c38\u4e45\u522a\u9664\u3002\u6b64\u64cd\u4f5c\u7121\u6cd5\u64a4\u92b7\u3002',
workspace_renamed: '\u5de5\u4f5c\u5340\u5df2\u91cd\u65b0\u547d\u540d',
cron_name_placeholder: '\u9078\u586b',
},
};
@@ -2530,6 +2744,7 @@ function applyLocaleToDOM() {
const val = t(key);
if (val && val !== key) el.placeholder = val;
});
if (typeof syncAppTitlebar === 'function') syncAppTitlebar();
}
// Apply saved locale immediately so there's no flash of English on reload.

View File

@@ -24,6 +24,7 @@ const LI_PATHS = {
'settings': '<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>',
'alert-triangle': '<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/>',
'refresh-cw': '<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"/>',
'undo': '<path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5v0a5.5 5.5 0 0 1-5.5 5.5H11"/>',
'check': '<polyline points="20 6 9 17 4 12"/>',
'lock': '<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>',
'star': '<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>',

View File

@@ -48,168 +48,157 @@
</script>
</head>
<body>
<header class="app-titlebar" role="banner">
<button class="app-titlebar-hamburger" id="btnHamburger" onclick="toggleMobileSidebar()" type="button" title="Menu" aria-label="Menu">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
</button>
<div class="app-titlebar-inner">
<span class="app-titlebar-icon" aria-hidden="true">
<svg viewBox="0 0 64 64" width="16" height="16" aria-hidden="true">
<defs>
<linearGradient id="app-titlebar-gold" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#F5C542"/>
<stop offset="100%" style="stop-color:#D4961C"/>
</linearGradient>
</defs>
<rect x="30" y="10" width="4" height="46" rx="2" fill="url(#app-titlebar-gold)"/>
<path d="M30 18 C24 14, 14 14, 10 18 C14 16, 22 16, 28 20" fill="#F5C542" opacity="0.9"/>
<path d="M34 18 C40 14, 50 14, 54 18 C50 16, 42 16, 36 20" fill="#F5C542" opacity="0.9"/>
<circle cx="32" cy="10" r="4" fill="#F5C542"/>
</svg>
</span>
<span class="app-titlebar-title" id="appTitlebarTitle">Hermes</span>
<span class="app-titlebar-sub" id="appTitlebarSub" hidden></span>
</div>
<div class="app-titlebar-spacer" aria-hidden="true"></div>
</header>
<div class="layout">
<nav class="rail" aria-label="Primary navigation">
<button class="rail-btn nav-tab active" data-panel="chat" onclick="switchPanel('chat')" title="Chat" data-i18n-title="tab_chat" aria-label="Chat"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg></button>
<button class="rail-btn nav-tab" data-panel="tasks" onclick="switchPanel('tasks')" title="Tasks" data-i18n-title="tab_tasks" aria-label="Tasks"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg></button>
<button class="rail-btn nav-tab" data-panel="skills" onclick="switchPanel('skills')" title="Skills" data-i18n-title="tab_skills" aria-label="Skills"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg></button>
<button class="rail-btn nav-tab" data-panel="memory" onclick="switchPanel('memory')" title="Memory" data-i18n-title="tab_memory" aria-label="Memory"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96-.44 2.5 2.5 0 0 1-2.96-3.08 3 3 0 0 1-.34-5.58 2.5 2.5 0 0 1 1.32-4.24 2.5 2.5 0 0 1 1.98-3A2.5 2.5 0 0 1 9.5 2z"/><path d="M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96-.44 2.5 2.5 0 0 0 2.96-3.08 3 3 0 0 0 .34-5.58 2.5 2.5 0 0 0-1.32-4.24 2.5 2.5 0 0 0-1.98-3A2.5 2.5 0 0 0 14.5 2z"/></svg></button>
<button class="rail-btn nav-tab" data-panel="workspaces" onclick="switchPanel('workspaces')" title="Spaces" data-i18n-title="tab_workspaces" aria-label="Spaces"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" 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="rail-btn nav-tab" data-panel="profiles" onclick="switchPanel('profiles')" title="Agent profiles" data-i18n-title="tab_profiles" aria-label="Agent profiles"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg></button>
<button class="rail-btn nav-tab" data-panel="todos" onclick="switchPanel('todos')" title="Current task list" data-i18n-title="tab_todos" aria-label="Todos"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="5" width="6" height="6" rx="1"/><path d="m3 17 2 2 4-4"/><path d="M13 6h8"/><path d="M13 12h8"/><path d="M13 18h8"/></svg></button>
<div class="rail-spacer"></div>
<button class="rail-btn nav-tab" data-panel="settings" onclick="switchPanel('settings')" title="Settings" data-i18n-title="tab_settings" aria-label="Settings"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></button>
</nav>
<aside class="sidebar">
<div class="sidebar-nav">
<button class="nav-tab active" data-panel="chat" data-label="Chat" onclick="switchPanel('chat')" title="Chat" data-i18n-title="tab_chat"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg></button>
<button class="nav-tab" data-panel="tasks" data-label="Tasks" onclick="switchPanel('tasks')" title="Tasks" data-i18n-title="tab_tasks"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg></button>
<button class="nav-tab" data-panel="skills" data-label="Skills" onclick="switchPanel('skills')" title="Skills" data-i18n-title="tab_skills"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg></button>
<button class="nav-tab" data-panel="memory" data-label="Memory" onclick="switchPanel('memory')" title="Memory" data-i18n-title="tab_memory"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 2a7 7 0 0 1 7 7c0 2.5-1.3 4.7-3.2 6H8.2C6.3 13.7 5 11.5 5 9a7 7 0 0 1 7-7z"/><line x1="9" y1="17" x2="15" y2="17"/><line x1="10" y1="20" x2="14" y2="20"/></svg></button>
<button class="nav-tab" data-panel="memory" data-label="Memory" onclick="switchPanel('memory')" title="Memory" data-i18n-title="tab_memory"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96-.44 2.5 2.5 0 0 1-2.96-3.08 3 3 0 0 1-.34-5.58 2.5 2.5 0 0 1 1.32-4.24 2.5 2.5 0 0 1 1.98-3A2.5 2.5 0 0 1 9.5 2z"/><path d="M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96-.44 2.5 2.5 0 0 0 2.96-3.08 3 3 0 0 0 .34-5.58 2.5 2.5 0 0 0-1.32-4.24 2.5 2.5 0 0 0-1.98-3A2.5 2.5 0 0 0 14.5 2z"/></svg></button>
<button class="nav-tab" data-panel="workspaces" data-label="Spaces" onclick="switchPanel('workspaces')" title="Spaces" data-i18n-title="tab_workspaces"><svg width="18" height="18" 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="nav-tab" data-panel="profiles" data-label="Profiles" onclick="switchPanel('profiles')" title="Agent profiles" data-i18n-title="tab_profiles"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg></button>
<button class="nav-tab" data-panel="todos" data-label="Todos" onclick="switchPanel('todos')" title="Current task list" data-i18n-title="tab_todos"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="5" width="6" height="6" rx="1"/><path d="m3 17 2 2 4-4"/><path d="M13 6h8"/><path d="M13 12h8"/><path d="M13 18h8"/></svg></button>
<!-- Settings button mirrored here for mobile (rail is desktop-only via @media >=768px). Keep in sync with rail entry. -->
<button class="nav-tab" data-panel="settings" onclick="switchPanel('settings')" title="Settings" data-i18n-title="tab_settings"><svg width="18" height="18" 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="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></button>
</div>
<!-- Chat panel -->
<div class="panel-view active" id="panelChat">
<div class="sidebar-section">
<button class="new-chat-btn" id="btnNewChat">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
<span data-i18n="new_conversation">New conversation</span> <span style="font-size:10px;opacity:.5;margin-left:4px">Cmd+K</span>
</button>
<div class="panel-head">
<span data-i18n="tab_chat">Chat</span>
<div class="panel-head-actions">
<button class="panel-head-btn" id="btnNewChat" title="New conversation (Cmd+K)" data-i18n-title="new_conversation" aria-label="New conversation">
<svg 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>
</div>
</div>
<div class="session-search"><input id="sessionSearch" placeholder="Filter conversations..." data-i18n-placeholder="filter_conversations" oninput="filterSessions()" autocomplete="off"></div>
<div class="session-search sidebar-search"><svg class="sidebar-search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg><input id="sessionSearch" placeholder="Filter conversations..." data-i18n-placeholder="filter_conversations" oninput="filterSessions()" autocomplete="off"></div>
<div class="session-list" id="sessionList"></div>
</div>
<!-- Tasks (cron) panel -->
<div class="panel-view" id="panelTasks">
<div class="sidebar-section" style="padding-bottom:4px;display:flex;align-items:center;justify-content:space-between">
<div style="font-size:11px;color:var(--muted)" data-i18n="scheduled_jobs">Scheduled jobs</div>
<div style="display:flex;gap:4px;align-items:center">
<button class="cron-btn" id="cronRefreshBtn" style="padding:3px 7px;font-size:10px;line-height:1" onclick="loadCrons(true)" title="Refresh job list" aria-label="Refresh job list"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg></button>
<button class="cron-btn run" style="padding:3px 8px;font-size:10px" onclick="toggleCronForm()">+ <span data-i18n="new_job">New job</span></button>
<div class="panel-head">
<span data-i18n="scheduled_jobs">Scheduled jobs</span>
<div class="panel-head-actions">
<button class="panel-head-btn" id="cronRefreshBtn" onclick="loadCrons(true)" title="Refresh job list" aria-label="Refresh job list"><svg 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"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg></button>
<button class="panel-head-btn" onclick="openCronCreate()" title="New job" data-i18n-title="new_job" aria-label="New job"><svg 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>
</div>
</div>
<!-- Create job form (hidden by default) -->
<div id="cronCreateForm" style="display:none;padding:8px 12px;border-bottom:1px solid var(--border);flex-shrink:0">
<input id="cronFormName" placeholder="Job name (optional)" style="width:100%;background:rgba(255,255,255,.05);border:1px solid var(--border2);border-radius:6px;color:var(--text);padding:5px 8px;font-size:12px;outline:none;margin-bottom:6px">
<input id="cronFormSchedule" placeholder="Schedule: '0 9 * * *' or 'every 1h'" style="width:100%;background:rgba(255,255,255,.05);border:1px solid var(--border2);border-radius:6px;color:var(--text);padding:5px 8px;font-size:12px;outline:none;margin-bottom:6px">
<textarea id="cronFormPrompt" rows="3" placeholder="Prompt (must be self-contained)" style="width:100%;background:rgba(255,255,255,.05);border:1px solid var(--border2);border-radius:6px;color:var(--text);padding:5px 8px;font-size:12px;outline:none;resize:none;font-family:inherit;margin-bottom:6px"></textarea>
<select id="cronFormDeliver" style="width:100%;background:rgba(255,255,255,.05);border:1px solid var(--border2);border-radius:6px;color:var(--text);padding:5px 8px;font-size:12px;outline:none;margin-bottom:6px">
<option value="local">Local (save output only)</option>
<option value="discord">Discord</option>
<option value="telegram">Telegram</option>
</select>
<div class="skill-picker-wrap" style="margin-bottom:8px">
<input id="cronFormSkillSearch" placeholder="Add skills (optional)..." style="width:100%;background:rgba(255,255,255,.05);border:1px solid var(--border2);border-radius:6px;color:var(--text);padding:5px 8px;font-size:12px;outline:none" autocomplete="off">
<div id="cronFormSkillDropdown" class="skill-picker-dropdown" style="display:none"></div>
<div id="cronFormSkillTags" class="skill-picker-tags"></div>
</div>
<div style="display:flex;gap:6px">
<button class="cron-btn run" style="flex:1" onclick="submitCronCreate()" data-i18n="create_job">Create job</button>
<button class="cron-btn" style="flex:1" onclick="toggleCronForm()" data-i18n="cancel">Cancel</button>
</div>
<div id="cronFormError" style="font-size:11px;color:var(--accent);margin-top:6px;display:none"></div>
</div>
<div class="cron-list" id="cronList"><div style="padding:12px;color:var(--muted);font-size:12px" data-i18n="loading">Loading...</div></div>
</div>
<!-- Skills panel -->
<div class="panel-view" id="panelSkills">
<div class="sidebar-section" style="padding-bottom:4px;display:flex;align-items:center;justify-content:space-between">
<div class="skills-search" style="flex:1;padding:0"><input id="skillsSearch" placeholder="Search skills..." data-i18n-placeholder="search_skills" oninput="filterSkills()"></div>
<button class="cron-btn run" style="padding:3px 8px;font-size:10px;flex-shrink:0;margin-left:6px" onclick="toggleSkillForm()">+ <span data-i18n="new_skill">New skill</span></button>
</div>
<!-- Skill create/edit form (hidden by default) -->
<div id="skillCreateForm" style="display:none;padding:8px 12px;border-bottom:1px solid var(--border);flex-shrink:0">
<input id="skillFormName" placeholder="Skill name (e.g. my-skill)" style="width:100%;background:rgba(255,255,255,.05);border:1px solid var(--border2);border-radius:6px;color:var(--text);padding:5px 8px;font-size:12px;outline:none;margin-bottom:6px;box-sizing:border-box">
<input id="skillFormCategory" placeholder="Category (optional, e.g. devops)" style="width:100%;background:rgba(255,255,255,.05);border:1px solid var(--border2);border-radius:6px;color:var(--text);padding:5px 8px;font-size:12px;outline:none;margin-bottom:6px;box-sizing:border-box">
<textarea id="skillFormContent" rows="6" placeholder="SKILL.md content (YAML frontmatter + markdown body)" style="width:100%;background:rgba(255,255,255,.05);border:1px solid var(--border2);border-radius:6px;color:var(--text);padding:5px 8px;font-size:12px;outline:none;resize:vertical;font-family:'SF Mono',ui-monospace,monospace;margin-bottom:6px;box-sizing:border-box"></textarea>
<div style="display:flex;gap:6px">
<button class="cron-btn run" style="flex:1" onclick="submitSkillSave()" data-i18n="save_skill">Save skill</button>
<button class="cron-btn" style="flex:1" onclick="toggleSkillForm()" data-i18n="cancel">Cancel</button>
<div class="panel-head">
<span data-i18n="tab_skills">Skills</span>
<div class="panel-head-actions">
<button class="panel-head-btn" onclick="openSkillCreate()" title="New skill" data-i18n-title="new_skill" aria-label="New skill"><svg 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>
</div>
<div id="skillFormError" style="font-size:11px;color:var(--accent);margin-top:6px;display:none"></div>
</div>
<div class="skills-search sidebar-search"><svg class="sidebar-search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg><input id="skillsSearch" placeholder="Search skills..." data-i18n-placeholder="search_skills" oninput="filterSkills()"></div>
<div class="skills-list" id="skillsList"><div style="padding:12px;color:var(--muted);font-size:12px" data-i18n="loading">Loading...</div></div>
</div>
<!-- Memory panel -->
<div class="panel-view" id="panelMemory">
<div style="padding:8px 12px 4px;display:flex;align-items:center;justify-content:space-between;flex-shrink:0">
<span style="font-size:11px;color:var(--muted)" data-i18n="personal_memory">Personal memory</span>
<button class="cron-btn run" id="memEditBtn" style="padding:3px 8px;font-size:10px" onclick="toggleMemoryEdit()"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg> <span data-i18n="edit">Edit</span></button>
</div>
<div class="memory-panel" id="memoryPanel"><div style="color:var(--muted);font-size:12px" data-i18n="loading">Loading...</div></div>
<!-- Memory edit form (hidden by default) -->
<div id="memoryEditForm" style="display:none;padding:8px 12px;border-top:1px solid var(--border);flex-shrink:0">
<div style="font-size:11px;color:var(--muted);margin-bottom:4px"><span data-i18n="editing">Editing</span>: <span id="memEditSection">memory</span></div>
<textarea id="memEditContent" rows="10" style="width:100%;background:rgba(255,255,255,.05);border:1px solid var(--border2);border-radius:6px;color:var(--text);padding:5px 8px;font-size:11px;outline:none;resize:vertical;font-family:'SF Mono',ui-monospace,monospace;box-sizing:border-box;margin-bottom:6px;line-height:1.5"></textarea>
<div style="display:flex;gap:6px">
<button class="cron-btn run" style="flex:1" onclick="submitMemorySave()" data-i18n="save">Save</button>
<button class="cron-btn" style="flex:1" onclick="closeMemoryEdit()" data-i18n="cancel">Cancel</button>
</div>
<div id="memEditError" style="font-size:11px;color:var(--accent);margin-top:6px;display:none"></div>
<div class="panel-head">
<span data-i18n="personal_memory">Personal memory</span>
</div>
<div class="side-menu" id="memoryPanel"><div style="padding:12px;color:var(--muted);font-size:12px" data-i18n="loading">Loading...</div></div>
</div>
<!-- Todo panel -->
<div class="panel-view" id="panelTodos">
<div style="padding:10px 12px 4px;font-size:11px;color:var(--muted);flex-shrink:0" data-i18n="current_task_list">Current task list</div>
<div class="panel-head">
<span data-i18n="current_task_list">Current task list</span>
</div>
<div id="todoPanel" style="flex:1;overflow-y:auto;padding:8px 12px"></div>
</div>
<!-- Workspaces panel -->
<div class="panel-view" id="panelWorkspaces">
<div style="padding:10px 12px 4px;font-size:11px;color:var(--muted)" data-i18n="workspace_desc">Add and switch workspaces for your sessions.</div>
<div style="flex:1;overflow-y:auto;padding:0 12px 12px" id="workspacesPanel"><div style="color:var(--muted);font-size:12px" data-i18n="loading">Loading...</div></div>
<div class="panel-head">
<span data-i18n="tab_workspaces">Spaces</span>
<div class="panel-head-actions">
<button class="panel-head-btn" onclick="openWorkspaceCreate()" title="Add space" data-i18n-title="workspace_add_title" aria-label="Add space"><svg 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>
</div>
</div>
<div class="panel-head-sub" data-i18n="workspace_desc">Add and switch workspaces for your sessions.</div>
<div style="flex:1;overflow-y:auto;padding:8px" id="workspacesPanel"><div style="color:var(--muted);font-size:12px" data-i18n="loading">Loading...</div></div>
</div>
<!-- Profiles panel -->
<div class="panel-view" id="panelProfiles">
<div class="sidebar-section" style="padding-bottom:4px;display:flex;align-items:center;justify-content:space-between">
<div style="font-size:11px;color:var(--muted)" data-i18n="tab_profiles">Agent profiles</div>
<button class="cron-btn run" style="padding:3px 8px;font-size:10px" onclick="toggleProfileForm()">+ <span data-i18n="new_profile">New profile</span></button>
</div>
<!-- Profile create form (hidden by default) -->
<div id="profileCreateForm" style="display:none;padding:8px 12px;border-bottom:1px solid var(--border);flex-shrink:0">
<input id="profileFormName" data-i18n-placeholder="profile_name_placeholder" placeholder="Profile name (lowercase, a-z 0-9 hyphens)" style="width:100%;background:rgba(255,255,255,.05);border:1px solid var(--border2);border-radius:6px;color:var(--text);padding:5px 8px;font-size:12px;outline:none;margin-bottom:6px;box-sizing:border-box">
<label style="display:flex;align-items:center;gap:6px;font-size:11px;color:var(--muted);margin-bottom:8px;cursor:pointer">
<input type="checkbox" id="profileFormClone" style="accent-color:var(--accent)"> <span data-i18n="profile_clone_label">Clone config from active profile</span>
</label>
<input id="profileFormBaseUrl" data-i18n-placeholder="profile_base_url_placeholder" placeholder="Base URL (optional, e.g. http://localhost:11434)" style="width:100%;background:rgba(255,255,255,.05);border:1px solid var(--border2);border-radius:6px;color:var(--text);padding:5px 8px;font-size:12px;outline:none;margin-bottom:6px;box-sizing:border-box">
<input id="profileFormApiKey" type="password" data-i18n-placeholder="profile_api_key_placeholder" placeholder="API key (optional)" style="width:100%;background:rgba(255,255,255,.05);border:1px solid var(--border2);border-radius:6px;color:var(--text);padding:5px 8px;font-size:12px;outline:none;margin-bottom:6px;box-sizing:border-box">
<div style="display:flex;gap:6px">
<button class="cron-btn run" style="flex:1" onclick="submitProfileCreate()" data-i18n="create">Create</button>
<button class="cron-btn" style="flex:1" onclick="toggleProfileForm()" data-i18n="cancel">Cancel</button>
<div class="panel-head">
<span data-i18n="tab_profiles">Agent profiles</span>
<div class="panel-head-actions">
<button class="panel-head-btn" onclick="openProfileCreate()" title="New profile" data-i18n-title="new_profile" aria-label="New profile"><svg 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>
</div>
<div id="profileFormError" style="font-size:11px;color:var(--accent);margin-top:6px;display:none"></div>
</div>
<div style="flex:1;overflow-y:auto;padding:0 12px 12px" id="profilesPanel"><div style="color:var(--muted);font-size:12px" data-i18n="loading">Loading...</div></div>
<div style="flex:1;overflow-y:auto;padding:8px" id="profilesPanel"><div style="color:var(--muted);font-size:12px" data-i18n="loading">Loading...</div></div>
</div>
<div class="sidebar-bottom">
<button class="hermes-launch-btn" id="btnHermesPanel" onclick="toggleSettings()" title="Open Hermes control center">
<span class="hermes-launch-icon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<defs>
<linearGradient id="hermes-gold-sidebar" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#F5C542;stop-opacity:1"/>
<stop offset="100%" style="stop-color:#D4961C;stop-opacity:1"/>
</linearGradient>
</defs>
<rect x="30" y="10" width="4" height="46" rx="2" fill="url(#hermes-gold-sidebar)"/>
<path d="M30 18 C24 14, 14 14, 10 18 C14 16, 22 16, 28 20" fill="#F5C542" opacity="0.9"/>
<path d="M30 22 C26 19, 18 19, 14 22 C18 20, 24 20, 28 24" fill="#D4961C" opacity="0.8"/>
<path d="M34 18 C40 14, 50 14, 54 18 C50 16, 42 16, 36 20" fill="#F5C542" opacity="0.9"/>
<path d="M34 22 C38 19, 46 19, 50 22 C46 20, 40 20, 36 24" fill="#D4961C" opacity="0.8"/>
<path d="M32 48 C22 44, 20 38, 26 34 C20 36, 18 42, 24 46 C18 40, 22 30, 30 28 C24 32, 22 38, 28 42" fill="none" stroke="#F5C542" stroke-width="2.5" stroke-linecap="round"/>
<path d="M32 48 C42 44, 44 38, 38 34 C44 36, 46 42, 40 46 C46 40, 42 30, 34 28 C40 32, 42 38, 36 42" fill="none" stroke="#D4961C" stroke-width="2.5" stroke-linecap="round"/>
<circle cx="32" cy="10" r="4" fill="#F5C542"/>
<circle cx="32" cy="10" r="2" fill="#FFF8E1" opacity="0.7"/>
</svg></span>
<span class="hermes-launch-copy">
<span class="hermes-launch-title">Hermes WebUI</span>
<span class="hermes-launch-meta">Preferences, imports, exports</span>
</span>
<span class="hermes-launch-chevron" aria-hidden="true"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg></span>
</button>
<!-- Settings panel (menu list; actual panes render in .main) -->
<div class="panel-view" id="panelSettings">
<div class="panel-head">
<span data-i18n="tab_settings">Settings</span>
</div>
<div class="side-menu" id="settingsMenu">
<button type="button" class="side-menu-item active" data-settings-section="conversation" onclick="switchSettingsSection('conversation')">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
<span>Conversation</span>
</button>
<button type="button" class="side-menu-item" data-settings-section="appearance" onclick="switchSettingsSection('appearance')">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>
<span>Appearance</span>
</button>
<button type="button" class="side-menu-item" data-settings-section="preferences" onclick="switchSettingsSection('preferences')">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="4" y1="21" x2="4" y2="14"/><line x1="4" y1="10" x2="4" y2="3"/><line x1="12" y1="21" x2="12" y2="12"/><line x1="12" y1="8" x2="12" y2="3"/><line x1="20" y1="21" x2="20" y2="16"/><line x1="20" y1="12" x2="20" y2="3"/><line x1="1" y1="14" x2="7" y2="14"/><line x1="9" y1="8" x2="15" y2="8"/><line x1="17" y1="16" x2="23" y2="16"/></svg>
<span>Preferences</span>
</button>
<button type="button" class="side-menu-item" data-settings-section="providers" onclick="switchSettingsSection('providers')">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/></svg>
<span data-i18n="providers_tab_title">Providers</span>
</button>
<button type="button" class="side-menu-item" data-settings-section="system" onclick="switchSettingsSection('system')">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="3" width="20" height="8" rx="2"/><rect x="2" y="13" width="20" height="8" rx="2"/><line x1="6" y1="7" x2="6.01" y2="7"/><line x1="6" y1="17" x2="6.01" y2="17"/></svg>
<span>System</span>
</button>
</div>
</div>
<div class="resize-handle" id="sidebarResize"></div>
</aside>
<main class="main">
<div class="topbar">
<button class="mobile-hamburger" id="btnHamburger" onclick="toggleMobileSidebar()" title="Menu">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
</button>
<div style="flex:1;min-width:0;overflow:hidden"><div class="topbar-title" id="topbarTitle">Hermes</div><div class="topbar-meta" id="topbarMeta" data-i18n="new_conversation">Start a new conversation</div></div>
<div class="topbar-chips">
<button class="chip workspace-toggle-btn" id="btnWorkspacePanelToggle" onclick="toggleWorkspacePanel()" title="Show workspace panel" aria-pressed="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"><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><span class="workspace-toggle-label">Files</span></button>
</div>
</div>
<div id="mainChat" class="main-view">
<div class="messages" id="messages">
<button id="scrollToBottomBtn" class="scroll-to-bottom-btn" aria-label="Scroll to bottom" onclick="scrollToBottom()" style="display:none"></button>
<div class="empty-state" id="emptyState">
@@ -340,11 +329,15 @@
</button>
</div>
<div class="composer-ws-wrap">
<button class="composer-workspace-chip ws-chip" id="composerWorkspaceChip" type="button" onclick="toggleComposerWsDropdown()" title="Switch workspace" disabled>
<span class="composer-workspace-icon" aria-hidden="true"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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></span>
<span class="composer-workspace-label" id="composerWorkspaceLabel"></span>
<span class="composer-workspace-chevron" aria-hidden="true"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></span>
</button>
<div class="composer-workspace-group ws-chip" id="composerWorkspaceGroup" role="group" aria-label="Workspace controls">
<button class="composer-workspace-files-btn" id="btnWorkspacePanelToggle" type="button" onclick="toggleWorkspacePanel()" title="Show workspace panel" aria-pressed="false" aria-label="Toggle workspace files panel">
<span class="composer-workspace-icon" aria-hidden="true"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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></span>
</button>
<button class="composer-workspace-chip" id="composerWorkspaceChip" type="button" onclick="toggleComposerWsDropdown()" title="Switch workspace" disabled>
<span class="composer-workspace-label" id="composerWorkspaceLabel"></span>
<span class="composer-workspace-chevron" aria-hidden="true"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></span>
</button>
</div>
</div>
<div class="composer-model-wrap">
<button class="composer-model-chip" id="composerModelChip" type="button" onclick="toggleModelDropdown()" title="Conversation model">
@@ -423,103 +416,98 @@
<div class="upload-bar-wrap" id="uploadBarWrap"><div class="upload-bar" id="uploadBar"></div></div>
</div>
</div>
</main>
<aside class="rightpanel">
<div class="resize-handle" id="rightpanelResize"></div>
<div class="panel-header">
<span>Workspace</span>
<span class="git-badge" id="gitBadge" style="display:none"></span>
<div class="panel-actions">
<button class="panel-icon-btn" id="btnCollapseWorkspacePanel" title="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>
<button class="panel-icon-btn" id="btnUpDir" title="Parent directory" onclick="navigateUp()" style="display:none"><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="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg></button>
<button class="panel-icon-btn" id="btnNewFile" title="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" id="btnNewFolder" title="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" id="btnRefreshPanel" title="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 close-preview" id="btnClearPreview" title="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>
<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">
<div class="preview-path" id="previewPath">
<span id="previewPathText"></span>
<span class="preview-badge" id="previewBadge"></span>
<button id="btnDownloadFile" class="panel-icon-btn" style="margin-left:auto;font-size:12px;width:auto;padding:2px 8px;display:inline-flex;align-items:center;gap:4px" onclick="downloadFile(_previewCurrentPath)" title="Download file to your computer"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg> Download</button>
<button id="btnEditFile" class="panel-icon-btn" style="font-size:12px;width:auto;padding:2px 8px;display:none;align-items:center;gap:4px" onclick="toggleEditMode()"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg> Edit</button>
</div>
<pre class="preview-code" id="previewCode"></pre>
<div class="preview-img-wrap" id="previewImgWrap" style="display:none"><img class="preview-img" id="previewImg" src="" alt=""></div>
<div class="preview-md" id="previewMd" style="display:none"></div>
<textarea id="previewEditArea" style="display:none;flex:1;width:100%;background:var(--code-bg);color:var(--pre-text);border:1px solid var(--border2);border-radius:8px;padding:12px;font-family:'SF Mono',ui-monospace,monospace;font-size:12px;line-height:1.6;resize:none;outline:none" oninput="_previewDirty=true;updateEditBtn()"></textarea>
</div>
</aside>
</div>
<div class="onboarding-overlay" id="onboardingOverlay" style="display:none" role="dialog" aria-modal="true" aria-labelledby="onboardingTitle">
<div class="onboarding-card">
<div class="onboarding-shell">
<div class="onboarding-sidebar">
<div class="onboarding-badge" data-i18n="onboarding_badge">FIRST RUN</div>
<h2 id="onboardingTitle" data-i18n="onboarding_title">Welcome to Hermes Web UI</h2>
<p id="onboardingLead" data-i18n="onboarding_lead">A quick guided setup will check your Hermes install, choose a workspace and model, and optionally protect the app with a password.</p>
<div class="onboarding-steps" id="onboardingSteps"></div>
</div>
<div class="onboarding-main">
<div class="onboarding-status" id="onboardingNotice"></div>
<div class="onboarding-body" id="onboardingBody"></div>
<div class="onboarding-actions">
<button class="sm-btn" id="onboardingBackBtn" onclick="prevOnboardingStep()" style="display:none" data-i18n="onboarding_back">Back</button>
<button class="sm-btn" id="onboardingSkipBtn" onclick="skipOnboarding()" style="margin-right:auto;opacity:.7" data-i18n="onboarding_skip">Skip setup</button>
<button class="sm-btn" id="onboardingNextBtn" onclick="nextOnboardingStep()" style="font-weight:700;color:var(--blue);border-color:rgba(124,185,255,.32)" data-i18n="onboarding_continue">Continue</button>
</div><!-- /#mainChat -->
<div id="mainSkills" class="main-view">
<div class="main-view-header">
<div class="main-view-title" id="skillDetailTitle"></div>
<div class="main-view-actions">
<button id="btnEditSkillDetail" class="panel-head-btn" title="Edit" data-i18n-title="skills_edit" onclick="editCurrentSkill()" style="display:none"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg></button>
<button id="btnDeleteSkillDetail" class="panel-head-btn" title="Delete" data-i18n-title="skills_delete" onclick="deleteCurrentSkill()" style="display:none"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg></button>
<button id="btnCancelSkillDetail" class="panel-head-btn" title="Cancel" data-i18n-title="cancel" onclick="cancelSkillForm()" style="display:none"><svg 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 id="btnSaveSkillDetail" class="panel-head-btn primary" title="Save" data-i18n-title="save" onclick="saveSkillForm()" style="display:none"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg></button>
</div>
</div>
</div>
</div>
</div>
<div class="settings-overlay" id="settingsOverlay" style="display:none">
<div class="settings-panel">
<div class="settings-header">
<div class="settings-heading">
<div class="settings-kicker">Hermes WebUI</div>
<h3 style="margin:0;font-size:18px">Control Center</h3>
<div class="settings-subtitle">Preferences, conversation tools, and system controls.</div>
<div class="main-view-body" id="skillDetailBody" style="display:none"></div>
<div class="main-view-empty" id="skillDetailEmpty">
<svg class="main-view-empty-icon" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
<div class="main-view-empty-title" data-i18n="skills_empty_title">Select a skill</div>
<div class="main-view-empty-sub" data-i18n="skills_empty_sub">Pick a skill from the sidebar to view its contents, or create a new one.</div>
</div>
<button class="panel-icon-btn" onclick="_closeSettingsPanel()" title="Close"><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>
</div>
<div class="settings-body">
<div class="settings-shell">
<select id="settingsSectionDropdown" class="settings-section-dropdown" aria-label="Settings section" onchange="switchSettingsSection(this.value)">
<option value="conversation">Conversation</option>
<option value="appearance">Appearance</option>
<option value="preferences">Preferences</option>
<option value="providers">Providers</option>
<option value="system">System</option>
</select>
<div class="settings-tabs" role="tablist" aria-label="Hermes control center sections">
<button class="settings-tab active" id="settingsTabConversation" type="button" role="tab" aria-selected="true" aria-controls="settingsPaneConversation" onclick="switchSettingsSection('conversation')">
<svg class="settings-tab-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
<span class="settings-tab-title">Conversation</span>
</button>
<button class="settings-tab" id="settingsTabAppearance" type="button" role="tab" aria-selected="false" aria-controls="settingsPaneAppearance" onclick="switchSettingsSection('appearance')">
<svg class="settings-tab-icon" width="16" height="16" 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="12" r="3"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>
<span class="settings-tab-title">Appearance</span>
</button>
<button class="settings-tab" id="settingsTabPreferences" type="button" role="tab" aria-selected="false" aria-controls="settingsPanePreferences" onclick="switchSettingsSection('preferences')">
<svg class="settings-tab-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="4" y1="21" x2="4" y2="14"/><line x1="4" y1="10" x2="4" y2="3"/><line x1="12" y1="21" x2="12" y2="12"/><line x1="12" y1="8" x2="12" y2="3"/><line x1="20" y1="21" x2="20" y2="16"/><line x1="20" y1="12" x2="20" y2="3"/><line x1="1" y1="14" x2="7" y2="14"/><line x1="9" y1="8" x2="15" y2="8"/><line x1="17" y1="16" x2="23" y2="16"/></svg>
<span class="settings-tab-title">Preferences</span>
</button>
<button class="settings-tab" id="settingsTabProviders" type="button" role="tab" aria-selected="false" aria-controls="settingsPaneProviders" onclick="switchSettingsSection('providers')">
<svg class="settings-tab-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/></svg>
<span class="settings-tab-title" data-i18n="providers_tab_title">Providers</span>
</button>
<button class="settings-tab" id="settingsTabSystem" type="button" role="tab" aria-selected="false" aria-controls="settingsPaneSystem" onclick="switchSettingsSection('system')">
<svg class="settings-tab-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="3" width="20" height="8" rx="2"/><rect x="2" y="13" width="20" height="8" rx="2"/><line x1="6" y1="7" x2="6.01" y2="7"/><line x1="6" y1="17" x2="6.01" y2="17"/></svg>
<span class="settings-tab-title">System</span>
</button>
<div id="mainMemory" class="main-view">
<div class="main-view-header">
<div class="main-view-title" id="memoryDetailTitle"></div>
<div class="main-view-actions">
<button id="btnEditMemoryDetail" class="panel-head-btn" title="Edit" aria-label="Edit" data-i18n-title="edit" onclick="editCurrentMemory()" style="display:none"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg></button>
<button id="btnCancelMemoryDetail" class="panel-head-btn" title="Cancel" data-i18n-title="cancel" onclick="cancelMemoryEdit()" style="display:none"><svg 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 id="btnSaveMemoryDetail" class="panel-head-btn primary" title="Save" data-i18n-title="save" onclick="submitMemorySave()" style="display:none"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg></button>
</div>
<div class="settings-main">
<div class="settings-pane active" id="settingsPaneConversation" role="tabpanel" aria-labelledby="settingsTabConversation">
</div>
<div class="main-view-body" id="memoryDetailBody" style="display:none"></div>
<div class="main-view-empty" id="memoryDetailEmpty">
<svg class="main-view-empty-icon" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96-.44 2.5 2.5 0 0 1-2.96-3.08 3 3 0 0 1-.34-5.58 2.5 2.5 0 0 1 1.32-4.24 2.5 2.5 0 0 1 1.98-3A2.5 2.5 0 0 1 9.5 2z"/><path d="M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96-.44 2.5 2.5 0 0 0 2.96-3.08 3 3 0 0 0 .34-5.58 2.5 2.5 0 0 0-1.32-4.24 2.5 2.5 0 0 0-1.98-3A2.5 2.5 0 0 0 14.5 2z"/></svg>
<div class="main-view-empty-title" data-i18n="memory_empty_title">Select a memory section</div>
<div class="main-view-empty-sub" data-i18n="memory_empty_sub">Pick a section from the sidebar to view or edit its contents.</div>
</div>
</div>
<div id="mainTasks" class="main-view">
<div class="main-view-header">
<div class="main-view-title" id="taskDetailTitle"></div>
<div class="main-view-actions">
<button id="btnRunTaskDetail" class="panel-head-btn" title="Run now" data-i18n-title="cron_run_now" onclick="runCurrentCron()" style="display:none"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polygon points="5 3 19 12 5 21 5 3"/></svg></button>
<button id="btnPauseTaskDetail" class="panel-head-btn" title="Pause" data-i18n-title="cron_pause" onclick="pauseCurrentCron()" style="display:none"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg></button>
<button id="btnResumeTaskDetail" class="panel-head-btn" title="Resume" data-i18n-title="cron_resume" onclick="resumeCurrentCron()" style="display:none"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polygon points="5 3 19 12 5 21 5 3"/></svg></button>
<button id="btnEditTaskDetail" class="panel-head-btn" title="Edit" data-i18n-title="edit" onclick="editCurrentCron()" style="display:none"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg></button>
<button id="btnDeleteTaskDetail" class="panel-head-btn" title="Delete" data-i18n-title="delete_title" onclick="deleteCurrentCron()" style="display:none"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg></button>
<button id="btnCancelTaskDetail" class="panel-head-btn" title="Cancel" data-i18n-title="cancel" onclick="cancelCronForm()" style="display:none"><svg 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 id="btnSaveTaskDetail" class="panel-head-btn primary" title="Save" data-i18n-title="save" onclick="saveCronForm()" style="display:none"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg></button>
</div>
</div>
<div class="main-view-body" id="taskDetailBody" style="display:none"></div>
<div class="main-view-empty" id="taskDetailEmpty">
<svg class="main-view-empty-icon" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
<div class="main-view-empty-title" data-i18n="tasks_empty_title">Select a scheduled job</div>
<div class="main-view-empty-sub" data-i18n="tasks_empty_sub">Pick a job from the sidebar to view its details and runs, or create a new one.</div>
</div>
</div>
<div id="mainWorkspaces" class="main-view">
<div class="main-view-header">
<div class="main-view-title" id="workspaceDetailTitle"></div>
<div class="main-view-actions">
<button id="btnActivateWorkspaceDetail" class="panel-head-btn" title="Use this space" data-i18n-title="workspace_use_title" onclick="activateCurrentWorkspace()" style="display:none"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg></button>
<button id="btnEditWorkspaceDetail" class="panel-head-btn" title="Rename" data-i18n-title="edit" onclick="editCurrentWorkspace()" style="display:none"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg></button>
<button id="btnDeleteWorkspaceDetail" class="panel-head-btn" title="Remove" data-i18n-title="remove" onclick="deleteCurrentWorkspace()" style="display:none"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg></button>
<button id="btnCancelWorkspaceDetail" class="panel-head-btn" title="Cancel" data-i18n-title="cancel" onclick="cancelWorkspaceForm()" style="display:none"><svg 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 id="btnSaveWorkspaceDetail" class="panel-head-btn primary" title="Save" data-i18n-title="save" onclick="saveWorkspaceForm()" style="display:none"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg></button>
</div>
</div>
<div class="main-view-body" id="workspaceDetailBody" style="display:none"></div>
<div class="main-view-empty" id="workspaceDetailEmpty">
<svg class="main-view-empty-icon" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" 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>
<div class="main-view-empty-title" data-i18n="workspaces_empty_title">Select a space</div>
<div class="main-view-empty-sub" data-i18n="workspaces_empty_sub">Pick a space from the sidebar to view its files and settings, or add a new one.</div>
</div>
</div>
<div id="mainProfiles" class="main-view">
<div class="main-view-header">
<div class="main-view-title" id="profileDetailTitle"></div>
<div class="main-view-actions">
<button id="btnActivateProfileDetail" class="panel-head-btn" title="Activate" data-i18n-title="profile_switch_title" onclick="activateCurrentProfile()" style="display:none"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg></button>
<button id="btnDeleteProfileDetail" class="panel-head-btn" title="Delete" data-i18n-title="profile_delete_title" onclick="deleteCurrentProfile()" style="display:none"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg></button>
<button id="btnCancelProfileDetail" class="panel-head-btn" title="Cancel" data-i18n-title="cancel" onclick="cancelProfileForm()" style="display:none"><svg 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 id="btnSaveProfileDetail" class="panel-head-btn primary" title="Save" data-i18n-title="save" onclick="saveProfileForm()" style="display:none"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg></button>
</div>
</div>
<div class="main-view-body" id="profileDetailBody" style="display:none"></div>
<div class="main-view-empty" id="profileDetailEmpty">
<svg class="main-view-empty-icon" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
<div class="main-view-empty-title" data-i18n="profiles_empty_title">Select a profile</div>
<div class="main-view-empty-sub" data-i18n="profiles_empty_sub">Pick an agent profile from the sidebar to view and edit its settings, or create a new one.</div>
</div>
</div>
<div id="mainSettings" class="main-view">
<div class="settings-main">
<div class="settings-pane active" id="settingsPaneConversation">
<div class="settings-section-head">
<div>
<div class="settings-section-title">Conversation</div>
@@ -534,7 +522,7 @@
</div>
<input type="file" id="importFileInput" accept=".json" style="display:none">
</div>
<div class="settings-pane" id="settingsPaneAppearance" role="tabpanel" aria-labelledby="settingsTabAppearance">
<div class="settings-pane" id="settingsPaneAppearance">
<div class="settings-section-head">
<div>
<div class="settings-section-title">Appearance</div>
@@ -597,7 +585,7 @@
</div>
<button class="sm-btn" onclick="saveSettings()" style="margin-top:12px;width:100%;padding:8px;font-weight:600" data-i18n="settings_save_btn">Save Settings</button>
</div>
<div class="settings-pane" id="settingsPanePreferences" role="tabpanel" aria-labelledby="settingsTabPreferences">
<div class="settings-pane" id="settingsPanePreferences">
<div class="settings-section-head">
<div>
<div class="settings-section-title">Preferences</div>
@@ -676,21 +664,21 @@
</div>
<button class="sm-btn" onclick="saveSettings()" style="margin-top:12px;width:100%;padding:8px;font-weight:600" data-i18n="settings_save_btn">Save Settings</button>
</div>
<div class="settings-pane" id="settingsPaneProviders" role="tabpanel" aria-labelledby="settingsTabProviders">
<div class="settings-pane" id="settingsPaneProviders">
<div class="settings-section-head">
<div>
<div class="settings-section-title" data-i18n="providers_section_title">Providers</div>
<div class="settings-section-meta" data-i18n="providers_section_meta">Manage API keys for AI providers. Changes take effect immediately.</div>
</div>
</div>
<div id="providersList" style="display:flex;flex-direction:column;gap:6px;margin-top:4px">
<div id="providersList" style="display:flex;flex-direction:column;margin-top:4px">
<!-- Populated dynamically by loadProvidersPanel() -->
</div>
<div id="providersEmpty" style="display:none;text-align:center;padding:32px 0;color:var(--muted);font-size:13px" data-i18n="providers_empty">
No configurable providers found.
</div>
</div>
<div class="settings-pane" id="settingsPaneSystem" role="tabpanel" aria-labelledby="settingsTabSystem">
<div class="settings-pane" id="settingsPaneSystem">
<div class="settings-section-head">
<div>
<div class="settings-section-title">System</div>
@@ -698,7 +686,7 @@
</div>
<span class="settings-version-badge"></span>
</div>
<div class="settings-field" style="border-top:1px solid var(--border);padding-top:12px;margin-top:8px">
<div class="settings-field">
<label for="settingsPassword" data-i18n="settings_label_password">Access Password</label>
<div style="font-size:11px;color:var(--muted);margin-bottom:6px" data-i18n="settings_desc_password">Enter a new password to set or change it. Leave blank to keep current setting.</div>
<input type="password" id="settingsPassword" placeholder="Enter new password…" data-i18n-placeholder="password_placeholder" style="width:100%;padding:8px;background:var(--code-bg);color:var(--text);border:1px solid var(--border2);border-radius:6px;font-size:13px">
@@ -706,8 +694,58 @@
<button class="sm-btn" id="btnDisableAuth" onclick="disableAuth()" style="margin-top:6px;width:100%;padding:8px;font-weight:600;color:#e8a030;border-color:rgba(232,160,48,.3);display:none" data-i18n="disable_auth">Disable Auth</button>
<button class="sm-btn" id="btnSignOut" onclick="signOut()" style="margin-top:6px;width:100%;padding:8px;font-weight:600;color:var(--accent);border-color:rgba(233,69,96,.3);display:none" data-i18n="sign_out">Sign Out</button>
</div>
</div>
</div>
</div>
</main>
<aside class="rightpanel">
<div class="resize-handle" id="rightpanelResize"></div>
<div class="panel-header">
<span>Workspace</span>
<span class="git-badge" id="gitBadge" style="display:none"></span>
<div class="panel-actions">
<button class="panel-icon-btn" id="btnCollapseWorkspacePanel" title="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>
<button class="panel-icon-btn" id="btnUpDir" title="Parent directory" onclick="navigateUp()" style="display:none"><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="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg></button>
<button class="panel-icon-btn" id="btnNewFile" title="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" id="btnNewFolder" title="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" id="btnRefreshPanel" title="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 close-preview" id="btnClearPreview" title="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>
<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">
<div class="preview-path" id="previewPath">
<span id="previewPathText"></span>
<span class="preview-badge" id="previewBadge"></span>
<button id="btnDownloadFile" class="panel-icon-btn" style="margin-left:auto;font-size:12px;width:auto;padding:2px 8px;display:inline-flex;align-items:center;gap:4px" onclick="downloadFile(_previewCurrentPath)" title="Download file to your computer"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg> Download</button>
<button id="btnEditFile" class="panel-icon-btn" style="font-size:12px;width:auto;padding:2px 8px;display:none;align-items:center;gap:4px" onclick="toggleEditMode()"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg> Edit</button>
</div>
<pre class="preview-code" id="previewCode"></pre>
<div class="preview-img-wrap" id="previewImgWrap" style="display:none"><img class="preview-img" id="previewImg" src="" alt=""></div>
<div class="preview-md" id="previewMd" style="display:none"></div>
<textarea id="previewEditArea" style="display:none;flex:1;width:100%;background:var(--code-bg);color:var(--pre-text);border:1px solid var(--border2);border-radius:8px;padding:12px;font-family:'SF Mono',ui-monospace,monospace;font-size:12px;line-height:1.6;resize:none;outline:none" oninput="_previewDirty=true;updateEditBtn()"></textarea>
</div>
</aside>
</div>
<div class="onboarding-overlay" id="onboardingOverlay" style="display:none" role="dialog" aria-modal="true" aria-labelledby="onboardingTitle">
<div class="onboarding-card">
<div class="onboarding-shell">
<div class="onboarding-sidebar">
<div class="onboarding-badge" data-i18n="onboarding_badge">FIRST RUN</div>
<h2 id="onboardingTitle" data-i18n="onboarding_title">Welcome to Hermes Web UI</h2>
<p id="onboardingLead" data-i18n="onboarding_lead">A quick guided setup will check your Hermes install, choose a workspace and model, and optionally protect the app with a password.</p>
<div class="onboarding-steps" id="onboardingSteps"></div>
</div>
<div class="onboarding-main">
<div class="onboarding-status" id="onboardingNotice"></div>
<div class="onboarding-body" id="onboardingBody"></div>
<div class="onboarding-actions">
<button class="sm-btn" id="onboardingBackBtn" onclick="prevOnboardingStep()" style="display:none" data-i18n="onboarding_back">Back</button>
<button class="sm-btn" id="onboardingSkipBtn" onclick="skipOnboarding()" style="margin-right:auto;opacity:.7" data-i18n="onboarding_skip">Skip setup</button>
<button class="sm-btn" id="onboardingNextBtn" onclick="nextOnboardingStep()" style="font-weight:700;color:var(--blue);border-color:rgba(124,185,255,.32)" data-i18n="onboarding_continue">Continue</button>
</div>
</div>
</div>
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -961,11 +961,13 @@ async function deleteSession(sid){
if(remaining.sessions&&remaining.sessions.length){
await loadSession(remaining.sessions[0].session_id);
}else{
$('topbarTitle').textContent=window._botName||'Hermes';
$('topbarMeta').textContent='Start a new conversation';
const _tt=$('topbarTitle');if(_tt)_tt.textContent=window._botName||'Hermes';
const _tm=$('topbarMeta');if(_tm)_tm.textContent='Start a new conversation';
$('msgInner').innerHTML='';
$('emptyState').style.display='';
$('fileTree').innerHTML='';
if(typeof S!=='undefined') S.session=null;
if(typeof syncAppTitlebar==='function') syncAppTitlebar();
}
}
showToast('Conversation deleted');

View File

@@ -113,7 +113,6 @@
:root:not(.dark) ::-webkit-scrollbar-thumb:hover{background:rgba(0,0,0,.3);}
:root:not(.dark) ::selection{background:var(--accent-bg-strong);}
:root:not(.dark) *{scrollbar-color:rgba(0,0,0,.15) transparent;}
:root:not(.dark) .settings-overlay{background:rgba(0,0,0,.3);}
/* ── Light mode: sidebar, roles, chips, active states ── */
:root:not(.dark) .session-item{color:#5a544a;}
:root:not(.dark) .session-item:hover{background:rgba(0,0,0,.06);color:#2c2825;}
@@ -135,8 +134,8 @@
:root:not(.dark) .chip.model{color:var(--accent-text);border-color:var(--accent-bg-strong);background:var(--accent-bg);}
:root:not(.dark) .new-chat-btn{border-color:var(--accent-bg-strong);color:var(--accent-text);}
:root:not(.dark) .new-chat-btn:hover{background:var(--accent-bg);}
:root:not(.dark) .session-search input{border-color:rgba(0,0,0,.1);background:rgba(0,0,0,.03);}
:root:not(.dark) .session-search input:focus{border-color:var(--accent);background:rgba(0,0,0,.02);}
:root:not(.dark) .sidebar-search input{border-color:rgba(0,0,0,.1);background:rgba(0,0,0,.03);}
:root:not(.dark) .sidebar-search input:focus{border-color:var(--accent);background:rgba(0,0,0,.02);}
:root:not(.dark) .cron-item{border-color:rgba(0,0,0,.08);background:rgba(0,0,0,.02);}
:root:not(.dark) .sm-btn{border-color:rgba(0,0,0,.1);}
:root:not(.dark) .sm-btn:hover{background:rgba(0,0,0,.06);border-color:rgba(0,0,0,.15);}
@@ -205,8 +204,17 @@
:root:not(.dark) .cron-btn:hover{background:rgba(0,0,0,.08);}
/* ── Smooth dark mode transitions ── */
body,header,footer,aside,nav,main,div,button,input,textarea,select{transition-property:background-color,border-color,color;transition-duration:.15s;transition-timing-function:ease;}
body{background:var(--bg);color:var(--text);height:100vh;height:100dvh;overflow:hidden;display:flex;}
.layout{display:flex;width:100%;height:100vh;height:100dvh;min-height:0;}
body{background:var(--bg);color:var(--text);height:100vh;height:100dvh;overflow:hidden;display:flex;flex-direction:column;}
.layout{display:flex;width:100%;flex:1 1 auto;min-height:0;}
.app-titlebar{display:flex;align-items:center;justify-content:space-between;height:38px;flex-shrink:0;background:var(--sidebar);border-bottom:1px solid var(--border);padding:0 12px;padding-top:env(safe-area-inset-top,0);padding-left:max(12px,env(safe-area-inset-left,0));padding-right:max(12px,env(safe-area-inset-right,0));box-sizing:content-box;font-size:12px;color:var(--muted);user-select:none;-webkit-app-region:drag;position:relative;z-index:20;}
.app-titlebar-inner{display:flex;align-items:center;gap:8px;min-width:0;max-width:100%;flex:1 1 auto;justify-content:center;}
.app-titlebar-icon{display:inline-flex;align-items:center;color:var(--accent);}
.app-titlebar-title{font-size:12px;font-weight:600;color:var(--text);letter-spacing:-.01em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:60vw;}
.app-titlebar-sub{font-size:10px;color:var(--muted);background:var(--hover-bg);padding:2px 7px;border-radius:4px;font-family:'SF Mono',ui-monospace,monospace;white-space:nowrap;flex-shrink:0;}
.app-titlebar-sub[hidden]{display:none;}
.app-titlebar-hamburger,.app-titlebar-spacer{display:none;width:32px;height:32px;flex-shrink:0;}
.app-titlebar-hamburger{-webkit-app-region:no-drag;align-items:center;justify-content:center;background:none;border:none;color:var(--muted);border-radius:8px;cursor:pointer;padding:0;-webkit-tap-highlight-color:transparent;transition:background-color .15s,color .15s;}
.app-titlebar-hamburger:hover{background:var(--hover-bg);color:var(--text);}
.sidebar{width:300px;background:var(--sidebar);border-right:1px solid var(--border);display:flex;flex-direction:column;overflow:visible;flex-shrink:0;}
.sidebar-header{padding:16px 18px 14px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:10px;}
.logo{width:32px;height:32px;border-radius:9px;background:linear-gradient(145deg,var(--accent-hover),var(--accent));display:flex;align-items:center;justify-content:center;font-weight:800;font-size:14px;color:#fff;flex-shrink:0;box-shadow:0 2px 8px var(--accent-bg-strong);}
@@ -215,10 +223,11 @@
.new-chat-btn{width:100%;padding:9px 12px;border-radius:9px;background:var(--accent-bg);border:1px solid var(--accent-bg-strong);color:var(--accent-text);font-size:13px;cursor:pointer;display:flex;align-items:center;gap:8px;transition:all .15s;margin-bottom:8px;font-weight:500;}
.new-chat-btn:hover{background:var(--accent-bg-strong);border-color:var(--accent);}
.session-list{flex:1;overflow-y:auto;padding:0 8px 8px;min-height:0;}
.session-search{padding:4px 10px 8px;flex-shrink:0;}
.session-search input{width:100%;background:var(--input-bg);border:1px solid var(--border);border-radius:8px;color:var(--text);padding:10px 12px;font-size:12px;outline:none;transition:all .15s;}
.session-search input:focus{border-color:var(--accent);background:var(--hover-bg);box-shadow:0 0 0 2px var(--accent-bg);}
.session-search input::placeholder{color:var(--muted);opacity:.7;}
.sidebar-search{position:relative;padding:8px 12px;flex-shrink:0;}
.sidebar-search input{width:100%;background:var(--bg);border:1px solid var(--border);border-radius:8px;color:var(--text);padding:7px 10px 7px 32px;font-size:13px;outline:none;transition:border-color .15s,box-shadow .15s,background .15s;box-sizing:border-box;}
.sidebar-search input:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-bg);}
.sidebar-search input::placeholder{color:var(--muted);opacity:.7;}
.sidebar-search-icon{position:absolute;left:22px;top:50%;transform:translateY(-50%);width:14px;height:14px;color:var(--muted);opacity:.7;pointer-events:none;}
/* Inline session title edit */
.session-title-input{flex:1;background:var(--surface);border:1px solid var(--accent);border-radius:6px;color:var(--text);padding:3px 8px;font-size:13px;outline:none;min-width:0;box-shadow:0 0 0 2px var(--accent-bg-strong);font-family:inherit;}
.session-item{padding:8px 40px 8px 8px;margin-bottom:2px;border-radius:8px;cursor:pointer;font-size:13px;color:var(--muted);transition:background .15s,color .15s;display:flex;align-items:flex-start;gap:8px;min-width:0;position:relative;}
@@ -304,8 +313,11 @@
.app-dialog-btn.confirm.danger{border-color:var(--error);background:rgba(239,83,80,.12);color:var(--error);}
.app-dialog-btn.confirm.danger:hover{background:rgba(239,83,80,.2);border-color:var(--error);}
.app-dialog-btn:focus-visible,.app-dialog-close:focus-visible{outline:2px solid var(--accent);outline-offset:2px;}
.toast{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:var(--surface);backdrop-filter:blur(12px);border:1px solid var(--accent-bg-strong);color:var(--text);font-size:13px;padding:10px 20px;border-radius:12px;pointer-events:none;opacity:0;transition:opacity .2s,transform .2s;z-index:100;box-shadow:0 4px 20px rgba(0,0,0,.3);letter-spacing:.01em;}
.toast.show{opacity:1;transform:translateX(-50%) translateY(-2px);}
.toast{position:fixed;top:24px;right:24px;left:auto;bottom:auto;transform:translateY(-6px);background:color-mix(in srgb,var(--accent) 14%,var(--surface));border:1px solid color-mix(in srgb,var(--accent) 45%,var(--surface));color:var(--accent-text);font-size:13px;font-weight:500;padding:10px 16px;border-radius:10px;pointer-events:none;opacity:0;transition:opacity .2s,transform .2s;z-index:100;box-shadow:0 6px 24px rgba(0,0,0,.12);letter-spacing:.01em;max-width:min(420px,calc(100vw - 48px));}
.toast.show{opacity:1;transform:translateY(0);}
.toast.success{background:color-mix(in srgb,var(--success) 14%,var(--surface));border-color:color-mix(in srgb,var(--success) 45%,var(--surface));color:var(--success);}
.toast.error{background:color-mix(in srgb,var(--error) 14%,var(--surface));border-color:color-mix(in srgb,var(--error) 45%,var(--surface));color:var(--error);}
.toast.warning{background:color-mix(in srgb,var(--warning) 14%,var(--surface));border-color:color-mix(in srgb,var(--warning) 45%,var(--surface));color:var(--warning);}
.onboarding-overlay{position:fixed;inset:0;z-index:1050;background:rgba(7,12,19,.78);backdrop-filter:blur(8px);display:none;align-items:center;justify-content:center;padding:24px;}
.onboarding-card{width:min(980px,100%);max-height:min(760px,94vh);overflow:auto;border:1px solid var(--accent-bg-strong);border-radius:24px;background:linear-gradient(180deg,rgba(20,30,44,.98),rgba(11,17,27,.98));box-shadow:0 24px 80px rgba(0,0,0,.45);}
.onboarding-shell{display:grid;grid-template-columns:minmax(240px,300px) minmax(0,1fr);}
@@ -417,6 +429,17 @@
.clarify-submit.loading{opacity:.75;cursor:wait;}
.clarify-hint{margin-top:8px;font-size:11px;line-height:1.45;color:var(--muted);}
.clarify-card.visible .clarify-question{padding-left:1px;}
/* Left rail (desktop primary navigation) */
.rail{display:none;width:48px;flex-shrink:0;flex-direction:column;align-items:center;gap:4px;padding:8px 0;background:var(--sidebar);border-right:1px solid var(--border);}
.rail-btn{width:36px;height:36px;border-radius:8px;border:none;background:none;color:var(--muted);cursor:pointer;display:flex;align-items:center;justify-content:center;position:relative;transition:color .15s,background .15s;flex-shrink:0;padding:0;}
.rail-btn:hover{color:var(--text);background:var(--hover-bg);}
.rail-btn.active{color:var(--accent-text);background:var(--accent-bg);}
.rail-btn.active::before{content:'';position:absolute;left:-6px;top:50%;transform:translateY(-50%);width:3px;height:16px;background:var(--accent);border-radius:0 2px 2px 0;}
.rail-spacer{flex:1;min-height:8px;}
.rail .nav-tab{flex:0 0 auto;padding:0;font-size:inherit;border-bottom:none;overflow:visible;}
.rail .nav-tab:hover::after{content:none;}
.rail .nav-tab.active::before{content:'';position:absolute;left:-6px;top:50%;bottom:auto;transform:translateY(-50%);width:3px;height:16px;background:var(--accent);border-radius:0 2px 2px 0;}
@media(min-width:768px){.rail{display:flex;}.sidebar > .sidebar-nav{display:none;}}
/* Sidebar navigation tabs */
.sidebar-nav{display:flex;border-bottom:1px solid var(--border);flex-shrink:0;padding:6px 8px 0;gap:2px;}
.nav-tab{flex:1;padding:10px 4px 8px;font-size:20px;text-align:center;cursor:pointer;color:var(--muted);border:none;background:none;transition:color .15s;border-bottom:2px solid transparent;white-space:nowrap;overflow:hidden;position:relative;display:flex;align-items:center;justify-content:center;}
@@ -429,7 +452,7 @@
.panel-view.active{display:flex;}
/* Cron panel */
.cron-list{flex:1;overflow-y:auto;padding:8px;}
.cron-item{border-radius:10px;border:1px solid var(--border);margin-bottom:6px;overflow:hidden;transition:border-color .15s,background .15s;background:rgba(255,255,255,.02);}
.cron-item{width:100%;min-width:0;box-sizing:border-box;border-radius:10px;border:1px solid var(--border);margin-bottom:6px;overflow:hidden;transition:border-color .15s,background .15s;background:rgba(255,255,255,.02);cursor:pointer;}
.cron-item:hover{border-color:var(--border2);}
.cron-header{display:flex;align-items:center;gap:8px;padding:10px 12px;cursor:pointer;}
.cron-name{flex:1;font-size:13px;color:var(--text);font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
@@ -451,14 +474,11 @@
.cron-last{font-size:11px;color:var(--muted);border-top:1px solid var(--border);padding-top:8px;max-height:220px;overflow-y:auto;white-space:pre-wrap;line-height:1.5;word-break:break-word;}
.cron-last-header{font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);margin-bottom:4px;}
/* Skills panel */
.skills-search{padding:8px;flex-shrink:0;}
.skills-search input{width:100%;background:var(--hover-bg);border:1px solid var(--border2);border-radius:8px;color:var(--text);padding:8px 10px;font-size:12px;outline:none;}
.skills-search input::placeholder{color:var(--muted);}
.skills-list{flex:1;overflow-y:auto;padding:0 8px 8px;}
.skills-category{margin-bottom:4px;}
.skills-cat-header{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);padding:8px 6px 4px;cursor:pointer;display:flex;align-items:center;gap:4px;}
.skills-cat-header:hover{color:var(--text);}
.skill-item{padding:8px 10px;border-radius:8px;cursor:pointer;font-size:12px;color:var(--muted);display:flex;align-items:flex-start;gap:6px;transition:all .12s;line-height:1.4;}
.skill-item{width:100%;min-width:0;box-sizing:border-box;padding:8px 10px;border-radius:8px;cursor:pointer;font-size:12px;color:var(--muted);display:flex;align-items:flex-start;gap:6px;transition:all .12s;line-height:1.4;}
.skill-item:hover{background:var(--hover-bg);color:var(--text);}
.skill-item.active{background:var(--accent-bg);color:var(--accent-text);}
.skill-name{font-weight:500;flex-shrink:0;}
@@ -471,16 +491,8 @@
.memory-content{font-size:12px;line-height:1.7;color:var(--text);}
.memory-content p{margin-bottom:6px;}
.memory-empty{color:var(--muted);font-size:12px;font-style:italic;}
.sidebar-bottom{border-top:1px solid var(--border);padding:12px 14px;flex-shrink:0;position:relative;z-index:10;overflow:visible;}
.hermes-launch-btn{width:100%;display:flex;align-items:center;gap:12px;padding:11px 12px;border-radius:12px;border:1px solid var(--border2);background:linear-gradient(180deg,rgba(255,255,255,.05),rgba(255,255,255,.03));color:var(--text);cursor:pointer;transition:background .15s,border-color .15s,transform .15s;text-align:left;}
.hermes-launch-btn:hover{background:rgba(255,255,255,.08);border-color:var(--accent-bg);transform:translateY(-1px);}
.hermes-launch-icon{width:32px;height:32px;border-radius:10px;background:linear-gradient(145deg,var(--accent-bg),var(--accent-bg));border:1px solid var(--accent-bg);display:flex;align-items:center;justify-content:center;flex-shrink:0;overflow:hidden;box-shadow:0 4px 16px var(--accent-bg);}
.hermes-launch-icon svg{display:block;width:22px;height:22px;flex-shrink:0;}
.hermes-launch-copy{display:flex;flex-direction:column;min-width:0;flex:1;}
.hermes-launch-title{font-size:13px;font-weight:700;letter-spacing:.01em;color:var(--text);}
.hermes-launch-meta{font-size:11px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.hermes-launch-chevron{color:var(--muted);flex-shrink:0;}
.field-label{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);margin-bottom:5px;opacity:.8;}
.memory-detail-mtime{font-size:11px;color:var(--muted);opacity:.7;margin-bottom:12px;}
.field-label{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);margin-bottom:5px;opacity:.8;}
select{width:100%;background:var(--input-bg);border:1px solid var(--border2);border-radius:8px;color:var(--text);padding:8px 28px 8px 10px;font-size:12px;outline:none;appearance:none;margin-bottom:6px;cursor:pointer;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%238888aa' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 10px center;}
select:focus{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-bg);}
optgroup{color:var(--muted);font-size:11px;font-weight:700;}
@@ -586,11 +598,17 @@
.composer-profile-icon,.composer-profile-chevron{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;line-height:1;}
.composer-profile-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.composer-ws-wrap{position:relative;flex:0 1 auto;min-width:0;}
.composer-workspace-chip{display:inline-flex;align-items:center;gap:8px;max-width:240px;padding:8px 10px 8px 12px;border-radius:999px;border:1px solid transparent;background-color:transparent;color:var(--muted);font-weight:500;cursor:pointer;transition:color .15s,background-color .15s,border-color .15s;}
.composer-workspace-chip:hover{color:var(--text);background-color:var(--hover-bg);}
.composer-workspace-group{display:inline-flex;align-items:stretch;max-width:240px;border-radius:999px;overflow:hidden;background-color:transparent;transition:background-color .15s;}
.composer-workspace-group:hover{background-color:var(--hover-bg);}
.composer-workspace-group:hover .composer-workspace-files-btn,
.composer-workspace-group:hover .composer-workspace-chip{color:var(--text);}
.composer-workspace-files-btn{display:inline-flex;align-items:center;justify-content:center;padding:8px 10px 8px 12px;background-color:transparent;border:none;border-radius:999px 0 0 999px;color:var(--muted);cursor:pointer;transition:color .15s;-webkit-tap-highlight-color:transparent;}
.composer-workspace-files-btn:disabled{opacity:.45;cursor:not-allowed;}
.composer-workspace-files-btn.active{color:var(--accent-text);background:var(--accent-bg);}
.composer-workspace-chip{display:inline-flex;align-items:center;gap:8px;min-width:0;max-width:200px;padding:8px 12px 8px 10px;border:none;border-left:1px solid transparent;border-radius:0 999px 999px 0;background-color:transparent;color:var(--muted);font-weight:500;cursor:pointer;transition:color .15s,border-color .15s;}
.composer-workspace-group:hover .composer-workspace-files-btn+.composer-workspace-chip{border-left-color:var(--border2);}
.composer-workspace-chip:disabled{opacity:.45;cursor:not-allowed;}
.composer-workspace-chip:disabled:hover{color:var(--muted);background-color:transparent;}
.composer-workspace-chip.active{color:var(--text);background:var(--accent-bg);border-color:var(--accent-bg);}
.composer-workspace-chip.active{color:var(--text);background:var(--accent-bg);}
.composer-workspace-icon,.composer-workspace-chevron{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;line-height:1;}
.composer-workspace-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.composer-reasoning-wrap{position:relative;flex:0 1 auto;min-width:0;}
@@ -599,11 +617,11 @@
.composer-reasoning-chip.active{color:var(--text);background:var(--accent-bg);border-color:var(--accent-bg);}
.composer-reasoning-icon,.composer-reasoning-chevron{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;line-height:1;}
.composer-reasoning-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.composer-reasoning-dropdown{display:none;position:absolute;bottom:calc(100% + 4px);left:0;background:var(--surface);border:1px solid var(--border2);border-radius:10px;box-shadow:0 -4px 24px rgba(0,0,0,.4);min-width:140px;z-index:200;padding:4px;animation:dropdown-in .12s ease-out;}
.composer-reasoning-dropdown{display:none;position:absolute;bottom:calc(100% + 4px);left:0;min-width:140px;background:var(--surface);border:1px solid var(--border2);border-radius:10px;box-shadow:0 -4px 24px rgba(0,0,0,.4);z-index:200;padding:4px;overflow:hidden;}
.composer-reasoning-dropdown.open{display:block;}
.reasoning-option{padding:8px 14px;border-radius:6px;cursor:pointer;font-size:13px;color:var(--text);white-space:nowrap;transition:background-color .1s;}
.reasoning-option:hover{background:var(--hover-bg);}
.reasoning-option.selected{color:var(--accent);font-weight:600;}
.reasoning-option{padding:8px 14px;border-radius:6px;cursor:pointer;font-size:13px;color:var(--text);white-space:nowrap;transition:background-color .12s;}
.reasoning-option:hover{background:rgba(255,255,255,.07);}
.reasoning-option.selected{background:var(--accent-bg);}
.composer-model-wrap{position:relative;flex:0 1 auto;min-width:0;}
.composer-model-chip{display:inline-flex;align-items:center;gap:8px;max-width:220px;padding:8px 10px 8px 12px;border-radius:999px;border:1px solid transparent;background-color:transparent;color:var(--muted);font-weight:500;cursor:pointer;transition:color .15s,background-color .15s,border-color .15s;}
.composer-model-chip:hover{color:var(--text);background-color:var(--hover-bg);}
@@ -739,14 +757,11 @@
@media(max-width:640px){
/* ── Sidebar: slide-in overlay instead of hidden ── */
.sidebar{position:fixed;left:-300px;top:0;bottom:0;width:280px;z-index:200;
transition:left .25s ease;box-shadow:4px 0 24px rgba(0,0,0,.4);}
transition:left .25s ease;}
.sidebar.mobile-open{left:0;}
.sidebar .resize-handle{display:none;}
/* Hamburger button */
.mobile-hamburger{display:flex;align-items:center;justify-content:center;
background:none;border:none;color:var(--muted);cursor:pointer;padding:4px;
flex-shrink:0;-webkit-tap-highlight-color:transparent;}
.mobile-hamburger:hover{color:var(--text);}
/* Hamburger button (in app titlebar on mobile) */
.app-titlebar-hamburger,.app-titlebar-spacer{display:flex;}
/* Overlay backdrop */
.mobile-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.5);
z-index:199;-webkit-tap-highlight-color:transparent;}
@@ -759,8 +774,6 @@
box-shadow:-4px 0 24px rgba(0,0,0,.4);}
.rightpanel.mobile-open{right:0;}
.rightpanel .resize-handle{display:none;}
/* Keep the Hermes control available at the bottom of the mobile sidebar */
.sidebar-bottom{display:block;padding:10px;}
/* Topbar adjustments */
.topbar{padding:8px 12px;gap:8px;}
.topbar-title{font-size:14px;}
@@ -782,13 +795,15 @@
.composer-model-label,
.composer-reasoning-label,
.composer-profile-chevron,
.composer-workspace-chevron,
.composer-model-chevron,
.composer-reasoning-chevron{display:none;}
.composer-profile-chip,
.composer-workspace-chip,
.composer-model-chip,
.composer-reasoning-chip{max-width:44px;min-width:44px;min-height:44px;padding:6px;justify-content:center;gap:0;font-size:11px;}
/* Workspace group: keep split layout on mobile — files icon + chevron-only picker */
.composer-workspace-group{min-height:44px;}
.composer-workspace-files-btn{min-width:44px;padding:6px 8px;}
.composer-workspace-chip{min-width:32px;padding:6px 8px;gap:0;}
.composer-divider{display:none;}
.composer-status{max-width:96px;font-size:10px;}
.send-btn{width:32px;height:32px;}
@@ -822,8 +837,6 @@
.app-dialog-btn{width:100%;min-height:44px;}
/* Tool cards */
.tool-card{margin-left:0!important;font-size:12px;}
/* Settings modal */
.settings-panel{width:95vw;max-width:95vw;min-height:min(580px,88vh);max-height:92vh;}
.onboarding-overlay{padding:12px;}
.onboarding-shell{grid-template-columns:1fr;}
.onboarding-sidebar{border-right:none;border-bottom:1px solid var(--border);padding:22px 18px;}
@@ -870,11 +883,13 @@
.ws-opt-icon{display:inline-flex;align-items:center;justify-content:center;opacity:.82;flex-shrink:0;}
.ws-opt-meta{font-size:11px;color:var(--muted);}
/* ── Workspace management panel ── */
.ws-row{display:flex;align-items:center;gap:8px;padding:8px 0;border-bottom:1px solid var(--border);}
.ws-row:last-of-type{border-bottom:none;}
.ws-row{display:flex;align-items:center;gap:8px;padding:8px 10px;margin-bottom:2px;border:1px solid transparent;border-radius:8px;cursor:pointer;color:var(--muted);transition:background .15s,border-color .15s,color .15s;}
.ws-row:hover{background:var(--surface);color:var(--text);}
.ws-row.active{background:var(--accent-bg);border-color:var(--accent-bg-strong);color:var(--accent-text);}
.ws-row-info{flex:1;min-width:0;}
.ws-row-name{font-size:13px;font-weight:500;color:var(--text);}
.ws-row-name{font-size:13px;font-weight:500;color:inherit;}
.ws-row-path{font-size:11px;color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.ws-row.active .ws-row-path{color:var(--accent-text);opacity:.8;}
.ws-row-actions{display:flex;gap:4px;flex-shrink:0;}
.ws-action-btn{padding:4px 8px;border-radius:6px;font-size:11px;font-weight:600;border:1px solid var(--border2);background:rgba(255,255,255,.05);color:var(--muted);cursor:pointer;transition:all .15s;white-space:nowrap;}
.ws-action-btn:hover{background:rgba(255,255,255,.1);color:var(--text);}
@@ -890,12 +905,14 @@
.profile-opt-badge{display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:5px;vertical-align:middle;}
.profile-opt-badge.running{background:#4caf50;box-shadow:0 0 4px rgba(76,175,80,.5);}
.profile-opt-badge.stopped{background:rgba(255,255,255,.2);}
.profile-card{padding:10px 0;border-bottom:1px solid var(--border);}
.profile-card:last-of-type{border-bottom:none;}
.profile-card{padding:8px 10px;margin-bottom:2px;border:1px solid transparent;border-radius:8px;cursor:pointer;color:var(--muted);transition:background .15s,border-color .15s,color .15s;}
.profile-card:hover{background:var(--surface);color:var(--text);}
.profile-card.active{background:var(--accent-bg);border-color:var(--accent-bg-strong);color:var(--accent-text);}
.profile-card-header{display:flex;align-items:center;justify-content:space-between;gap:8px;}
.profile-card-name{font-size:13px;font-weight:600;color:var(--text);}
.profile-card-name{font-size:13px;font-weight:600;color:inherit;}
.profile-card-name.is-active{color:var(--accent-text);}
.profile-card-meta{font-size:11px;color:var(--muted);margin-top:3px;padding-left:12px;}
.profile-card.active .profile-card-meta{color:var(--accent-text);opacity:.8;}
.profile-card-actions{display:flex;gap:4px;flex-shrink:0;}
/* ── Slash command autocomplete dropdown ── */
.cmd-dropdown{display:none;position:absolute;left:0;right:0;bottom:calc(100% + 4px);width:auto;max-width:100%;background:var(--surface);border:1px solid var(--border2);border-radius:10px;box-shadow:0 -8px 24px rgba(0,0,0,.4);z-index:200;max-height:240px;overflow-y:auto;}
@@ -985,9 +1002,6 @@
/* Right panel icons: tighter */
.panel-actions{gap:2px;}
/* Workspace hint text: no intrusion */
.sidebar-bottom > div[style*="topbar"]{pointer-events:none;}
/* Topbar: border should match the subtle sidebar border */
:root.dark .topbar{border-bottom:1px solid rgba(255,255,255,.07);}
@@ -1011,8 +1025,6 @@
padding: 8px 10px 3px !important;
font-size: 10px !important;
}
/* Sidebar bottom: tighten spacing */
.sidebar-bottom { padding: 10px 14px 12px; }
/* Right panel file tree: more padding for breathing room */
/* Composer footer: even spacing */
@@ -1114,6 +1126,13 @@ body.resizing{user-select:none;cursor:col-resize;}
.skill-tag{background:var(--accent-bg);border:1px solid var(--accent-bg);border-radius:12px;padding:2px 8px;font-size:11px;color:var(--accent-text);display:flex;align-items:center;gap:4px;}
.remove-tag{cursor:pointer;opacity:.6;font-size:13px;line-height:1;}
.remove-tag:hover{opacity:1;color:var(--accent);}
/* Skill frontmatter (collapsible metadata) */
.skill-frontmatter{margin-bottom:16px;border:1px solid var(--border);border-radius:8px;background:var(--surface);overflow:hidden;}
.skill-frontmatter>summary{padding:8px 12px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);cursor:pointer;user-select:none;list-style:none;}
.skill-frontmatter>summary::-webkit-details-marker{display:none;}
.skill-frontmatter>summary:hover{color:var(--text);}
.skill-frontmatter[open]>summary{border-bottom:1px solid var(--border);}
.skill-frontmatter pre{margin:0;padding:10px 12px;font-size:12px;line-height:1.5;color:var(--text);background:transparent;overflow-x:auto;}
/* Skill linked files section */
.skill-linked-files{margin-top:16px;border-top:1px solid var(--border);padding-top:12px;}
.skill-linked-section{margin-bottom:8px;}
@@ -1478,52 +1497,231 @@ body.resizing{user-select:none;cursor:col-resize;}
::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,.22);}
* { scrollbar-width: thin; scrollbar-color: rgba(255,255,255,.12) transparent; }
/* ── Settings overlay ── */
.settings-overlay{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:1000;display:flex;align-items:center;justify-content:center;}
.settings-panel{background:var(--bg);border:1px solid var(--border);border-radius:12px;padding:0;width:860px;max-width:92vw;height:min(700px,92vh);overflow:visible;box-shadow:0 12px 40px rgba(0,0,0,.5);display:flex;flex-direction:column;}
.settings-header{display:flex;align-items:flex-start;justify-content:space-between;padding:18px 24px 14px;border-bottom:1px solid var(--border);gap:16px;}
.settings-heading{display:flex;flex-direction:column;gap:3px;}
.settings-kicker{font-size:10px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--blue);}
.settings-subtitle{font-size:12px;color:var(--muted);line-height:1.5;}
.settings-body{padding:0;flex:1;display:flex;min-height:0;overflow:hidden;}
.settings-shell{display:grid;grid-template-columns:220px minmax(0,1fr);gap:0;flex:1;min-height:0;min-width:0;}
.settings-tabs{display:flex;flex-direction:column;gap:4px;padding:18px 12px;border-right:1px solid var(--border);align-self:stretch;min-height:0;}
.settings-tab{display:flex;flex-direction:row;gap:12px;align-items:center;padding:10px 12px;border-radius:8px;border:1px solid transparent;background:transparent;color:var(--muted);cursor:pointer;transition:background .15s,border-color .15s,color .15s;text-align:left;width:100%;}
.settings-tab:hover{background:rgba(255,255,255,.05);color:var(--text);}
.settings-tab.active{background:var(--accent-bg);border-color:var(--accent-bg-strong);color:var(--text);}
.settings-tab-icon{flex-shrink:0;opacity:.9;}
.settings-tab-title{font-size:13px;font-weight:600;letter-spacing:.01em;}
.settings-main{overflow-y:auto;padding:22px 24px;min-width:0;}
/* ── Settings view (rail cog → sidebar menu + main pane) ── */
/* --border-subtle: used inside cards for softer dividers than the
outer --border; resolves cleanly in both light+dark via color-mix. */
:root{--border-subtle:color-mix(in srgb, var(--border) 60%, transparent);}
:root.dark{--border-subtle:color-mix(in srgb, var(--border) 60%, transparent);}
/* ── Generalized main-view switching ──
<main class="main"> holds multiple sibling #main* containers (one per view).
Exactly one is visible at a time; chat is the default when no showing-*
class is set. Adding a new view needs: a #main<Name> sibling with
class="main-view", inclusion in the hidden-by-default list, and a
main.main.showing-<name> > #main<Name> { display:flex } rule. */
.main-view{flex:1;min-height:0;min-width:0;display:flex;flex-direction:column;background:var(--bg);}
main.main > #mainChat,
main.main > #mainSettings,
main.main > #mainSkills,
main.main > #mainMemory,
main.main > #mainTasks,
main.main > #mainWorkspaces,
main.main > #mainProfiles{display:none;}
main.main:not(.showing-settings):not(.showing-skills):not(.showing-memory):not(.showing-tasks):not(.showing-workspaces):not(.showing-profiles) > #mainChat{display:flex;}
main.main.showing-settings > #mainSettings{display:flex;overflow-y:auto;}
main.main.showing-skills > #mainSkills{display:flex;}
main.main.showing-memory > #mainMemory{display:flex;}
main.main.showing-tasks > #mainTasks{display:flex;}
main.main.showing-workspaces > #mainWorkspaces{display:flex;}
main.main.showing-profiles > #mainProfiles{display:flex;}
#mainSettings{overflow-y:auto;}
/* Sidebar menu (lives in the left sidebar under the cog panel) */
.panel-view#panelSettings{gap:0;padding:0;}
.panel-head{display:flex;align-items:center;justify-content:space-between;gap:8px;min-height:41px;padding:8px 12px;border-bottom:1px solid var(--border);font-size:11px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.08em;flex-shrink:0;}
.panel-head-actions{display:flex;align-items:center;gap:4px;text-transform:none;letter-spacing:normal;}
.panel-head-btn{width:24px;height:24px;padding:0;display:inline-flex;align-items:center;justify-content:center;border:none;background:transparent;border-radius:6px;color:var(--muted);cursor:pointer;transition:background .15s,color .15s;flex-shrink:0;}
.panel-head-btn:hover{background:var(--accent-bg);color:var(--accent-text);}
.panel-head-btn svg{width:14px;height:14px;display:block;}
.panel-head-sub{padding:6px 12px 8px;font-size:11px;color:var(--muted);flex-shrink:0;}
.side-menu{display:flex;flex-direction:column;gap:2px;padding:8px;overflow-y:auto;}
.side-menu-item{display:flex;align-items:center;gap:8px;padding:8px 10px;border-radius:8px;border:none;background:transparent;color:var(--text);cursor:pointer;text-align:left;font-size:13px;font-weight:500;transition:background .15s,color .15s;width:100%;}
.side-menu-item:hover{background:var(--surface);}
.side-menu-item.active{background:var(--accent-bg);color:var(--accent-text);}
.side-menu-item.active svg{color:var(--accent-text);}
.side-menu-item svg{flex-shrink:0;width:16px;height:16px;opacity:.9;}
/* Main canvas: centered, comfortable max-width */
.settings-main{padding:24px;max-width:768px;width:100%;margin:0 auto;min-width:0;}
.settings-pane{display:none;}
.settings-pane.active{display:block;}
.settings-section-dropdown{display:none;}
.settings-section-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:14px;}
.settings-section-title{font-size:13px;font-weight:700;letter-spacing:.01em;color:var(--text);}
.settings-section-meta{font-size:11px;color:var(--muted);margin-top:3px;line-height:1.5;}
.settings-version-badge{display:inline-flex;align-items:center;padding:4px 8px;border-radius:999px;border:1px solid var(--accent-bg);background:var(--accent-bg);color:var(--accent-text);font-size:11px;font-weight:700;flex-shrink:0;}
.hermes-action-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;}
.settings-action-btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;padding:10px 12px;border-radius:10px;border:1px solid var(--border2);background:var(--input-bg);color:var(--text);font-size:12px;font-weight:600;cursor:pointer;transition:background .15s,border-color .15s,color .15s;}
.settings-action-btn:hover{background:rgba(255,255,255,.08);border-color:rgba(255,255,255,.18);}
.settings-action-btn.danger{color:var(--error);border-color:var(--error);}
.settings-action-btn.danger:hover{background:rgba(239,83,80,.08);border-color:var(--error);}
.settings-action-btn:disabled,.settings-action-btn.disabled{opacity:.45;cursor:not-allowed;}
.settings-action-btn:disabled:hover,.settings-action-btn.disabled:hover{background:var(--input-bg);border-color:var(--border2);}
.settings-field{margin-bottom:16px;}
.settings-field label{display:block;font-size:11px;font-weight:600;letter-spacing:.05em;text-transform:uppercase;color:var(--muted);margin-bottom:6px;}
/* Save button inside the settings panel */
.settings-panel .settings-btn{background:var(--accent);color:#fff;border:none;border-radius:6px;padding:8px 16px;cursor:pointer;font-weight:600;font-size:13px;}
.settings-panel .settings-btn:hover{opacity:.9;}
/* ── Provider cards (settings panel) ── */
.provider-card{padding:10px 12px;border:1px solid var(--border);border-radius:8px;background:var(--code-bg);transition:border-color .15s;}
.provider-card:hover{border-color:var(--border2);}
.provider-card-name{font-weight:600;font-size:13px;}
.provider-card .sm-btn:disabled{opacity:.4;cursor:not-allowed;}
.provider-card-actions{margin-top:6px;display:flex;gap:6px;align-items:center;width:100%;}
.provider-save-btn{padding:5px 12px;font-size:12px;white-space:nowrap;display:inline-flex;align-items:center;gap:5px;flex:none;}
.provider-remove-btn{padding:5px 10px;font-size:12px;color:var(--error);border-color:rgba(233,69,96,.25);white-space:nowrap;display:inline-flex;align-items:center;gap:5px;flex:none;}
.provider-btn-icon{flex-shrink:0;}
.provider-btn-label{}
/* Page-style section header (title + subtitle + optional badge). */
.settings-section-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:20px;padding-bottom:14px;border-bottom:1px solid var(--border);}
.settings-section-title{font-size:18px;font-weight:600;letter-spacing:-.01em;color:var(--text);line-height:1.3;margin-bottom:4px;}
.settings-section-meta{font-size:13px;color:var(--muted);line-height:1.55;}
.settings-version-badge{display:inline-flex;align-items:center;padding:3px 8px;border-radius:999px;background:var(--surface);color:var(--muted);font-size:11px;font-weight:600;font-family:'SF Mono',ui-monospace,SFMono-Regular,Menlo,monospace;flex-shrink:0;align-self:flex-start;letter-spacing:.02em;}
/* Each logical form row is a card surface. Stack with comfortable gap. */
#mainSettings .settings-field{margin-bottom:12px;padding:16px;border:1px solid var(--border);border-radius:12px;background:var(--sidebar);}
#mainSettings .settings-field:last-of-type{margin-bottom:16px;}
#mainSettings .settings-field > label{display:block;font-size:12px;font-weight:500;letter-spacing:.01em;color:var(--muted);margin-bottom:6px;}
/* Checkbox rows use inline flex labels — don't turn them into stacked caps */
#mainSettings .settings-field > label[style*="display:flex"]{color:var(--text);font-size:13px;font-weight:500;margin-bottom:0;}
#mainSettings .settings-field > div[style*="color:var(--muted)"],
#mainSettings .settings-field > div[style*="color: var(--muted)"]{font-size:12px;color:var(--muted);line-height:1.5;}
/* Selects & text/password inputs — uniform. Uses !important to win
over legacy inline styles that were written for the modal era. */
#mainSettings select,
#mainSettings input[type="text"],
#mainSettings input[type="password"]{
background:var(--bg)!important;
color:var(--text)!important;
border:1px solid var(--border)!important;
border-radius:8px!important;
padding:8px 12px!important;
font-size:13px!important;
width:100%;
transition:border-color .15s,box-shadow .15s;
}
#mainSettings select:focus,
#mainSettings input[type="text"]:focus,
#mainSettings input[type="password"]:focus{border-color:var(--accent)!important;box-shadow:0 0 0 3px var(--accent-bg);outline:none;}
#mainSettings input:disabled,
#mainSettings select:disabled{opacity:.6;cursor:not-allowed;}
/* Quick-action grid in Conversation pane — outlined card buttons. */
.hermes-action-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin-bottom:18px;}
.settings-action-btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;padding:10px 14px;border-radius:10px;border:1px solid var(--border);background:var(--sidebar);color:var(--text);font-size:13px;font-weight:500;cursor:pointer;transition:background .15s,border-color .15s,color .15s;}
.settings-action-btn:hover{background:var(--accent-bg);border-color:var(--accent-bg-strong);color:var(--accent-text);}
.settings-action-btn.danger{color:var(--error,#e05);border-color:color-mix(in srgb,var(--error,#e05) 30%,transparent);}
.settings-action-btn.danger:hover{background:color-mix(in srgb,var(--error,#e05) 8%,transparent);border-color:var(--error,#e05);color:var(--error,#e05);}
.settings-action-btn:disabled,.settings-action-btn.disabled{opacity:.45;cursor:not-allowed;}
.settings-action-btn:disabled:hover,.settings-action-btn.disabled:hover{background:var(--sidebar);border-color:var(--border);color:var(--text);}
/* Primary save buttons (Appearance / Preferences "Save Settings"). */
#mainSettings .settings-btn,
#mainSettings .sm-btn[onclick^="saveSettings"]{background:var(--accent);color:#fff;border:1px solid var(--accent);border-radius:8px;padding:9px 16px;cursor:pointer;font-weight:600;font-size:13px;transition:background .15s,border-color .15s,opacity .15s;}
#mainSettings .settings-btn:hover,
#mainSettings .sm-btn[onclick^="saveSettings"]:hover{background:var(--accent-hover);border-color:var(--accent-hover);}
/* Secondary sm-btn fallback (Sign out / Disable auth) — neutral outline. */
#mainSettings .sm-btn{border-radius:8px;padding:9px 16px;font-weight:600;font-size:13px;background:var(--surface);border:1px solid var(--border);color:var(--text);cursor:pointer;transition:background .15s,border-color .15s;}
#mainSettings .sm-btn:hover{background:var(--accent-bg);border-color:var(--accent-bg-strong);}
/* Neutralise legacy inline accent tints so secondary buttons stay theme-safe */
#mainSettings #btnDisableAuth[style],
#mainSettings #btnSignOut[style]{color:var(--text)!important;border-color:var(--border)!important;}
#mainSettings #btnDisableAuth:hover,
#mainSettings #btnSignOut:hover{color:var(--accent-text)!important;border-color:var(--accent-bg-strong)!important;}
/* Picker grids (theme / skin / font-size): make the card chrome use
tokens so all skins flip correctly. */
#mainSettings .theme-pick-btn,
#mainSettings .skin-pick-btn,
#mainSettings .font-size-pick-btn{background:var(--bg)!important;border-color:var(--border)!important;}
#mainSettings .theme-pick-btn:hover,
#mainSettings .skin-pick-btn:hover,
#mainSettings .font-size-pick-btn:hover{border-color:var(--accent-bg-strong)!important;background:var(--surface)!important;}
/* Responsive: tighten canvas on small screens. */
@media (max-width: 768px){
.settings-main{padding:16px 12px;}
.settings-section-title{font-size:16px;}
.hermes-action-grid{grid-template-columns:1fr;}
#mainSettings .settings-field{padding:14px;}
}
/* ── Provider cards (Settings → Providers) ──
Matches hermes-desktop LLM Providers panel. Card uses --sidebar (surface-1),
hover rows use --surface (surface-2). Body divider uses a subtle tint. */
#providersList{gap:12px;}
.provider-card{
border:1px solid var(--border);
border-radius:12px;
background:var(--sidebar);
overflow:hidden;
margin-bottom:12px;
transition:border-color .15s ease;
}
.provider-card:last-child{margin-bottom:0;}
.provider-card-header{
display:flex;align-items:center;gap:12px;
width:100%;
padding:12px 16px;
background:transparent;border:0;
color:var(--text);
text-align:left;cursor:pointer;
font:inherit;
transition:background .15s ease;
}
.provider-card-header:hover{background:var(--surface);}
.provider-card-info{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px;}
.provider-card-name{font-size:13px;font-weight:500;color:var(--text);line-height:1.2;}
.provider-card-meta{font-size:11px;color:var(--muted);line-height:1.2;opacity:.82;}
.provider-card-badge{
flex-shrink:0;
font-size:10.5px;font-weight:500;
padding:2px 8px;border-radius:999px;
/* Literal tailwind green-500/10 + green-600 to match desktop reference */
background:rgba(34,197,94,.12);
color:#16a34a;
letter-spacing:.01em;
}
:root.dark .provider-card-badge{background:rgba(34,197,94,.16);color:#4ade80;}
.provider-card-chevron{
flex-shrink:0;color:var(--muted);
transition:transform .2s ease;
}
.provider-card.open .provider-card-chevron{transform:rotate(180deg);}
.provider-card-body{
display:none;
padding:12px 16px 16px;
border-top:1px solid color-mix(in srgb, var(--border) 55%, transparent);
}
.provider-card.open .provider-card-body{display:block;}
.provider-card-hint{font-size:12px;color:var(--muted);line-height:1.4;}
.provider-card-field{display:flex;flex-direction:column;gap:6px;}
.provider-card-label{
font-size:11px;font-weight:500;color:var(--muted);
letter-spacing:.02em;
}
.provider-card-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}
.provider-card-input{
flex:1;min-width:160px;
padding:8px 10px;
background:var(--surface);
color:var(--text);
border:1px solid var(--border);
border-radius:8px;
font-size:12px;
font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;
outline:none;
transition:border-color .15s ease, box-shadow .15s ease;
}
.provider-card-input:focus{
border-color:var(--accent);
box-shadow:0 0 0 3px var(--focus-ring);
}
.provider-card-btn{
padding:7px 12px;
font-size:12px;font-weight:500;
border-radius:8px;
border:1px solid transparent;
background:var(--surface);
color:var(--text);
cursor:pointer;white-space:nowrap;
transition:background .15s ease, color .15s ease, border-color .15s ease;
}
.provider-card-btn:hover{background:var(--hover-bg);}
.provider-card-btn:disabled{opacity:.45;cursor:not-allowed;}
.provider-card-btn-ghost{background:var(--surface);}
.provider-card-btn-primary{
background:var(--accent);
border-color:var(--accent);
color:#fff;
}
:root.dark .provider-card-btn-primary{color:#0D0D1A;}
.provider-card-btn-primary:hover:not(:disabled){
background:var(--accent-hover);
border-color:var(--accent-hover);
}
.provider-card-btn-danger{
color:var(--error);
background:transparent;
}
.provider-card-btn-danger:hover{
background:color-mix(in srgb, var(--error) 10%, transparent);
}
/* ── Session pin indicator (inline, only when pinned) ── */
.session-pin-indicator{
@@ -1906,18 +2104,96 @@ body.resizing{user-select:none;cursor:col-resize;}
.msg-row[data-error="1"] .msg-body { padding: 8px 12px; }
}
/* Mobile overrides — must live after all base rules to win the cascade without !important */
@media(max-width:640px){
/* Settings dialog: replace left tab strip with a dropdown, panel goes full-width */
.settings-shell{display:flex;flex-direction:column;gap:0;}
.settings-section-dropdown{display:block;margin:10px 14px;width:calc(100% - 28px);}
.settings-tabs{display:none;}
.settings-main{padding:18px 16px;width:100%;}
/* Provider buttons: icon-only on mobile, input fills remaining space */
.provider-save-btn,.provider-remove-btn{padding:7px;gap:0;flex-shrink:0;}
.provider-btn-label{display:none;}
/* ── Shared main-view tokens (used by #mainSkills, #mainTasks, #mainWorkspaces, #mainProfiles) ── */
.main-view-header{
display:flex;align-items:center;justify-content:space-between;gap:16px;
min-height:41px;padding:8px 32px;border-bottom:1px solid var(--border);
flex-shrink:0;background:var(--bg);
}
.main-view-title{font-size:18px;font-weight:600;letter-spacing:-.01em;color:var(--text);line-height:1.3;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.main-view-actions{display:flex;align-items:center;gap:4px;flex-shrink:0;}
.main-view-body{flex:1;min-height:0;overflow-y:auto;padding:24px 32px;}
.main-view-content{max-width:960px;margin:0 auto;width:100%;}
.main-view-empty{
flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;
gap:12px;padding:40px 24px;color:var(--muted);text-align:center;
}
.main-view-empty-icon{color:var(--muted);opacity:.35;}
.main-view-empty-title{font-size:18px;font-weight:600;color:var(--text);margin-top:4px;}
.main-view-empty-sub{font-size:13px;color:var(--muted);line-height:1.55;max-width:420px;}
/* Message controls: always visible on touch screens (no hover available) */
/* Hide header title/actions entirely when empty state is active */
main.main > .main-view:not([id="mainChat"]):not([id="mainSettings"]) .main-view-header:has(.main-view-title:empty){display:none;}
/* Skill-file breadcrumb (shown when clicking a linked file) */
.skill-file-breadcrumb{display:flex;align-items:center;gap:12px;margin-bottom:16px;padding-bottom:10px;border-bottom:1px solid var(--border-subtle,var(--border));}
.skill-file-back{font-size:12px;color:var(--accent-text);text-decoration:none;font-weight:500;}
.skill-file-back:hover{text-decoration:underline;}
.skill-file-path{font-size:12px;color:var(--muted);font-family:'SF Mono',ui-monospace,monospace;}
.skill-file-code{background:var(--sidebar);border:1px solid var(--border);border-radius:8px;padding:12px 14px;font-size:12px;overflow:auto;max-width:960px;margin:0 auto;}
@media(max-width:700px){
.main-view-header{padding:8px 16px;}
.main-view-body{padding:16px;}
}
/* ── Shared detail-card styling (cron / workspace / profile detail pages) ── */
.detail-card{border:1px solid var(--border);border-radius:12px;background:var(--surface);padding:16px 18px;margin-bottom:16px;}
.detail-card-title{font-size:11px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:10px;}
.detail-row{display:flex;gap:12px;padding:6px 0;border-bottom:1px solid var(--border);font-size:13px;align-items:flex-start;}
.detail-row:last-child{border-bottom:none;}
.detail-row-label{flex:0 0 140px;color:var(--muted);font-size:12px;}
.detail-row-value{flex:1;min-width:0;color:var(--text);word-break:break-word;}
.detail-row-value code{background:var(--sidebar);border:1px solid var(--border);border-radius:4px;padding:1px 6px;font-size:12px;font-family:'SF Mono',ui-monospace,monospace;}
.detail-badge{display:inline-block;padding:2px 8px;border-radius:999px;font-size:11px;font-weight:600;border:1px solid var(--border);color:var(--muted);}
.detail-badge.active{background:var(--accent-bg);color:var(--accent-text);border-color:var(--accent-bg-strong,var(--accent-bg));}
.detail-badge.ok{background:rgba(100,200,100,.12);color:rgba(100,200,100,.95);border-color:rgba(100,200,100,.3);}
.detail-badge.warn{background:rgba(220,140,60,.12);color:rgba(220,140,60,.95);border-color:rgba(220,140,60,.3);}
.detail-badge.err{background:rgba(220,80,80,.12);color:rgba(220,80,80,.95);border-color:rgba(220,80,80,.3);}
.detail-prompt{background:var(--sidebar);border:1px solid var(--border);border-radius:8px;padding:10px 12px;font-size:12px;white-space:pre-wrap;line-height:1.55;color:var(--text);font-family:'SF Mono',ui-monospace,monospace;max-height:240px;overflow-y:auto;}
.detail-run-item{border-top:1px solid var(--border);padding:8px 0;}
.detail-run-item:first-child{border-top:none;}
.detail-run-head{display:flex;align-items:center;justify-content:space-between;cursor:pointer;font-size:12px;color:var(--muted);}
.detail-run-body{display:none;margin-top:6px;font-size:12px;color:var(--muted);white-space:pre-wrap;line-height:1.5;max-height:260px;overflow-y:auto;background:var(--sidebar);border:1px solid var(--border);border-radius:6px;padding:8px 10px;}
.detail-run-item.open .detail-run-body{display:block;}
.cron-item.active,.ws-row.active,.profile-card.active{background:var(--accent-bg);}
.cron-item.active .cron-name,.ws-row.active .ws-row-name,.profile-card.active .profile-card-name{color:var(--accent-text);}
/* ── Shared in-main form (create/edit) for skills/tasks/workspaces/profiles ── */
.detail-form{display:flex;flex-direction:column;gap:14px;}
.detail-form-row{display:flex;flex-direction:column;gap:6px;}
.detail-form-row > label{font-size:12px;font-weight:500;color:var(--muted);letter-spacing:.01em;}
.detail-form-row input[type="text"],
.detail-form-row input[type="password"],
.detail-form-row input:not([type]),
.detail-form-row select,
.detail-form-row textarea{
width:100%;box-sizing:border-box;
background:var(--sidebar);border:1px solid var(--border);border-radius:8px;
color:var(--text);padding:9px 11px;font-size:13px;outline:none;
font-family:inherit;line-height:1.5;
transition:border-color .15s,background .15s;
}
.detail-form-row input[type="text"]:focus,
.detail-form-row input[type="password"]:focus,
.detail-form-row input:not([type]):focus,
.detail-form-row select:focus,
.detail-form-row textarea:focus{border-color:var(--accent,var(--accent-bg-strong,var(--border)));}
.detail-form-row input:disabled{opacity:.6;cursor:not-allowed;}
.detail-form-row textarea{resize:vertical;font-family:'SF Mono',ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;}
.detail-form-row .detail-form-hint{font-size:11px;color:var(--muted);line-height:1.5;}
.detail-form-row label.detail-form-check{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--text);cursor:pointer;font-weight:400;}
.detail-form-row label.detail-form-check input{accent-color:var(--accent,var(--link));}
.detail-form-error{font-size:12px;color:var(--error,#e05);padding:8px 10px;border:1px solid color-mix(in srgb,var(--error,#e05) 35%,transparent);background:color-mix(in srgb,var(--error,#e05) 8%,transparent);border-radius:8px;line-height:1.5;}
.panel-head-btn.primary{background:var(--accent,var(--link));color:#fff;border:none;}
.panel-head-btn.primary:hover{background:var(--accent-hover,var(--accent,var(--link)));color:#fff;}
.panel-head-btn.primary svg{color:#fff;}
/* Skill-picker inside in-main forms reuses the sidebar styles but needs light tweaks */
.detail-form-row .skill-picker-wrap{position:relative;}
.detail-form-row .skill-picker-tags{margin-top:6px;display:flex;flex-wrap:wrap;gap:4px;}
/* Mobile: message controls always visible on touch screens (no hover available) */
@media(max-width:640px){
.msg-row[data-role="user"] .msg-foot,
.msg-row[data-role="assistant"] .msg-foot,
.assistant-turn .msg-foot{opacity:1;}

View File

@@ -966,7 +966,7 @@ function updateQueueBadge(sessionId){
badge.remove();
}
}
function showToast(msg,ms){const el=$('toast');el.textContent=msg;el.classList.add('show');clearTimeout(el._t);el._t=setTimeout(()=>el.classList.remove('show'),ms||2800);}
function showToast(msg,ms,type){const el=$('toast');if(!el)return;const s=String(msg==null?'':msg);let t=type;if(!t){const low=s.toLowerCase();if(/fail|error|denied|invalid|unavailable|no active|no workspace match|no model match|no personalities/.test(low))t='error';else if(/warn|queued|takes effect|skipped|fallback/.test(low))t='warning';else if(/saved|created|imported|restored|switched|set to|updated|duplicated|moved to|renamed|deleted|complete|pinned|archived|cleared|stopped/.test(low))t='success';else t='info';}el.textContent=s;el.className='toast show '+t;clearTimeout(el._t);el._t=setTimeout(()=>{el.classList.remove('show');},ms||2800);}
// ── Shared app dialogs ───────────────────────────────────────────────────────
// showConfirmDialog(opts) and showPromptDialog(opts) replace browser-native dialog calls
@@ -1384,13 +1384,15 @@ function syncTopbar(){
sidebarName.textContent=t('no_workspace');
}
}
if(typeof syncAppTitlebar==='function') syncAppTitlebar();
return;
}
const sessionTitle=S.session.title||t('untitled');
$('topbarTitle').textContent=sessionTitle;
const _topbarTitle=$('topbarTitle');if(_topbarTitle)_topbarTitle.textContent=sessionTitle;
document.title=sessionTitle+' \u2014 '+(window._botName||'Hermes');
const vis=S.messages.filter(m=>m&&m.role&&m.role!=='tool');
$('topbarMeta').textContent=t('n_messages',vis.length);
const _topbarMeta=$('topbarMeta');if(_topbarMeta)_topbarMeta.textContent=t('n_messages',vis.length);
if(typeof syncAppTitlebar==='function') syncAppTitlebar();
// If a profile switch just happened, apply its model rather than the session's stale value.
// S._pendingProfileModel is set by switchToProfile() and cleared here after one application.
const modelOverride=S._pendingProfileModel;
@@ -1786,7 +1788,7 @@ function renderMessages(){
const bodyHtml = isUser ? esc(String(content)).replace(/\n/g,'<br>') : renderMd(_stripXmlToolCallsDisplay(String(content)));
const isEditableUser=isUser&&rawIdx===lastUserRawIdx;
const editBtn = isEditableUser ? `<button class="msg-action-btn" title="${t('edit_message')}" onclick="editMessage(this)">${li('pencil',13)}</button>` : '';
const undoBtn = isLastAssistant ? `<button class="msg-action-btn" title="${t('undo_exchange')}" onclick="undoLastExchange()">${li('undo-2',13)}</button>` : '';
const undoBtn = isLastAssistant ? `<button class="msg-action-btn" title="${t('undo_exchange')}" onclick="undoLastExchange()">${li('undo',13)}</button>` : '';
const retryBtn = isLastAssistant ? `<button class="msg-action-btn" title="${t('regenerate')}" onclick="regenerateResponse(this)">${li('rotate-ccw',13)}</button>` : '';
const copyBtn = `<button class="msg-copy-btn msg-action-btn" title="${t('copy')}" onclick="copyMsg(this)">${li('copy',13)}</button>`;
const tsVal=m._ts||m.timestamp;

View File

@@ -77,32 +77,40 @@ class TestCronSkillCacheInvalidation:
def test_cache_busted_on_form_open(self):
src = self._panels_src()
# toggleCronForm should set cache to null unconditionally
# openCronCreate() opens the task create form (renamed from toggleCronForm
# in the main-view refactor). It must null the skills cache before fetching.
m = re.search(
r'function toggleCronForm\(\)\{.*?_cronSkillsCache=null',
r'function openCronCreate\(\)\{.*?_cronSkillsCache\s*=\s*null',
src, re.DOTALL
)
assert m, (
"toggleCronForm must unconditionally null _cronSkillsCache "
"openCronCreate must unconditionally null _cronSkillsCache "
"before fetching skills"
)
def test_cache_not_guarded_by_if_on_open(self):
src = self._panels_src()
# The old guard should be gone
assert "if(!_cronSkillsCache)" not in src, (
"toggleCronForm should not use 'if(!_cronSkillsCache)' guard — "
# openCronCreate must not gate the fetch behind an if(!_cronSkillsCache) guard.
m = re.search(
r'function openCronCreate\(\)\{.*?\}',
src, re.DOTALL
)
assert m, "openCronCreate definition not found"
assert "if(!_cronSkillsCache)" not in m.group(0), (
"openCronCreate should not use 'if(!_cronSkillsCache)' guard — "
"cache must always be busted on open"
)
def test_cache_busted_on_skill_save(self):
src = self._panels_src()
# After submitSkillSave's api() call, _cronSkillsCache must be nulled
# saveSkillForm() is the handler invoked on skill save (renamed from
# submitSkillSave in the main-view refactor; the old name still aliases it).
m = re.search(
r'async function submitSkillSave\(\).*?_skillsData\s*=\s*null.*?_cronSkillsCache\s*=\s*null',
r'async function saveSkillForm\(\).*?_skillsData\s*=\s*null.*?_cronSkillsCache\s*=\s*null',
src, re.DOTALL
)
assert m, (
"_cronSkillsCache must be set to null in submitSkillSave() "
"_cronSkillsCache must be set to null in saveSkillForm() "
"right after _skillsData = null"
)

View File

@@ -47,7 +47,7 @@ class TestCronRefreshButtonHtml:
button so the header layout stays tight."""
html = _read("static/index.html")
ref_pos = html.find('id="cronRefreshBtn"')
newjob_pos = html.find('toggleCronForm()')
newjob_pos = html.find('openCronCreate()')
assert ref_pos != -1 and newjob_pos != -1
# Must be close enough to be in the same header row (single SVG-inline
# button can be around 500 chars by itself due to inline styles/attrs).

View File

@@ -9,7 +9,7 @@ def test_workspace_suggest_endpoint_is_wired():
def test_spaces_panel_uses_workspace_suggest_autocomplete():
src = pathlib.Path("static/panels.js").read_text(encoding="utf-8")
assert "/api/workspaces/suggest" in src
assert "wsAddSuggestions" in src
assert "workspaceFormPathSuggestions" in src
assert "scheduleWorkspacePathSuggestions" in src
assert "if(!prefix)" in src
assert "dataset.path" in src

View File

@@ -0,0 +1,100 @@
"""Regression coverage for settings navigation and master-detail refresh state."""
import pathlib
import re
REPO_ROOT = pathlib.Path(__file__).parent.parent
PANELS_JS = (REPO_ROOT / "static" / "panels.js").read_text(encoding="utf-8")
class TestSettingsNavigationGuard:
"""Leaving Settings through the rail must still honor save/discard semantics."""
def test_switch_panel_checks_settings_guard(self):
assert "if (!opts.bypassSettingsGuard && !_beforePanelSwitch(nextPanel)) return false;" in PANELS_JS, (
"switchPanel() must consult the settings guard before leaving Settings "
"so rail/sidebar navigation cannot bypass the unsaved-changes flow"
)
def test_dirty_settings_capture_requested_destination(self):
m = re.search(
r"function _beforePanelSwitch\(nextPanel\)\s*\{.*?"
r"_pendingSettingsTargetPanel = nextPanel \|\| 'chat';.*?"
r"_showSettingsUnsavedBar\(\);.*?"
r"return false;",
PANELS_JS,
re.DOTALL,
)
assert m, (
"_beforePanelSwitch() must remember the requested destination and "
"block navigation while settings are dirty"
)
def test_hiding_settings_resumes_pending_target_with_bypass(self):
m = re.search(
r"function _hideSettingsPanel\(\)\s*\{.*?"
r"const target = _consumeSettingsTargetPanel\('chat'\);.*?"
r"switchPanel\(target, \{bypassSettingsGuard:true\}\);",
PANELS_JS,
re.DOTALL,
)
assert m, (
"_hideSettingsPanel() must resume the pending target after save/discard "
"instead of always falling back to chat"
)
def test_entering_settings_starts_fresh_preview_session(self):
assert "if (prevPanel !== 'settings' && nextPanel === 'settings') _beginSettingsPanelSession();" in PANELS_JS, (
"switchPanel() must snapshot the preview baseline when entering Settings "
"through the main navigation"
)
class TestMasterDetailRefreshClearsRemovedSelections:
"""Refreshes must not leave dead detail panes visible after a selection disappears."""
def test_tasks_clear_empty_state_detail(self):
assert "if (_cronMode !== 'create' && _cronMode !== 'edit') _clearCronDetail();" in PANELS_JS, (
"loadCrons() must clear the detail pane when the jobs list becomes empty"
)
def test_tasks_clear_missing_selected_job(self):
m = re.search(
r"if \(_currentCronDetail && _cronMode !== 'create' && _cronMode !== 'edit'\) \{.*?"
r"if \(refreshed\) _renderCronDetail\(refreshed\);\s*else _clearCronDetail\(\);",
PANELS_JS,
re.DOTALL,
)
assert m, (
"loadCrons() must clear the detail pane when the selected job disappears "
"during refresh"
)
def test_workspaces_clear_missing_selected_workspace(self):
m = re.search(
r"if \(_currentWorkspaceDetail && _workspaceMode !== 'create' && _workspaceMode !== 'edit'\) \{.*?"
r"if \(refreshed\) _renderWorkspaceDetail\(refreshed\);\s*else _clearWorkspaceDetail\(\);",
PANELS_JS,
re.DOTALL,
)
assert m, (
"renderWorkspacesPanel() must clear the detail pane when the selected "
"workspace disappears during refresh"
)
def test_profiles_clear_empty_state_detail(self):
assert "if (_profileMode !== 'create') _clearProfileDetail();" in PANELS_JS, (
"loadProfilesPanel() must clear the detail pane when there are no profiles"
)
def test_profiles_clear_missing_selected_profile(self):
m = re.search(
r"if \(_currentProfileDetail && _profileMode !== 'create'\) \{.*?"
r"if \(refreshed\) _renderProfileDetail\(refreshed, data.active\);\s*else _clearProfileDetail\(\);",
PANELS_JS,
re.DOTALL,
)
assert m, (
"loadProfilesPanel() must clear the detail pane when the selected "
"profile disappears during refresh"
)

View File

@@ -106,8 +106,10 @@ def test_crons_output_limit_param(cleanup_test_sessions):
def test_cron_history_button_in_panels_js(cleanup_test_sessions):
src, _ = get_text("/static/panels.js")
assert "loadCronHistory" in src
assert "cron_all_runs" in src # i18n key (was hardcoded 'All runs' before i18n hardening)
# After the main-view refactor, cron runs load inline into the detail card
# via _loadCronDetailRuns() instead of a separate "All runs" button.
assert "_loadCronDetailRuns" in src
assert "cron_last_output" in src # i18n key used by the runs card
def test_cron_output_snippet_helper(cleanup_test_sessions):
src, _ = get_text("/static/panels.js")

View File

@@ -253,9 +253,11 @@ def test_control_center_resets_active_section_on_close():
def test_control_center_tab_highlight_on_open():
"""Opening the control center must use settings-tabs for section navigation."""
"""The settings left-rail menu must have a CSS rule that highlights the active section."""
css = open(pathlib.Path(__file__).parent.parent / 'static' / 'style.css').read()
assert 'settings-tabs' in css, 'settings-tabs CSS class for control center tabs missing from style.css'
assert 'side-menu-item' in css, 'side-menu-item CSS class for left-rail nav missing from style.css'
assert '.side-menu-item.active' in css or 'side-menu-item.active' in css, \
'No active-state style for .side-menu-item — sidebar section highlight missing'
# ── apply_onboarding_setup: unsupported/OAuth providers complete gracefully ──

View File

@@ -35,8 +35,8 @@ def test_index_html_served():
raw, headers, status = get_raw("/")
assert status == 200
assert b"sidebarResize" in raw, "Resize handle not found in HTML"
assert b"cronCreateForm" in raw, "Cron create form not found in HTML"
assert b"btnHermesPanel" in raw, "Hermes control center trigger not found in HTML"
assert b'id="mainTasks"' in raw, "Tasks main-view not found in HTML"
assert b'id="settingsMenu"' in raw, "Settings left-rail menu not found in HTML"
assert b"btnExportJSON" in raw, "Export JSON button not found in HTML"
def test_index_html_file_exists():