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:
@@ -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){
|
||||
|
||||
215
static/i18n.js
215
static/i18n.js
@@ -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.
|
||||
|
||||
@@ -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"/>',
|
||||
|
||||
@@ -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>
|
||||
|
||||
1811
static/panels.js
1811
static/panels.js
File diff suppressed because it is too large
Load Diff
@@ -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');
|
||||
|
||||
498
static/style.css
498
static/style.css
@@ -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;}
|
||||
|
||||
10
static/ui.js
10
static/ui.js
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
100
tests/test_settings_navigation_and_detail_refresh.py
Normal file
100
tests/test_settings_navigation_and_detail_refresh.py
Normal 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"
|
||||
)
|
||||
@@ -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")
|
||||
|
||||
@@ -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 ──
|
||||
|
||||
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user